From 3d25461944266216a367d7a0fef8e26d66c78477 Mon Sep 17 00:00:00 2001 From: GitHub <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 27 Jul 2024 02:09:48 +0000 Subject: [PATCH 001/119] =?UTF-8?q?=F0=9F=94=A7=20Prepare=20for=20next=20r?= =?UTF-8?q?elease?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0d501fa61..dbc1b07d2 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ https://github.com/ebullient/ttrpg-convert-cli/issues - 2.3.18 + 299-SNAPSHOT 3.4.0 3.13.0 From ebd05457e591c9adc877edce8f77b53346f52074 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:19:37 +0000 Subject: [PATCH 002/119] Bump com.github.victools:jsonschema-generator from 4.35.0 to 4.36.0 Bumps [com.github.victools:jsonschema-generator](https://github.com/victools/jsonschema-generator) from 4.35.0 to 4.36.0. - [Release notes](https://github.com/victools/jsonschema-generator/releases) - [Changelog](https://github.com/victools/jsonschema-generator/blob/main/CHANGELOG.md) - [Commits](https://github.com/victools/jsonschema-generator/compare/v4.35.0...v4.36.0) --- updated-dependencies: - dependency-name: com.github.victools:jsonschema-generator dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dbc1b07d2..a2e6e8e60 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.3.0 3.0.7 75.1 - 4.35.0 + 4.36.0 uber-jar ttrpg-convert From fbee3981fcf0b44a75de2d48f4c98ae0bf431252 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:13:03 +0000 Subject: [PATCH 003/119] Bump github/codeql-action from 3.25.13 to 3.25.15 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.25.13 to 3.25.15. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2d790406f505036ef40ecba973cc774a50395aac...afb54ba388a7dca6ecae48f608c4ff05ff4cc77a) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bd52b1d4e..2cb2ab42d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13 + uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13 + uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4b60ec49b..442bcc77f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13 + uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: sarif_file: results.sarif From d0ff0de8f4427b29d136b9466016c52eb12971dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:13:05 +0000 Subject: [PATCH 004/119] Bump ossf/scorecard-action from 2.3.3 to 2.4.0 Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.3.3 to 2.4.0. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/dc50aa9510b46c811795eb24b2f1ba02a914e534...62b2cac7ed8198b15735ed49ab1e5cf35480ba46) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 442bcc77f..a7cca0896 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif From 5b3f3596b93022ae499b79040fe51e278fc32e69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:25:35 +0000 Subject: [PATCH 005/119] Bump actions/upload-artifact from 4.3.4 to 4.3.5 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.4 to 4.3.5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/0b2256b8c012f0828dc542b3febcab082c67f72b...89ef406dd8d7e03cfd12d9e0a4a378f454709029) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9768ff4e2..a079edaf0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,14 +85,14 @@ jobs: zip -r target/${ARTIFACT}-${NEXT}-examples.zip docs examples default - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 id: upload-jar with: name: artifacts-runner path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 id: upload-zip with: name: artifacts-examples diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a7cca0896..2bbd437b2 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,7 +60,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 with: name: SARIF file path: results.sarif From e82347039635d734c6f5c9c392618a04c0dcb4d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:51:12 +0000 Subject: [PATCH 006/119] Bump quarkus.platform.version from 3.12.3 to 3.13.2 Bumps `quarkus.platform.version` from 3.12.3 to 3.13.2. Updates `io.quarkus.platform:quarkus-bom` from 3.12.3 to 3.13.2 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.12.3...3.13.2) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.12.3 to 3.13.2 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.12.3...3.13.2) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a2e6e8e60..4b478894b 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.12.3 + 3.13.2 3.26.3 3.3.0 From b326617c101e29921b4c2f811f727c2662409a57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:04:51 +0000 Subject: [PATCH 007/119] Bump graalvm/setup-graalvm from 1.2.2 to 1.2.3 Bumps [graalvm/setup-graalvm](https://github.com/graalvm/setup-graalvm) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/graalvm/setup-graalvm/releases) - [Commits](https://github.com/graalvm/setup-graalvm/compare/2911b2304bee2c2f59b9a67bf45f025a6b6de4b1...22cc13fe88ef133134b3798e128fb208df55e1f5) --- updated-dependencies: - dependency-name: graalvm/setup-graalvm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 7c4e0012a..9e8eb31c8 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -117,7 +117,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@2911b2304bee2c2f59b9a67bf45f025a6b6de4b1 # v1.2.2 + - uses: graalvm/setup-graalvm@22cc13fe88ef133134b3798e128fb208df55e1f5 # v1.2.3 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 166ed19cb..7ca3e103a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -97,7 +97,7 @@ jobs: Data-Pf2eTools enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@2911b2304bee2c2f59b9a67bf45f025a6b6de4b1 # v1.2.2 + - uses: graalvm/setup-graalvm@22cc13fe88ef133134b3798e128fb208df55e1f5 # v1.2.3 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 4fb3da05d..e2eb21130 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -131,7 +131,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@2911b2304bee2c2f59b9a67bf45f025a6b6de4b1 # v1.2.2 + - uses: graalvm/setup-graalvm@22cc13fe88ef133134b3798e128fb208df55e1f5 # v1.2.3 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} From fa9962b9766654bc9ddbccb1ba77075f4b6c6e26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:04:48 +0000 Subject: [PATCH 008/119] Bump actions/upload-artifact from 4.3.5 to 4.3.6 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.5 to 4.3.6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/89ef406dd8d7e03cfd12d9e0a4a378f454709029...834a144ee995460fba8ed112a2fc961b36a5ec5a) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a079edaf0..f29658185 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,14 +85,14 @@ jobs: zip -r target/${ARTIFACT}-${NEXT}-examples.zip docs examples default - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 id: upload-jar with: name: artifacts-runner path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 id: upload-zip with: name: artifacts-examples diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2bbd437b2..d2bc70cc5 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,7 +60,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: SARIF file path: results.sarif From 641ea75cbfb1559f6737a4615d6ae785884c302a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:04:46 +0000 Subject: [PATCH 009/119] Bump actions/setup-java from 4.2.1 to 4.2.2 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.2.1 to 4.2.2. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/99b8673ff64fbf99d8d325f52d9a5bdedb8483e9...6a0805fcefea3d4657a47ac4c165951e33482018) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2cb2ab42d..154f9eeb6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 9e8eb31c8..cba8cc8cb 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -83,7 +83,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7ca3e103a..89e2d7fe0 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -51,7 +51,7 @@ jobs: Data-Pf2eTools - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f29658185..d42f1f32b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index e2eb21130..0aca60468 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -94,7 +94,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} From 44c3e5cc7f519d4aa9aa8e5d9c706cd9be3dda35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:04:42 +0000 Subject: [PATCH 010/119] Bump github/codeql-action from 3.25.15 to 3.26.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.25.15 to 3.26.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/afb54ba388a7dca6ecae48f608c4ff05ff4cc77a...eb055d739abdc2e8de2e5f4ba1a8b246daa779aa) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 154f9eeb6..882f7acc7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d2bc70cc5..f0308d1b2 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: sarif_file: results.sarif From 329d762d6b5cb292eac3cb983a6079317d5a28ce Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Tue, 13 Aug 2024 07:58:41 -0400 Subject: [PATCH 011/119] =?UTF-8?q?=F0=9F=91=B7=20=20keep=20existing=20cac?= =?UTF-8?q?he?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tools-data.yml | 35 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 0aca60468..799657a43 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -29,9 +29,10 @@ jobs: - name: Tools release cache key id: test-data-key run: | - LATEST_RELEASE=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/5etools-mirror-2/5etools-mirror-2.github.io/releases/latest) - LATEST_VERSION=$(echo $LATEST_RELEASE | grep tag_name | sed -e 's/.*"tag_name": "\([^"]*\)".*/\1/') - echo $LATEST_VERSION + # LATEST_RELEASE=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/5etools-mirror-2/5etools-mirror-2.github.io/releases/latest) + # LATEST_VERSION=$(echo $LATEST_RELEASE | grep tag_name | sed -e 's/.*"tag_name": "\([^"]*\)".*/\1/') + # echo $LATEST_VERSION + LATEST_VERSION="v1.209.3" echo "🔹 Use $LATEST_VERSION" echo "tools_version=${LATEST_VERSION}" >> $GITHUB_OUTPUT @@ -55,23 +56,23 @@ jobs: run: | mkdir -p sources - echo "🔹 Download $LATEST_VERSION" - ARTIFACT_URL="https://github.com/5etools-mirror-2/5etools-mirror-2.github.io/archive/refs/tags/$LATEST_VERSION.tar.gz" - VER=$(echo $LATEST_VERSION | cut -c 2-) - ROOT="5etools-mirror-2.github.io-$VER" + # echo "🔹 Download $LATEST_VERSION" + # ARTIFACT_URL="https://github.com/5etools-mirror-2/5etools-mirror-2.github.io/archive/refs/tags/$LATEST_VERSION.tar.gz" + # VER=$(echo $LATEST_VERSION | cut -c 2-) + # ROOT="5etools-mirror-2.github.io-$VER" - curl -LsS -o 5etools.tar.gz $ARTIFACT_URL - tar xzf 5etools.tar.gz ${ROOT}/data - mv ${ROOT} sources/5etools-mirror-2.github.io + # curl -LsS -o 5etools.tar.gz $ARTIFACT_URL + # tar xzf 5etools.tar.gz ${ROOT}/data + # mv ${ROOT} sources/5etools-mirror-2.github.io - gh repo clone 5etools-mirror-2/5etools-img sources/5etools-img -- --depth=1 - gh repo clone TheGiddyLimit/unearthed-arcana sources/5e-unearthed-arcana -- --depth=1 - gh repo clone TheGiddyLimit/homebrew sources/5e-homebrew -- --depth=1 + # gh repo clone 5etools-mirror-2/5etools-img sources/5etools-img -- --depth=1 + # gh repo clone TheGiddyLimit/unearthed-arcana sources/5e-unearthed-arcana -- --depth=1 + # gh repo clone TheGiddyLimit/homebrew sources/5e-homebrew -- --depth=1 - # Remove image contents. We just need the files to exist (linking) - find sources -type f -type f \ - \( -iname \*.jpg -o -iname \*.png -o -iname \*.webp \) \ - | while read FILE; do echo > "$FILE"; done + # # Remove image contents. We just need the files to exist (linking) + # find sources -type f -type f \ + # \( -iname \*.jpg -o -iname \*.png -o -iname \*.webp \) \ + # | while read FILE; do echo > "$FILE"; done ls -al sources From d0bdd3d1f249bf09471e6d90dbee0ac6eb1c4155 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Tue, 13 Aug 2024 08:10:32 -0400 Subject: [PATCH 012/119] =?UTF-8?q?=F0=9F=93=9D=20data=20no=20longer=20ava?= =?UTF-8?q?ilable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tools-data.yml | 3 - CONTRIBUTING.md | 12 +--- README-WINDOWS.md | 95 +++++++++++++++++++------------- README.md | 3 + docs/configuration.md | 4 -- 5 files changed, 64 insertions(+), 53 deletions(-) diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 799657a43..5fe7420d9 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -29,9 +29,6 @@ jobs: - name: Tools release cache key id: test-data-key run: | - # LATEST_RELEASE=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/5etools-mirror-2/5etools-mirror-2.github.io/releases/latest) - # LATEST_VERSION=$(echo $LATEST_RELEASE | grep tag_name | sed -e 's/.*"tag_name": "\([^"]*\)".*/\1/') - # echo $LATEST_VERSION LATEST_VERSION="v1.209.3" echo "🔹 Use $LATEST_VERSION" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2692096e..0544b8558 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,18 +25,12 @@ If there isn't an issue yet, open one! Be as specific as you can, and include th - **Use maven:** `./mvnw install` - **Use the Quarkus CLI**: `quarkus build` -To test with actual/live data, clone 5eTools and/or PF2eTools into a `sources` directory. +To test with actual/live data, add 5eTools and/or PF2eTools into a `sources` directory. Using the GitHub CLI: ```shell mkdir -p sources -# 5eTools -gh repo clone 5etools-mirror-2/5etools-mirror-2.github.io sources/5etools-mirror-2.github.io -- --depth=1 -gh repo clone TheGiddyLimit/homebrew sources/5e-homebrew -- --depth=1 -gh repo clone TheGiddyLimit/unearthed-arcana sources/5e-unearthed-arcana -- --depth=1 -# Optional: 5eTools images; requires an extra config step -gh repo clone 5etools-mirror-2/5etools-img sources/5etools-img -- --depth=1 # PF2eTools gh repo clone Pf2eToolsOrg/Pf2eTools sources/Pf2eTools -- --depth=1 @@ -118,7 +112,7 @@ Next navigate to *Java* -> *Code Style* -> *Organize Imports*. Click *Import* an "" ], ``` - + #### IntelliJ 1. Open the *Settings* window @@ -129,7 +123,7 @@ Next navigate to *Java* -> *Code Style* -> *Organize Imports*. Click *Import* an 6. Ensure that the *Layout static imports separately* checkbox is checked 7. Underneath that, change the entries so that they look like this: - ``` + ```shell import static all other imports import java.* diff --git a/README-WINDOWS.md b/README-WINDOWS.md index 5b70b1f0f..2836a8114 100644 --- a/README-WINDOWS.md +++ b/README-WINDOWS.md @@ -7,12 +7,13 @@ [TTRPG-Convert-CLI PF2e]: https://obsidianttrpgtutorials.com/Obsidian+TTRPG+Tutorials/Plugin+Tutorials/TTRPG-Convert-CLI/TTRPG-Convert-CLI+PF2e ## Requirements + - [Git][] is recommended to easily download and update the JSON sources - Existing experience with using a command line isn't required, but may be useful. These instructions should be sufficient, but you can look at the following resources for additional background on how to use the Windows command line: - - [A Beginner's Guide to the Windows Command Line](https://www.makeuseof.com/tag/a-beginners-guide-to-the-windows-command-line/) - - [How to Open Command Prompt in a Folder](https://www.lifewire.com/open-command-prompt-in-a-folder-5185505) + - [A Beginner's Guide to the Windows Command Line](https://www.makeuseof.com/tag/a-beginners-guide-to-the-windows-command-line/) + - [How to Open Command Prompt in a Folder](https://www.lifewire.com/open-command-prompt-in-a-folder-5185505) ## Instructions @@ -27,7 +28,7 @@ - `ttrpg-convert-cli-2.3.18-windows-x86_64.zip` - `ttrpg-convert-cli-2.3.18-examples.zip` -2. Unzip the downloaded files into a place you'll remember. For example, `Downloads`. +2. Unzip the downloaded files into a place you'll remember. For example, `Downloads`. 3. Navigate to the `bin` directory inside the unzipped files. It might be nested within another folder. You should see a `ttrpg-convert` EXE file in the folder - see the screenshot below. 4. In Explorer, hold **Shift** and **Right Click** within the folder (not on any particular file). Select *Open in Terminal* (this may also be *Open PowerShell window here*, or *Open command window here* if you @@ -40,46 +41,52 @@ ![A screenshot of a Windows Powershell window opened to the ttrpg-convert-cli directory](docs/screenshots/windows-powershell-open.png) 6. Acquire the JSON data sources following the instructions in [Convert 5eTools JSON data][] or [Convert Pf2eTools JSON data][] by entering commands in this window and pressing **Enter**. - - For example, for 5eTools, run the following command (assuming that you have [Git][] installed): - ``` - git clone --depth 1 https://github.com/5etools-mirror-2/5etools-mirror-2.github.io.git - ``` - - Or, for Pf2eTools: - ``` + + - For example, for Pf2eTools: + + ```shell git clone --depth 1 https://github.com/Pf2eToolsOrg/Pf2eTools.git ``` - - If you don't have Git, you can instead manually download the latest [5eTools release](https://github.com/Pf2eToolsOrg/Pf2eTools/releases/latest) or [Pf2eTools release](https://github.com/Pf2eToolsOrg/Pf2eTools/releases/latest) and extract the zip file to the `bin/` directory, so that it sits alongside the `ttrpg-convert.exe` file. + + - If you don't have Git, you can instead manually download the latest [Pf2eTools release](https://github.com/Pf2eToolsOrg/Pf2eTools/releases/latest) and extract the zip file to the `bin/` directory, so that it sits alongside the `ttrpg-convert.exe` file. + - At this point, it should look like this: ![A screenshot of a Windows Explorer window with a ttrpg-convert.exe file next to a 5etools-mirror-2.github.io folder, and a Powershell window showing the output of the git command to download the sources](docs/screenshots/windows-explorer-powershell-with-sources.png) - + 7. Run the tool to check that it works. Enter `./ttrpg-convert --version` following into the terminal and press Enter to run the command. You should see something like the following: - ``` - PS C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.14-windows-x86_64\bin> .\ttrpg-convert --version - ttrpg-convert version 2.3.14 - Git commit: 6ecb310 - Build time: 2024-05-18T12:36:51Z - ``` + + ```shell + PS C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.14-windows-x86_64\bin> .\ttrpg-convert --version + ttrpg-convert version 2.3.14 + Git commit: 6ecb310 + Build time: 2024-05-18T12:36:51Z + ``` + If this works, then you're good to run the command to generate your notes. Otherwise, look below for troubleshooting instructions + 8. Run the tool to generate your notes. What this looks like depends on what you want the tool to do and is described more in detail elsewhere in the README. For example, to generate notes from the D&D5e SRD into a folder called `dm`, run: - ``` - ./ttrpg-convert --index -o dm 5etools-mirror-2.github.io-master/data - ``` - - You should see output like the following, listing out how many notes of each type were generated, and a new `dm` folder should be in that directory. - ![A screenshot of a Windows Explorer window with a ttrpg-convert.exe file next to a 5etools-mirror-2.github.io folder and a dm folder, and a Powershell window showing the output of the ttrpg-convert command](docs/screenshots/windows-explorer-powershell-after-run.png) + ```shell + ./ttrpg-convert --index -o dm 5etools-mirror-2.github.io-master/data + ``` + + - You should see output like the following, listing out how many notes of each type were generated, and a new `dm` folder should be in that directory. + + ![A screenshot of a Windows Explorer window with a ttrpg-convert.exe file next to a 5etools-mirror-2.github.io folder and a dm folder, and a Powershell window showing the output of the ttrpg-convert command](docs/screenshots/windows-explorer-powershell-after-run.png) + 9. To use additional sources, templates, or books, or for more configuration options, [create a config file][3] and [see the main README][4]. - - - For example, assuming you have a custom configuration located in a file called `dm-sources.json`, you can - use this command to generate notes using that configuration: - ``` - ./ttrpg-convert --index -o dm -c dm-sources.json 5etools-mirror-2.github.io-master/data - ``` + + - For example, assuming you have a custom configuration located in a file called `dm-sources.json`, you can use this command to generate notes using that configuration: + + ```shell + ./ttrpg-convert --index -o dm -c dm-sources.json 5etools-mirror-2.github.io-master/data + ``` [Convert 5eTools JSON data]: https://github.com/ebullient/ttrpg-convert-cli/tree/main?tab=readme-ov-file#convert-5etools-json-data [Convert Pf2eTools JSON data]: https://github.com/ebullient/ttrpg-convert-cli/tree/main?tab=readme-ov-file#convert-pf2etools-json-data @@ -91,33 +98,42 @@ ## Uh oh, something went wrong ### What are the weird characters in the output? + On Windows, the command output will look like this, with weird characters at the start of lines. -``` + +```shell [ Γ£à OK] Finished reading config. ΓÅ▒∩╕Å Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.18-windows-x86_64\ttrpg-convert-cli-2.3.18-windows-x86_64\bin\5etools-mirror-2.github.io\data [ Γ£à OK] Finished reading data. ``` + These are emoji that Windows is having trouble displaying. This doesn't affect the functionality at all, but if you want to see these properly, choose a font with emoji support in the command line, and run the following: -``` + +```shell chcp 65001 ``` You should then start seeing the emoji correctly: -``` + +```shell [ ✅ OK] Finished reading config. ⏱️ Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.18-windows-x86_64\ttrpg-convert-cli-2.3.18-windows-x86_64\bin\5etools-mirror-2.github.io\data [ ✅ OK] Finished reading data. ``` ### 'ttrpg-convert' is not recognized + If you see the following: -``` + +```shell 'ttrpg-convert' is not recognized as an internal or external command, operable program or batch file. ``` + or -``` + +```shell ttrpg-convert : The term 'ttrpg-convert' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:1 @@ -131,7 +147,8 @@ This means that the command line can't find the program. This is usually because the wrong directory, or there's a typo somewhere in the name of the command. Type in `dir` and press **Enter**. You should see output similar to this: -``` + +```shell Directory: C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.18-windows-x86_64\ttrpg-convert-cli-2.3.18-windows-x86_64\bin @@ -148,21 +165,25 @@ file to the wrong directory. Make sure that you're opening the command line in t If there *is* a `ttrpg-convert.exe` in the list, then the next most likely culprit is a typo. Make sure that the command starts with `./ttrpg-convert`. Try copy/pasting this command: -``` + +```shell ./ttrpg-convert --help ``` + If everything is set up correctly, you should see output starting with the following: -``` + +```shell Convert TTRPG JSON data to markdown Usage: ttrpg-convert [-dhlvV] [--index] [-c=] [-g=] -o= [] [...] [COMMAND] ``` ### No output at all + If you don't get any output at all when running the `ttrpg-convert` command, try running `./ttrpg-convert --help`. If you still get no output, like this: -``` +```shell C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.14-windows-x86_64\bin>.\ttrpg-convert --help C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.14-windows-x86_64\bin> diff --git a/README.md b/README.md index c0414a2f7..0f31a152a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ I use [Obsidian](https://obsidian.md) to keep track of my campaign notes. This p > - 🚜 [**Review the changelog**](CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥). > - 🔮 Check out [**Conventions**](#conventions) and [**Recommendations**](#recommendations-for-using-the-cli) +> [!WARNING] +> The 5eTools data repositories have been taken down. This tool will still work to create Obsidian notes for data in this JSON format (homebrew, for example). + ## Using the Command Line This tool works in the command line, which is a text-based way to give instructions to your computer. diff --git a/docs/configuration.md b/docs/configuration.md index cf8af332d..27e8fe309 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -413,10 +413,6 @@ The following configuration options allow you to change how the CLI treats these - Create a shallow clone of the images repository, and set `images.internalRoot` in your configuration file to tell the CLI where it can locally find "internal" images. - ```shell - git clone --depth 1 https://github.com/5etools-mirror-2/5etools-img.git - ``` - ```json "images": { "copyInternal": true, From 09cda6d2775160ce46eadd6ae18898144048e201 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:20:00 +0000 Subject: [PATCH 013/119] Bump github/codeql-action from 3.26.0 to 3.26.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.0 to 3.26.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/eb055d739abdc2e8de2e5f4ba1a8b246daa779aa...429e1977040da7a23b6822b13c129cd1ba93dbb2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 882f7acc7..f740f02bc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 + uses: github/codeql-action/init@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 + uses: github/codeql-action/analyze@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f0308d1b2..1f174132e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 + uses: github/codeql-action/upload-sarif@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 with: sarif_file: results.sarif From 3019c36b8b44bc914f2223c334c8d2c6091ae020 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:11:48 +0000 Subject: [PATCH 014/119] Bump surefire-plugin.version from 3.3.1 to 3.4.0 Bumps `surefire-plugin.version` from 3.3.1 to 3.4.0. Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.3.1...surefire-3.4.0) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.3.1...surefire-3.4.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4b478894b..70513dac5 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 3.8.0 UTF-8 UTF-8 - 3.3.1 + 3.4.0 9.0.1 false true From f03819ba5a9a4e87db4adcff7add7be20b314018 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:38:36 +0000 Subject: [PATCH 015/119] Bump github/codeql-action from 3.26.2 to 3.26.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.2 to 3.26.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/429e1977040da7a23b6822b13c129cd1ba93dbb2...2c779ab0d087cd7fe7b826087247c2c81f27bfa6) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f740f02bc..257bce841 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 + uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 + uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 1f174132e..9ef46c305 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 + uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: sarif_file: results.sarif From 144510734b6290bd46007b70dbfb9f259102d98f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:30:50 +0000 Subject: [PATCH 016/119] Bump quarkus.platform.version from 3.13.2 to 3.13.3 Bumps `quarkus.platform.version` from 3.13.2 to 3.13.3. Updates `io.quarkus.platform:quarkus-bom` from 3.13.2 to 3.13.3 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.13.2...3.13.3) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.13.2 to 3.13.3 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.13.2...3.13.3) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 70513dac5..a76edbab5 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.13.2 + 3.13.3 3.26.3 3.3.0 From e5b9aa2fa1471c610234f5fc222f86a3fce8e54d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:19:13 +0000 Subject: [PATCH 017/119] Bump surefire-plugin.version from 3.4.0 to 3.5.0 Bumps `surefire-plugin.version` from 3.4.0 to 3.5.0. Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.4.0...surefire-3.5.0) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.4.0...surefire-3.5.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a76edbab5..5e51f25bf 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 3.8.0 UTF-8 UTF-8 - 3.4.0 + 3.5.0 9.0.1 false true From db27494d4e0191cb01a296cdb2084d3b6ef3e210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:54:43 +0000 Subject: [PATCH 018/119] Bump actions/upload-artifact from 4.3.6 to 4.4.0 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.6 to 4.4.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/834a144ee995460fba8ed112a2fc961b36a5ec5a...50769540e7f4bd5e21e526ee35c689e35e0d6874) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d42f1f32b..454fe2c04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,14 +85,14 @@ jobs: zip -r target/${ARTIFACT}-${NEXT}-examples.zip docs examples default - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 id: upload-jar with: name: artifacts-runner path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 id: upload-zip with: name: artifacts-examples diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 9ef46c305..8f1a5cc49 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,7 +60,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: SARIF file path: results.sarif From c2e23c9bfcf1500c13b59940db14cf07e533333e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:54:39 +0000 Subject: [PATCH 019/119] Bump github/codeql-action from 3.26.5 to 3.26.6 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.5 to 3.26.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2c779ab0d087cd7fe7b826087247c2c81f27bfa6...4dd16135b69a43b6c8efb853346f8437d92d3c93) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 257bce841..108ff8039 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8f1a5cc49..957b29af3 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: sarif_file: results.sarif From 243bef91d076bd202d88bd02e1d4c3b2d5140b13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:19:21 +0000 Subject: [PATCH 020/119] Bump quarkus.platform.version from 3.13.3 to 3.14.1 Bumps `quarkus.platform.version` from 3.13.3 to 3.14.1. Updates `io.quarkus.platform:quarkus-bom` from 3.13.3 to 3.14.1 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.13.3...3.14.1) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.13.3 to 3.14.1 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.13.3...3.14.1) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5e51f25bf..e16758de4 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.13.3 + 3.14.1 3.26.3 3.3.0 From 8e331636f37ef8b399f7c02d32cc870a0f2df9ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:58:25 -0400 Subject: [PATCH 021/119] Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.8.0 to 3.10.0 (#526) * Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.8.0 to 3.10.0 Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.8.0 to 3.10.0. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.8.0...maven-javadoc-plugin-3.10.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * update parameters --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Erin Schnabel --- pom.xml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index e16758de4..d35e041b1 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 17 2.24.1 1.11.0 - 3.8.0 + 3.10.0 UTF-8 UTF-8 3.5.0 @@ -279,10 +279,13 @@ dev.ebullient.convert,dev.ebullient.convert.config,dev.ebullient.convert.io,dev.ebullient.convert.tools,dev.ebullient.convert.tools.dnd5e,dev.ebullient.convert.tools.pf2e dev.ebullient.convert.io.MarkdownDoclet - ./target/classes - ./ - ./ - -d javadoc/ + ${project.basedir}/target/classes + ${project.basedir} + ${project.basedir} + + -d + javadoc + false From 2b4a932149718831654135c4c4168544498d9404 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 07:12:58 -0400 Subject: [PATCH 022/119] Bump net.revelc.code:impsort-maven-plugin from 1.11.0 to 1.12.0 (#531) Bumps net.revelc.code:impsort-maven-plugin from 1.11.0 to 1.12.0. --- updated-dependencies: - dependency-name: net.revelc.code:impsort-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d35e041b1..0e0edc228 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ 3.13.0 17 2.24.1 - 1.11.0 + 1.12.0 3.10.0 UTF-8 UTF-8 From b578dfb658759d815150359a92c20f523f726b24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 07:13:10 -0400 Subject: [PATCH 023/119] Bump quarkus.platform.version from 3.14.1 to 3.14.2 (#530) Bumps `quarkus.platform.version` from 3.14.1 to 3.14.2. Updates `io.quarkus.platform:quarkus-bom` from 3.14.1 to 3.14.2 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.14.1...3.14.2) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.14.1 to 3.14.2 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.14.1...3.14.2) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0e0edc228..def3ab330 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.14.1 + 3.14.2 3.26.3 3.3.0 From 793e8b2f358850b86dcfddc0a305b2dac34adca0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:57:50 -0400 Subject: [PATCH 024/119] Bump github/codeql-action from 3.26.6 to 3.26.7 (#535) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.6 to 3.26.7. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4dd16135b69a43b6c8efb853346f8437d92d3c93...8214744c546c1e5c8f03dde8fab3a7353211988d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 108ff8039..0ab272469 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 957b29af3..60a31b460 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: sarif_file: results.sarif From 856f856e2266928b1d6c21760f42e8eda75d5c4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:48:19 +0000 Subject: [PATCH 025/119] Bump actions/setup-java from 4.2.2 to 4.3.0 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.2.2 to 4.3.0. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/6a0805fcefea3d4657a47ac4c165951e33482018...2dfa2011c5b2a0f1489bf9e433881c92c1631f88) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0ab272469..562dc1384 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 + uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index cba8cc8cb..9a3b5ea02 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -83,7 +83,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 + uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 89e2d7fe0..2ed46af23 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -51,7 +51,7 @@ jobs: Data-Pf2eTools - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 + uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 454fe2c04..c50146fb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 + uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 5fe7420d9..21950a290 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -92,7 +92,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 + uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} From c7d480a02ec9e3a762cd38e1ddea5a9f5955ca8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:26:18 +0000 Subject: [PATCH 026/119] Bump quarkus.platform.version from 3.14.2 to 3.14.4 Bumps `quarkus.platform.version` from 3.14.2 to 3.14.4. Updates `io.quarkus.platform:quarkus-bom` from 3.14.2 to 3.14.4 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.14.2...3.14.4) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.14.2 to 3.14.4 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.14.2...3.14.4) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index def3ab330..20f17ed69 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.14.2 + 3.14.4 3.26.3 3.3.0 From 7c3b7ab4879163e67593a40eb820cda5ec7df626 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:38:55 +0000 Subject: [PATCH 027/119] Bump github/codeql-action from 3.26.7 to 3.26.8 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.7 to 3.26.8. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8214744c546c1e5c8f03dde8fab3a7353211988d...294a9d92911152fe08befb9ec03e240add280cb3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 562dc1384..7fee4cc25 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 60a31b460..a727693b2 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: sarif_file: results.sarif From aba81b91bd8f4c65107728011ed3317fa72e7f76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:59:45 +0000 Subject: [PATCH 028/119] Bump actions/setup-java from 4.3.0 to 4.4.0 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/2dfa2011c5b2a0f1489bf9e433881c92c1631f88...b36c23c0d998641eff861008f374ee103c25ac73) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7fee4cc25..5349dbd9c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 9a3b5ea02..33c9288cd 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -83,7 +83,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2ed46af23..24488f920 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -51,7 +51,7 @@ jobs: Data-Pf2eTools - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c50146fb5..1a5065058 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 21950a290..70a1fe357 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -92,7 +92,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} From 3b0a7e793575e138ffb67452718d03c1c99a7c25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:59:49 +0000 Subject: [PATCH 029/119] Bump actions/checkout from 4.1.7 to 4.2.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/692973e3d937129bcbf40652eb9f2f61becf3332...d632683dd7b4114ad314bca15554477dd762a938) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 8 ++++---- .github/workflows/pull-request.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/tools-data.yml | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5349dbd9c..51fc16e82 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 33c9288cd..ac3f12bf8 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -24,7 +24,7 @@ jobs: cache_key: ${{ steps.test-data-key.outputs.cache_key }} steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Pf2e Tools release cache key id: test-data-key @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache @@ -107,7 +107,7 @@ jobs: steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache @@ -153,7 +153,7 @@ jobs: needs: [test-with-data, native-test-with-data] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - id: gh-issue env: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 24488f920..2e1d5c9a4 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,7 +30,7 @@ jobs: needs: [metadata] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: tools5e-cache @@ -75,7 +75,7 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: tools5e-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a5065058..11f22dd4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: contents: write actions: write steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 # Fetches all tags for the repo - name: Fetch tags diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a727693b2..87aaa9c53 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -33,7 +33,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: persist-credentials: false diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 70a1fe357..f4730e98d 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -22,7 +22,7 @@ jobs: cache_key: ${{ steps.test-data-key.outputs.cache_key }} steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 1 @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 1 @@ -117,7 +117,7 @@ jobs: steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 1 @@ -161,7 +161,7 @@ jobs: needs: [test-with-data, native-test-with-data] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 1 From aa239f4a11122ea68ca46ad8bfc34d734a8a4928 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:43:21 +0000 Subject: [PATCH 030/119] Bump quarkus.platform.version from 3.14.4 to 3.15.1 Bumps `quarkus.platform.version` from 3.14.4 to 3.15.1. Updates `io.quarkus.platform:quarkus-bom` from 3.14.4 to 3.15.1 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.14.4...3.15.1) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.14.4 to 3.15.1 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.14.4...3.15.1) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 20f17ed69..cc7735abf 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.14.4 + 3.15.1 3.26.3 3.3.0 From ab92130514f10ad174fe281bc5bedd8820cee561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:59:41 +0000 Subject: [PATCH 031/119] Bump github/codeql-action from 3.26.8 to 3.26.9 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.8 to 3.26.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/294a9d92911152fe08befb9ec03e240add280cb3...461ef6c76dfe95d5c364de2f431ddbd31a417628) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 51fc16e82..d5ee6551a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 87aaa9c53..100439c23 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: results.sarif From b59a90c42dd8a82188cb09a13798355eff0318ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:41:22 +0000 Subject: [PATCH 032/119] Bump surefire-plugin.version from 3.5.0 to 3.5.1 Bumps `surefire-plugin.version` from 3.5.0 to 3.5.1. Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.0...surefire-3.5.1) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.0...surefire-3.5.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cc7735abf..d35b4d0b3 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 3.10.0 UTF-8 UTF-8 - 3.5.0 + 3.5.1 9.0.1 false true From 7727b37c46e13ab38d0d29b209c1ddaec032c7e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:41:16 +0000 Subject: [PATCH 033/119] Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.10.0 to 3.10.1 Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.10.0 to 3.10.1. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.10.0...maven-javadoc-plugin-3.10.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d35b4d0b3..17f451e6e 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 17 2.24.1 1.12.0 - 3.10.0 + 3.10.1 UTF-8 UTF-8 3.5.1 From 3bb83ab1e612d711c0cfaae3c70e630f54782748 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:24:54 +0000 Subject: [PATCH 034/119] Bump github/codeql-action from 3.26.9 to 3.26.11 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.9 to 3.26.11. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/461ef6c76dfe95d5c364de2f431ddbd31a417628...6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d5ee6551a..e6df4be4e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 100439c23..2f17fcf64 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: sarif_file: results.sarif From a7259307742843e8312ec05ba0000dac00c16f49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:24:58 +0000 Subject: [PATCH 035/119] Bump actions/cache from 4.0.2 to 4.1.0 Bumps [actions/cache](https://github.com/actions/cache) from 4.0.2 to 4.1.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/0c45773b623bea8c8e75f6c82b208c3cf94ea4f9...2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 6 +++--- .github/workflows/pull-request.yml | 8 ++++---- .github/workflows/tools-data.yml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index ac3f12bf8..50eb60fff 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -39,7 +39,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: sources/Pf2eTools key: ${{ steps.test-data-key.outputs.cache_key }} @@ -75,7 +75,7 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: cache with: path: sources/Pf2eTools @@ -109,7 +109,7 @@ jobs: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2e1d5c9a4..e617c553d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: tools5e-cache with: path: sources @@ -41,7 +41,7 @@ jobs: Data-5etools- Data-5etools - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: pf2e-cache with: path: sources/Pf2eTools @@ -77,7 +77,7 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: tools5e-cache with: path: sources @@ -87,7 +87,7 @@ jobs: Data-5etools enableCrossOsArchive: true - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index f4730e98d..91563bab7 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -37,7 +37,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 with: path: sources key: ${{ steps.test-data-key.outputs.cache_key }} @@ -84,7 +84,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: cache with: path: sources @@ -121,7 +121,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 id: cache with: path: sources From 21c85be3ffcb21112f6f54e14f6661aaf57f1edf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:53:05 +0000 Subject: [PATCH 036/119] Bump actions/upload-artifact from 4.4.0 to 4.4.3 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.0 to 4.4.3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/50769540e7f4bd5e21e526ee35c689e35e0d6874...b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11f22dd4b..867591b99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,14 +85,14 @@ jobs: zip -r target/${ARTIFACT}-${NEXT}-examples.zip docs examples default - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 id: upload-jar with: name: artifacts-runner path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 id: upload-zip with: name: artifacts-examples diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2f17fcf64..6a3a5e1f7 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,7 +60,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: SARIF file path: results.sarif From fc143364060485759d96278bf41d94bd0e412666 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:53:03 +0000 Subject: [PATCH 037/119] Bump actions/cache from 4.1.0 to 4.1.1 Bumps [actions/cache](https://github.com/actions/cache) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2...3624ceb22c1c5a301c8db4169662070a689d9ea8) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 6 +++--- .github/workflows/pull-request.yml | 8 ++++---- .github/workflows/tools-data.yml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 50eb60fff..855c9ff74 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -39,7 +39,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 with: path: sources/Pf2eTools key: ${{ steps.test-data-key.outputs.cache_key }} @@ -75,7 +75,7 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache with: path: sources/Pf2eTools @@ -109,7 +109,7 @@ jobs: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e617c553d..6ea32ff8b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: tools5e-cache with: path: sources @@ -41,7 +41,7 @@ jobs: Data-5etools- Data-5etools - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: pf2e-cache with: path: sources/Pf2eTools @@ -77,7 +77,7 @@ jobs: steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: tools5e-cache with: path: sources @@ -87,7 +87,7 @@ jobs: Data-5etools enableCrossOsArchive: true - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 91563bab7..17e8a25b3 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -37,7 +37,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 with: path: sources key: ${{ steps.test-data-key.outputs.cache_key }} @@ -84,7 +84,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache with: path: sources @@ -121,7 +121,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4.1.0 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache with: path: sources From 06f39e9a6d2bad2e42482370a68d08d28dd34ab9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:49:09 +0000 Subject: [PATCH 038/119] Bump com.ezylang:EvalEx from 3.3.0 to 3.4.0 Bumps [com.ezylang:EvalEx](https://github.com/ezylang/EvalEx) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/ezylang/EvalEx/releases) - [Commits](https://github.com/ezylang/EvalEx/compare/3.3.0...3.4.0) --- updated-dependencies: - dependency-name: com.ezylang:EvalEx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 17f451e6e..2d4e97cbf 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 3.15.1 3.26.3 - 3.3.0 + 3.4.0 3.0.7 75.1 4.36.0 From 1be1e90213a57e8d2af38be790d8eb1793d54296 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:52:59 +0000 Subject: [PATCH 039/119] Bump github/codeql-action from 3.26.11 to 3.26.12 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.11 to 3.26.12. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea...c36620d31ac7c881962c3d9dd939c40ec9434f2b) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e6df4be4e..f73cb8034 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 6a3a5e1f7..7fef204f9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: sarif_file: results.sarif From d46bf3918deae642fd613d2f2a20f75e175f7de6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:03:33 +0000 Subject: [PATCH 040/119] Bump actions/checkout from 4.2.0 to 4.2.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.0 to 4.2.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/d632683dd7b4114ad314bca15554477dd762a938...eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 8 ++++---- .github/workflows/pull-request.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/tools-data.yml | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f73cb8034..d9ff3a3e0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 855c9ff74..8018283ef 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -24,7 +24,7 @@ jobs: cache_key: ${{ steps.test-data-key.outputs.cache_key }} steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Pf2e Tools release cache key id: test-data-key @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache @@ -107,7 +107,7 @@ jobs: steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache @@ -153,7 +153,7 @@ jobs: needs: [test-with-data, native-test-with-data] steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - id: gh-issue env: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6ea32ff8b..b37c5bb34 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,7 +30,7 @@ jobs: needs: [metadata] steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: tools5e-cache @@ -75,7 +75,7 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: tools5e-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 867591b99..9862eacd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: contents: write actions: write steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 # Fetches all tags for the repo - name: Fetch tags diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 7fef204f9..a81287350 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -33,7 +33,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 17e8a25b3..95cc6861f 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -22,7 +22,7 @@ jobs: cache_key: ${{ steps.test-data-key.outputs.cache_key }} steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 1 @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 1 @@ -117,7 +117,7 @@ jobs: steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 1 @@ -161,7 +161,7 @@ jobs: needs: [test-with-data, native-test-with-data] steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 1 From cd31fe88bbfe6ac1b6a6eec6512d1ec071db4936 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:45:59 +0000 Subject: [PATCH 041/119] Bump graalvm/setup-graalvm from 1.2.3 to 1.2.4 Bumps [graalvm/setup-graalvm](https://github.com/graalvm/setup-graalvm) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/graalvm/setup-graalvm/releases) - [Commits](https://github.com/graalvm/setup-graalvm/compare/22cc13fe88ef133134b3798e128fb208df55e1f5...6f327093bb6a42fe5eac053d21b168c46aa46f22) --- updated-dependencies: - dependency-name: graalvm/setup-graalvm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 8018283ef..a19027d0d 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -117,7 +117,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@22cc13fe88ef133134b3798e128fb208df55e1f5 # v1.2.3 + - uses: graalvm/setup-graalvm@6f327093bb6a42fe5eac053d21b168c46aa46f22 # v1.2.4 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b37c5bb34..752676627 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -97,7 +97,7 @@ jobs: Data-Pf2eTools enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@22cc13fe88ef133134b3798e128fb208df55e1f5 # v1.2.3 + - uses: graalvm/setup-graalvm@6f327093bb6a42fe5eac053d21b168c46aa46f22 # v1.2.4 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 95cc6861f..2315f3b8a 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -129,7 +129,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@22cc13fe88ef133134b3798e128fb208df55e1f5 # v1.2.3 + - uses: graalvm/setup-graalvm@6f327093bb6a42fe5eac053d21b168c46aa46f22 # v1.2.4 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} From 985ad051058f21672ecea09bdfa33cbfe3cc1422 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:40:07 +0000 Subject: [PATCH 042/119] Bump com.ibm.icu:icu4j from 75.1 to 76.1 Bumps [com.ibm.icu:icu4j](https://github.com/unicode-org/icu) from 75.1 to 76.1. - [Release notes](https://github.com/unicode-org/icu/releases) - [Commits](https://github.com/unicode-org/icu/commits) --- updated-dependencies: - dependency-name: com.ibm.icu:icu4j dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2d4e97cbf..494a74121 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 3.26.3 3.4.0 3.0.7 - 75.1 + 76.1 4.36.0 uber-jar From 314b93d21474b105ca6e7e2653648757083b9100 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:56:27 +0000 Subject: [PATCH 043/119] Bump actions/setup-java from 4.4.0 to 4.5.0 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/b36c23c0d998641eff861008f374ee103c25ac73...8df1039502a15bceb9433410b1a100fbe190c53b) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d9ff3a3e0..1a0c26429 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index a19027d0d..03742dd16 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -83,7 +83,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 752676627..b4e05137f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -51,7 +51,7 @@ jobs: Data-Pf2eTools - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9862eacd6..f54d8ff26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 2315f3b8a..5c627b91e 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -92,7 +92,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} From 562203cbc0f0de7d549e36c61964a9d30e016d84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:56:31 +0000 Subject: [PATCH 044/119] Bump actions/checkout from 4.2.1 to 4.2.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.1 to 4.2.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871...11bd71901bbe5b1630ceea73d27597364c9af683) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 8 ++++---- .github/workflows/pull-request.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/tools-data.yml | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1a0c26429..9be356599 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 03742dd16..09c9613e4 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -24,7 +24,7 @@ jobs: cache_key: ${{ steps.test-data-key.outputs.cache_key }} steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Pf2e Tools release cache key id: test-data-key @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache @@ -107,7 +107,7 @@ jobs: steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: cache @@ -153,7 +153,7 @@ jobs: needs: [test-with-data, native-test-with-data] steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - id: gh-issue env: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b4e05137f..4cc909330 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,7 +30,7 @@ jobs: needs: [metadata] steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: tools5e-cache @@ -75,7 +75,7 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 id: tools5e-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f54d8ff26..8209a0a72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: contents: write actions: write steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Fetches all tags for the repo - name: Fetch tags diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a81287350..df2d58347 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -33,7 +33,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 5c627b91e..df37af276 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -22,7 +22,7 @@ jobs: cache_key: ${{ steps.test-data-key.outputs.cache_key }} steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -117,7 +117,7 @@ jobs: steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 @@ -161,7 +161,7 @@ jobs: needs: [test-with-data, native-test-with-data] steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 From 99be17e2fd6b11f7347463ab8072904dff642264 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:56:34 +0000 Subject: [PATCH 045/119] Bump actions/cache from 4.1.1 to 4.1.2 Bumps [actions/cache](https://github.com/actions/cache) from 4.1.1 to 4.1.2. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/3624ceb22c1c5a301c8db4169662070a689d9ea8...6849a6489940f00c2f30c0fb92c6274307ccb58a) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 6 +++--- .github/workflows/pull-request.yml | 8 ++++---- .github/workflows/tools-data.yml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 09c9613e4..1453fb9eb 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -39,7 +39,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: sources/Pf2eTools key: ${{ steps.test-data-key.outputs.cache_key }} @@ -75,7 +75,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: sources/Pf2eTools @@ -109,7 +109,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4cc909330..2c83107cf 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: tools5e-cache with: path: sources @@ -41,7 +41,7 @@ jobs: Data-5etools- Data-5etools - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: pf2e-cache with: path: sources/Pf2eTools @@ -77,7 +77,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: tools5e-cache with: path: sources @@ -87,7 +87,7 @@ jobs: Data-5etools enableCrossOsArchive: true - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index df37af276..b8067a371 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -37,7 +37,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: sources key: ${{ steps.test-data-key.outputs.cache_key }} @@ -84,7 +84,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: sources @@ -121,7 +121,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 id: cache with: path: sources From b76512993dbe8ebb637ef1337ea1d706bf8da20b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:56:40 +0000 Subject: [PATCH 046/119] Bump github/codeql-action from 3.26.12 to 3.27.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.12 to 3.27.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/c36620d31ac7c881962c3d9dd939c40ec9434f2b...662472033e021d55d94146f66f6058822b0b39fd) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9be356599..351f899ea 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index df2d58347..2ae4bb019 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 with: sarif_file: results.sarif From c5f984e69651ec028ceb6774cb1ee50388185de2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:15:00 +0000 Subject: [PATCH 047/119] Bump graalvm/setup-graalvm from 1.2.4 to 1.2.5 Bumps [graalvm/setup-graalvm](https://github.com/graalvm/setup-graalvm) from 1.2.4 to 1.2.5. - [Release notes](https://github.com/graalvm/setup-graalvm/releases) - [Commits](https://github.com/graalvm/setup-graalvm/compare/6f327093bb6a42fe5eac053d21b168c46aa46f22...557ffcf459751b4d92319ee255bf3bec9b73964c) --- updated-dependencies: - dependency-name: graalvm/setup-graalvm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 1453fb9eb..15f5089f8 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -117,7 +117,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@6f327093bb6a42fe5eac053d21b168c46aa46f22 # v1.2.4 + - uses: graalvm/setup-graalvm@557ffcf459751b4d92319ee255bf3bec9b73964c # v1.2.5 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2c83107cf..76ee6ff99 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -97,7 +97,7 @@ jobs: Data-Pf2eTools enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@6f327093bb6a42fe5eac053d21b168c46aa46f22 # v1.2.4 + - uses: graalvm/setup-graalvm@557ffcf459751b4d92319ee255bf3bec9b73964c # v1.2.5 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index b8067a371..f8842d105 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -129,7 +129,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@6f327093bb6a42fe5eac053d21b168c46aa46f22 # v1.2.4 + - uses: graalvm/setup-graalvm@557ffcf459751b4d92319ee255bf3bec9b73964c # v1.2.5 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} From cba65d6178d28988f6f9925de6b316a0ab8cc55e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:25:47 +0000 Subject: [PATCH 048/119] Bump quarkus.platform.version from 3.15.1 to 3.16.1 Bumps `quarkus.platform.version` from 3.15.1 to 3.16.1. Updates `io.quarkus.platform:quarkus-bom` from 3.15.1 to 3.16.1 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.15.1...3.16.1) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.15.1 to 3.16.1 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.15.1...3.16.1) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 494a74121..320e94abf 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.15.1 + 3.16.1 3.26.3 3.4.0 From ef3763f496a033575fdc8542b8ad0a0a2dcb2c4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:25:40 +0000 Subject: [PATCH 049/119] Bump surefire-plugin.version from 3.5.1 to 3.5.2 Bumps `surefire-plugin.version` from 3.5.1 to 3.5.2. Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.1 to 3.5.2 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.2) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.1 to 3.5.2 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 320e94abf..63d56b9b0 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 3.10.1 UTF-8 UTF-8 - 3.5.1 + 3.5.2 9.0.1 false true From e3da0dae256861bb41896eac11de06ff61d90e0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:23:56 +0000 Subject: [PATCH 050/119] Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.10.1 to 3.11.1 Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.10.1 to 3.11.1. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.10.1...maven-javadoc-plugin-3.11.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 63d56b9b0..cdb2a4115 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 17 2.24.1 1.12.0 - 3.10.1 + 3.11.1 UTF-8 UTF-8 3.5.2 From 7a2b86ef2aa09cd3c2d2fd929682c01e5a528583 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:23:50 +0000 Subject: [PATCH 051/119] Bump quarkus.platform.version from 3.16.1 to 3.16.2 Bumps `quarkus.platform.version` from 3.16.1 to 3.16.2. Updates `io.quarkus.platform:quarkus-bom` from 3.16.1 to 3.16.2 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.16.1...3.16.2) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.16.1 to 3.16.2 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.16.1...3.16.2) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cdb2a4115..a90247765 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.16.1 + 3.16.2 3.26.3 3.4.0 From 233c74b4675852694eba69f9e69bf81ed8becbef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:17:54 +0000 Subject: [PATCH 052/119] Bump github/codeql-action from 3.27.0 to 3.27.1 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.0 to 3.27.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/662472033e021d55d94146f66f6058822b0b39fd...4f3212b61783c3c68e8309a0f18a699764811cda) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 351f899ea..e84f4fbda 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2ae4bb019..3313a2c37 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 with: sarif_file: results.sarif From 670f74905c77139b3a90e37532569ec0193ff756 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:56:44 +0000 Subject: [PATCH 053/119] Bump github/codeql-action from 3.27.1 to 3.27.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.1 to 3.27.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4f3212b61783c3c68e8309a0f18a699764811cda...f09c1c0a94de965c15400f5634aa42fac8fb8f88) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e84f4fbda..9806746b4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3313a2c37..47889ba86 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: sarif_file: results.sarif From 6ef987667a0145c510aa4d3ed48e7026faa69685 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:21:26 +0000 Subject: [PATCH 054/119] Bump com.github.victools:jsonschema-generator from 4.36.0 to 4.37.0 Bumps [com.github.victools:jsonschema-generator](https://github.com/victools/jsonschema-generator) from 4.36.0 to 4.37.0. - [Release notes](https://github.com/victools/jsonschema-generator/releases) - [Changelog](https://github.com/victools/jsonschema-generator/blob/main/CHANGELOG.md) - [Commits](https://github.com/victools/jsonschema-generator/compare/v4.36.0...v4.37.0) --- updated-dependencies: - dependency-name: com.github.victools:jsonschema-generator dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a90247765..47046c4c4 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.4.0 3.0.7 76.1 - 4.36.0 + 4.37.0 uber-jar ttrpg-convert From 14ef741af9f98ea9a1270dbd0ff8e053bbc6e09c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:02:38 +0000 Subject: [PATCH 055/119] Bump graalvm/setup-graalvm from 1.2.5 to 1.2.6 Bumps [graalvm/setup-graalvm](https://github.com/graalvm/setup-graalvm) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/graalvm/setup-graalvm/releases) - [Commits](https://github.com/graalvm/setup-graalvm/compare/557ffcf459751b4d92319ee255bf3bec9b73964c...4a200f28cd70d1940b5e33bd00830b7dc71a7e2b) --- updated-dependencies: - dependency-name: graalvm/setup-graalvm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 15f5089f8..a7509a2fa 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -117,7 +117,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@557ffcf459751b4d92319ee255bf3bec9b73964c # v1.2.5 + - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 76ee6ff99..e947045ec 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -97,7 +97,7 @@ jobs: Data-Pf2eTools enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@557ffcf459751b4d92319ee255bf3bec9b73964c # v1.2.5 + - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index f8842d105..7880e18c3 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -129,7 +129,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@557ffcf459751b4d92319ee255bf3bec9b73964c # v1.2.5 + - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} From f9afab244358751ad961438c662c44673e231568 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Sat, 28 Sep 2024 15:03:57 -0400 Subject: [PATCH 056/119] =?UTF-8?q?=F0=9F=94=A5=F0=9F=A4=AF=20Support=2020?= =?UTF-8?q?24=20rule=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ remove tests for `-s`; group 5e-specific resources 📝🐛 fix template documentation rendering - Some types and fields were missing and/or not being rendered/linked properly. - Lots of fixes to more directly/obviously use markdown in comments. ♻️ Clean up user configuration 🔧 🔥 group "sources" in config 🩹 remove date from version ✨ New AC/HP strings ✨ 🐛 New attack strings ✨ coerce booleans from string values 🐛 ✨ Additional source fields ✨ 🐛 updated basicRules / SRD source flags 🐛 detail string for spellcasting focus 🐛 ✨ handle classic edition magic variants 🎨 fully specified index types for reprints 🐛 srd/freeRules tests; fix variant rule links 🎲🐛🎬 dice rendering 🩹 string utility for uppercase 🐛 Pf2e: replace text in names of resistances 🚧 Item types/properties for 2024 rules 🚧 Revisit deities, variants; add bastions 🔊🔥 change log output; remove writeTablesAndNotes 🧪 test output + log files 🔎 fix optional features / optional feature types --- .gitignore | 2 - .markdownlint.yaml | 3 + CHANGELOG.md | 37 +- README-WINDOWS.md | 28 +- README.md | 230 ++- docs/configuration.md | 174 ++- docs/sourceMap.md | 506 +++---- docs/templates/ImageRef.md | 16 +- docs/templates/QuteBase.md | 9 +- docs/templates/QuteNote.md | 12 +- docs/templates/README.md | 9 +- docs/templates/Reprinted.md | 16 + docs/templates/SourceAndPage.md | 3 +- docs/templates/TtrpgTemplateExtension.md | 37 + docs/templates/dnd5e/AbilityScores.md | 7 +- docs/templates/dnd5e/AcHp.md | 11 +- docs/templates/dnd5e/ImmuneResist.md | 4 +- docs/templates/dnd5e/QuteBackground.md | 6 +- docs/templates/dnd5e/QuteBastion/Hireling.md | 28 + docs/templates/dnd5e/QuteBastion/README.md | 84 ++ docs/templates/dnd5e/QuteBastion/Space.md | 31 + docs/templates/dnd5e/QuteClass.md | 6 +- docs/templates/dnd5e/QuteDeck/README.md | 6 +- docs/templates/dnd5e/QuteDeity.md | 6 +- docs/templates/dnd5e/QuteFeat.md | 6 +- docs/templates/dnd5e/QuteHazard.md | 6 +- docs/templates/dnd5e/QuteItem/README.md | 32 +- docs/templates/dnd5e/QuteItem/Variant.md | 114 +- docs/templates/dnd5e/QuteMonster/README.md | 31 +- .../dnd5e/QuteMonster/SavesAndSkills.md | 12 +- .../dnd5e/QuteMonster/Spellcasting.md | 33 +- docs/templates/dnd5e/QuteObject.md | 21 +- docs/templates/dnd5e/QutePsionic.md | 6 +- docs/templates/dnd5e/QuteRace.md | 6 +- docs/templates/dnd5e/QuteReward.md | 6 +- docs/templates/dnd5e/QuteSpell.md | 6 +- docs/templates/dnd5e/QuteSubclass.md | 6 +- docs/templates/dnd5e/QuteVehicle/README.md | 16 +- docs/templates/dnd5e/QuteVehicle/ShipAcHp.md | 11 +- .../dnd5e/QuteVehicle/ShipCrewCargoPace.md | 18 +- .../dnd5e/QuteVehicle/ShipSection.md | 4 +- docs/templates/dnd5e/README.md | 39 +- docs/templates/dnd5e/Tools5eQuteBase.md | 12 +- docs/templates/dnd5e/Tools5eQuteNote.md | 9 +- docs/templates/pf2e/Pf2eQuteBase.md | 12 +- docs/templates/pf2e/Pf2eQuteNote.md | 12 +- docs/templates/pf2e/QuteAbility.md | 33 +- .../templates/pf2e/QuteAbilityOrAffliction.md | 21 + docs/templates/pf2e/QuteAction/ActionType.md | 4 +- docs/templates/pf2e/QuteAction/README.md | 9 +- .../pf2e/QuteAffliction/QuteAfflictionSave.md | 8 +- .../QuteAffliction/QuteAfflictionStage.md | 8 +- .../README.md} | 16 +- docs/templates/pf2e/QuteArchetype.md | 6 +- docs/templates/pf2e/QuteBackground.md | 6 +- docs/templates/pf2e/QuteBook/BookInfo.md | 4 +- docs/templates/pf2e/QuteBook/README.md | 6 +- docs/templates/pf2e/QuteCreature.md | 100 -- .../pf2e/QuteCreature/CreatureAbilities.md | 13 +- .../pf2e/QuteCreature/CreatureLanguages.md | 11 +- .../QuteCreature/CreatureRitualCasting.md | 8 +- .../pf2e/QuteCreature/CreatureSense.md | 10 +- .../pf2e/QuteCreature/CreatureSkills.md | 15 +- .../QuteCreature/CreatureSpellReference.md | 15 +- .../pf2e/QuteCreature/CreatureSpellcasting.md | 44 +- .../pf2e/QuteCreature/CreatureSpells.md | 22 +- docs/templates/pf2e/QuteCreature/README.md | 13 +- docs/templates/pf2e/QuteDataActivity.md | 22 +- docs/templates/pf2e/QuteDataArmorClass.md | 14 +- docs/templates/pf2e/QuteDataDefenses.md | 39 - .../pf2e/QuteDataDefenses/QuteSavingThrows.md | 15 +- .../templates/pf2e/QuteDataDefenses/README.md | 36 +- docs/templates/pf2e/QuteDataDuration.md | 16 + docs/templates/pf2e/QuteDataFrequency.md | 30 +- .../QuteDataGenericStat/QuteDataNamedBonus.md | 16 +- .../pf2e/QuteDataGenericStat/README.md | 5 + .../pf2e/QuteDataGenericStat/SimpleStat.md | 5 +- docs/templates/pf2e/QuteDataHpHardness.md | 25 - docs/templates/pf2e/QuteDataHpHardnessBt.md | 26 - .../pf2e/QuteDataHpHardnessBt/HpStat.md | 11 +- .../pf2e/QuteDataHpHardnessBt/README.md | 39 + docs/templates/pf2e/QuteDataRange/README.md | 9 +- docs/templates/pf2e/QuteDataSkillBonus.md | 26 - docs/templates/pf2e/QuteDataSpeed.md | 20 +- .../pf2e/QuteDataTimedDuration/README.md | 28 +- .../pf2e/QuteDeity/QuteDeityCleric.md | 4 +- .../pf2e/QuteDeity/QuteDivineAvatar.md | 4 +- .../pf2e/QuteDeity/QuteDivineAvatarAbility.md | 15 - .../pf2e/QuteDeity/QuteDivineAvatarAction.md | 30 - .../pf2e/QuteDeity/QuteDivineIntercession.md | 4 +- docs/templates/pf2e/QuteDeity/README.md | 13 +- docs/templates/pf2e/QuteFeat.md | 22 +- docs/templates/pf2e/QuteHazard.md | 93 -- .../pf2e/QuteHazard/QuteHazardAttributes.md | 26 - .../pf2e/QuteHazard/QuteHazardStealth.md | 14 +- docs/templates/pf2e/QuteHazard/README.md | 42 +- .../QuteAfflictionStage.md | 16 - .../pf2e/QuteInlineAffliction/README.md | 70 - docs/templates/pf2e/QuteInlineAttack.md | 59 - .../templates/pf2e/QuteInlineAttack/README.md | 10 +- .../pf2e/QuteItem/QuteItemActivate.md | 7 +- .../pf2e/QuteItem/QuteItemArmorData.md | 4 +- .../pf2e/QuteItem/QuteItemShieldData.md | 9 +- .../pf2e/QuteItem/QuteItemVariant.md | 30 +- .../pf2e/QuteItem/QuteItemWeaponData.md | 30 +- docs/templates/pf2e/QuteItem/README.md | 6 +- .../pf2e/QuteRitual/QuteRitualCasting.md | 7 +- .../pf2e/QuteRitual/QuteRitualChecks.md | 4 +- docs/templates/pf2e/QuteRitual/README.md | 6 +- docs/templates/pf2e/QuteSpell/QuteSpellAmp.md | 3 +- .../pf2e/QuteSpell/QuteSpellCasting.md | 30 - .../pf2e/QuteSpell/QuteSpellDuration.md | 17 +- .../templates/pf2e/QuteSpell/QuteSpellSave.md | 18 +- .../pf2e/QuteSpell/QuteSpellSaveDuration.md | 22 - .../pf2e/QuteSpell/QuteSpellTarget.md | 5 +- docs/templates/pf2e/QuteSpell/README.md | 26 +- docs/templates/pf2e/QuteTrait.md | 6 +- docs/templates/pf2e/QuteTraitIndex.md | 10 +- docs/templates/pf2e/README.md | 52 +- examples/config/config.5e.json | 30 +- examples/config/config.5e.yaml | 20 +- examples/config/config.pf2e.json | 23 +- examples/config/config.pf2e.yaml | 15 +- examples/config/config.schema.json | 42 + pom.xml | 16 +- .../ebullient/convert/RpgDataConvertCli.java | 65 +- .../dev/ebullient/convert/StringUtil.java | 12 + .../ebullient/convert/VersionProvider.java | 3 +- .../convert/config/CompendiumConfig.java | 214 +-- .../convert/config/ReprintBehavior.java | 7 + .../ebullient/convert/config/TtrpgConfig.java | 156 +- .../ebullient/convert/config/UserConfig.java | 202 +++ .../ebullient/convert/io/JavadocIgnore.java | 12 + .../ebullient/convert/io/JavadocVerbatim.java | 13 + .../ebullient/convert/io/MarkdownDoclet.java | 174 ++- .../ebullient/convert/io/MarkdownWriter.java | 4 +- .../java/dev/ebullient/convert/io/Msg.java | 56 + .../convert/io/NoStackTraceException.java | 26 + .../dev/ebullient/convert/io/Templates.java | 10 +- .../java/dev/ebullient/convert/io/Tui.java | 177 ++- .../dev/ebullient/convert/qute/ImageRef.java | 30 +- .../dev/ebullient/convert/qute/NamedText.java | 5 +- .../dev/ebullient/convert/qute/QuteBase.java | 13 +- .../dev/ebullient/convert/qute/QuteNote.java | 3 +- .../dev/ebullient/convert/qute/QuteUtil.java | 3 + .../dev/ebullient/convert/qute/Reprinted.java | 13 + .../convert/qute/TtrpgTemplateExtension.java | 51 +- .../ebullient/convert/qute/package-info.java | 18 +- .../convert/tools/CompendiumSources.java | 156 +- .../convert/tools/JsonNodeReader.java | 75 +- .../convert/tools/JsonSourceCopier.java | 50 +- .../convert/tools/JsonTextConverter.java | 253 +++- .../convert/tools/MarkdownConverter.java | 4 +- .../ebullient/convert/tools/ParseState.java | 28 + .../dev/ebullient/convert/tools/Tags.java | 16 +- .../ebullient/convert/tools/ToolsIndex.java | 16 + .../convert/tools/dnd5e/ItemMastery.java | 72 + .../convert/tools/dnd5e/ItemProperty.java | 348 ++--- .../convert/tools/dnd5e/ItemType.java | 454 +++--- .../convert/tools/dnd5e/ItemTypeGroup.java | 20 + .../convert/tools/dnd5e/Json2QuteBastion.java | 109 ++ .../convert/tools/dnd5e/Json2QuteClass.java | 15 +- .../convert/tools/dnd5e/Json2QuteCommon.java | 195 ++- .../convert/tools/dnd5e/Json2QuteCompose.java | 52 +- .../convert/tools/dnd5e/Json2QuteDeck.java | 6 +- .../convert/tools/dnd5e/Json2QuteDeity.java | 74 +- .../convert/tools/dnd5e/Json2QuteFeat.java | 9 + .../convert/tools/dnd5e/Json2QuteItem.java | 549 ++++--- .../convert/tools/dnd5e/Json2QuteMonster.java | 84 +- .../dnd5e/Json2QuteOptionalFeatureType.java | 21 +- .../tools/dnd5e/Json2QutePsionicTalent.java | 8 +- .../convert/tools/dnd5e/Json2QuteReward.java | 5 +- .../convert/tools/dnd5e/Json2QuteSpell.java | 10 +- .../convert/tools/dnd5e/Json2QuteVehicle.java | 24 +- .../convert/tools/dnd5e/JsonSource.java | 104 +- .../tools/dnd5e/JsonTextReplacement.java | 344 +++-- .../convert/tools/dnd5e/MagicVariant.java | 234 ++- .../tools/dnd5e/OptionalFeatureIndex.java | 277 ++++ .../tools/dnd5e/Tools5eHomebrewIndex.java | 285 ++++ .../convert/tools/dnd5e/Tools5eIndex.java | 1302 +++++++---------- .../convert/tools/dnd5e/Tools5eIndexType.java | 358 +++-- .../tools/dnd5e/Tools5eJsonSourceCopier.java | 9 +- .../tools/dnd5e/Tools5eMarkdownConverter.java | 36 +- .../convert/tools/dnd5e/Tools5eSources.java | 226 ++- .../tools/dnd5e/qute/AbilityScores.java | 10 +- .../convert/tools/dnd5e/qute/AcHp.java | 3 +- .../tools/dnd5e/qute/ImmuneResist.java | 3 +- .../tools/dnd5e/qute/QuteBackground.java | 3 +- .../convert/tools/dnd5e/qute/QuteBastion.java | 147 ++ .../convert/tools/dnd5e/qute/QuteClass.java | 3 +- .../convert/tools/dnd5e/qute/QuteDeck.java | 3 +- .../convert/tools/dnd5e/qute/QuteDeity.java | 3 +- .../convert/tools/dnd5e/qute/QuteFeat.java | 3 +- .../convert/tools/dnd5e/qute/QuteHazard.java | 3 +- .../convert/tools/dnd5e/qute/QuteItem.java | 250 ++-- .../convert/tools/dnd5e/qute/QuteMonster.java | 32 +- .../convert/tools/dnd5e/qute/QuteObject.java | 3 +- .../convert/tools/dnd5e/qute/QutePsionic.java | 3 +- .../convert/tools/dnd5e/qute/QuteRace.java | 3 +- .../convert/tools/dnd5e/qute/QuteReward.java | 3 +- .../convert/tools/dnd5e/qute/QuteSpell.java | 3 +- .../tools/dnd5e/qute/QuteSubclass.java | 7 +- .../convert/tools/dnd5e/qute/QuteVehicle.java | 31 +- .../tools/dnd5e/qute/Tools5eQuteBase.java | 31 +- .../tools/dnd5e/qute/Tools5eQuteNote.java | 3 +- .../tools/pf2e/Json2QuteAffliction.java | 6 +- .../convert/tools/pf2e/Json2QuteCreature.java | 22 +- .../convert/tools/pf2e/Json2QuteDeity.java | 2 +- .../convert/tools/pf2e/Json2QuteHazard.java | 6 +- .../convert/tools/pf2e/Json2QuteItem.java | 12 +- .../convert/tools/pf2e/JsonSource.java | 5 +- .../tools/pf2e/JsonTextReplacement.java | 16 +- .../convert/tools/pf2e/Pf2eIndex.java | 3 + .../convert/tools/pf2e/Pf2eIndexType.java | 50 +- .../tools/pf2e/Pf2eJsonNodeReader.java | 28 +- .../convert/tools/pf2e/Pf2eMarkdown.java | 16 +- .../convert/tools/pf2e/qute/Pf2eQuteBase.java | 3 +- .../convert/tools/pf2e/qute/Pf2eQuteNote.java | 3 +- .../convert/tools/pf2e/qute/QuteAbility.java | 11 +- .../pf2e/qute/QuteAbilityOrAffliction.java | 2 - .../convert/tools/pf2e/qute/QuteAction.java | 7 +- .../tools/pf2e/qute/QuteAffliction.java | 3 +- .../tools/pf2e/qute/QuteArchetype.java | 3 +- .../tools/pf2e/qute/QuteBackground.java | 3 +- .../convert/tools/pf2e/qute/QuteBook.java | 7 +- .../convert/tools/pf2e/qute/QuteCreature.java | 45 +- .../tools/pf2e/qute/QuteDataArmorClass.java | 11 +- .../tools/pf2e/qute/QuteDataDefenses.java | 33 +- .../tools/pf2e/qute/QuteDataDuration.java | 2 - .../tools/pf2e/qute/QuteDataFrequency.java | 19 +- .../tools/pf2e/qute/QuteDataGenericStat.java | 14 +- .../tools/pf2e/qute/QuteDataHpHardnessBt.java | 25 +- .../tools/pf2e/qute/QuteDataSpeed.java | 14 +- .../pf2e/qute/QuteDataTimedDuration.java | 17 +- .../convert/tools/pf2e/qute/QuteDeity.java | 41 +- .../convert/tools/pf2e/qute/QuteFeat.java | 13 +- .../convert/tools/pf2e/qute/QuteHazard.java | 24 +- .../tools/pf2e/qute/QuteInlineAttack.java | 2 - .../convert/tools/pf2e/qute/QuteItem.java | 71 +- .../convert/tools/pf2e/qute/QuteRitual.java | 7 +- .../convert/tools/pf2e/qute/QuteSpell.java | 36 +- .../convert/tools/pf2e/qute/QuteTrait.java | 3 +- .../tools/pf2e/qute/QuteTraitIndex.java | 6 +- src/main/resources/convertData.json | 664 ++++++--- src/main/resources/sourceMap.json | 490 ------- src/main/resources/sourceMap.yaml | 910 ++++++++++++ .../templates/tools5e/bastion2md.txt | 28 + src/scss/dnd5e-compendium.scss | 10 +- .../ebullient/convert/CustomTemplatesIT.java | 2 +- .../convert/CustomTemplatesTest.java | 26 +- .../ebullient/convert/Pf2eDataConvertIT.java | 2 +- .../convert/Pf2eDataConvertTest.java | 54 +- .../java/dev/ebullient/convert/TestUtils.java | 9 +- .../convert/Tools5eDataConvertIT.java | 2 +- .../convert/Tools5eDataConvertTest.java | 228 +-- .../convert/config/ConfiguratorTest.java | 41 +- .../convert/config/ConfiguratorUtil.java | 5 +- ...onExampleTest.java => ExportDocsTest.java} | 128 +- .../ebullient/convert/qute/ImageRefTest.java | 6 +- .../convert/tools/dnd5e/CommonDataTests.java | 336 +++-- .../tools/dnd5e/FilterAllNewestTest.java | 224 +++ .../convert/tools/dnd5e/FilterAllTest.java | 488 ++++++ .../tools/dnd5e/FilterNoneEditionTest.java | 224 +++ .../convert/tools/dnd5e/FilterNoneTest.java | 224 +++ .../tools/dnd5e/FilterSrd2014Test.java | 222 +++ .../tools/dnd5e/FilterSrd2024Test.java | 222 +++ .../tools/dnd5e/FilterSubset2014Test.java | 222 +++ .../tools/dnd5e/FilterSubset2024Test.java | 221 +++ .../convert/tools/dnd5e/JsonDataNoneTest.java | 117 -- .../tools/dnd5e/JsonDataSubsetTest.java | 142 -- .../convert/tools/dnd5e/JsonDataTest.java | 289 ---- ...egexTest.java => TextReplacementTest.java} | 66 +- .../convert/tools/pf2e/CommonDataTests.java | 28 +- .../tools/pf2e/Pf2eJsonDataNoneTest.java | 11 +- .../tools/pf2e/Pf2eJsonDataSubsetTest.java | 11 +- .../convert/tools/pf2e/Pf2eJsonDataTest.java | 11 +- src/test/resources/5e-sourceTypes.json | 145 ++ src/test/resources/{ => 5e}/ermis-bg.json | 0 .../resources/{ => 5e}/images-from-local.json | 0 .../resources/{ => 5e}/images-remote.json | 0 src/test/resources/{ => 5e}/psion.json | 0 src/test/resources/5e/sources-2014-srd.yaml | 4 + src/test/resources/5e/sources-2024-srd.yaml | 4 + .../{ => 5e}/sources-book-adventure.json | 0 .../resources/{ => 5e}/sources-homebrew.json | 7 +- src/test/resources/5e/sources-images.yaml | 43 + .../resources/{ => 5e}/sources-no-phb.yaml | 0 src/test/resources/5e/sources-single.yaml | 3 + src/test/resources/5e/sources-subset.json | 5 + .../resources/{ => 5e}/sources-templates.json | 0 src/test/resources/{ => 5e}/sources-ua.json | 0 src/test/resources/5e/sources.json | 24 + src/test/resources/sourcemap.txt | 3 + src/test/resources/sources-images.yaml | 12 - src/test/resources/sources.json | 20 - 295 files changed, 11297 insertions(+), 6175 deletions(-) create mode 100644 docs/templates/Reprinted.md create mode 100644 docs/templates/dnd5e/QuteBastion/Hireling.md create mode 100644 docs/templates/dnd5e/QuteBastion/README.md create mode 100644 docs/templates/dnd5e/QuteBastion/Space.md create mode 100644 docs/templates/pf2e/QuteAbilityOrAffliction.md rename docs/templates/pf2e/{QuteAffliction.md => QuteAffliction/README.md} (70%) delete mode 100644 docs/templates/pf2e/QuteCreature.md delete mode 100644 docs/templates/pf2e/QuteDataDefenses.md create mode 100644 docs/templates/pf2e/QuteDataDuration.md create mode 100644 docs/templates/pf2e/QuteDataGenericStat/README.md delete mode 100644 docs/templates/pf2e/QuteDataHpHardness.md delete mode 100644 docs/templates/pf2e/QuteDataHpHardnessBt.md create mode 100644 docs/templates/pf2e/QuteDataHpHardnessBt/README.md delete mode 100644 docs/templates/pf2e/QuteDataSkillBonus.md delete mode 100644 docs/templates/pf2e/QuteDeity/QuteDivineAvatarAbility.md delete mode 100644 docs/templates/pf2e/QuteDeity/QuteDivineAvatarAction.md delete mode 100644 docs/templates/pf2e/QuteHazard.md delete mode 100644 docs/templates/pf2e/QuteHazard/QuteHazardAttributes.md delete mode 100644 docs/templates/pf2e/QuteInlineAffliction/QuteAfflictionStage.md delete mode 100644 docs/templates/pf2e/QuteInlineAffliction/README.md delete mode 100644 docs/templates/pf2e/QuteInlineAttack.md delete mode 100644 docs/templates/pf2e/QuteSpell/QuteSpellCasting.md delete mode 100644 docs/templates/pf2e/QuteSpell/QuteSpellSaveDuration.md create mode 100644 src/main/java/dev/ebullient/convert/config/ReprintBehavior.java create mode 100644 src/main/java/dev/ebullient/convert/config/UserConfig.java create mode 100644 src/main/java/dev/ebullient/convert/io/JavadocIgnore.java create mode 100644 src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java create mode 100644 src/main/java/dev/ebullient/convert/io/Msg.java create mode 100644 src/main/java/dev/ebullient/convert/io/NoStackTraceException.java create mode 100644 src/main/java/dev/ebullient/convert/qute/Reprinted.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTypeGroup.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java delete mode 100644 src/main/resources/sourceMap.json create mode 100644 src/main/resources/sourceMap.yaml create mode 100644 src/main/resources/templates/tools5e/bastion2md.txt rename src/test/java/dev/ebullient/convert/config/{ConfigurationExampleTest.java => ExportDocsTest.java} (50%) create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java delete mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataNoneTest.java delete mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataSubsetTest.java delete mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataTest.java rename src/test/java/dev/ebullient/convert/tools/dnd5e/{RegexTest.java => TextReplacementTest.java} (75%) create mode 100644 src/test/resources/5e-sourceTypes.json rename src/test/resources/{ => 5e}/ermis-bg.json (100%) rename src/test/resources/{ => 5e}/images-from-local.json (100%) rename src/test/resources/{ => 5e}/images-remote.json (100%) rename src/test/resources/{ => 5e}/psion.json (100%) create mode 100644 src/test/resources/5e/sources-2014-srd.yaml create mode 100644 src/test/resources/5e/sources-2024-srd.yaml rename src/test/resources/{ => 5e}/sources-book-adventure.json (100%) rename src/test/resources/{ => 5e}/sources-homebrew.json (94%) create mode 100644 src/test/resources/5e/sources-images.yaml rename src/test/resources/{ => 5e}/sources-no-phb.yaml (100%) create mode 100644 src/test/resources/5e/sources-single.yaml create mode 100644 src/test/resources/5e/sources-subset.json rename src/test/resources/{ => 5e}/sources-templates.json (100%) rename src/test/resources/{ => 5e}/sources-ua.json (100%) create mode 100644 src/test/resources/5e/sources.json delete mode 100644 src/test/resources/sources-images.yaml delete mode 100644 src/test/resources/sources.json diff --git a/.gitignore b/.gitignore index e3926d0ee..57cb6ea63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -5etools-mirror-1.github.io -Pf2eTools sources excludes.json publish diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 6ccb872fb..f7d1cd1e2 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,3 +1,6 @@ MD007: indent: 4 MD013: false +MD033: + allowed_elements: + - blockquote diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aed18d83..567e6f924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,35 @@ > > To run the template: Use 'Templater: Open Insert Template Modal' with an existing note or 'Templater: Create new note from Template' to create a new note, and choose the migration template from the list. +## 🔥✨ 3.0.0: Moving the things + +- The `-s` option is no more. All sources must be specified in config files. +- There have been some changes to how dice strings are rendered. +- `from` and `full-source` have been merged. The [`sources` key](./docs/configuration.md#specify-content-with-the-sources-key) now defines all types of source: `reference` for reference-only, `book`/`adventure` for complete source text, and `homebrew` for homebrew. + + ```json + { + "sources": { + "adventure": [ + ... + ], + "book": [ + ... + ], + "homebrew": [ + ... + ], + "reference": [ + ... + ] + }, + } + ``` + +- The [Source Map](./docs/sourceMap.md) for 5e sources now indicates if the source is a `book` or `adventure`. +- The Players Handbook directory has changed from `players-handbook` to `players-handbook-2014` +- **Reprint behavior** has always been knd of obscure, but it really matters now. The CLI will always default to one-note-per-thing, perferring the most recent version of said thing. This means that the 2024 PHB content will be preferred if you have that source available. Use [`include` and `exclude` configuration](docs/configuration.md#refine-content-choices) to tweak that behavior. + ## 🔖 ✨ 2.3.14: Improvements to Pathfinder rendering @miscoined has made significant contributions to improve support for Pf2e creatures (the bestiary). 🙏🎉💖 @@ -43,13 +72,11 @@ If you are using the Fantasy Statblocks plugin to render your statblocks, you ca ## 🔖 ✨ 2.3.0: 5eTools moving to mirror2 -[5eTools Mirror1](https://github.com/5etools-mirror-1/5etools-mirror-1.github.io) has been deprecated in favor of [5eTools Mirror2](https://github.com/5etools-mirror-2/5etools-mirror-2.github.io), which uses a separate repository for [images](https://github.com/5etools-mirror-2/5etools-img). - -When you run `ttrpg-convert` against mirror2 content, no images will be copied into your vault by default. +The 5eTools Mirror now uses a separate repository for images. When you run `ttrpg-convert` against 5eTools content, images will not be copied into your vault by default. Links to external images will be used instead. This can greatly reduce the size of your vault, but you will need to be connected to the internet for images to render. -See [Copying "internal" images](./docs/configuration.md#copying-internal-images) for options on how to copy tokens and other item/spell/monster fluff images into your notes. +See [Copying "internal" images](./docs/configuration.md#copying-internal-images) for options on how to copy tokens and other item/spell/monster fluff images into your vault. -See [Copying "external" images](./docs/configuration.md#copying-external-images) to copy additional images referenced in general text into your notes. +See [Copying "external" images](./docs/configuration.md#copying-external-images) to copy additional images referenced in general text into your vault. ## 🔖 ✨ 2.2.12: 5e support for generic and magic item variants diff --git a/README-WINDOWS.md b/README-WINDOWS.md index 2836a8114..6e5db622d 100644 --- a/README-WINDOWS.md +++ b/README-WINDOWS.md @@ -40,57 +40,39 @@ ![A screenshot of a Windows Powershell window opened to the ttrpg-convert-cli directory](docs/screenshots/windows-powershell-open.png) -6. Acquire the JSON data sources following the instructions in [Convert 5eTools JSON data][] or [Convert Pf2eTools JSON data][] by entering commands in this window and pressing **Enter**. - - - For example, for Pf2eTools: - - ```shell - git clone --depth 1 https://github.com/Pf2eToolsOrg/Pf2eTools.git - ``` - - - If you don't have Git, you can instead manually download the latest [Pf2eTools release](https://github.com/Pf2eToolsOrg/Pf2eTools/releases/latest) and extract the zip file to the `bin/` directory, so that it sits alongside the `ttrpg-convert.exe` file. - - - At this point, it should look like this: - - ![A screenshot of a Windows Explorer window with a ttrpg-convert.exe file next to a 5etools-mirror-2.github.io folder, and a Powershell window showing the output of the git command to download the sources](docs/screenshots/windows-explorer-powershell-with-sources.png) - -7. Run the tool to check that it works. Enter `./ttrpg-convert --version` following into the terminal and press Enter +6. Run the tool to check that it works. Enter `./ttrpg-convert --version` following into the terminal and press Enter to run the command. You should see something like the following: ```shell PS C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.14-windows-x86_64\bin> .\ttrpg-convert --version ttrpg-convert version 2.3.14 Git commit: 6ecb310 - Build time: 2024-05-18T12:36:51Z ``` If this works, then you're good to run the command to generate your notes. Otherwise, look below for troubleshooting instructions -8. Run the tool to generate your notes. What this looks like depends on what you want the tool to do +7. Run the tool to generate your notes. What this looks like depends on what you want the tool to do and is described more in detail elsewhere in the README. For example, to generate notes from the D&D5e SRD into a folder called `dm`, run: ```shell - ./ttrpg-convert --index -o dm 5etools-mirror-2.github.io-master/data + ./ttrpg-convert --index -o dm ``` - You should see output like the following, listing out how many notes of each type were generated, and a new `dm` folder should be in that directory. ![A screenshot of a Windows Explorer window with a ttrpg-convert.exe file next to a 5etools-mirror-2.github.io folder and a dm folder, and a Powershell window showing the output of the ttrpg-convert command](docs/screenshots/windows-explorer-powershell-after-run.png) -9. To use additional sources, templates, or books, or for more configuration options, +8. To use additional sources, templates, or books, or for more configuration options, [create a config file][3] and [see the main README][4]. - For example, assuming you have a custom configuration located in a file called `dm-sources.json`, you can use this command to generate notes using that configuration: ```shell - ./ttrpg-convert --index -o dm -c dm-sources.json 5etools-mirror-2.github.io-master/data + ./ttrpg-convert --index -o dm -c dm-sources.json ``` -[Convert 5eTools JSON data]: https://github.com/ebullient/ttrpg-convert-cli/tree/main?tab=readme-ov-file#convert-5etools-json-data -[Convert Pf2eTools JSON data]: https://github.com/ebullient/ttrpg-convert-cli/tree/main?tab=readme-ov-file#convert-pf2etools-json-data - [1]: https://github.com/ebullient/ttrpg-convert-cli/releases/latest [3]: docs/configuration.md [4]: README.md diff --git a/README.md b/README.md index 0f31a152a..9d93db43c 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ A Command-Line Interface designed to convert TTRPG data from 5eTools and Pf2eToo 📖 Homebrew -I use [Obsidian](https://obsidian.md) to keep track of my campaign notes. This project parses json sources for materials that I own from the 5etools mirror to create linked and formatted markdown that I can reference in my notes. +I use [Obsidian](https://obsidian.md) to keep track of my campaign notes. This project parses JSON sources for materials that I own from the 5etools mirror to create linked and formatted markdown that I can reference in my notes. > [!TIP] > > - 🚜 [**Review the changelog**](CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥). -> - 🔮 Check out [**Conventions**](#conventions) and [**Recommendations**](#recommendations-for-using-the-cli) - +> - 🔮 Check out [**Conventions**](#conventions) and [**Recommendations**](#recommendations-for-using-the-cli). +> > [!WARNING] > The 5eTools data repositories have been taken down. This tool will still work to create Obsidian notes for data in this JSON format (homebrew, for example). @@ -36,27 +36,27 @@ If you're new to it, we have resources to help you get started below. If you don't have a favorite method already, or you don't know what those words mean, here are some resources to get you started: -- For MacOS / OSX Users: +- For macOS / OSX users: - Start with the built-in `Terminal` application. - - [Learn the Mac OS X Command Line][] -- For Windows Users: + - [Learn the macOS Command Line][] +- For Windows users: - [A Beginner's Guide to the Windows Command Line][] - See the [Windows README](README-WINDOWS.md) -[Learn the Mac OS X Command Line]: https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line +[Learn the macOS Command Line]: https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line [A Beginner's Guide to the Windows Command Line]: https://www.makeuseof.com/tag/a-beginners-guide-to-the-windows-command-line/ ## Install the TTRPG Convert CLI -There are several (many!) options for running `ttrpg-convert`. -Choose whichever you are the most comfortable with: +There are several options for running `ttrpg-convert`. +Choose the one you are most comfortable with: -- **Using Windows?** See the [Windows README](README-WINDOWS.md) -- `jbang`: [Use JBang!][jbang] (hides Java; sets up command aliases) -- `brew`: [Use Homebrew (MacOS or Linux)][brew] (uses platform binaries) -- `bin`: [Use a pre-built platform binary][bin] (no Java required) -- `jar`: [Use Java to run the jar][jar] -- `src`: [Build from source][src] +- **Using Windows?** See the [Windows README](README-WINDOWS.md). +- `jbang`: [Use JBang!][jbang] (hides Java; sets up command aliases). +- `brew`: [Use Homebrew (macOS or Linux)][brew] (uses platform binaries). +- `bin`: [Use a pre-built platform binary][bin] (no Java required). +- `jar`: [Use Java to run the jar][jar]. +- `src`: [Build from source][src]. | Platform | Options | |----------------|----------| @@ -75,13 +75,13 @@ Choose whichever you are the most comfortable with: ## Recommendations for using the CLI -- 🔐 Treat generated content as a big ball of mud. Stick it in a corner of your vault *treat it as read-only*. +- 🔐 Treat generated content as a big ball of mud. Stick it in a corner of your vault and *treat it as read-only*. Trust us, you will want to regenerate content from time to time. It is cheap and easy to do if you don't have your own edits to worry about. -- 🔎 Have the CLI generate output into a separate directory and use a comparison tool to preview changes. +- 🔎 Have the CLI generate output into a separate directory and use a comparison tool to preview changes. - You can use `git diff` to compare arbitrary directories, for example: + You can use `git diff` to compare arbitrary directories. For example: ```bash git diff --no-index vault/compendium/bestiary generated/compendium/bestiary @@ -92,109 +92,106 @@ Choose whichever you are the most comfortable with: [rsync]: https://stackoverflow.com/a/19540611 [sync-discussion]: https://github.com/ebullient/ttrpg-convert-cli/discussions/220 -### Required plugins +### Required Plugins -- **Admonitions** ([git](https://github.com/javalent/admonitions)/[obsidian](obsidian://show-plugin?id=obsidian-admonition)): The admonitions plugin supports a codeblock style that is used for more complicated content like statblocks. See [Admonition plugin notes](docs/README.md#admonitions) for more recommendations. +- **Admonitions** ([git](https://github.com/javalent/admonitions)/[obsidian](obsidian://show-plugin?id=obsidian-admonition)): The Admonitions plugin supports a codeblock style admonition used for more complex embedded content. See [Admonition plugin notes](docs/README.md#admonitions) for more recommendations. -### Recommended plugins +### Recommended Plugins -- **Force note view mode by front matter** ([git](https://github.com/bwydoogh/obsidian-force-view-mode-of-note)/[obsidian](obsidian://show-plugin?id=obsidian-view-mode-by-frontmatter)): I use this plugin to treat these generated notes as essentially read-only. See [Force note view mode plugin settings](docs/README.md#force-note-view-mode-by-front-matter) for recommendations. +- **Force Note View Mode by Front Matter** ([git](https://github.com/bwydoogh/obsidian-force-view-mode-of-note)/[obsidian](obsidian://show-plugin?id=obsidian-view-mode-by-frontmatter)): I use this plugin to treat these generated notes as essentially read-only. See [Force Note View Mode plugin settings](docs/README.md#force-note-view-mode-by-front-matter) for recommendations. -- **Fantasy Statblocks** ([git](https://github.com/javalent/fantasy-statblocks)/[obsidian](obsidian://show-plugin?id=obsidian-5e-statblocks)): Templates for rendering monsters can define a `statblock` in the document body or provide a full or abridged yaml monster in the document header to update monsters in the plugin's bestiary. +- **Fantasy Statblocks** ([git](https://github.com/javalent/fantasy-statblocks)/[obsidian](obsidian://show-plugin?id=obsidian-5e-statblocks)): Templates for rendering monsters can define a `statblock` in the document body or provide a full or abridged YAML monster in the document header to update monsters in the plugin's bestiary. - See [Fantasy Statblocks plugin settings](docs/README.md#fantasy-statblocks) for recommendations. - See [Templates](examples/templates) for related template customization. -- **Initiative Tracker** ([git](https://github.com/javalent/initiative-tracker)/[obsidian](obsidian://show-plugin?id=initiative-tracker)): Templates for rendering monsters can include information in the header to define monsters that initiative tracker can use when constructing encounters. See [Initiative Tracker plugin settings](docs/README.md#initiative-tracker) for recommendations. +- **Initiative Tracker** ([git](https://github.com/javalent/initiative-tracker)/[obsidian](obsidian://show-plugin?id=initiative-tracker)): Templates for rendering monsters can include information in the header to define monsters that the Initiative Tracker can use when constructing encounters. See [Initiative Tracker plugin settings](docs/README.md#initiative-tracker) for recommendations. -- **Dataview** ([git](https://github.com/blacksmithgu/obsidian-dataview)/[obsidian](obsidian://show-plugin?id=dataview)): This plugin can be used to create custom views of the data, and to create custom queries to find and display data in your vault. See [Working with dataview](docs/README.md#working-with-dataview) for recommendations. +- **Dataview** ([git](https://github.com/blacksmithgu/obsidian-dataview)/[obsidian](obsidian://show-plugin?id=dataview)): This plugin can be used to create custom views of the data and to create custom queries to find and display data in your vault. See [Working with Dataview](docs/README.md#working-with-dataview) for recommendations. ## Conventions -- **Links.** Documents generated by this plugin will use markdown links rather than wiki links. A [css snippet](examples/css-snippets/hide-markdown-link-url.css) can make these links less invasive in edit mode by hiding the URL portion of the string. +- **Links.** Documents generated by this plugin will use markdown links rather than wiki links. A [CSS snippet](examples/css-snippets/hide-markdown-link-url.css) can make these links less intrusive in edit mode by hiding the URL portion of the string. -- **File names.** To avoid conflicts and issues with different operating systems, all file names are slugified (all lower case, symbols stripped, and spaces replaced by dashes). This is a familiar convention for those used to jekyll, hugo, or other blogging systems. +- **File names.** To avoid conflicts and issues with different operating systems, all file names are slugified (all lowercase, symbols stripped, and spaces replaced by dashes). This is a familiar convention for those used to Jekyll, Hugo, or other blogging systems. - - File names for resources outside of the core books (PHB, MM, and DMG) have the abbreviated source name appended to the end to avoid file collisions. + - File names for resources outside of the core books (PHB, MM, and DMG) have the abbreviated source name appended to the end to avoid file name collisions. - All files have an `aliases` attribute that contains the original name of the resource. -- **Organization.** Files are generated in two roots: `compendium` and `rules`. The location of these roots is [configurable](docs/configuration.md#specify-target-paths-paths-key). These directories will be populated depending on the sources you have enabled. +- **Organization.** Files are generated in two roots: `compendium` and `rules`. The location of these roots is [configurable](docs/configuration.md#specify-target-paths-paths-key). These directories will be populated based on the sources you have enabled. - - `compendium` contains files for items, spells, monsters, etc. - The `compendium` directory is further organized into subdirectories for each type of content. For example, all items are in the `compendium/items` directory. + - `compendium` contains files for items, spells, monsters, etc. The `compendium` directory is further organized into subdirectories for each type of content. For example, all items are in the `compendium/items` directory. - `rules` contains files for conditions, weapon properties, variant rules, etc. - `css-snippets` will contain **CSS files for special fonts** used by some content. You will need to copy these snippets into your vault (`.obsidian/snippets`) and enable them (`Appearance -> Snippets`) to ensure all content in your vault is styled correctly. - **Styles.** Every document has a `cssclasses` attribute that assigns a CSS class. We have some [CSS snippets](examples/css-snippets/) that you can use to customize elements of the compendium. - - 5e tools: `json5e-background`, `json5e-class`, `json5e-deck`, `json5e-deity`, `json5e-feat`, `json5e-hazard`, `json5e-item`, `json5e-monster`, `json5e-note`, `json5e-object`, `json5e-psionic`, `json5e-race`, `json5e-reward`, `json5e-spell`, and `json5e-vehicle`. - - pf2e tools: `pf2e`, `pf2e-ability`, `pf2e-action`, `pf2e-affliction`, `pf2e-archetype`, `pf2e-background`, `pf2e-book`, `pf2e-delity`, `pf2e-feat`, `pf2e-hazard`, `pf2e-index`, `pf2e-item`, `pf2e-note`, `pf2e-ritual`, `pf2e-sleep`, `pf2e-trait`. + - 5eTools: `json5e-background`, `json5e-class`, `json5e-deck`, `json5e-deity`, `json5e-feat`, `json5e-hazard`, `json5e-item`, `json5e-monster`, `json5e-note`, `json5e-object`, `json5e-psionic`, `json5e-race`, `json5e-reward`, `json5e-spell`, and `json5e-vehicle`. + - Pf2eTools: `pf2e`, `pf2e-ability`, `pf2e-action`, `pf2e-affliction`, `pf2e-archetype`, `pf2e-background`, `pf2e-book`, `pf2e-deity`, `pf2e-feat`, `pf2e-hazard`, `pf2e-index`, `pf2e-item`, `pf2e-note`, `pf2e-ritual`, `pf2e-spell`, `pf2e-trait`. - **Admonitions.** Generated content uses code-block-style [Admonitions](docs/README.md#admonitions) in addition to Obsidian callouts. We have [Admonition definitions](examples/admonitions/) that you can import to ensure these admonition/callout types are defined. - `ad-statblock` - - 5e tools: `ad-flowchart`, `ad-gallery`, `ad-embed-action`, `ad-embed-feat`, `ad-embed-monster`, `ad-embed-object`, `ad-embed-race`, `ad-embed-spell`, `ad-embed-table` - - pf2e tools: `ad-embed-ability`, `ad-embed-action`, `ad-embed-affliction`, `ad-embed-avatar`, `ad-embed-disease`, `ad-embed-feat`, `ad-embed-item`, `ad-pf2-note`, `ad-pf2-ritual`. + - 5eTools: `ad-flowchart`, `ad-gallery`, `ad-embed-action`, `ad-embed-feat`, `ad-embed-monster`, `ad-embed-object`, `ad-embed-race`, `ad-embed-spell`, `ad-embed-table` + - Pf2eTools: `ad-embed-ability`, `ad-embed-action`, `ad-embed-affliction`, `ad-embed-avatar`, `ad-embed-disease`, `ad-embed-feat`, `ad-embed-item`, `ad-pf2-note`, `ad-pf2-ritual`. ## Convert 5eTools JSON data > [!NOTE] -> Instructions here use backslashes to wrap lines for readability (a common practice for linux-based command shells). +> Instructions here use backslashes to wrap lines for readability (a common practice for Linux-based command shells). +> > *If you are using Windows*, you will need to remove the backslashes and put the command on a single line. You may also need to append `.exe` to the command name (though it should work without). -1. Create a shallow clone of the 5eTools mirror repo (which can/should be deleted afterwards): +1. Invoke the CLI with the `--version` option. ```shell - git clone --depth 1 https://github.com/5etools-mirror-2/5etools-mirror-2.github.io.git + ttrpg-convert --version ``` -2. Invoke the CLI. In this first example, let's generate indexes and markdown for SRD content: + You should see output similar to the following: + + ```shell + ttrpg-convert version 2.3.18 + Git commit: ed56f76 + ``` + + If you do, we know that the CLI is working! + + If you don't, there may be something wrong with your installation. Windows users, see the [Windows README for help](./README-WINDOWS.md#uh-oh-something-went-wrong). + +2. Invoke the CLI to generate indexes and markdown for SRD content: ```shell ttrpg-convert \ --index \ -o dm \ - 5etools-mirror-2.github.io/data + <5etools-data-dir> ``` - `--index` generates two index files: `all-index.json` and `src-index.json`. > 🚀 TIP: > - Use `all-index.json` to see the reference keys for all discovered content. This can confirm that an included source was actually read. - > - Use `src-index.json` to see the reference keys for content that was included in the generated output. This can confirm that your source selection is working as expected. + > - Use `src-index.json` to see the reference keys for content that was included in the generated output. Use this to confirm that your source selection is working as expected. - `-o dm` The target output directory (`dm` in this case). Files will be created in this directory. - The rest of the command-line specifies input files: - - - `5etools-mirror-2.github.io/data` Path to the 5etools `data` directory (from a clone or release of the repo) + - `<5etools-data-dir>` is a placeholder for the location of downloaded 5eTools source data directory. This should produce a set of markdown files in the `dm` directory that contains only SRD content. -3. Invoke the command again and include additional sources: +3. Next, you'll want to create a [configuration file](docs/configuration.md) to set up your sources. + + The configuration is provided to the CLI using the `-c` option: ```shell ttrpg-convert \ --index \ -o dm \ - -s PHB,DMG,SCAG \ - 5etools-mirror-2.github.io/data + -c my-config.json \ + <5etools-data-dir> ``` - - `-s PHB,DMG,SCAG` will include reference material from the *Player's Handbook*, the *Dungeon Master's Guide*, and the *Sword Coast Adventurer's Guide*. - - > 🚀 Note: Only include content you own. Find the identifier for your sources in the [Source Map](./docs/sourceMap.md#source-name-mapping-for-5etools). - -We now know that the CLI is working! - -Specifying sources on the command line with the `-s` option gets messy in a hurry. Configuration beyond this basic example should use a configuration file, specified with the `-c` option, like this: - -```shell -ttrpg-convert \ - --index \ - -o dm \ - -c my-config.json \ - 5etools-mirror-2.github.io/data -``` + > 🚀 Note: Only include content you own. Find the identifier for your sources in the [Source Map](./docs/sourceMap.md#source-name-mapping-for-5etools). Next step: @@ -205,15 +202,27 @@ Next step: 🚜 🚧 🚜 🚧 🚜 🚧 🚜 🚧 > [!NOTE] -> Instructions here use backslashes to wrap lines for readability (a common practice for linux-based command shells). +> Instructions here use backslashes to wrap lines for readability (a common practice for Linux-based command shells). +> > *If you are using Windows*, you will need to remove the backslashes and put the command on a single line. You may also need to append `.exe` to the command name (though it should work without). -1. Download a release of the Pf2eTools mirror, or create a shallow clone of the repo (which can/should be deleted afterwards): +1. Invoke the CLI with the `--version` option: ```shell - git clone --depth 1 https://github.com/Pf2eToolsOrg/Pf2eTools.git + ttrpg-convert --version ``` + You should see output similar to the following: + + ```shell + ttrpg-convert version 2.3.18 + Git commit: ed56f76 + ``` + + If you do, we know that the CLI is working! + + If you don't, there may be something wrong with your installation. Windows users, see the [Windows README for help](./README-WINDOWS.md#uh-oh-something-went-wrong). + 2. Invoke the CLI. In this first example, let's generate indexes and markdown for default content: ```shell @@ -221,7 +230,7 @@ Next step: -g pf2e \ --index \ -o dm \ - Pf2eTools/data + ``` - `-g pf2e` The game system! Pathfinder 2e! @@ -229,41 +238,26 @@ Next step: > 🚀 TIP: > - Use `all-index.json` to see the reference keys for all discovered content. This can confirm that an included source was actually read. - > - Use `src-index.json` to see the reference keys for content that was included in the generated output. This can confirm that your source selection is working as expected. + > - Use `src-index.json` to see the reference keys for content that was included in the generated output. Use this to confirm that your source selection is working as expected. - `-o dm` The target output directory. Files will be created in this directory. - The rest of the command-line specifies input files: + - `` is a placeholder for the location of downloaded 5eTools source data directory. - - `Pf2eTools/data` Path to the Pf2eTools `data` directory (from a clone or release of the repo) +3. Next, you'll want to create a [configuration file](docs/configuration.md) to set up your sources. -3. Invoke the command again and include additional sources: + The configuration is provided to the CLI using the `-c` option: ```shell ttrpg-convert \ -g pf2e \ --index \ -o dm \ - -s AV1,GMG \ - Pf2eTools/data + -c my-config.json + ``` - - `-s AV1,GMG` will include reference material from the *Abomination Vaults #1: Ruins of Gauntlight*, and the *Gamemastery Guide*. - - > 🚀 Note: Only include content you own. Find the identifier for your sources in the [Source Map](./docs/sourceMap.md#source-name-mapping-for-pf2etools). - -We now know that the CLI is working! - -Specifying sources on the command line with the `-s` option gets messy in a hurry. Configuration beyond this basic example should use a configuration file, specified with the `-c` option, like this: - -```shell -ttrpg-convert \ - -g pf2e \ - --index \ - -o dm \ - -c my-config.json \ - Pf2eTools/data -``` + > 🚀 Note: Only include content you own. Find the identifier for your sources in the [Source Map](./docs/sourceMap.md#source-name-mapping-for-pf2etools). Next step: @@ -271,59 +265,61 @@ Next step: ## Convert Homebrew JSON data -The CLI tool also has the ability to import homebrewed content, though this content must still fit the json standards that are set by in the [5eTools json spec][5etools JSON] or the PF2eTools json spec (coming soon, similar to 5eTools). +The CLI tool can also import homebrewed content, though this content must still fit the JSON standards set by the [5eTools JSON spec][5etools JSON] or the PF2eTools JSON spec (coming soon, similar to 5eTools). -Perhaps the simplest thing to do to import homebrew is to use already existing homebrew data from the 5etools homebrew github repo: . +Perhaps the simplest way to import homebrew is to use existing homebrew data from the 5eTools homebrew GitHub repo: . > [!TIP] -> 🍺 *You only need the particular file you wish to import*. +> 🍺 *You only need the specific file you wish to import*. > -> Homebrew data is different from the 5etools data. Each homebrew file is a complete reference. If you compare it to cooking: the 5etools mirror repo is organized by ingredient (all of the carrots, all of the onions, ... ); homebrew data is organized by prepared meal / complete receipe. +> Homebrew data is different from the 5eTools data. Each homebrew file is a complete reference. If you compare it to cooking: the 5eTools mirror repo is organized by ingredient (all of the carrots, all of the onions, ...); homebrew data is organized by prepared meal / complete recipe. -Adding homebrew content is easiest if you use a [configuration file](./docs/configuration.md), we will assume a file named `my-config.json` for the example below, but you can use any name you like. +Adding homebrew content is easiest if you use a [configuration file](./docs/configuration.md). We will assume a file named `my-config.json` for the example below, but you can use any name you like. > [!IMPORTANT] > 🚀 Respect copyrights and support content creators; use only the sources you own. -For example, if you wanted to use Benjamin Huffman's popular homebrewed [Pugilist class](https://www.dmsguild.com/product/184921/The-Pugilist-Class): +For example, if you want to use Benjamin Huffman's popular homebrewed [Pugilist class](https://www.dmsguild.com/product/184921/The-Pugilist-Class): -1. Download a copy of the [Pugilist json file](https://github.com/TheGiddyLimit/homebrew/blob/master/class/Benjamin%20Huffman%3B%20Pugilist.json). +1. Download a copy of the [Pugilist JSON file](https://github.com/TheGiddyLimit/homebrew/blob/master/class/Benjamin%20Huffman%3B%20Pugilist.json). - Save this file to a well-known location on your computer. It is probably easiest if it sits next your 5eTools or Pf2eTools directory. + Save this file to a well-known location on your computer. It is probably easiest if it sits next to your 5eTools or Pf2eTools directory. -2. Update your [configuration file](docs/configuration.md) to add a `homebrew` section under `full-source`: +2. Update your [configuration file](docs/configuration.md) to add a `homebrew` section under `sources`: ```json { - "full-source": { - "homebrew": [ - "path/to/Benjamin Huffman; Pugilist.json" - ] - } + "sources": { + ... + "homebrew": [ + "path/to/Benjamin Huffman; Pugilist.json" + ] + } } ``` - - `path/to/` is a placeholder for a relative or absolute path to the file[^1]. There are a few ways to figure out the path to a file. + - `path/to/` is a placeholder for a relative or absolute path to the file[^1]. Here are a few ways to determine the path to a file: - You may be able to drag and drop the file into the terminal window. - - You may have the ability to right-click on the file and select "Copy Path". + - You may be able to right-click on the file and select "Copy Path". - *Windows users*: When pasting the path into a text editor, use find/replace to replace all `\` with `/`. 3. Run the command like so (for 5e homebrew): - ``` shell + ```shell ttrpg-convert \ --index \ -o hb-compendium \ - -c my-config.json - 5etools-mirror-2.github.io/data + -c my-config.json \ + <5etools-data-dir> ``` - - `-o hb-compendium` is the output directory for generated content. - - `-c my-config.json'` is the name and/or path to your configuration file. + - `-o hb-compendium` specifies the output directory for generated content. + - `-c my-config.json` specifies the name and/or path to your configuration file. + - `<5etools-data-dir>` is a placeholder for the 5eTools source data directory. See [configuration](docs/configuration.md) for more details on how to configure the CLI. -The process is similar for other homebrew, including your own, so long as it is broadly compatible with the [5e-tools json spec](https://wiki.tercept.net/en/Homebrew/FromZeroToHero). +The process is similar for other homebrew, including your own, as long as it is broadly compatible with the [5eTools JSON spec](https://wiki.tercept.net/en/Homebrew/FromZeroToHero). ## Where to find help @@ -334,17 +330,17 @@ The process is similar for other homebrew, including your own, so long as it is ### Want to help fix it? -- If you're familiar with the command line and are comfortable running the tool, I hope you'll consider running [unreleased snapshots](docs/alternateRun.md#using-unreleased-snapshots) and reporting issues. -- If you want to contribute, I'll take help of all kinds: documentation, examples, sample templates, stylesheets are just as important as Java code. See [CONTRIBUTING](CONTRIBUTING.md). +- If you're familiar with the command line and are comfortable running the tool, please consider running [unreleased snapshots](docs/alternateRun.md#using-unreleased-snapshots) and reporting issues. +- If you want to contribute, I'll take help of all kinds: documentation, examples, sample templates, and stylesheets are just as important as Java code. See [CONTRIBUTING](CONTRIBUTING.md). ## Other notes -This project uses Quarkus, the Supersonic Subatomic Java Framework. If you want to learn more about Quarkus, please visit its website: . +This project uses Quarkus, the Supersonic Subatomic Java Framework. To learn more about Quarkus, please visit its website: . -This project is a derivative of [fc5-convert-cli](https://github.com/ebullient/fc5-convert-cli), which focused on working to and from FightClub5 Compendium XML files. It has also stolen some bits and pieces from [pockets-cli](https://github.com/ebullient/pockets-cli). +This project is a derivative of [fc5-convert-cli](https://github.com/ebullient/fc5-convert-cli), which focused on working with FightClub5 Compendium XML files. It has also borrowed some bits and pieces from [pockets-cli](https://github.com/ebullient/pockets-cli). -[5etools JSON]: https://wiki.tercept.net/en/Homebrew/FromZeroToHero +[5eTools JSON]: https://wiki.tercept.net/en/Homebrew/FromZeroToHero -Buy Me A Coffee +Buy Me A Coffee -[^1]: Description of relative vs absolute file paths: . If you use a relative path, it will be resolved relative to the current working directory, as described here: . +[^1]: Description of relative vs absolute file paths: . If you use a relative path, it will be resolved relative to the current working directory, as described here: . diff --git a/docs/configuration.md b/docs/configuration.md index 27e8fe309..fa72d6a95 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -12,8 +12,7 @@ This guide introduces you to configuring data transformations using the Command - [Basic configuration example](#basic-configuration-example) - [Advanced configuration example](#advanced-configuration-example) - [Source identifiers](#source-identifiers) -- [Include reference data with the `from` key](#include-reference-data-with-the-from-key) -- [Include complete text with `full-source`](#include-complete-text-with-full-source) +- [Specify content with the `sources` key](#specify-content-with-the-sources-key) - [Homebrew](#homebrew) - [Additional notes about homebrew](#additional-notes-about-homebrew) - [Reporting content errors to 5eTools](#reporting-content-errors-to-5etools) @@ -31,6 +30,7 @@ This guide introduces you to configuring data transformations using the Command - [Copying internal images](#copying-internal-images) - [Copying external images](#copying-external-images) - [Fallback paths](#fallback-paths) +- [🚚 Migrating `from`, `full-source`, and `convert`](#migrating-from-full-source-and-convert) ## Overview @@ -50,21 +50,23 @@ Below is a straightforward `config.json` file. In this format, settings are note ``` json { - "from": [ - "DMG", - "PHB", - "MM" - ], - "paths": { - "compendium": "z_compendium/", - "rules": "z_compendium/rules" - } + "sources": { + "book": [ + "DMG", + "PHB", + "MM" + ] + }, + "paths": { + "compendium": "z_compendium/", + "rules": "z_compendium/rules" + } } ``` This example performs two basic functions: -1. **Select Input Sources:** The `from` key lists the sources to be included, identified by their [source identifiers](#source-identifiers). +1. **Select Input Sources:** The `sources` key lists the sources to be included, identified by their [source identifiers](#source-identifiers). 2. **Define Vault Paths:** The [`paths`](#specify-target-paths-paths-key) key sets the destination paths for the `compendium` and `rules` content. These paths are relative to the output directory set in the CLI command with `-o`. > [!WARNING] @@ -76,7 +78,7 @@ Here's a more comprehensive `config.json` file. ```json { - "full-source": { + "sources": { "adventure": [ "LMoP", "LoX" @@ -84,22 +86,22 @@ Here's a more comprehensive `config.json` file. "book": [ "PHB" ], + "reference": [ + "AI", + "DMG", + "TCE", + "ESK", + "DIP", + "XGE", + "FTD", + "MM", + "MTF", + "VGM" + ], "homebrew": [ "homebrew/creature/MCDM Productions; Flee, Mortals!.json" ] }, - "from": [ - "AI", - "DMG", - "TCE", - "ESK", - "DIP", - "XGE", - "FTD", - "MM", - "MTF", - "VGM" - ], "paths": { "rules": "/compendium/rules/" }, @@ -125,14 +127,13 @@ Here's a more comprehensive `config.json` file. Additional capabilities: -1. **Incorporate complete sources:** The [`full-source`](#include-complete-text-with-full-source) key is used to include complete text of specified sources. -2. **Select Input Sources:** As before, the [`from`](#include-reference-data-with-the-from-key) key lists the sources to include. -3. **Define Vault Paths:** The [`path`](#specify-target-paths-paths-key) sets the vault path destination for `rules` content. -4. **Targeted exclusion:** [`excludePattern`](#excluding-content-matching-an-excludepattern) and [`exclude`](#excluding-specific-content-with-exclude) leaves out specific content. -5. **Targeted inclusion:** The [`include`](#including-specific-content-with-include) specifies content that is *always included*. -6. **Use the dice roller plugin:** The [`useDiceRoller`](#use-the-dice-roller-plugin) key enables the dice roller plugin. -7. **Tag prefix:** The [`tagPrefix`](#tag-prefix) key sets the prefix for tags generated by the CLI. -8. **Templates:** The [`template`](#templates) key specifies the templates to use for different types of content. +1. **Select input sources:** The [`sources`](#specify-content-with-the-sources-key) key is used to select included sources (full text from two adventures and a book, reference content from a slew of other official sources, and one [homebrew source](#homebrew)). +2. **Define Vault Paths:** The [`path`](#specify-target-paths-paths-key) sets the vault path destination for `rules` content. +3. **Targeted exclusion:** [`excludePattern`](#excluding-content-matching-an-excludepattern) and [`exclude`](#excluding-specific-content-with-exclude) leaves out specific content. +4. **Targeted inclusion:** The [`include`](#including-specific-content-with-include) specifies content that is *always included*. +5. **Use the dice roller plugin:** The [`useDiceRoller`](#use-the-dice-roller-plugin) key enables the dice roller plugin. +6. **Tag prefix:** The [`tagPrefix`](#tag-prefix) key sets the prefix for tags generated by the CLI. +7. **Templates:** The [`template`](#templates) key specifies the templates to use for different types of content. > [!WARNING] > **Windows Users**: Replace any `\` in your paths with '/' in your JSON and YAML files. @@ -143,47 +144,41 @@ Additional capabilities: Sources in 5eTools and Pf2eTools are referenced by unique identifiers. Find the identifiers for your sources in the [Source Map](sourceMap.md). -Some are split into multiple files, in which case, you will need to specify each identifier separately. For example, *Tales from the Yawning Portal* is split into seven files. Content appears using any one of the seven (`TftYP-*`), in addition to `TftYP` for common content. If you want to include all of them, you will need to specify each identifier separately. - -If you're expecting to see content from a book or adventure and it's not showing up, run the CLI with the `--index` option, and check the `all-index.json` file to see which source identifier you should be using. - -## Include reference data with the `from` key - -The CLI creates notes for reference items like backgrounds, monsters, classes, and more. - -To generate reference notes for your own sources, add their [identifiers](#source-identifiers) to the `from` key. +Content is classified as a `book` or `adventure` (shown as the third column in the source map). Use this classification when [specifying your sources](#specify-content-with-the-sources-key). -Here is an example that will create reference notes (spells, classes, etc. but not the full text) for the *Player's Handbook* (PHB) and *Acquisitions Incorporated* (AI): +Some sources are split into multiple files, in which case, you will need to specify each identifier separately. For example, *Tales from the Yawning Portal* is split into seven files. Content appears using any one of the seven (`TftYP-*`), in addition to `TftYP` for common content. If you want to include all of them, you will need to specify each identifier separately. -```json - "from": [ - "AI", - "PHB", - ... - ] -``` +If you're expecting to see content from a book or adventure and it's not showing up, run the CLI with the `--index` option, and check the `all-index.json` file to see which source identifier you should be using. -## Include complete text with `full-source` +## Specify content with the `sources` key -> [!TIP] -> You only need to list your source once. Either in `from` or in `full-source`, but not both. +The CLI can emit content from a source in two ways: -Use the `full-source` key to create notes for all content and reference data from your sources. Add the [source identifiers](#source-identifiers) to `full-source` to include them in the vault. +- "full text": notes for all content and reference data from your sources. + When including the full text, use the `book`, `adventure`, or `homebrew` key as appropriate for the source material. +- "reference only": only emit reference notes (spells, classes, etc.). + Use the `reference` key to include reference content from books or adventures. -Here is an example that will create notes for the *Player's Handbook* (a book, PHB) and *The Wild Beyond the Witchlight* (an adventure, WBtW): +With that in mind, specify your sources in this way: ```json -"full-source": { +"sources": { "adventure": [ "WBtW" ], "book": [ "PHB" + ], + "reference": [ + "MPMM" ] } ``` -Adventures are found in the `adventure` subdirectory, while books are found in the `book` subdirectory. Look for `adventure/adventure-.json` or `book/book-.json` to determine which category to use. +The above example that will include full text for the *Player's Handbook* (a book, PHB) and *The Wild Beyond the Witchlight* (an adventure, WBtW), but will only create reference notes (backgrounds, cults/boons, races, traps/hazards) from *Mordenkainen Presents: Monsters of the Multiverse* (MPMM). + +> [!TIP] +> You only need to list your source once. ### Homebrew @@ -194,7 +189,7 @@ Adventures are found in the `adventure` subdirectory, while books are found in t > > Support your content creators! Only use homebrew that you own. -To include Homebrew in your notes, specify the path to the homebrew json file in a `homebrew` section inside of `full-source`. +To include Homebrew in your notes, specify the path to the homebrew json file in a `homebrew` section inside of `sources`. For example, if you wanted to use Benjamin Huffman's popular homebrewed [Pugilist class](https://www.dmsguild.com/product/184921/The-Pugilist-Class): @@ -202,11 +197,11 @@ For example, if you wanted to use Benjamin Huffman's popular homebrewed [Pugilis Save this file to a well-known location on your computer. It is probably easiest if it sits next your 5eTools or Pf2eTools directory. -2. Add the path to this file to a `homebrew` section under `full-source`: +2. Add the path to this file to a `homebrew` section under `sources`: ```json { - "full-source": { + "sources": { "homebrew": [ "path/to/Benjamin Huffman; Pugilist.json" ] @@ -224,7 +219,7 @@ There are a few ways to figure out the path to a file: > [!WARNING] > **Windows Users**: Replace any `\` with `/` in paths in JSON or YAML files -### Additional notes about homebrew +#### Additional notes about homebrew Homebrew json files are not rigorously validated. There may be errors when importing. I've done what I can to make the errors clear, or to highlight the suspect json, but I can't catch everything. @@ -233,9 +228,12 @@ Here are some examples of what you may see, and how to fix them: - `Unable to find image 'img/bestiary/MM/Green Hag.jpg'` (or similar) - This kind of path refers to an "internal" (meaning part of the base 5e corpus of stuff) image. These paths are computed relative to a known base. With mirror2, the base has changed (to a different repository structure, see [Copying "internal" images](#copying-internal-images)), and images have, by and large, been converted from `.jpg` or `.png` to `.webp`. Fixing this kind of error is usually a case of fixing the path. + This kind of path refers to an "internal" (meaning part of the base 5e corpus of stuff) image. These paths are computed relative to a known base. + + Recent releases of 5eTools use a different repository structure, see [Copying "internal" images](#copying-internal-images)), and images have, by and large, been converted from `.jpg` or `.png` to `.webp`. + Fixing this kind of error is usually a case of fixing the path. - - If you can fix the link yourself (change to `.webp`, mirror1 path to revised mirror2 path by removing `img/`), please [report it in 5eTools #brew-conversion](#reporting-content-errors-to-5etools). + - If you can fix the link yourself (change to `.webp` and guess the new location by removing `img/`), please [report it in 5eTools #brew-conversion](#reporting-content-errors-to-5etools). - If you can't find the image that should be used instead, please [ask for CLI help](../README.md#where-to-find-help)and we'll help you find the right one. - `Unknown spell school Curse in sources[spell|ventus|wandsnwizards]`; similar for item types, item properties, conditions, skills, abilities, etc. @@ -310,7 +308,7 @@ Just as source material has an identifier, so does each piece of data. The *Mons The CLI `--index` option compiles two lists of data keys: - `all-index.json`: Lists all discovered data keys. -- `src-index.json`: Lists the data keys after filters (`full-source`, `from`, and the config options below) have been applied. +- `src-index.json`: Lists the data keys after source filters (`adventure`, `book`, `reference`, and the config options below) have been applied. ### Excluding content matching an `excludePattern` @@ -351,9 +349,7 @@ The CLI can generate notes that include inline dice rolls. To enable this featur ## Render with Fantasy Statblocks -If you are using the Fantasy Statblocks plugin to render your statblocks, set `yamlStatblocks` to `true`. - -This will avoid adding backticks or other formatting related to dice rolls in statblock text (which will make Fantasy Statblocks happier), whether you have dice roller plugin enabled or not. +If you are using the Fantasy Statblocks plugin to render your statblocks, set `yamlStatblocks` to `true`. This will remove backticks and other formatting from statblock text. ## Tag prefix @@ -386,6 +382,8 @@ Documentation is generated for [**template attributes**](./templates/). Not everything is customizable. Some indenting, organizing, formatting, and linking is easier to do consistently while rendering big blobs of text. +See the [examples templates](../examples/templates) for reference. + ## Images The CLI can copy images referenced in the content to your vault. This is useful if you want to use the content offline or if you want to ensure that images are available in your vault. @@ -461,6 +459,48 @@ Note: - The key (original path) must match what the Json source is specifying. - The value (replacement path) should be either: a valid path to a local file[^2] or a valid URL to a remote file[^3]. +## Migrating `from`, `full-source`, and `convert` + +Older configurations looked a little different. + +I've used the same values from the [example above](#specify-content-with-the-sources-key) in the following examples. It should be straight-forward to update your config to the new syntax. + +```json +"from": [ + "MPMM" +], +"full-source": { + "adventure": [ + "WBtW" + ], + "book": [ + "PHB" + ], + "homebrew": [ + ... + ] +} +``` + +OR + +```json +"from": [ + "MPMM" +], +"convert": { + "adventure": [ + "WBtW" + ], + "book": [ + "PHB" + ], + "homebrew": [ + ... + ] +} +``` + [^1]: The working directory is the directory you were in (in the terminal) when you launched the CLI. See for more information [^2]: Example/explanation of absolute vs. relative path: . If you're using relative paths with the CLI, they should be relative to the working directory (see [^1]). [^3]: A URL is a uniform resource locator, more information at . diff --git a/docs/sourceMap.md b/docs/sourceMap.md index 150d2076d..a6d076725 100644 --- a/docs/sourceMap.md +++ b/docs/sourceMap.md @@ -9,244 +9,181 @@ _Support content creators. Only use or include sources that you own._ ## Source name mapping for 5etools -### Abbreviations to long name +- **2014** (sources/reference): "srd", "basicrules" +- **2024** (sources/reference): "srd52", "freerules2024" -| Abbreviation | Long name | -|--------------|-----------| -| AAG | Astral Adventurer's Guide | -| AATM | Adventure Atlas: The Mortuary | -| AI | Acquisitions Incorporated | -| AL | Adventurers' League | -| ALCoS | Adventurers League: Curse of Strahd | -| ALEE | Adventurers League: Elemental Evil | -| ALRoD | Adventurers League: Rage of Demons | -| AWM | Adventure with Muk | -| AZfyT | A Zib for your Thoughts | -| AitFR | Adventures in the Forgotten Realms | -| AitFR-AVT | Adventures in the Forgotten Realms: A Verdant Tomb | -| AitFR-DN | Adventures in the Forgotten Realms: Deepest Night | -| AitFR-FCD | Adventures in the Forgotten Realms: From Cyan Depths | -| AitFR-ISF | Adventures in the Forgotten Realms: In Scarlet Flames | -| AitFR-THP | Adventures in the Forgotten Realms: The Hidden Page | -| BAM | Boo's Astral Menagerie | -| BGDIA | Baldur's Gate: Descent Into Avernus | -| BGG | Bigby Presents: Glory of the Giants | -| BMT | The Book of Many Things | -| CM | Candlekeep Mysteries | -| CRCotN | Critical Role: Call of the Netherdeep | -| CoA | Chains of Asmodeus | -| CoS | Curse of Strahd | -| DC | Divine Contention | -| DD | Dangerous Designs | -| DIP | Dragon of Icespire Peak | -| DMG | Dungeon Master's Guide | -| DMTCRG | The Deck of Many Things: Card Reference Guide | -| DSotDQ | Dragonlance: Shadow of the Dragon Queen | -| DitLCoT | Descent into the Lost Caverns of Tsojcanth | -| DoD | Domains of Delight | -| DoDk | Dungeons of Drakkenheim | -| DoSI | Dragons of Stormwreck Isle | -| EEPC | Elemental Evil Player's Companion | -| EET | Elemental Evil: Trinkets | -| EFR | Eberron: Forgotten Relics | -| EGW | Explorer's Guide to Wildemount | -| EGW_DD | Dangerous Designs | -| EGW_FS | Frozen Sick | -| EGW_ToR | Tide of Retribution | -| EGW_US | Unwelcome Spirits | -| ERLW | Eberron: Rising from the Last War | -| ESK | Essentials Kit | -| FS | Frozen Sick | -| FTD | Fizban's Treasury of Dragons | -| GGR | Guildmasters' Guide to Ravnica | -| GHLoE | Grim Hollow: Lairs of Etharis | -| GoS | Ghosts of Saltmarsh | -| GotSF | Giants of the Star Forge | -| HAT-LMI | Honor Among Thieves: Legendary Magic Items | -| HAT-TG | Honor Among Thieves: Thieves' Gallery | -| HF | Heroes' Feast | -| HFFotM | Heroes' Feast Flavors of the Multiverse | -| HFStCM | Heroes' Feast: Saving the Children's Menu | -| HWAitW | Humblewood: Adventure in the Wood | -| HWCS | Humblewood Campaign Setting | -| HftT | Hunt for the Thessalhydra | -| HoL | The House of Lament | -| HotDQ | Hoard of the Dragon Queen | -| IDRotF | Icewind Dale: Rime of the Frostmaiden | -| IMR | Infernal Machine Rebuild | -| JttRC | Journeys through the Radiant Citadel | -| KKW | Krenko's Way | -| KftGV | Keys from the Golden Vault | -| LK | Lightning Keep | -| LLK | Lost Laboratory of Kwalish | -| LMoP | Lost Mine of Phandelver | -| LR | Locathah Rising | -| LRDT | Red Dragon's Tale: A LEGO Adventure | -| LoX | Light of Xaryxis | -| MCV1SC | Monstrous Compendium Volume 1: Spelljammer Creatures | -| MCV2DC | Monstrous Compendium Volume 2: Dragonlance Creatures | -| MCV3MC | Monstrous Compendium Volume 3: Minecraft Creatures | -| MCV4EC | Monstrous Compendium Volume 3: 4: Eldraine Creatures | -| MFF | Mordenkainen's Fiendish Folio | -| MGELFT | Muk's Guide To Everything He Learned From Tasha | -| MM | Monster Manual | -| MOT | Mythic Odysseys of Theros | -| MPMM | Mordenkainen Presents: Monsters of the Multiverse | -| MPP | Morte's Planar Parade | -| MTF | Mordenkainen's Tome of Foes | -| MaBJoV | Minsc and Boo's Journal of Villainy | -| MisMV1 | Misplaced Monsters: Volume 1 | -| NRH | NERDS Restoring Harmony | -| NRH-ASS | NERDS Restoring Harmony: A Sticky Situation | -| NRH-AT | NERDS Restoring Harmony: Adventure Together | -| NRH-AVitW | NERDS Restoring Harmony: A Voice in the Wilderness | -| NRH-AWoL | NERDS Restoring Harmony: A Web of Lies | -| NRH-CoI | NERDS Restoring Harmony: Circus of Illusions | -| NRH-TCMC | NERDS Restoring Harmony: The Candy Mountain Caper | -| NRH-TLT | NERDS Restoring Harmony: The Lost Tomb | -| OGA | One Grung Above | -| OoW | The Orrery of the Wanderer | -| OotA | Out of the Abyss | -| PHB | Player's Handbook | -| PSA | Plane Shift: Amonkhet | -| PSD | Plane Shift: Dominaria | -| PSI | Plane Shift: Innistrad | -| PSK | Plane Shift: Kaladesh | -| PSX | Plane Shift: Ixalan | -| PSZ | Plane Shift: Zendikar | -| PaBTSO | Phandelver and Below: The Shattered Obelisk | -| PiP | Peril in Pinegrove | -| PotA | Princes of the Apocalypse | -| QftIS | Quests from the Infinite Staircase | -| RMBRE | The Lost Dungeon of Rickedness: Big Rick Energy | -| RMR | Dungeons & Dragons vs. Rick and Morty: Basic Rules | -| RoT | The Rise of Tiamat | -| RoTOS | The Rise of Tiamat Online Supplement | -| RtG | Return to Glory | -| SAC | Sage Advice Compendium | -| SADS | Sapphire Anniversary Dice Set | -| SAiS | Spelljammer: Adventures in Space | -| SCAG | Sword Coast Adventurer's Guide | -| SCC | Strixhaven: A Curriculum of Chaos | -| SCC-ARiR | A Reckoning in Ruins | -| SCC-CK | Campus Kerfuffle | -| SCC-HfMT | Hunt for Mage Tower | -| SCC-TMM | The Magister's Masquerade | -| SCREEN | Dungeon Master's Screen | -| SCREEN_DUNGEON_KIT | Dungeon Master's Screen: Dungeon Kit | -| SCREEN_WILDERNESS_KIT | Dungeon Master's Screen: Wilderness Kit | -| SDW | Sleeping Dragon's Wake | -| SJA | Spelljammer Academy | -| SKT | Storm King's Thunder | -| SLW | Storm Lord's Wrath | -| SRC_SCREEN_SPELLJAMMER | ScreenSpelljammer | -| SatO | Sigil and the Outlands | -| SjA | Spelljammer Academy | -| TCE | Tasha's Cauldron of Everything | -| TD | Tarot Deck | -| TDCSR | Tal'Dorei Campaign Setting Reborn | -| TLK | The Lost Kenku | -| TTP | The Tortle Package | -| TftYP | Tales from the Yawning Portal | -| TftYP-AtG | Tales from the Yawning Portal: Against the Giants | -| TftYP-DiT | Tales from the Yawning Portal: Dead in Thay | -| TftYP-TFoF | Tales from the Yawning Portal: The Forge of Fury | -| TftYP-THSoT | Tales from the Yawning Portal: The Hidden Shrine of Tamoachan | -| TftYP-TSC | Tales from the Yawning Portal: The Sunless Citadel | -| TftYP-ToH | Tales from the Yawning Portal: Tomb of Horrors | -| TftYP-WPM | Tales from the Yawning Portal: White Plume Mountain | -| ToA | Tomb of Annihilation | -| ToB1-2023 | Tome of Beasts 1 (2023 Edition) | -| ToD | Tyranny of Dragons | -| ToFW | Turn of Fortune's Wheel | -| ToR | Tide of Retribution | -| UA20F | Unearthed Arcana: 2020 Feats | -| UA20POR | Unearthed Arcana: 2020 Psionic Options Revisited | -| UA20SC1 | Unearthed Arcana: 2020 Subclasses: Part 1 | -| UA20SC2 | Unearthed Arcana: 2020 Subclasses: Part 2 | -| UA20SC3 | Unearthed Arcana: 2020 Subclasses: Part 3 | -| UA20SC4 | Unearthed Arcana: 2020 Subclasses: Part 4 | -| UA20SC5 | Unearthed Arcana: 2020 Subclasses: Part 5 | -| UA20SCR | Unearthed Arcana: 2020 Subclasses Revisited | -| UA20SMT | Unearthed Arcana: 2020 Spells and Magic Tattoos | -| UA21DO | Unearthed Arcana: 2021 Draconic Options | -| UA21FF | Unearthed Arcana: 2021 Folk of the Feywild | -| UA21GL | Unearthed Arcana: 2021 Gothic Lineages | -| UA21MoS | Unearthed Arcana: 2021 Mages of Strixhaven | -| UA21TotM | Unearthed Arcana: 2021 Travelers of the Multiverse | -| UA22GO | Unearthed Arcana: 2022 Giant Options | -| UA22HoK | Unearthed Arcana: 2022 Heroes of Krynn | -| UA22HoKR | Unearthed Arcana: 2022 Heroes of Krynn Revisited | -| UA22WotM | Unearthed Arcana: 2022 Wonders of the Multivers | -| UA3PE | Unearthed Arcana: Three-Pillar Experience | -| UAA | Unearthed Arcana: Artificer | -| UAAR | Unearthed Arcana: Artificer Revisited | -| UAATOSC | Unearthed Arcana: A Trio of Subclasses | -| UABAM | Unearthed Arcana: Barbarian and Monk | -| UABAP | Unearthed Arcana: Bard and Paladin | -| UABBC | Unearthed Arcana: Bard: Bard Colleges | -| UABPP | Unearthed Arcana: Barbarian Primal Paths | -| UACAM | Unearthed Arcana: Centaurs and Minotaurs | -| UACDD | Unearthed Arcana: Cleric: Divine Domains | -| UACDW | Unearthed Arcana: Cleric, Druid, and Wizard | -| UACFV | Unearthed Arcana: Class Feature Variants | -| UAD | Unearthed Arcana: Druid | -| UAEAG | Unearthed Arcana: Eladrin and Gith | -| UAEBB | Unearthed Arcana: Eberron | -| UAESR | Unearthed Arcana: Elf Subraces | -| UAF | Unearthed Arcana: Fighter | -| UAFFR | Unearthed Arcana: Feats for Races | -| UAFFS | Unearthed Arcana: Feats for Skills | -| UAFO | Unearthed Arcana: Fiendish Options | -| UAFRR | Unearthed Arcana: Fighter, Ranger, and Rogue | -| UAFRW | Unearthed Arcana: Fighter, Rogue, and Wizard | -| UAFT | Unearthed Arcana: Feats | -| UAGH | Unearthed Arcana: Gothic Heroes | -| UAGHI | Unearthed Arcana: Greyhawk Initiative | -| UAGSS | Unearthed Arcana: Giant Soul Sorcerer | -| UAKOO | Unearthed Arcana: Kits of Old | -| UALDR | Unearthed Arcana: Light, Dark, Underdark! | -| UAM | Unearthed Arcana: Monk | -| UAMAC | Unearthed Arcana: Mass Combat | -| UAMC | Unearthed Arcana: Modifying Classes | -| UAMDM | Unearthed Arcana: Modern Magic | -| UAOD | Unearthed Arcana: Order Domain | -| UAOSS | Unearthed Arcana: Of Ships and the Sea | -| UAP | Unearthed Arcana: Paladin | -| UAPCRM | Unearthed Arcana: Prestige Classes and Rune Magic | -| UAR | Unearthed Arcana: Ranger | -| UARAR | Unearthed Arcana: Ranger and Rogue | -| UARCO | Unearthed Arcana: Revised Class Options | -| UARSC | Unearthed Arcana: Revised Subclasses | -| UARoE | Unearthed Arcana: Races of Eberron | -| UARoR | Unearthed Arcana: Races of Ravnica | -| UAS | Unearthed Arcana: Sorcerer | -| UASAW | Unearthed Arcana: Sorcerer and Warlock | -| UASIK | Unearthed Arcana: Sidekicks | -| UASSP | Unearthed Arcana: Starter Spells | -| UATF | Unearthed Arcana: The Faithful | -| UATMC | Unearthed Arcana: The Mystic Class | -| UATOBM | Unearthed Arcana: That Old Black Magic | -| UATRR | Unearthed Arcana: The Ranger, Revised | -| UATSC | Unearthed Arcana: Three Subclasses | -| UAVR | Unearthed Arcana: Variant Rules | -| UAWA | Unearthed Arcana: Waterborne Adventures | -| UAWAW | Unearthed Arcana: Warlock and Wizard | -| UAWGE | Wayfinder's Guide to Eberron | -| UAWR | Unearthed Arcana: Wizard Revisited | -| US | Unwelcome Spirits | -| VD | Vecna Dossier | -| VEoR | Vecna: Eve of Ruin | -| VGM | Volo's Guide to Monsters | -| VNotEE | Vecna: Nest of the Eldritch Eye | -| VRGR | Van Richten's Guide to Ravenloft | -| WBtW | The Wild Beyond the Witchlight | -| WDH | Waterdeep: Dragon Heist | -| WDMM | Waterdeep: Dungeon of the Mad Mage | -| XGE | Xanathar's Guide to Everything | -| XMtS | X Marks the Spot | +### 5eTools Abbreviations to long name + +| Abbreviation | Long name | Type | +|--------------|-----------|-------| +| AAG | Astral Adventurer's Guide | book | +| AATM | Adventure Atlas: The Mortuary | book | +| AI | Acquisitions Incorporated | book | +| AL | Adventurers' League | book | +| ALCoS | Adventurers League: Curse of Strahd | reference | +| ALEE | Adventurers League: Elemental Evil | reference | +| ALRoD | Adventurers League: Rage of Demons | reference | +| AWM | Adventure with Muk | reference | +| AZfyT | A Zib for your Thoughts | adventure | +| AitFR | Adventures in the Forgotten Realms | reference | +| AitFR-AVT | Adventures in the Forgotten Realms: A Verdant Tomb | adventure | +| AitFR-DN | Adventures in the Forgotten Realms: Deepest Night | adventure | +| AitFR-FCD | Adventures in the Forgotten Realms: From Cyan Depths | adventure | +| AitFR-ISF | Adventures in the Forgotten Realms: In Scarlet Flames | adventure | +| AitFR-THP | Adventures in the Forgotten Realms: The Hidden Page | adventure | +| BAM | Boo's Astral Menagerie | book | +| BGDIA | Baldur's Gate: Descent Into Avernus | adventure | +| BGG | Bigby Presents: Glory of the Giants | book | +| BMT | The Book of Many Things | book | +| CM | Candlekeep Mysteries | adventure | +| CRCotN | Critical Role: Call of the Netherdeep | adventure | +| CoA | Chains of Asmodeus | adventure | +| CoS | Curse of Strahd | adventure | +| DC | Divine Contention | adventure | +| DD | Dangerous Designs | adventure | +| DIP | Dragon of Icespire Peak | adventure | +| DMG | Dungeon Master's Guide | book | +| DMTCRG | The Deck of Many Things: Card Reference Guide | book | +| DSotDQ | Dragonlance: Shadow of the Dragon Queen | adventure | +| DitLCoT | Descent into the Lost Caverns of Tsojcanth | adventure | +| DoD | Domains of Delight | book | +| DoDk | Dungeons of Drakkenheim | adventure | +| DoSI | Dragons of Stormwreck Isle | adventure | +| EEPC | Elemental Evil Player's Companion | reference | +| EET | Elemental Evil: Trinkets | reference | +| EFR | Eberron: Forgotten Relics | adventure | +| EGW | Explorer's Guide to Wildemount | book | +| EGW_DD | Dangerous Designs | reference | +| EGW_FS | Frozen Sick | reference | +| EGW_ToR | Tide of Retribution | reference | +| EGW_US | Unwelcome Spirits | reference | +| ERLW | Eberron: Rising from the Last War | book | +| ESK | Essentials Kit | reference | +| FS | Frozen Sick | adventure | +| FTD | Fizban's Treasury of Dragons | book | +| GGR | Guildmasters' Guide to Ravnica | book | +| GHLoE | Grim Hollow: Lairs of Etharis | adventure | +| GoS | Ghosts of Saltmarsh | adventure | +| GotSF | Giants of the Star Forge | adventure | +| HAT-LMI | Honor Among Thieves: Legendary Magic Items | reference | +| HAT-TG | Honor Among Thieves: Thieves' Gallery | book | +| HF | Heroes' Feast | book | +| HFDoMM | Heroes' Feast: The Deck of Many Morsels | reference | +| HFFotM | Heroes' Feast Flavors of the Multiverse | book | +| HFStCM | Heroes' Feast: Saving the Children's Menu | adventure | +| HWAitW | Humblewood: Adventure in the Wood | adventure | +| HWCS | Humblewood Campaign Setting | book | +| HftT | Hunt for the Thessalhydra | adventure | +| HoL | The House of Lament | adventure | +| HotDQ | Hoard of the Dragon Queen | adventure | +| IDRotF | Icewind Dale: Rime of the Frostmaiden | adventure | +| IMR | Infernal Machine Rebuild | adventure | +| JttRC | Journeys through the Radiant Citadel | adventure | +| KKW | Krenko's Way | adventure | +| KftGV | Keys from the Golden Vault | adventure | +| LK | Lightning Keep | adventure | +| LLK | Lost Laboratory of Kwalish | adventure | +| LMoP | Lost Mine of Phandelver | adventure | +| LR | Locathah Rising | adventure | +| LRDT | Red Dragon's Tale: A LEGO Adventure | adventure | +| LoX | Light of Xaryxis | adventure | +| MCV1SC | Monstrous Compendium Volume 1: Spelljammer Creatures | reference | +| MCV2DC | Monstrous Compendium Volume 2: Dragonlance Creatures | reference | +| MCV3MC | Monstrous Compendium Volume 3: Minecraft Creatures | reference | +| MCV4EC | Monstrous Compendium Volume 3: 4: Eldraine Creatures | book | +| MFF | Mordenkainen's Fiendish Folio | reference | +| MGELFT | Muk's Guide To Everything He Learned From Tasha | reference | +| MM | Monster Manual | book | +| MOT | Mythic Odysseys of Theros | book | +| MPMM | Mordenkainen Presents: Monsters of the Multiverse | book | +| MPP | Morte's Planar Parade | book | +| MTF | Mordenkainen's Tome of Foes | book | +| MaBJoV | Minsc and Boo's Journal of Villainy | book | +| MisMV1 | Misplaced Monsters: Volume 1 | reference | +| NRH | NERDS Restoring Harmony | reference | +| NRH-ASS | NERDS Restoring Harmony: A Sticky Situation | adventure | +| NRH-AT | NERDS Restoring Harmony: Adventure Together | adventure | +| NRH-AVitW | NERDS Restoring Harmony: A Voice in the Wilderness | adventure | +| NRH-AWoL | NERDS Restoring Harmony: A Web of Lies | adventure | +| NRH-CoI | NERDS Restoring Harmony: Circus of Illusions | adventure | +| NRH-TCMC | NERDS Restoring Harmony: The Candy Mountain Caper | adventure | +| NRH-TLT | NERDS Restoring Harmony: The Lost Tomb | adventure | +| OGA | One Grung Above | book | +| OoW | The Orrery of the Wanderer | adventure | +| OotA | Out of the Abyss | adventure | +| PHB | Player's Handbook | book | +| PSA | Plane Shift: Amonkhet | reference | +| PSD | Plane Shift: Dominaria | reference | +| PSI | Plane Shift: Innistrad | reference | +| PSK | Plane Shift: Kaladesh | reference | +| PSX | Plane Shift: Ixalan | reference | +| PSZ | Plane Shift: Zendikar | reference | +| PaBTSO | Phandelver and Below: The Shattered Obelisk | adventure | +| PaF | Puncheons and Flagons | book | +| PiP | Peril in Pinegrove | adventure | +| PotA | Princes of the Apocalypse | adventure | +| QftIS | Quests from the Infinite Staircase | adventure | +| RMBRE | The Lost Dungeon of Rickedness: Big Rick Energy | adventure | +| RMR | Dungeons & Dragons vs. Rick and Morty: Basic Rules | book | +| RoT | The Rise of Tiamat | adventure | +| RoTOS | The Rise of Tiamat Online Supplement | reference | +| RtG | Return to Glory | adventure | +| SAC | Sage Advice Compendium | book | +| SADS | Sapphire Anniversary Dice Set | reference | +| SAiS | Spelljammer: Adventures in Space | reference | +| SCAG | Sword Coast Adventurer's Guide | book | +| SCC | Strixhaven: A Curriculum of Chaos | book | +| SCC-ARiR | A Reckoning in Ruins | adventure | +| SCC-CK | Campus Kerfuffle | adventure | +| SCC-HfMT | Hunt for Mage Tower | adventure | +| SCC-TMM | The Magister's Masquerade | adventure | +| SCREEN | Dungeon Master's Screen | reference | +| SCREEN_DUNGEON_KIT | Dungeon Master's Screen: Dungeon Kit | reference | +| SCREEN_SPELLJAMMER | Dungeon Master's Screen: Spelljammer | reference | +| SCREEN_WILDERNESS_KIT | Dungeon Master's Screen: Wilderness Kit | reference | +| SDW | Sleeping Dragon's Wake | adventure | +| SKT | Storm King's Thunder | adventure | +| SLW | Storm Lord's Wrath | adventure | +| SatO | Sigil and the Outlands | book | +| ScoEE | Scions of Elemental Evil | adventure | +| SjA | Spelljammer Academy | adventure | +| TCE | Tasha's Cauldron of Everything | book | +| TD | Tarot Deck | book | +| TDCSR | Tal'Dorei Campaign Setting Reborn | book | +| TLK | The Lost Kenku | adventure | +| TTP | The Tortle Package | adventure | +| TftYP | Tales from the Yawning Portal | reference | +| TftYP-AtG | Tales from the Yawning Portal: Against the Giants | adventure | +| TftYP-DiT | Tales from the Yawning Portal: Dead in Thay | adventure | +| TftYP-TFoF | Tales from the Yawning Portal: The Forge of Fury | adventure | +| TftYP-THSoT | Tales from the Yawning Portal: The Hidden Shrine of Tamoachan | adventure | +| TftYP-TSC | Tales from the Yawning Portal: The Sunless Citadel | adventure | +| TftYP-ToH | Tales from the Yawning Portal: Tomb of Horrors | adventure | +| TftYP-WPM | Tales from the Yawning Portal: White Plume Mountain | adventure | +| ToA | Tomb of Annihilation | adventure | +| ToB1-2023 | Tome of Beasts 1 (2023 Edition) | book | +| ToD | Tyranny of Dragons | reference | +| ToFW | Turn of Fortune's Wheel | adventure | +| ToR | Tide of Retribution | adventure | +| UATMC | Unearthed Arcana: The Mystic Class | reference | +| US | Unwelcome Spirits | adventure | +| UtHftLH | Uni and the Hunt for the Lost Horn | adventure | +| VD | Vecna Dossier | reference | +| VEoR | Vecna: Eve of Ruin | adventure | +| VGM | Volo's Guide to Monsters | book | +| VNotEE | Vecna: Nest of the Eldritch Eye | adventure | +| VRGR | Van Richten's Guide to Ravenloft | book | +| WBtW | The Wild Beyond the Witchlight | adventure | +| WDH | Waterdeep: Dragon Heist | adventure | +| WDMM | Waterdeep: Dungeon of the Mad Mage | adventure | +| XDMG | Dungeon Master's Guide (2024) | book | +| XGE | Xanathar's Guide to Everything | book | +| XMM | Monster Manual (2024) | book | +| XMtS | X Marks the Spot | adventure | +| XPHB | Player's Handbook (2024) | book | +| XScreen | Dungeon Master's Screen (2024) | book | -### Alternate abbreviation mapping +### 5eTools Alternate abbreviation mapping You may see these abbreviations referenced in source material, this is how they map to sources listed above. @@ -278,103 +215,12 @@ You may see these abbreviations referenced in source material, this is how they | TYP_TSC | TftYP-TSC | | TYP_ToH | TftYP-ToHs | | TYP_WPM | TftYP-WPM | -| UA2020F | UA20F | -| UA2020Feats | UA20F | -| UA2020POR | UA20POR | -| UA2020PsionicOptionsRevisited | UA20POR | -| UA2020SC1 | UA20S1 | -| UA2020SC2 | UA20S2 | -| UA2020SC3 | UA20S3 | -| UA2020SC4 | UA20S4 | -| UA2020SC5 | UA20S5 | -| UA2020SCR | UA20SCR | -| UA2020SMT | UA20SMT | -| UA2020SpellsAndMagicTattoos | UA20SMT | -| UA2020SubclassesPt1 | UA20S1 | -| UA2020SubclassesPt2 | UA20S2 | -| UA2020SubclassesPt3 | UA20S3 | -| UA2020SubclassesPt4 | UA20S4 | -| UA2020SubclassesPt5 | UA20S5 | -| UA2020SubclassesRevisited | UA20SCR | -| UA2021DO | UA21DO | -| UA2021DraconicOptions | UA21DO | -| UA2021FF | UA21FF | -| UA2021FolkOfTheFeywild | UA21FF | -| UA2021GL | UA21GL | -| UA2021GothicLineages | UA21GL | -| UA2021MagesOfStrixhaven | UA21MoS | -| UA2021MoS | UA21MoS | -| UA2021TotM | UA21TotM | -| UA2021TravelersOfTheMultiverse | UA21TotM | -| UA2022GO | UA22GO | -| UA2022GiantOptions | UA22GO | -| UA2022HeroesOfKrynn | UA22HoK | -| UA2022HeroesOfKrynnRevisited | UA22HoKR | -| UA2022HoK | UA22HoK | -| UA2022HoKR | UA22HoKR | -| UA2022WondersOfTheMultiverse | UA22WotM | -| UA2022WotM | UA22WotM | -| UAATrioOfSubclasses | UAATOSC | -| UAArtificer | UAA | -| UAArtificerRevisited | UAAR | -| UABarbarianAndMonk | UABAM | -| UABarbarianPrimalPaths | UABPP | -| UABardAndPaladin | UABAP | -| UABardBardColleges | UABBC | -| UACentaursMinotaurs | UACAM | -| UAClassFeatureVariants | UACFV | -| UAClericDivineDomains | UACDD | -| UAClericDruidWizard | UACDW | -| UADruid | UAD | -| UAEberron | UAEBB | -| UAEladrinAndGith | UAEAG | -| UAElfSubraces | UAESR | -| UAFeats | UAFT | -| UAFeatsForRaces | UAFFR | -| UAFeatsForSkills | UAFFS | -| UAFiendishOptions | UAFO | -| UAFighter | UAF | -| UAFighterRangerRogue | UAFRR | -| UAFighterRogueWizard | UAFRW | -| UAGiantSoulSorcerer | UAGSS | -| UAGothicHeroes | UAGH | -| UAGreyhawkInitiative | UAGHI | -| UAKitsOfOld | UAKOO | -| UALightDarkUnderdark | UALDR | -| UAMassCombat | UAMAC | -| UAModernMagic | UAMDM | -| UAModifyingClasses | UAMC | -| UAMonk | UAM | -| UAOfShipsAndSea | UAOSS | -| UAOrderDomain | UAOD | -| UAPaladin | UAP | -| UAPrestigeClassesRunMagic | UAPCRM | -| UARacesOfEberron | UARoE | -| UARacesOfRavnica | UARoR | -| UARanger | UAR | -| UARangerAndRogue | UARAR | -| UARevisedClassOptions | UARCO | -| UARevisedSubclasses | UARSC | -| UASidekicks | UASIK | -| UASorcerer | UAS | -| UASorcererAndWarlock | UASAW | -| UAStarterSpells | UASSP | -| UAThatOldBlackMagic | UATOBM | -| UATheFaithful | UATF | | UATheMysticClass | UATMC | -| UATheRangerRevised | UATRR | -| UAThreePillarExperience | UA3PE | -| UAThreeSubclasses | UATSC | -| UAVariantRules | UAVR | -| UAWarlockAndWizard | UAWAW | -| UAWaterborneAdventures | UAWA | -| UAWayfindersGuideToEberron | UAWGE | -| UAWizardRevisited | UAWR | ## Source name mapping for Pf2eTools -### Abbreviations to long name +### Pf2eTools Abbreviations to long name | Abbreviation | Long name | |--------------|-----------| @@ -439,6 +285,7 @@ You may see these abbreviations referenced in source material, this is how they | GW2 | Gatewalkers #2: They Watched the Stars | | GW3 | Gatewalkers #3: Dreamers of the Nameless Spires | | HPD | Hero Point Deck | +| HStR | Head-Shot the Rot | | LOACLO | Lost Omens: Absalom, City of Lost Omens | | LOAG | Lost Omens: Ancestry Guide | | LOCG | Lost Omens: Character Guide | @@ -494,6 +341,7 @@ You may see these abbreviations referenced in source material, this is how they | TaL | Torment and Legacy | | TiO | Troubles in Otari | | ToK | Threshold of Knowledge | +| TotT0 | Triumph of the Tusk Player's Guide | | WoW0 | Wardens of Wildwood Player's Guide | | WoW1 | Wardens of Wildwood #1: Pactbreaker | | WoW2 | Wardens of Wildwood #2: Severed at the Root | @@ -504,7 +352,7 @@ You may see these abbreviations referenced in source material, this is how they | WtD4 | Wake the Dead #4 | | WtD5 | Wake the Dead #5 | -### Alternate abbreviation mapping +### Pf2eTools Alternate abbreviation mapping You may see these abbreviations referenced in source material, this is how they map to sources listed above. diff --git a/docs/templates/ImageRef.md b/docs/templates/ImageRef.md index e32ec5c37..516662e7f 100644 --- a/docs/templates/ImageRef.md +++ b/docs/templates/ImageRef.md @@ -2,7 +2,11 @@ Create links to referenced images. -The general form of a markdown image link is: `![alt text](vaultPath "title")`. You can also use anchors to position the image within the page, which creates links that look like this: `![alt text](vaultPath#anchor "title")`. ## Anchor Tags +The general form of a markdown image link is: `![alt text](vaultPath "title")`. +You can also use anchors to position the image within the page, +which creates links that look like this: `![alt text](vaultPath#anchor "title")`. + +## Anchor Tags Anchor tags are used to position images within a page and are styled with CSS. Examples: @@ -19,11 +23,15 @@ Anchor tags are used to position images within a page and are styled with CSS. E ### embeddedLink -Return an embedded markdown link to the image, using an optional anchor tag to position the image in the page. For example: `{resource.image.getEmbeddedLink("symbol")}` +Return an embedded markdown link to the image, using an optional +anchor tag to position the image in the page. +For example: `{resource.image.getEmbeddedLink("symbol")}` -If the title is longer than 50 characters: `![{resource.shortTitle}]({resource.vaultPath}#anchor "{resource.title}")`, +If the title is longer than 50 characters: +`![{resource.shortTitle}]({resource.vaultPath}#anchor "{resource.title}")`, -If the title is 50 characters or less: `![{resource.title}]({resource.vaultPath}#anchor)`, +If the title is 50 characters or less: +`![{resource.title}]({resource.vaultPath}#anchor)`, Links will be generated using "center" as the anchor by default. diff --git a/docs/templates/QuteBase.md b/docs/templates/QuteBase.md index 1376a5789..c0835efe4 100644 --- a/docs/templates/QuteBase.md +++ b/docs/templates/QuteBase.md @@ -2,11 +2,12 @@ Defines attributes inherited by other Qute templates. -Notes created from `QuteBase` (or a derivative) will use a specific template for the type. For example, `QuteBackground` will use `background2md.txt`. +Notes created from `QuteBase` (or a derivative) will use a specific template +for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -21,6 +22,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/QuteNote.md b/docs/templates/QuteNote.md index c6203f389..c73e53c84 100644 --- a/docs/templates/QuteNote.md +++ b/docs/templates/QuteNote.md @@ -1,12 +1,14 @@ # QuteNote -Common attributes for simple notes. THese attributes are more often used by books, adventures, rules, etc. +Common attributes for simple notes. THese attributes are more +often used by books, adventures, rules, etc. -Notes created from `QuteNote` (or a derivative) will look for a template named `note2md.txt` by default. +Notes created from `QuteNote` (or a derivative) will look for a template +named `note2md.txt` by default. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -21,6 +23,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/README.md b/docs/templates/README.md index 6cb795fda..07369388a 100644 --- a/docs/templates/README.md +++ b/docs/templates/README.md @@ -1,8 +1,10 @@ # Qute Template Reference -The following pages describe attributes that you can use to customize generated output in Qute templates. +The following pages describe attributes that you can use to customize +generated output in Qute templates. -Use a `resource.` prefix to access these attributes unless otherwise noted. For example, `resource.title`. +Use a `resource.` prefix to access these attributes unless otherwise noted. +For example, `resource.title`. For more information about Qute, see the [Qute guide](https://quarkus.io/guides/qute). @@ -17,5 +19,6 @@ For more information about Qute, see the [Qute guide](https://quarkus.io/guides/ - [NamedText](NamedText.md): Holder of a name or category and associated descriptive text. - [QuteBase](QuteBase.md): Defines attributes inherited by other Qute templates. - [QuteNote](QuteNote.md): Common attributes for simple notes. +- [Reprinted](Reprinted.md): A simple record to hold the name and source of a reprinted item. - [SourceAndPage](SourceAndPage.md): A representation of a source and page number. -- [TtrpgTemplateExtension](TtrpgTemplateExtension.md) +- [TtrpgTemplateExtension](TtrpgTemplateExtension.md): Qute template extensions for TTRPG data. diff --git a/docs/templates/Reprinted.md b/docs/templates/Reprinted.md new file mode 100644 index 000000000..63fc301b7 --- /dev/null +++ b/docs/templates/Reprinted.md @@ -0,0 +1,16 @@ +# Reprinted + +A simple record to hold the name and source of a reprinted item. + +## Attributes + +[name](#name), [source](#source) + + +### name + +Name of the reprinted item + +### source + +Primary source of the reprinted item diff --git a/docs/templates/SourceAndPage.md b/docs/templates/SourceAndPage.md index fccca3023..6dd6d5780 100644 --- a/docs/templates/SourceAndPage.md +++ b/docs/templates/SourceAndPage.md @@ -1,6 +1,7 @@ # SourceAndPage -A representation of a source and page number. This attribute will print itself nicely if you don't reference sub-attributes. +A representation of a source and page number. This attribute will print +itself nicely if you don't reference sub-attributes. ## Attributes diff --git a/docs/templates/TtrpgTemplateExtension.md b/docs/templates/TtrpgTemplateExtension.md index 88bf6a1ec..8c3983b12 100644 --- a/docs/templates/TtrpgTemplateExtension.md +++ b/docs/templates/TtrpgTemplateExtension.md @@ -1,4 +1,41 @@ # TtrpgTemplateExtension +Qute template extensions for TTRPG data. + +Use these functions to help render TTRPG data in Qute templates. ## Attributes + +[asBonus](#asbonus), [capitalized](#capitalized), [join](#join), [joinConjunct](#joinconjunct), [pluralizeLabel](#pluralizelabel), [prefixSpace](#prefixspace) + + +### asBonus + +Return the value formatted with a bonus with a +/- prefix. Example: `{perception.asBonus}` + +### capitalized + +Return the string capitalized. Example: `{resource.name.capitalized}` + +### join + +Return the given collection converted into a string and joined using the specified joiner. + +Example: `{resource.components.join(", ")}` + +### joinConjunct + +Return the given list joined into a single string, using a different delimiter for the last element. + +Example: `{resource.components.joinConjunct(", ", " or ")}` + +### pluralizeLabel + +Return the string pluralized based on the size of the collection. + +Example: `{resource.name.pluralized(resource.components)}` + +### prefixSpace + +Return the given object as a string, with a space prepended if it's non-empty and non-null. +Example: `{resource.name.prefixSpace}` diff --git a/docs/templates/dnd5e/AbilityScores.md b/docs/templates/dnd5e/AbilityScores.md index 03bdaab30..e3aeca9ad 100644 --- a/docs/templates/dnd5e/AbilityScores.md +++ b/docs/templates/dnd5e/AbilityScores.md @@ -4,8 +4,11 @@ Used to describe a monster, object or vehicle's ability scores. -If referenced as a unit (ignoring inner attributes), it will render ability scores as a `|` separated list of values, in `STR,DEX,CON,INT,WIS,CHA` order, for example: - `10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)`. +If referenced as a unit (ignoring inner attributes), it will render ability scores as +a `|` separated list of values, in `STR,DEX,CON,INT,WIS,CHA` order. + +For example: +`10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)`. ## Attributes diff --git a/docs/templates/dnd5e/AcHp.md b/docs/templates/dnd5e/AcHp.md index be893b0d1..3d807f474 100644 --- a/docs/templates/dnd5e/AcHp.md +++ b/docs/templates/dnd5e/AcHp.md @@ -2,7 +2,9 @@ 5eTools armor class and hit points attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly. ## Attributes @@ -27,8 +29,11 @@ Hit points (number or —) ### hpDiceRoller -Hit points as a dice roller formula: \`dice: 1d20+7|text(37)\` (\`1d20+7\`) +Hit points as a dice roller formula: +\`dice: 1d20+7|text(37)\` (\`1d20+7\`) ### hpText -Additional hit point text. In the case of summoned creatures, this will contain notes for how hit points should be calculated relative to the player's modifiers. +Additional hit point text. +In the case of summoned creatures, this will contain notes for how hit points +should be calculated relative to the player's modifiers. diff --git a/docs/templates/dnd5e/ImmuneResist.md b/docs/templates/dnd5e/ImmuneResist.md index 5d770eaa9..734db01ad 100644 --- a/docs/templates/dnd5e/ImmuneResist.md +++ b/docs/templates/dnd5e/ImmuneResist.md @@ -2,7 +2,9 @@ 5eTools vulnerabilities, resistances, immunities, and condition immunities -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly. ## Attributes diff --git a/docs/templates/dnd5e/QuteBackground.md b/docs/templates/dnd5e/QuteBackground.md index cf1fd5617..6f4862c74 100644 --- a/docs/templates/dnd5e/QuteBackground.md +++ b/docs/templates/dnd5e/QuteBackground.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### fluffImages @@ -29,6 +29,10 @@ Note name Formatted text listing other prerequisite conditions (optional) +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteBastion/Hireling.md b/docs/templates/dnd5e/QuteBastion/Hireling.md new file mode 100644 index 000000000..cbbf261ad --- /dev/null +++ b/docs/templates/dnd5e/QuteBastion/Hireling.md @@ -0,0 +1,28 @@ +# Hireling + +Hireling information. Either exact or min must be present. + +## Attributes + +[description](#description), [exact](#exact), [max](#max), [min](#min), [space](#space) + + +### description + +Formatted string description of the hirelings for a Bastion + +### exact + +Exact number of hirelings (either exact or min) + +### max + +Maximum number of hirelings (optional) + +### min + +Minimum number of hirelings (either exact or min) + +### space + +Size of bastion space required for these hirelings (optional) diff --git a/docs/templates/dnd5e/QuteBastion/README.md b/docs/templates/dnd5e/QuteBastion/README.md new file mode 100644 index 000000000..c3dfa3b16 --- /dev/null +++ b/docs/templates/dnd5e/QuteBastion/README.md @@ -0,0 +1,84 @@ +# QuteBastion + +5eTools background attributes (`bastion2md.txt`). + +Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). + +## Attributes + +[fluffImages](#fluffimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath) + + +### fluffImages + +List of images for this bastion (as [ImageRef](../../ImageRef.md), optional) + +### hasSections + +True if the content (text) contains sections + +### hirelingDescription + +Hirelings as a descriptive string (if hirelings is present) + +### hirelings + +List of possible hirelings this bastion can have (as [Hireling](Hireling.md), +optional) + +### labeledSource + +Formatted string describing the content's source(s): `_Source: _` + +### level + +Bastion level (optional) + +### name + +Note name + +### orders + +Bastion orders (optional) + +### prerequisite + +Formatted text listing other prerequisite conditions (optional) + +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + +### source + +String describing the content's source(s) + +### sourceAndPage + +Book sources as list of [SourceAndPage](../../SourceAndPage.md) + +### space + +List of possible spaces this bastion can occupy (as [Space](Space.md), +optional) + +### spaceDescription + +Space as a descriptive string (if space is present) + +### tags + +Collected tags for inclusion in frontmatter + +### text + +Formatted text. For most templates, this is the bulk of the content. + +### type + +Type + +### vaultPath + +Path to this note in the vault diff --git a/docs/templates/dnd5e/QuteBastion/Space.md b/docs/templates/dnd5e/QuteBastion/Space.md new file mode 100644 index 000000000..b69cbe6ca --- /dev/null +++ b/docs/templates/dnd5e/QuteBastion/Space.md @@ -0,0 +1,31 @@ +# Space + + +## Attributes + +[cost](#cost), [description](#description), [name](#name), [prevSpace](#prevspace), [squares](#squares), [time](#time) + + +### cost + +Cost (GP) of building a bastion of this size + +### description + +Formatted string description of the space required for (or occupied by) a Bastion + +### name + +Name of this size/space + +### prevSpace + +Previous space to enlarge from (optional) + +### squares + +Maximum number of 5-foot squares a bastion this size can occupy + +### time + +Time to construct a bastion of this size diff --git a/docs/templates/dnd5e/QuteClass.md b/docs/templates/dnd5e/QuteClass.md index 6373c2be0..3b8c35414 100644 --- a/docs/templates/dnd5e/QuteClass.md +++ b/docs/templates/dnd5e/QuteClass.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classProgression](#classprogression), [hasSections](#hassections), [hitDice](#hitdice), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[classProgression](#classprogression), [hasSections](#hassections), [hitDice](#hitdice), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### classProgression @@ -37,6 +37,10 @@ Formatted text section describing how to multiclass with this class Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteDeck/README.md b/docs/templates/dnd5e/QuteDeck/README.md index c26afbf7d..337c335bd 100644 --- a/docs/templates/dnd5e/QuteDeck/README.md +++ b/docs/templates/dnd5e/QuteDeck/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[cardBack](#cardback), [cards](#cards), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[cardBack](#cardback), [cards](#cards), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### cardBack @@ -29,6 +29,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteDeity.md b/docs/templates/dnd5e/QuteDeity.md index f80d886b1..6386943c1 100644 --- a/docs/templates/dnd5e/QuteDeity.md +++ b/docs/templates/dnd5e/QuteDeity.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[alignment](#alignment), [altNames](#altnames), [category](#category), [domains](#domains), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [source](#source), [sourceAndPage](#sourceandpage), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) +[alignment](#alignment), [altNames](#altnames), [category](#category), [domains](#domains), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) ### alignment @@ -49,6 +49,10 @@ Pantheon to which this deity belongs: Celtic Province of this deity: Discovery, Luck, Storms, Travel, ... +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteFeat.md b/docs/templates/dnd5e/QuteFeat.md index dee77e2db..07c4bf4a2 100644 --- a/docs/templates/dnd5e/QuteFeat.md +++ b/docs/templates/dnd5e/QuteFeat.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -29,6 +29,10 @@ Note name Formatted text listing other prerequisite conditions (optional) +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteHazard.md b/docs/templates/dnd5e/QuteHazard.md index 400b7d1f4..7637565dc 100644 --- a/docs/templates/dnd5e/QuteHazard.md +++ b/docs/templates/dnd5e/QuteHazard.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -25,6 +25,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteItem/README.md b/docs/templates/dnd5e/QuteItem/README.md index b258f87ac..081d054b1 100644 --- a/docs/templates/dnd5e/QuteItem/README.md +++ b/docs/templates/dnd5e/QuteItem/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [source](#source), [sourceAndPage](#sourceandpage), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) +[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [source](#source), [sourceAndPage](#sourceandpage), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) ### armorClass @@ -15,11 +15,11 @@ Changes to armor class provided by the item, if applicable ### cost -Cost of the item (gp, sp, cp). Usually missing for magic items. +Cost of the item (gp, sp, cp). Optional. ### costCp -Cost of the item (cp) as number. Usually missing for magic items. +Cost of the item (cp) as number. Optional. ### damage @@ -35,7 +35,7 @@ Formatted string of item details. Will include some combination of tier, rarity, ### fluffImages -List of images for this item (as [ImageRef](../../ImageRef.md)) +List of images for this item as [ImageRef](../../ImageRef.md) ### hasSections @@ -45,6 +45,10 @@ True if the content (text) contains sections Formatted string describing the content's source(s): `_Source: _` +### mastery + +Formatted string listing applicable item mastery (with links to rules if the source is present) + ### name Note name @@ -61,6 +65,14 @@ Formatted string listing item's properties (with links to rules if the source is Item's range, if applicable +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + +### rootVariant + +Detailed information about this item as [Variant](Variant.md) + ### source String describing the content's source(s) @@ -77,6 +89,10 @@ True if the item imposes a stealth penalty, if applicable Strength requirement as a numerical value, if applicable +### subtypeString + +Formatted string of additional item attributes. Optional. + ### tags Collected tags for inclusion in frontmatter @@ -87,15 +103,17 @@ Formatted text. For most templates, this is the bulk of the content. ### variantAliases -String: list (`- "alias"`) of aliases for variants. Use in YAML frontmatter with `aliases:`. Will return an empty string if there are no variants +String: list (`- "alias"`) of aliases for variants. Use in YAML frontmatter with `aliases:`. +Will return an empty string if there are no variants ### variantSectionLinks -String: list (`- [name](#anchor)`) of links to variant sections. Will return an empty string if there are no variants. +String: list (`- [name](#anchor)`) of links to variant sections. +Will return an empty string if there are no variants. ### variants -List of magic item variants (as [Variant](Variant.md), optional) +List of magic item variants as [Variant](Variant.md). Optional. ### vaultPath diff --git a/docs/templates/dnd5e/QuteItem/Variant.md b/docs/templates/dnd5e/QuteItem/Variant.md index a191fb1c1..beceaff35 100644 --- a/docs/templates/dnd5e/QuteItem/Variant.md +++ b/docs/templates/dnd5e/QuteItem/Variant.md @@ -3,12 +3,29 @@ ## Attributes -[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [weight](#weight) +[age](#age), [ammo](#ammo), [armorClass](#armorclass), [attunement](#attunement), [baseItem](#baseitem), [cost](#cost), [costCp](#costcp), [cursed](#cursed), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [firearm](#firearm), [focus](#focus), [focusType](#focustype), [mastery](#mastery), [masteryList](#masterylist), [name](#name), [poison](#poison), [poisonTypes](#poisontypes), [prerequisite](#prerequisite), [properties](#properties), [propertiesList](#propertieslist), [range](#range), [rarity](#rarity), [staff](#staff), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tattoo](#tattoo), [tier](#tier), [type](#type), [typeAlt](#typealt), [weaponCategory](#weaponcategory), [weight](#weight), [wondrous](#wondrous) +### age + +Age/Era of item. Optional. Known values: futuristic, industrial, modern, renaissance, victorian. + +### ammo + +True if this is ammunition + ### armorClass -Changes to armor class provided by the item, if applicable +Changes to armor class provided by the item. Optional. + +### attunement + +Attunement requirements. Optional. One of: required, optional, prerequisites/conditions (implies +required). + +### baseItem + +Markdown link to base item. Optional. ### cost @@ -18,38 +35,115 @@ Cost of the item (gp, sp, cp). Usually missing for magic items. Cost of the item (cp) as number. Usually missing for magic items. +### cursed + +True if this is a cursed item + ### damage -One-handed Damage string, if applicable. Contains dice formula and damage type +One-handed Damage string. Contains dice formula and damage type. Optional. ### damage2h -Two-handed Damage string, if applicable. Contains dice formula and damage type +Two-handed Damage string. Contains dice formula and damage type. Optional. + +### detail + +Formatted string of item details. Will include some combination of tier, rarity, category, and attunement + +### firearm + +True if this is a firearm + +### focus + +True if this is a spellcasting focus. + +### focusType + +Spellcasting focus type. Optional. One of: "arcane", "druid", "holy", and/or a list of required classes. + +### mastery + +Formatted string listing applicable item mastery (with links to rules if the source is present) + +### masteryList + +List of item mastery that apply to this item. ### name -Name of the variant +Name of the variant. + +### poison + +True if this is a poison. + +### poisonTypes + +Poison type(s). Optional. ### prerequisite -Formatted text listing other prerequisite conditions (optional) +Formatted text listing other prerequisite conditions. Optional. ### properties Formatted string listing item's properties (with links to rules if the source is present) +### propertiesList + +List of item's properties (with links to rules if the source is present). + ### range -Item's range, if applicable +Item's range. Optional. + +### rarity + +Item rarity. Optional. One of: "none": mundane items; "unknown (magic)": miscellaneous magical items; +"unknown": miscellaneous mundane items; "varies": item groups or magic variants. + +### staff + +True if this is a staff ### stealthPenalty -True if the item imposes a stealth penalty, if applicable +True if the item imposes a stealth penalty. Optional. ### strengthRequirement -Strength requirement as a numerical value, if applicable +Strength requirement as a numerical value. Optional. + +### subtypeString + +Item subtype string. Optional. + +### tattoo + +True if this is a tattoo + +### tier + +Item tier. Optional. One of: "minor", "major". + +### type + +Item type + +### typeAlt + +Alternate item type. Optional. + +### weaponCategory + +Weapon category. Optional. One of: "simple", "martial". ### weight -Weight of the item (pounds) as a decimal value +Weight of the item (pounds) as a decimal value. + +### wondrous + +True if this is a wondrous item diff --git a/docs/templates/dnd5e/QuteMonster/README.md b/docs/templates/dnd5e/QuteMonster/README.md index e495c703e..8b1bff4b2 100644 --- a/docs/templates/dnd5e/QuteMonster/README.md +++ b/docs/templates/dnd5e/QuteMonster/README.md @@ -6,16 +6,19 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml -A minimal YAML snippet containing monster attributes required by the Initiative Tracker plugin. Use this in frontmatter. +A minimal YAML snippet containing monster attributes required by the +Initiative Tracker plugin. Use this in frontmatter. ### 5eStatblockYaml -Complete monster attributes in the format required by the Fantasy statblock plugin. Uses double-quoted syntax to deal with a variety of characters occuring in trait descriptions. Usable in frontmatter or Fantasy Statblock code blocks. +Complete monster attributes in the format required by the Fantasy statblock plugin. +Uses double-quoted syntax to deal with a variety of characters occuring in +trait descriptions. Usable in frontmatter or Fantasy Statblock code blocks. ### ac @@ -27,7 +30,7 @@ Creature AC and HP as [AcHp](../AcHp.md) ### acText -See [AcHp#acText](../AcHp.md#acText) +See [AcHp#acText](../AcHp.md#actext) ### action @@ -47,7 +50,7 @@ List of source books (abbreviated name). Fantasy statblock uses this list. ### conditionImmune -See [ImmuneResist#conditionImmune](../ImmuneResist.md#conditionImmune) +See [ImmuneResist#conditionImmune](../ImmuneResist.md#conditionimmune) ### cr @@ -75,7 +78,7 @@ True if the content (text) contains sections ### hitDice -See [AcHp#hitDice](../AcHp.md#hitDice) +See [AcHp#hitDice](../AcHp.md#hitdice) ### hp @@ -83,7 +86,7 @@ See [AcHp#hp](../AcHp.md#hp) ### hpText -See [AcHp#hpText](../AcHp.md#hpText) +See [AcHp#hpText](../AcHp.md#hptext) ### immune @@ -111,7 +114,9 @@ Creature legendary traits as a list of [NamedText](../../NamedText.md) ### legendaryGroup -Map of grouped legendary traits (Lair Actions, Regional Effects, etc.). The key the group name, and the value is a list of [NamedText](../../NamedText.md). +Map of grouped legendary traits (Lair Actions, Regional Effects, etc.). The key the group name, and the value is a list +of +[NamedText](../../NamedText.md). ### legendaryGroupLink @@ -133,6 +138,10 @@ Proficiency bonus (modifier) Creature reactions as a list of [NamedText](../../NamedText.md) +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### resist See [ImmuneResist#resist](../ImmuneResist.md#resist) @@ -143,7 +152,8 @@ Creature saving throws and skill modifiers as [SavesAndSkills](SavesAndSkills.md ### savingThrows -String representation of saving throws. Equivalent to `{resource.savesSkills.saves}` +String representation of saving throws. +Equivalent to `{resource.savesSkills.saves}` ### scores @@ -159,7 +169,8 @@ Creature size (capitalized) ### skills -String representation of saving throws. Equivalent to `{resource.savesSkills.skills}` +String representation of saving throws. +Equivalent to `{resource.savesSkills.skills}` ### source diff --git a/docs/templates/dnd5e/QuteMonster/SavesAndSkills.md b/docs/templates/dnd5e/QuteMonster/SavesAndSkills.md index df15fa8a8..d54ce31da 100644 --- a/docs/templates/dnd5e/QuteMonster/SavesAndSkills.md +++ b/docs/templates/dnd5e/QuteMonster/SavesAndSkills.md @@ -9,8 +9,10 @@ ### saveMap -Creature saving throws as a map of key-value pairs. Iterate over all map entries to display the values: - `{#each resource.savesSkills.saveMap}**{it.key}** {it.value}{/each}` +Creature saving throws as a map of key-value pairs. +Iterate over all map entries to display the values: + +`{#each resource.savesSkills.saveMap}**{it.key}** {it.value}{/each}` ### saves @@ -18,8 +20,10 @@ Creature saving throws as a list: Constitution +6, Intelligence +8 ### skillMap -Creature skills as a map of key-value pairs. Iterate over all map entries to display the values: - `{#each resource.savesSkills.skillMap}**{it.key}** {it.value}{/each}` +Creature skills as a map of key-value pairs. +Iterate over all map entries to display the values: + +`{#each resource.savesSkills.skillMap}**{it.key}** {it.value}{/each}` ### skills diff --git a/docs/templates/dnd5e/QuteMonster/Spellcasting.md b/docs/templates/dnd5e/QuteMonster/Spellcasting.md index adf21ff53..48f21f73e 100644 --- a/docs/templates/dnd5e/QuteMonster/Spellcasting.md +++ b/docs/templates/dnd5e/QuteMonster/Spellcasting.md @@ -2,18 +2,24 @@ 5eTools creature spellcasting attributes. -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: - ``` - {#for spellcasting in resource.spellcasting} - {spellcasting} - {/for} - ``` - or, using `{#each}` instead: - ``` - {#each resource.spellcasting} - {it} - {/each} - ``` +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. + +To use it, reference it directly: + +```md +{#for spellcasting in resource.spellcasting} +{spellcasting} +{/for} +``` + +or, using `{#each}` instead: + +```md +{#each resource.spellcasting} +{it} +{/each} +``` ## Attributes @@ -45,7 +51,8 @@ Name: "Spellcasting" or "Innate Spellcasting" ### spells -Map: key = spell level, value: spell level information as [Spells](Spells.md) +Map: key = spell level, value: spell level information as +[Spells](Spells.md) ### will diff --git a/docs/templates/dnd5e/QuteObject.md b/docs/templates/dnd5e/QuteObject.md index 3a9db8bb5..8fae1d303 100644 --- a/docs/templates/dnd5e/QuteObject.md +++ b/docs/templates/dnd5e/QuteObject.md @@ -6,16 +6,19 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [resist](#resist), [scores](#scores), [senses](#senses), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml -A minimal YAML snippet containing object attributes required by the Initiative Tracker plugin. Use this in frontmatter. +A minimal YAML snippet containing object attributes required by the +Initiative Tracker plugin. Use this in frontmatter. ### 5eStatblockYaml -Complete object attributes in the format required by the Fantasy statblock plugin. Uses double-quoted syntax to deal with a variety of characters occuring in trait descriptions. Usable in frontmatter or Fantasy Statblock code blocks. +Complete object attributes in the format required by the Fantasy statblock plugin. +Uses double-quoted syntax to deal with a variety of characters occuring in +trait descriptions. Usable in frontmatter or Fantasy Statblock code blocks. ### ac @@ -27,7 +30,7 @@ Object AC and HP as [AcHp](AcHp.md) ### acText -See [AcHp#acText](AcHp.md#acText) +See [AcHp#acText](AcHp.md#actext) ### action @@ -39,7 +42,7 @@ List of source books (abbreviated name). Fantasy statblock uses this list. ### conditionImmune -See [ImmuneResist#conditionImmune](ImmuneResist.md#conditionImmune) +See [ImmuneResist#conditionImmune](ImmuneResist.md#conditionimmune) ### creatureType @@ -55,7 +58,7 @@ True if the content (text) contains sections ### hitDice -See [AcHp#hitDice](AcHp.md#hitDice) +See [AcHp#hitDice](AcHp.md#hitdice) ### hp @@ -63,7 +66,7 @@ See [AcHp#hp](AcHp.md#hp) ### hpText -See [AcHp#hpText](AcHp.md#hpText) +See [AcHp#hpText](AcHp.md#hptext) ### immune @@ -89,6 +92,10 @@ Note name Object type +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### resist See [ImmuneResist#resist](ImmuneResist.md#resist) diff --git a/docs/templates/dnd5e/QutePsionic.md b/docs/templates/dnd5e/QutePsionic.md index 3816c2cbf..258e320e7 100644 --- a/docs/templates/dnd5e/QutePsionic.md +++ b/docs/templates/dnd5e/QutePsionic.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[focus](#focus), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) +[focus](#focus), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) ### focus @@ -29,6 +29,10 @@ Psionic mode as list of [NamedText](../NamedText.md) Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteRace.md b/docs/templates/dnd5e/QuteRace.md index 7ac60c0ce..150c9b433 100644 --- a/docs/templates/dnd5e/QuteRace.md +++ b/docs/templates/dnd5e/QuteRace.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [description](#description), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) +[ability](#ability), [description](#description), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) ### ability @@ -33,6 +33,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### size Size: Small or Medium diff --git a/docs/templates/dnd5e/QuteReward.md b/docs/templates/dnd5e/QuteReward.md index 7cfc1ce0c..bbe12babe 100644 --- a/docs/templates/dnd5e/QuteReward.md +++ b/docs/templates/dnd5e/QuteReward.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [detail](#detail), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[ability](#ability), [detail](#detail), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### ability @@ -29,6 +29,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### signatureSpells Formatted text describing sigature spells. Not commonly used. diff --git a/docs/templates/dnd5e/QuteSpell.md b/docs/templates/dnd5e/QuteSpell.md index 82d269d8b..418bb5fe7 100644 --- a/docs/templates/dnd5e/QuteSpell.md +++ b/docs/templates/dnd5e/QuteSpell.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [ritual](#ritual), [school](#school), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) +[classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) ### classList @@ -49,6 +49,10 @@ Note name Formatted: spell range +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### ritual true for ritual spells diff --git a/docs/templates/dnd5e/QuteSubclass.md b/docs/templates/dnd5e/QuteSubclass.md index d5a08f54f..8d70d7f29 100644 --- a/docs/templates/dnd5e/QuteSubclass.md +++ b/docs/templates/dnd5e/QuteSubclass.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classProgression](#classprogression), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [source](#source), [sourceAndPage](#sourceandpage), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[classProgression](#classprogression), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### classProgression @@ -37,6 +37,10 @@ Markdown link to the parent class Source of the parent class (abbreviation) +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteVehicle/README.md b/docs/templates/dnd5e/QuteVehicle/README.md index 185ba222e..142e5a78e 100644 --- a/docs/templates/dnd5e/QuteVehicle/README.md +++ b/docs/templates/dnd5e/QuteVehicle/README.md @@ -2,13 +2,15 @@ 5eTools vehicle attributes (`vehicle2md.txt`) -Several different types of vehicle use this template, including: Ship, spelljammer, infernal war manchie, objects and creatures. They can have very different properties. Treat most as optional. +Several different types of vehicle use this template, including: +Ship, spelljammer, infernal war manchie, objects and creatures. +They can have very different properties. Treat most as optional. Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[action](#action), [fluffImages](#fluffimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) +[action](#action), [fluffImages](#fluffimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) ### action @@ -55,9 +57,14 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### scores -Object ability scores as [AbilityScores](../AbilityScores.md) Used by Ship, Infernal War Machine, Creature, Object +Object ability scores as [AbilityScores](../AbilityScores.md) +Used by Ship, Infernal War Machine, Creature, Object ### shipCrewCargoPace @@ -65,7 +72,8 @@ Ship capacity and pace attributes as [ShipCrewCargoPace](ShipCrewCargoPace.md). ### shipSections -Ship sections and traits as [ShipAcHp](ShipAcHp.md) (hull, sails, oars, .. ) +Ship sections and traits as [ShipAcHp](ShipAcHp.md) (hull, sails, +oars, .. ) ### sizeDimension diff --git a/docs/templates/dnd5e/QuteVehicle/ShipAcHp.md b/docs/templates/dnd5e/QuteVehicle/ShipAcHp.md index 082a2598e..bcea945d9 100644 --- a/docs/templates/dnd5e/QuteVehicle/ShipAcHp.md +++ b/docs/templates/dnd5e/QuteVehicle/ShipAcHp.md @@ -2,7 +2,9 @@ 5eTools vehicle armor class and hit points attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly. ## Attributes @@ -35,11 +37,14 @@ Hit points (number or —) ### hpDiceRoller -Hit points as a dice roller formula: \`dice: 1d20+7|text(37)\` (\`1d20+7\`) +Hit points as a dice roller formula: +\`dice: 1d20+7|text(37)\` (\`1d20+7\`) ### hpText -Additional hit point text. In the case of summoned creatures, this will contain notes for how hit points should be calculated relative to the player's modifiers. +Additional hit point text. +In the case of summoned creatures, this will contain notes for how hit points +should be calculated relative to the player's modifiers. ### mt diff --git a/docs/templates/dnd5e/QuteVehicle/ShipCrewCargoPace.md b/docs/templates/dnd5e/QuteVehicle/ShipCrewCargoPace.md index c459aaae1..11255e9b0 100644 --- a/docs/templates/dnd5e/QuteVehicle/ShipCrewCargoPace.md +++ b/docs/templates/dnd5e/QuteVehicle/ShipCrewCargoPace.md @@ -2,13 +2,16 @@ 5eTools Ship crew, cargo, and pace attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: - ``` - {#if resource.shipCrewCargoPace} - {resource.shipCrewCargoPace} - {/if} - ``` +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: + +```md +{#if resource.shipCrewCargoPace} +{resource.shipCrewCargoPace} +{/if} +``` ## Attributes @@ -41,7 +44,8 @@ Passenger capacity (number) ### shipPace -Ship pace (number, mph) Ship speed is pace * 10 (*Special Travel Pace*, DMG p242). +Ship pace (number, mph) +Ship speed is pace * 10 (*Special Travel Pace*, DMG p242). ### speedPace diff --git a/docs/templates/dnd5e/QuteVehicle/ShipSection.md b/docs/templates/dnd5e/QuteVehicle/ShipSection.md index 4d2f8cc68..46bfc7319 100644 --- a/docs/templates/dnd5e/QuteVehicle/ShipSection.md +++ b/docs/templates/dnd5e/QuteVehicle/ShipSection.md @@ -2,7 +2,9 @@ 5eTools vehicle sections -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly. ## Attributes diff --git a/docs/templates/dnd5e/README.md b/docs/templates/dnd5e/README.md index bd6cc5dc8..35e4c2545 100644 --- a/docs/templates/dnd5e/README.md +++ b/docs/templates/dnd5e/README.md @@ -1,25 +1,62 @@ # 5eTools templates - Qute templates for generating content from 5eTools data. + +Qute templates for generating content from 5eTools data. ## References - [AbilityScores](AbilityScores.md): 5eTools Ability Score attributes. - [AcHp](AcHp.md): 5eTools armor class and hit points attributes + +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. - [ImmuneResist](ImmuneResist.md): 5eTools vulnerabilities, resistances, immunities, and condition immunities + +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. - [QuteBackground](QuteBackground.md): 5eTools background attributes (`background2md.txt`). +- [QuteBastion](QuteBastion/README.md): 5eTools background attributes (`bastion2md.txt`). - [QuteClass](QuteClass.md): 5eTools class attributes (`class2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteDeck](QuteDeck/README.md): 5eTools deck attributes (`deck2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteDeity](QuteDeity.md): 5eTools deity attributes (`deity2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteFeat](QuteFeat.md): 5eTools feat and optional feat attributes (`feat2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteHazard](QuteHazard.md): 5eTools hazard attributes (`hazard2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteItem](QuteItem/README.md): 5eTools item attributes (`item2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteMonster](QuteMonster/README.md): 5eTools creature attributes (`monster2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteObject](QuteObject.md): 5eTools object attributes (`object2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QutePsionic](QutePsionic.md): 5eTools psionic talent attributes (`psionic2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteRace](QuteRace.md): 5eTools race attributes (`race2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteReward](QuteReward.md): 5eTools reward attributes (`reward2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteSpell](QuteSpell.md): 5eTools spell attributes (`spell2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteSubclass](QuteSubclass.md): 5eTools subclass attributes (`subclass2md.txt`) + +Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteVehicle](QuteVehicle/README.md): 5eTools vehicle attributes (`vehicle2md.txt`) + +Several different types of vehicle use this template, including: +Ship, spelljammer, infernal war manchie, objects and creatures. - [Tools5eQuteBase](Tools5eQuteBase.md): Attributes for notes that are generated from the 5eTools data. - [Tools5eQuteNote](Tools5eQuteNote.md): Attributes for notes that are generated from the 5eTools data. diff --git a/docs/templates/dnd5e/Tools5eQuteBase.md b/docs/templates/dnd5e/Tools5eQuteBase.md index cb76b2ab1..ea39f7144 100644 --- a/docs/templates/dnd5e/Tools5eQuteBase.md +++ b/docs/templates/dnd5e/Tools5eQuteBase.md @@ -1,12 +1,14 @@ # Tools5eQuteBase -Attributes for notes that are generated from the 5eTools data. This is a trivial extension of [QuteBase](../QuteBase.md). +Attributes for notes that are generated from the 5eTools data. +This is a trivial extension of [QuteBase](../QuteBase.md). -Notes created from `Tools5eQuteBase` will use a specific template for the type. For example, `QuteBackground` will use `background2md.txt`. +Notes created from `Tools5eQuteBase` will use a specific template +for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -21,6 +23,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/Tools5eQuteNote.md b/docs/templates/dnd5e/Tools5eQuteNote.md index cc02992e9..2b7e27efc 100644 --- a/docs/templates/dnd5e/Tools5eQuteNote.md +++ b/docs/templates/dnd5e/Tools5eQuteNote.md @@ -1,12 +1,13 @@ # Tools5eQuteNote -Attributes for notes that are generated from the 5eTools data. This is a trivial extension of [QuteNote](../QuteNote.md). +Attributes for notes that are generated from the 5eTools data. +This is a trivial extension of [QuteNote](../QuteNote.md). Notes created from `Tools5eQuteNote` will use the `note2md.txt` template. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -21,6 +22,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/Pf2eQuteBase.md b/docs/templates/pf2e/Pf2eQuteBase.md index 247f95483..a087f6f54 100644 --- a/docs/templates/pf2e/Pf2eQuteBase.md +++ b/docs/templates/pf2e/Pf2eQuteBase.md @@ -1,12 +1,14 @@ # Pf2eQuteBase -Attributes for notes that are generated from the Pf2eTools data. This is a trivial extension of [QuteBase](../QuteBase.md). +Attributes for notes that are generated from the Pf2eTools data. +This is a trivial extension of [QuteBase](../QuteBase.md). -Notes created from `Pf2eQuteBase` will use a specific template for the type. For example, `QuteBackground` will use `background2md.txt`. +Notes created from `Pf2eQuteBase` will use a specific template +for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -21,6 +23,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/Pf2eQuteNote.md b/docs/templates/pf2e/Pf2eQuteNote.md index 6a4701bfc..61b5ecd6d 100644 --- a/docs/templates/pf2e/Pf2eQuteNote.md +++ b/docs/templates/pf2e/Pf2eQuteNote.md @@ -1,12 +1,14 @@ # Pf2eQuteNote -Attributes for notes that are generated from the Pf2eTools data. This is a trivial extension of [QuteNote](../QuteNote.md). +Attributes for notes that are generated from the Pf2eTools data. +This is a trivial extension of [QuteNote](../QuteNote.md). -Notes created from `Pf2eQuteNote` will use the `note2md.txt` template unless otherwise noted. Folder index notes use `index2md.txt`. +Notes created from `Pf2eQuteNote` will use the `note2md.txt` template +unless otherwise noted. Folder index notes use `index2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -21,6 +23,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/QuteAbility.md b/docs/templates/pf2e/QuteAbility.md index 7d0843275..16d29dbc0 100644 --- a/docs/templates/pf2e/QuteAbility.md +++ b/docs/templates/pf2e/QuteAbility.md @@ -2,15 +2,18 @@ Pf2eTools Ability attributes (`ability2md.txt` or `inline-ability2md.txt`). -Abilities are rendered both standalone and inline (as an admonition block). The default template can render both. It contains some special syntax to handle the inline case. +Abilities are rendered both standalone and inline (as an admonition block). +The default template can render both. It contains some special syntax to handle +the inline case. -Use `%%--` to mark the end of the preamble (frontmatter and other leading content only appropriate to the standalone case). +Use `%%--` to mark the end of the preamble (frontmatter and +other leading content only appropriate to the standalone case). Extension of [Pf2eQuteNote](Pf2eQuteNote.md) ## Attributes -[activity](#activity), [bareTraitList](#baretraitlist), [components](#components), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasActivity](#hasactivity), [hasAttributes](#hasattributes), [hasDetails](#hasdetails), [hasEffect](#haseffect), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [note](#note), [prerequisites](#prerequisites), [range](#range), [reference](#reference), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[activity](#activity), [bareTraitList](#baretraitlist), [components](#components), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasActivity](#hasactivity), [hasAttributes](#hasattributes), [hasDetails](#hasdetails), [hasEffect](#haseffect), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [note](#note), [prerequisites](#prerequisites), [range](#range), [reference](#reference), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### activity @@ -31,11 +34,13 @@ The cost of using this ability ### embedded -True if this ability is embedded in another note (admonition block). When this is true, the `inline-ability` template is used. +True if this ability is embedded in another note (admonition block). +When this is true, the `inline-ability` template is used. ### frequency -[QuteDataFrequency](QuteDataFrequency.md). How often this ability can be used/activated. Use directly to get a formatted string. +[QuteDataFrequency](QuteDataFrequency.md). +How often this ability can be used/activated. Use directly to get a formatted string. ### hasActivity @@ -43,11 +48,16 @@ True if an activity (with text), components, or traits are present. ### hasAttributes -True if hasActivity is true, hasEffect is true or cost is present. In other words, this is true if a list of attributes could have been rendered. Use this to test for the end of those attributes (add whitespace or a special character ahead of ability text) +True if hasActivity is true, hasEffect is true or cost is present. +In other words, this is true if a list of attributes could have been rendered. + +Use this to test for the end of those attributes (add whitespace or a special +character ahead of ability text) ### hasDetails -True if the ability is a short, one-line name and description. Use this to test to choose between a detailed or simple rendering. +True if the ability is a short, one-line name and description. +Use this to test to choose between a detailed or simple rendering. ### hasEffect @@ -75,12 +85,16 @@ Formatted string. Prerequisites before this ability can be activated or taken. ### range -[QuteDataRange](QuteDataRange.md). The targeting range for this ability. +[QuteDataRange](QuteDataRange/README.md). The targeting range for this ability. ### reference A formatted string which is a link to the base ability that this ability references. Embedded only. +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### requirements Formatted string. Requirements for activating this ability @@ -107,7 +121,8 @@ Formatted text. For most templates, this is the bulk of the content. ### traits -Collection of trait links. Use `{#for}` or `{#each}` to iterate over the collection. See [traitList](#traitlist) or [bareTraitList](#baretraitlist). +Collection of trait links. Use `{#for}` or `{#each}` to iterate over the collection. +See [traitList](#traitlist) or [bareTraitList](#baretraitlist). ### trigger diff --git a/docs/templates/pf2e/QuteAbilityOrAffliction.md b/docs/templates/pf2e/QuteAbilityOrAffliction.md new file mode 100644 index 000000000..6f0ff55f2 --- /dev/null +++ b/docs/templates/pf2e/QuteAbilityOrAffliction.md @@ -0,0 +1,21 @@ +# QuteAbilityOrAffliction + +A union type which is either a [QuteAbility](QuteAbility.md) +or a [QuteAffliction](QuteAffliction/README.md). + +Use [isAbility](#ability) +and [isAffliction](#affliction) +to tell whether it's an ability or an affliction. + +## Attributes + +[ability](#ability), [affliction](#affliction) + + +### ability + +Returns true if this object is a [QuteAbility](QuteAbility.md) + +### affliction + +Returns true if this object is a [QuteAffliction](QuteAffliction/README.md) diff --git a/docs/templates/pf2e/QuteAction/ActionType.md b/docs/templates/pf2e/QuteAction/ActionType.md index dcce0d1b5..a2078e659 100644 --- a/docs/templates/pf2e/QuteAction/ActionType.md +++ b/docs/templates/pf2e/QuteAction/ActionType.md @@ -2,7 +2,9 @@ Pf2eTools Action type attributes. -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference this attribute directly: `{resource.actionType}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference this attribute directly: `{resource.actionType}`. ## Attributes diff --git a/docs/templates/pf2e/QuteAction/README.md b/docs/templates/pf2e/QuteAction/README.md index 69bfc2c13..ebee7051d 100644 --- a/docs/templates/pf2e/QuteAction/README.md +++ b/docs/templates/pf2e/QuteAction/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[actionType](#actiontype), [activity](#activity), [aliases](#aliases), [basic](#basic), [cost](#cost), [frequency](#frequency), [hasSections](#hassections), [item](#item), [labeledSource](#labeledsource), [name](#name), [prerequisites](#prerequisites), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[actionType](#actiontype), [activity](#activity), [aliases](#aliases), [basic](#basic), [cost](#cost), [frequency](#frequency), [hasSections](#hassections), [item](#item), [labeledSource](#labeledsource), [name](#name), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### actionType @@ -31,7 +31,8 @@ The cost of using this action ### frequency -[QuteDataFrequency](../QuteDataFrequency.md). How often this action can be used/activated. Use directly to get a formatted string. +[QuteDataFrequency](../QuteDataFrequency.md). +How often this action can be used/activated. Use directly to get a formatted string. ### hasSections @@ -53,6 +54,10 @@ Note name Prerequisite trait or characteristic for performing this action +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### requirements Situational requirements for performing this action diff --git a/docs/templates/pf2e/QuteAffliction/QuteAfflictionSave.md b/docs/templates/pf2e/QuteAffliction/QuteAfflictionSave.md index db5b71188..a55a37aae 100644 --- a/docs/templates/pf2e/QuteAffliction/QuteAfflictionSave.md +++ b/docs/templates/pf2e/QuteAffliction/QuteAfflictionSave.md @@ -7,10 +7,6 @@ Affliction saving throw [notes](#notes), [save](#save), [value](#value) -### value - -The DC of the saving throw - ### notes Any notes relating to the saving throw @@ -18,3 +14,7 @@ Any notes relating to the saving throw ### save The type of save associated with the throw e.g. Fortitude + +### value + +The DC of the saving throw diff --git a/docs/templates/pf2e/QuteAffliction/QuteAfflictionStage.md b/docs/templates/pf2e/QuteAffliction/QuteAfflictionStage.md index db918e9e7..6f64a70c1 100644 --- a/docs/templates/pf2e/QuteAffliction/QuteAfflictionStage.md +++ b/docs/templates/pf2e/QuteAffliction/QuteAfflictionStage.md @@ -7,10 +7,10 @@ Pf2eTools affliction stage attributes. [duration](#duration), [text](#text) -### text - -Formatted text. Affliction stage - ### duration Formatted text. Affliction duration + +### text + +Formatted text. Affliction stage diff --git a/docs/templates/pf2e/QuteAffliction.md b/docs/templates/pf2e/QuteAffliction/README.md similarity index 70% rename from docs/templates/pf2e/QuteAffliction.md rename to docs/templates/pf2e/QuteAffliction/README.md index c9319b1ae..d41e46373 100644 --- a/docs/templates/pf2e/QuteAffliction.md +++ b/docs/templates/pf2e/QuteAffliction/README.md @@ -2,11 +2,11 @@ Pf2eTools Affliction attributes (inline/embedded, `inline-affliction2md.txt`) -Extension of [Pf2eQuteNote](Pf2eQuteNote.md) +Extension of [Pf2eQuteNote](../Pf2eQuteNote.md) ## Attributes -[aliases](#aliases), [category](#category), [effect](#effect), [hasSections](#hassections), [isEmbedded](#isembedded), [labeledSource](#labeledsource), [level](#level), [maxDuration](#maxduration), [name](#name), [notes](#notes), [onset](#onset), [savingThrow](#savingthrow), [source](#source), [sourceAndPage](#sourceandpage), [stages](#stages), [tags](#tags), [temptedCurse](#temptedcurse), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[aliases](#aliases), [category](#category), [effect](#effect), [hasSections](#hassections), [isEmbedded](#isembedded), [labeledSource](#labeledsource), [level](#level), [maxDuration](#maxduration), [name](#name), [notes](#notes), [onset](#onset), [reprintOf](#reprintof), [savingThrow](#savingthrow), [source](#source), [sourceAndPage](#sourceandpage), [stages](#stages), [tags](#tags), [temptedCurse](#temptedcurse), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### aliases @@ -53,9 +53,14 @@ Any additional notes associated with the affliction. Formatted text. Maximum duration of the infliction. +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### savingThrow -The saving throw required to not contract or advance the affliction as [QuteAfflictionSave](QuteAffliction/QuteAfflictionSave.md) +The saving throw required to not contract or advance the affliction as +[QuteAfflictionSave](QuteAfflictionSave.md) ### source @@ -63,11 +68,12 @@ String describing the content's source(s) ### sourceAndPage -Book sources as list of [SourceAndPage](../SourceAndPage.md) +Book sources as list of [SourceAndPage](../../SourceAndPage.md) ### stages -Affliction stages: map of name to stage data as [QuteAfflictionStage](QuteAffliction/QuteAfflictionStage.md) +Affliction stages: map of name to stage data as +[QuteAfflictionStage](QuteAfflictionStage.md) ### tags diff --git a/docs/templates/pf2e/QuteArchetype.md b/docs/templates/pf2e/QuteArchetype.md index c70b8bfbc..70c88229c 100644 --- a/docs/templates/pf2e/QuteArchetype.md +++ b/docs/templates/pf2e/QuteArchetype.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[benefits](#benefits), [dedicationLevel](#dedicationlevel), [feats](#feats), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[benefits](#benefits), [dedicationLevel](#dedicationlevel), [feats](#feats), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### benefits @@ -30,6 +30,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/QuteBackground.md b/docs/templates/pf2e/QuteBackground.md index 70ec868a9..f881724b7 100644 --- a/docs/templates/pf2e/QuteBackground.md +++ b/docs/templates/pf2e/QuteBackground.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### hasSections @@ -21,6 +21,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/QuteBook/BookInfo.md b/docs/templates/pf2e/QuteBook/BookInfo.md index 299b0e56d..af4479bd2 100644 --- a/docs/templates/pf2e/QuteBook/BookInfo.md +++ b/docs/templates/pf2e/QuteBook/BookInfo.md @@ -2,7 +2,9 @@ Pf2eTools book information -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.actionType}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.actionType}`. ## Attributes diff --git a/docs/templates/pf2e/QuteBook/README.md b/docs/templates/pf2e/QuteBook/README.md index 195db2ae2..85ed0273d 100644 --- a/docs/templates/pf2e/QuteBook/README.md +++ b/docs/templates/pf2e/QuteBook/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteNote](../Pf2eQuteNote.md) ## Attributes -[aliases](#aliases), [bookInfo](#bookinfo), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[aliases](#aliases), [bookInfo](#bookinfo), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### aliases @@ -29,6 +29,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/QuteCreature.md b/docs/templates/pf2e/QuteCreature.md deleted file mode 100644 index 5dd9a0c79..000000000 --- a/docs/templates/pf2e/QuteCreature.md +++ /dev/null @@ -1,100 +0,0 @@ -# QuteCreature - -Pf2eTools Creature attributes (`creature2md.txt`) - -Use `%%--` to mark the end of the preamble (frontmatter and other leading content only appropriate to the standalone case). - -Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - -## Attributes - -[abilities](#abilities), [abilityMods](#abilitymods), [aliases](#aliases), [attacks](#attacks), [defenses](#defenses), [description](#description), [hasSections](#hassections), [items](#items), [labeledSource](#labeledsource), [languages](#languages), [level](#level), [name](#name), [perception](#perception), [senses](#senses), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) - - -### abilities - -The creature's abilities, as a [CreatureAbilities](QuteCreature/CreatureAbilities.md). - -### abilityMods - -Ability modifiers as a map of (name, modifier) - -### aliases - -Aliases for this note (optional) - -### attacks - -The creature's attacks, as a list of [QuteInlineAttack](QuteInlineAttack.md) - -### defenses - -Defenses (AC, saves, etc) as [QuteDataDefenses](QuteDataDefenses.md) - -### description - -Short creature description (optional) - -### hasSections - -True if the content (text) contains sections - -### items - -Items held by the creature as a list of strings - -### labeledSource - -Formatted string describing the content's source(s): `_Source: _` - -### languages - -Languages as [CreatureLanguages](QuteCreature/CreatureLanguages.md) - -### level - -Creature level (number, optional) - -### name - -Note name - -### perception - -Creature perception (number, optional) - -### senses - -Senses as a list of [CreatureSense](QuteCreature/CreatureSense.md) - -### skills - -Skill bonuses as [CreatureSkills](QuteCreature/CreatureSkills.md) - -### source - -String describing the content's source(s) - -### sourceAndPage - -Book sources as list of [SourceAndPage](../SourceAndPage.md) - -### speed - -The creature's speed, as an [QuteDataSpeed](QuteDataSpeed.md) - -### tags - -Collected tags for inclusion in frontmatter - -### text - -Formatted text. For most templates, this is the bulk of the content. - -### traits - -Collection of traits (decorated links, optional) - -### vaultPath - -Path to this note in the vault diff --git a/docs/templates/pf2e/QuteCreature/CreatureAbilities.md b/docs/templates/pf2e/QuteCreature/CreatureAbilities.md index cf8a99ffb..eb92bf7c7 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureAbilities.md +++ b/docs/templates/pf2e/QuteCreature/CreatureAbilities.md @@ -1,20 +1,23 @@ # CreatureAbilities -A creature's abilities, split into the section of the statblock where they should be displayed. Each section is a list of [QuteAbilityOrAffliction](../QuteAbilityOrAffliction.md). Using an entry in one of these lists directly will give you a pre-formatted ability according to the embedded template defined for [QuteAbility](../QuteAbility.md) or [QuteAffliction](../QuteAffliction.md) as appropriate. +A creature's abilities, split into the section of the statblock where they should be displayed. Each section is +a list of [QuteAbilityOrAffliction](../QuteAbilityOrAffliction.md). Using an entry in one of these lists directly +will give you a pre-formatted ability according to the embedded template defined for [QuteAbility](../QuteAbility.md) or +[QuteAffliction](../QuteAffliction/README.md) as appropriate. ## Attributes [bottom](#bottom), [middle](#middle), [top](#top) -### top +### bottom -Abilities which should be displayed in the top section of the statblock +Abilities which should be displayed in the bottom section of the statblock ### middle Abilities which should be displayed in the middle section of the statblock -### bottom +### top -Abilities which should be displayed in the bottom section of the statblock +Abilities which should be displayed in the top section of the statblock diff --git a/docs/templates/pf2e/QuteCreature/CreatureLanguages.md b/docs/templates/pf2e/QuteCreature/CreatureLanguages.md index 601124153..d02b74653 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureLanguages.md +++ b/docs/templates/pf2e/QuteCreature/CreatureLanguages.md @@ -1,12 +1,17 @@ # CreatureLanguages -The languages and language features known by a creature. Example default output:
Common, Sylvan; telepathy 100ft; knows any language the summoner does
+The languages and language features known by a creature. Example default output: +`Common, Sylvan; telepathy 100ft; knows any language the summoner does` ## Attributes [abilities](#abilities), [languages](#languages), [notes](#notes) +### abilities + +Language-related abilities (optional) + ### languages Languages known (optional) @@ -14,7 +19,3 @@ Languages known (optional) ### notes Language-related notes (optional) - -### abilities - -Language-related abilities (optional) diff --git a/docs/templates/pf2e/QuteCreature/CreatureRitualCasting.md b/docs/templates/pf2e/QuteCreature/CreatureRitualCasting.md index 81580fe14..c2428d8cd 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureRitualCasting.md +++ b/docs/templates/pf2e/QuteCreature/CreatureRitualCasting.md @@ -7,10 +7,6 @@ Information about a type of ritual casting available to this creature. [dc](#dc), [ranks](#ranks), [tradition](#tradition) -### tradition - -The tradition for these rituals - ### dc The spell save DC for these rituals @@ -18,3 +14,7 @@ The spell save DC for these rituals ### ranks The ritual ranks, as a list of [CreatureSpells](CreatureSpells.md) + +### tradition + +The tradition for these rituals diff --git a/docs/templates/pf2e/QuteCreature/CreatureSense.md b/docs/templates/pf2e/QuteCreature/CreatureSense.md index 8f3981a28..3c1d14543 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureSense.md +++ b/docs/templates/pf2e/QuteCreature/CreatureSense.md @@ -1,6 +1,6 @@ # CreatureSense -A creature's senses. Example default output:
tremorsense (imprecise) 20ft
+A creature's senses. Example default output: `tremorsense (imprecise) 20ft` ## Attributes @@ -11,10 +11,10 @@ A creature's senses. Example default output:
tremorsense (imprecise The name of the sense (required, string) -### type - -The type of the sense - e.g. precise, imprecise (optional, string) - ### range The range of the sense (optional, integer) + +### type + +The type of the sense - e.g. precise, imprecise (optional, string) diff --git a/docs/templates/pf2e/QuteCreature/CreatureSkills.md b/docs/templates/pf2e/QuteCreature/CreatureSkills.md index ea955fe53..c3aebd13c 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureSkills.md +++ b/docs/templates/pf2e/QuteCreature/CreatureSkills.md @@ -1,16 +1,21 @@ # CreatureSkills -A creature's skill information. Example default output:
Athletics +10, Cult Lore +10 (lore on their cult), Stealth +10 (+12 in forests); Some skill note
+A creature's skill information. Example default output: + +```md +Athletics +10, Cult Lore +10 (lore on their cult), Stealth +10 (+12 in forests); Some skill note +``` ## Attributes [notes](#notes), [skills](#skills) -### skills - -Skill bonuses for the creature, as a list of [QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md) - ### notes Notes for the creature's skills (list of strings, optional) + +### skills + +Skill bonuses for the creature, as a list of +[QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md) diff --git a/docs/templates/pf2e/QuteCreature/CreatureSpellReference.md b/docs/templates/pf2e/QuteCreature/CreatureSpellReference.md index 4447bc0db..d208f9ede 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureSpellReference.md +++ b/docs/templates/pf2e/QuteCreature/CreatureSpellReference.md @@ -1,23 +1,28 @@ # CreatureSpellReference -A spell known by the creature.
[shadow siphon](#) (acid only) (×2)
+A spell known by the creature. + +```md +[shadow siphon](#) (acid only) (×2) +``` ## Attributes [amount](#amount), [link](#link), [name](#name), [notes](#notes) -### name +### amount -The name of the spell +The number of casts available for this spell. A value of 0 represents an at will spell. Use +[CreatureSpellReference#formattedAmount](#formattedamount) to get this as a formatted string. ### link A formatted link to the spell's note, or just the spell's name if we couldn't get a link. -### amount +### name -The number of casts available for this spell. A value of 0 represents an at will spell. Use [CreatureSpellReference#formattedAmount()](CreatureSpellReference.md#formattedAmount()) to get this as a formatted string. +The name of the spell ### notes diff --git a/docs/templates/pf2e/QuteCreature/CreatureSpellcasting.md b/docs/templates/pf2e/QuteCreature/CreatureSpellcasting.md index 4135199b0..bc740e289 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureSpellcasting.md +++ b/docs/templates/pf2e/QuteCreature/CreatureSpellcasting.md @@ -4,41 +4,57 @@ Information about a type of spellcasting available to this creature. ## Attributes -[attackBonus](#attackbonus), [constantRanks](#constantranks), [customName](#customname), [dc](#dc), [focusPoints](#focuspoints), [notes](#notes), [preparation](#preparation), [ranks](#ranks), [tradition](#tradition) +[attackBonus](#attackbonus), [constantRanks](#constantranks), [customName](#customname), [dc](#dc), [focusPoints](#focuspoints), [formattedStats](#formattedstats), [name](#name), [notes](#notes), [preparation](#preparation), [ranks](#ranks), [tradition](#tradition) -### customName +### attackBonus -A custom name for this set of spells, e.g. "Champion Devotion Spells". Use [CreatureSpellcasting#name()](CreatureSpellcasting.md#name()) to get a name which takes this into account if it exists. +The spell attack bonus for these spells (integer) -### preparation +### constantRanks -The type of preparation for these spells, as a [SpellcastingPreparation](SpellcastingPreparation.md) +The constant spells for each rank, as a list of [CreatureSpells](CreatureSpells.md) -### tradition +### customName -The tradition for these spells, as a [SpellcastingTradition](SpellcastingTradition.md) +A custom name for this set of spells, e.g. "Champion Devotion Spells". Use +[CreatureSpellcasting#name](#name) to get a name which takes this into account +if it exists. + +### dc + +The spell save DC for these spells (integer) ### focusPoints -The number of focus points available to this creature for these spells. Present only if these are focus spells. +The number of focus points available to this creature for these spells. Present only if these +are focus spells. -### attackBonus +### formattedStats -The spell attack bonus for these spells (integer) +Stats for this kind of spellcasting, including the DC, attack bonus, and any focus points. -### dc +```md +DC 20, attack +25, 2 Focus Points +``` -The spell save DC for these spells (integer) +### name + +The name for this set of spells. This is either the custom name, or derived from the tradition and +preparation - e.g. "Occult Prepared Spells", or "Divine Innate Spells". ### notes Any notes associated with these spells +### preparation + +The type of preparation for these spells, as a [SpellcastingPreparation](SpellcastingPreparation.md) + ### ranks The spells for each rank, as a list of [CreatureSpells](CreatureSpells.md). -### constantRanks +### tradition -The constant spells for each rank, as a list of [CreatureSpells](CreatureSpells.md) +The tradition for these spells, as a [SpellcastingTradition](SpellcastingTradition.md) diff --git a/docs/templates/pf2e/QuteCreature/CreatureSpells.md b/docs/templates/pf2e/QuteCreature/CreatureSpells.md index ce80b94d6..eb4633ec0 100644 --- a/docs/templates/pf2e/QuteCreature/CreatureSpells.md +++ b/docs/templates/pf2e/QuteCreature/CreatureSpells.md @@ -1,20 +1,32 @@ # CreatureSpells -A collection of spells with some additional information.
**Cantrips (9th)** [daze](#), [shadow siphon](#) (acid only) (×2)
**4th** [confusion](#), [phantasmal killer](#) (2 slots)
+A collection of spells with some additional information. -## Attributes +```md +**Cantrips (9th)** [daze](#), [shadow siphon](#) (acid only) (×2) +``` -[cantripRank](#cantriprank), [cantrips](#cantrips), [knownRank](#knownrank), [slots](#slots), [spells](#spells) +```md +**4th** [confusion](#), [phantasmal killer](#) (2 slots) +``` +## Attributes -### knownRank +[cantripRank](#cantriprank), [cantrips](#cantrips), [knownRank](#knownrank), [slots](#slots), [spells](#spells) -The rank that these spells are known at (0 for cantrips). May be absent for rituals. ### cantripRank The rank that these spells are auto-heightened to. Present only for cantrips. +### cantrips + +True if these are cantrip spells + +### knownRank + +The rank that these spells are known at (0 for cantrips). May be absent for rituals. + ### slots The number of slots available for these spells. Not present for constant spells or rituals. diff --git a/docs/templates/pf2e/QuteCreature/README.md b/docs/templates/pf2e/QuteCreature/README.md index 2ffdc1fbc..2bca8690f 100644 --- a/docs/templates/pf2e/QuteCreature/README.md +++ b/docs/templates/pf2e/QuteCreature/README.md @@ -6,12 +6,13 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[abilities](#abilities), [abilityMods](#abilitymods), [aliases](#aliases), [attacks](#attacks), [defenses](#defenses), [description](#description), [hasSections](#hassections), [items](#items), [labeledSource](#labeledsource), [languages](#languages), [level](#level), [name](#name), [perception](#perception), [ritualCasting](#ritualcasting), [senses](#senses), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[abilities](#abilities), [abilityMods](#abilitymods), [aliases](#aliases), [attacks](#attacks), [defenses](#defenses), [description](#description), [hasSections](#hassections), [items](#items), [labeledSource](#labeledsource), [languages](#languages), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [ritualCasting](#ritualcasting), [senses](#senses), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### abilities -The creature's abilities, as a [CreatureAbilities](CreatureAbilities.md). +The creature's abilities, as a +[CreatureAbilities](CreatureAbilities.md). ### abilityMods @@ -23,11 +24,11 @@ Aliases for this note (optional) ### attacks -The creature's attacks, as a list of [QuteInlineAttack](../QuteInlineAttack.md) +The creature's attacks, as a list of [QuteInlineAttack](../QuteInlineAttack/README.md) ### defenses -Defenses (AC, saves, etc) as [QuteDataDefenses](../QuteDataDefenses.md) +Defenses (AC, saves, etc) as [QuteDataDefenses](../QuteDataDefenses/README.md) ### description @@ -61,6 +62,10 @@ Note name Creature perception (number, optional) +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### ritualCasting The creature's ritual casting capabilities, as a list of [CreatureRitualCasting](CreatureRitualCasting.md) diff --git a/docs/templates/pf2e/QuteDataActivity.md b/docs/templates/pf2e/QuteDataActivity.md index bf758d114..0dc16d25f 100644 --- a/docs/templates/pf2e/QuteDataActivity.md +++ b/docs/templates/pf2e/QuteDataActivity.md @@ -1,24 +1,28 @@ # QuteDataActivity -Pf2eTools activity attributes. This attribute will render itself as a formatted link:
 [textGlyph](rulesPath "glyph.title") 
+Pf2eTools activity attributes. This attribute will render itself as a formatted link: + +
+[textGlyph](rulesPath "glyph.title")
+
## Attributes [glyph](#glyph), [rulesPath](#rulespath), [text](#text), [textGlyph](#textglyph) -### text - -The text associated with the action - may be null. - ### glyph icon/image representing this activity as a [ImageRef](../ImageRef.md) -### textGlyph - -A textual representation of the glyph, used as the link text - ### rulesPath The path which leads to an explanation of this particular activity + +### text + +The text associated with the action - may be null. + +### textGlyph + +A textual representation of the glyph, used as the link text diff --git a/docs/templates/pf2e/QuteDataArmorClass.md b/docs/templates/pf2e/QuteDataArmorClass.md index d707edf17..d54c33816 100644 --- a/docs/templates/pf2e/QuteDataArmorClass.md +++ b/docs/templates/pf2e/QuteDataArmorClass.md @@ -1,17 +1,21 @@ # QuteDataArmorClass -Pf2eTools armor class attributes. Default representation example: +Pf2eTools armor class attributes. +Default representation example: + +```md **AC** 15 (10 with mage armor) note ability +``` ## Attributes [abilities](#abilities), [alternateValues](#alternatevalues), [notes](#notes), [value](#value) -### value +### abilities -The AC value +Any AC related abilities ### alternateValues @@ -21,6 +25,6 @@ Alternate AC values as a map of (condition, AC value) Any notes associated with the AC e.g. "with mage armor" -### abilities +### value -Any AC related abilities +The AC value diff --git a/docs/templates/pf2e/QuteDataDefenses.md b/docs/templates/pf2e/QuteDataDefenses.md deleted file mode 100644 index 4fa768702..000000000 --- a/docs/templates/pf2e/QuteDataDefenses.md +++ /dev/null @@ -1,39 +0,0 @@ -# QuteDataDefenses - -Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard. Example: - -- **AC** 23 (33 with mage armor); **Fort** +15, **Ref** +12, **Will** +10 -- **Floor Hardness** 18, **Floor HP** 72 (BT 36); **Channel Hardness** 12, **Channel HP** 48 (BT24 ) to destroy a channel gate; **Immunities** critical hits; **Resistances** precision damage; **Weaknesses** bludgeoning damage - -## Attributes - -[ac](#ac), [additionalHpHardnessBt](#additionalhphardnessbt), [hpHardnessBt](#hphardnessbt), [immunities](#immunities), [resistances](#resistances), [savingThrows](#savingthrows), [weaknesses](#weaknesses) - - -### ac - -The armor class as a [QuteDataArmorClass](QuteDataArmorClass.md) - -### savingThrows - -The saving throws, as [QuteSavingThrows](QuteDataDefenses/QuteSavingThrows.md) - -### hpHardnessBt - -HP, hardness, and broken threshold stored in a [QuteDataHpHardnessBt](QuteDataHpHardnessBt.md) - -### additionalHpHardnessBt - -Additional HP, hardness, or broken thresholds for other HP components as a map of names to [QuteDataHpHardnessBt](QuteDataHpHardnessBt.md) - -### immunities - -List of strings, optional - -### resistances - -Map of (name, [QuteDataGenericStat](QuteDataGenericStat.md)) - -### weaknesses - -Map of (name, [QuteDataGenericStat](QuteDataGenericStat.md)) diff --git a/docs/templates/pf2e/QuteDataDefenses/QuteSavingThrows.md b/docs/templates/pf2e/QuteDataDefenses/QuteSavingThrows.md index 4d2dbf496..2f720d0cb 100644 --- a/docs/templates/pf2e/QuteDataDefenses/QuteSavingThrows.md +++ b/docs/templates/pf2e/QuteDataDefenses/QuteSavingThrows.md @@ -1,12 +1,21 @@ # QuteSavingThrows -Pathfinder 2e saving throws. Example default rendering:
**Fort** +10 (+12 vs. poison), **Ref** +5 (+7 vs. traps), **Will** +4 (+6 vs. mental); +1 status to all saves vs. magic
+Pathfinder 2e saving throws. Example default rendering: + +```md +**Fort** +10 (+12 vs. poison), **Ref** +5 (+7 vs. traps), **Will** +4 (+6 vs. mental); +1 status to +all saves vs. magic +``` ## Attributes [abilities](#abilities), [fort](#fort), [ref](#ref), [will](#will) +### abilities + +Any saving throw related abilities + ### fort Fortitude saving throw bonus, as a [QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md) @@ -18,7 +27,3 @@ Reflex saving throw bonus, as a [QuteDataNamedBonus](../QuteDataGenericStat/Qute ### will Will saving throw bonus, as a [QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md) - -### abilities - -Any saving throw related abilities diff --git a/docs/templates/pf2e/QuteDataDefenses/README.md b/docs/templates/pf2e/QuteDataDefenses/README.md index ae9dc6c33..7b5a61449 100644 --- a/docs/templates/pf2e/QuteDataDefenses/README.md +++ b/docs/templates/pf2e/QuteDataDefenses/README.md @@ -1,9 +1,20 @@ # QuteDataDefenses -Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard. Example: +Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard. -- **AC** 23 (33 with mage armor); **Fort** +15, **Ref** +12, **Will** +10 -- **Floor Hardness** 18, **Floor HP** 72 (BT 36); **Channel Hardness** 12, **Channel HP** 48 (BT24 ) to destroy a channel gate; **Immunities** critical hits; **Resistances** precision damage; **Weaknesses** bludgeoning damage +Example: + +```md +**AC** 23 (33 with mage armor); **Fort** +15, **Ref** +12, **Will** +10 +``` + +```md +**Floor Hardness** 18, **Floor HP** 72 (BT 36); +**Channel Hardness** 12, **Channel HP** 48 (BT24 ) to destroy a channel gate; +**Immunities** critical hits; +**Resistances** precision damage; +**Weaknesses** bludgeoning damage +``` ## Attributes @@ -14,17 +25,14 @@ Pf2eTools Armor class, Saving Throws, and other attributes describing defenses o The armor class as a [QuteDataArmorClass](../QuteDataArmorClass.md) -### savingThrows +### additionalHpHardnessBt -The saving throws, as [QuteSavingThrows](QuteSavingThrows.md) +Additional HP, hardness, or broken thresholds for other HP components as a map of +names to [QuteDataHpHardnessBt](../QuteDataHpHardnessBt/README.md) ### hpHardnessBt -HP, hardness, and broken threshold stored in a [QuteDataHpHardnessBt](../QuteDataHpHardnessBt.md) - -### additionalHpHardnessBt - -Additional HP, hardness, or broken thresholds for other HP components as a map of names to [QuteDataHpHardnessBt](../QuteDataHpHardnessBt.md) +HP, hardness, and broken threshold stored in a [QuteDataHpHardnessBt](../QuteDataHpHardnessBt/README.md) ### immunities @@ -32,8 +40,12 @@ List of strings, optional ### resistances -List of strings, optional +Map of (name, [QuteDataGenericStat](../QuteDataGenericStat/README.md)) + +### savingThrows + +The saving throws, as [QuteSavingThrows](QuteSavingThrows.md) ### weaknesses -List of strings, optional +Map of (name, [QuteDataGenericStat](../QuteDataGenericStat/README.md)) diff --git a/docs/templates/pf2e/QuteDataDuration.md b/docs/templates/pf2e/QuteDataDuration.md new file mode 100644 index 000000000..c50e4baf6 --- /dev/null +++ b/docs/templates/pf2e/QuteDataDuration.md @@ -0,0 +1,16 @@ +# QuteDataDuration + +A duration of time. This may be either a [QuteDataTimedDuration](QuteDataTimedDuration/README.md), which represents a period of time longer +than an activity, or a [QuteDataActivity](QuteDataActivity.md). Use [QuteDataDuration#Activity](#activity) to check whether this +duration is an activity. + +Using this directly will give the default representation for either object. + +## Attributes + +[activity](#activity) + + +### activity + +Returns true if this duration is a [QuteDataActivity](QuteDataActivity.md). diff --git a/docs/templates/pf2e/QuteDataFrequency.md b/docs/templates/pf2e/QuteDataFrequency.md index 51e4429e8..c4bb31431 100644 --- a/docs/templates/pf2e/QuteDataFrequency.md +++ b/docs/templates/pf2e/QuteDataFrequency.md @@ -1,6 +1,8 @@ # QuteDataFrequency -A description of a frequency e.g. "once", which may include an interval that this is repeated for. Examples: +A description of a frequency e.g. "once", which may include an interval that this is repeated for. + +Examples: - once per day - once per hour @@ -14,26 +16,28 @@ A description of a frequency e.g. "once", which may include an interval that thi [interval](#interval), [notes](#notes), [overcharge](#overcharge), [recurs](#recurs), [unit](#unit), [value](#value) -### value +### interval -The number represented by the frequency, integer +The interval that the frequency is repeated for -### unit +### notes -The unit the frequency is in, string. Required. +Any notes associated with the frequency. May include a custom string, for frequencies which cannot be +represented using the normal parts. If this is present, then the other parameters will be null. -### recurs +### overcharge -Whether the unit recurs. In the default representation, this makes it render "every" instead of "per" +Whether there's an overcharge involved. Used for wands mostly. In the default representation, this +adds ", plus overcharge". -### overcharge +### recurs -Whether there's an overcharge involved. Used for wands mostly. In the default representation, this adds ", plus overcharge". +Whether the unit recurs. In the default representation, this makes it render "every" instead of "per" -### interval +### unit -The interval that the frequency is repeated for +The unit the frequency is in, string. Required. -### notes +### value -Any notes associated with the frequency. May include a custom string, for frequencies which cannot be represented using the normal parts. If this is present, then the other parameters will be null. +The number represented by the frequency, integer diff --git a/docs/templates/pf2e/QuteDataGenericStat/QuteDataNamedBonus.md b/docs/templates/pf2e/QuteDataGenericStat/QuteDataNamedBonus.md index 85fced5d6..bdec83acc 100644 --- a/docs/templates/pf2e/QuteDataGenericStat/QuteDataNamedBonus.md +++ b/docs/templates/pf2e/QuteDataGenericStat/QuteDataNamedBonus.md @@ -1,6 +1,9 @@ # QuteDataNamedBonus -A Pathfinder 2e named bonus, potentially with other conditional bonuses. Example default representation:
Stealth +36 (+42 in forests) (ignores tremorsense)
+A Pathfinder 2e named bonus, potentially with other conditional bonuses. + +Example default representation: +`Stealth +36 (+42 in forests) (ignores tremorsense)` ## Attributes @@ -11,14 +14,15 @@ A Pathfinder 2e named bonus, potentially with other conditional bonuses. Example The name of the skill -### value +### notes -The standard bonus associated with this skill +Any notes associated with this skill bonus ### otherBonuses -Any additional bonuses, as a map of descriptions to bonuses. Iterate over all map entries to display the values, e.g.: `{#each resource.skills.otherBonuses}{it.key}: {it.value}{/each}` +Any additional bonuses, as a map of descriptions to bonuses. Iterate over all map entries to +display the values, e.g.: `{#each resource.skills.otherBonuses}{it.key}: {it.value}{/each}` -### notes +### value -Any notes associated with this skill bonus +The standard bonus associated with this skill diff --git a/docs/templates/pf2e/QuteDataGenericStat/README.md b/docs/templates/pf2e/QuteDataGenericStat/README.md new file mode 100644 index 000000000..a722b6a75 --- /dev/null +++ b/docs/templates/pf2e/QuteDataGenericStat/README.md @@ -0,0 +1,5 @@ +# QuteDataGenericStat + +A generic container for a PF2e stat value which may have an attached note. + +## Attributes diff --git a/docs/templates/pf2e/QuteDataGenericStat/SimpleStat.md b/docs/templates/pf2e/QuteDataGenericStat/SimpleStat.md index 07ef19bbf..7bd707122 100644 --- a/docs/templates/pf2e/QuteDataGenericStat/SimpleStat.md +++ b/docs/templates/pf2e/QuteDataGenericStat/SimpleStat.md @@ -1,5 +1,8 @@ # SimpleStat -A basic [QuteDataGenericStat](../QuteDataGenericStat.md) which provides only a value and possibly a note. Default representation:
10 (some note) (some other note)
+A basic [QuteDataGenericStat](README.md) which provides +only a value and possibly a note. + +Default representation: `10 (some note) (some other note)` ## Attributes diff --git a/docs/templates/pf2e/QuteDataHpHardness.md b/docs/templates/pf2e/QuteDataHpHardness.md deleted file mode 100644 index 81e1989c6..000000000 --- a/docs/templates/pf2e/QuteDataHpHardness.md +++ /dev/null @@ -1,25 +0,0 @@ -# QuteDataHpHardness - -Pf2eTools Hit Points and Hardiness attributes - -## Attributes - -[brokenThreshold](#brokenthreshold), [hardnessNotes](#hardnessnotes), [hardnessValue](#hardnessvalue), [hpNotes](#hpnotes), [hpValue](#hpvalue), [name](#name) - - -### brokenThreshold - - -### hardnessNotes - - -### hardnessValue - - -### hpNotes - - -### hpValue - - -### name diff --git a/docs/templates/pf2e/QuteDataHpHardnessBt.md b/docs/templates/pf2e/QuteDataHpHardnessBt.md deleted file mode 100644 index 3fd68f4e1..000000000 --- a/docs/templates/pf2e/QuteDataHpHardnessBt.md +++ /dev/null @@ -1,26 +0,0 @@ -# QuteDataHpHardnessBt - -Hit Points, Hardness, and a broken threshold for hazards and shields. Used for creatures, hazards, and shields. - -Hazard example with a broken threshold and note:
**Hardness** 10, **HP (BT)** 30 (15) to destroy a channel gate
- -Hazard example with a name, broken threshold, and note:
**Floor Hardness** 10, **Floor HP** 30 (BT 15) to destroy a channel gate
- -Creature example with a name and ability:
**Head Hardness** 10, **Head HP** 30 (hydra regeneration)
- -## Attributes - -[brokenThreshold](#brokenthreshold), [hardness](#hardness), [hp](#hp) - - -### hp - -The HP as a [HpStat](QuteDataHpHardnessBt/HpStat.md) (optional) - -### hardness - -Hardness as a [SimpleStat](QuteDataGenericStat/SimpleStat.md) (optional) - -### brokenThreshold - -Broken threshold as an integer (optional, not populated for creatures) diff --git a/docs/templates/pf2e/QuteDataHpHardnessBt/HpStat.md b/docs/templates/pf2e/QuteDataHpHardnessBt/HpStat.md index e49dd076d..1af06d7db 100644 --- a/docs/templates/pf2e/QuteDataHpHardnessBt/HpStat.md +++ b/docs/templates/pf2e/QuteDataHpHardnessBt/HpStat.md @@ -1,16 +1,13 @@ # HpStat -HP value and associated notes. Referencing this directly provides a default representation, e.g.
15 to destroy a head (head regrowth)
+HP value and associated notes. Referencing this directly provides a default representation, e.g. +`15 to destroy a head (head regrowth)` ## Attributes [abilities](#abilities), [notes](#notes), [value](#value) -### value - -The HP value itself - ### abilities Any abilities associated with the HP @@ -18,3 +15,7 @@ Any abilities associated with the HP ### notes Any notes associated with the HP. + +### value + +The HP value itself diff --git a/docs/templates/pf2e/QuteDataHpHardnessBt/README.md b/docs/templates/pf2e/QuteDataHpHardnessBt/README.md new file mode 100644 index 000000000..3fadc34b0 --- /dev/null +++ b/docs/templates/pf2e/QuteDataHpHardnessBt/README.md @@ -0,0 +1,39 @@ +# QuteDataHpHardnessBt + +Hit Points, Hardness, and a broken threshold for hazards and shields. Used for creatures, hazards, and shields. + +Hazard example with a broken threshold and note: + +```md +**Hardness** 10, **HP (BT)** 30 (15) to destroy a channel gate +``` + +Hazard example with a name, broken threshold, and note: + +```md +**Floor Hardness** 10, **Floor HP** 30 (BT 15) to destroy a channel gate +``` + +Creature example with a name and ability: + +```md +**Head Hardness** 10, **Head HP** 30 (hydra regeneration) +``` + +## Attributes + +[brokenThreshold](#brokenthreshold), [hardness](#hardness), [hp](#hp) + + +### brokenThreshold + +Broken threshold as an integer (optional, not populated for creatures) + +### hardness + +Hardness as a [SimpleStat](../QuteDataGenericStat/SimpleStat.md) +(optional) + +### hp + +The HP as a [HpStat](HpStat.md) (optional) diff --git a/docs/templates/pf2e/QuteDataRange/README.md b/docs/templates/pf2e/QuteDataRange/README.md index 527f777a2..8ebd532c5 100644 --- a/docs/templates/pf2e/QuteDataRange/README.md +++ b/docs/templates/pf2e/QuteDataRange/README.md @@ -7,14 +7,15 @@ A range with a given value and unit of measurement for that value. [notes](#notes), [unit](#unit), [value](#value) -### value +### notes -An integer value for the range +Any associated notes, or an alternate rendering when the range can't be represented using just +a unit and value. ### unit What unit of measurement the `value` is given in, as a [RangeUnit](RangeUnit.md) -### notes +### value -Any associated notes, or an alternate rendering when the range can't be represented using just a unit and value. +An integer value for the range diff --git a/docs/templates/pf2e/QuteDataSkillBonus.md b/docs/templates/pf2e/QuteDataSkillBonus.md deleted file mode 100644 index 3d8e88f02..000000000 --- a/docs/templates/pf2e/QuteDataSkillBonus.md +++ /dev/null @@ -1,26 +0,0 @@ -# QuteDataSkillBonus - -A Pathfinder 2e skill and associated bonuses. - -Using this directly provides a default representation, e.g. `Stealth +36 (+42 in forests) (some other note)` - -## Attributes - -[name](#name), [notes](#notes), [otherBonuses](#otherbonuses), [value](#value) - - -### name - -The name of the skill - -### value - -The standard bonus associated with this skill - -### otherBonuses - -Any additional bonuses, as a map of descriptions to bonuses. Iterate over all map entries to display the values: `{#each resource.skills.otherBonuses}{it.key}: {it.value}{/each}` - -### notes - -Any notes associated with this skill bonus diff --git a/docs/templates/pf2e/QuteDataSpeed.md b/docs/templates/pf2e/QuteDataSpeed.md index df9035b4c..849dc728b 100644 --- a/docs/templates/pf2e/QuteDataSpeed.md +++ b/docs/templates/pf2e/QuteDataSpeed.md @@ -1,23 +1,27 @@ # QuteDataSpeed +Examples: + +- `10 feet, swim 20 feet (some note); some ability` +- `10 feet, swim 20 feet, some ability` ## Attributes [abilities](#abilities), [notes](#notes), [otherSpeeds](#otherspeeds), [value](#value) -### value - -The land speed in feet - -### otherSpeeds +### abilities -Other speeds, as a map of (name, speed in feet) +Any speed-related abilities ### notes Any speed-related notes -### abilities +### otherSpeeds -Any speed-related abilities +Other speeds, as a map of (name, speed in feet) + +### value + +The land speed in feet diff --git a/docs/templates/pf2e/QuteDataTimedDuration/README.md b/docs/templates/pf2e/QuteDataTimedDuration/README.md index 4a56b9cc0..8fb42b47e 100644 --- a/docs/templates/pf2e/QuteDataTimedDuration/README.md +++ b/docs/templates/pf2e/QuteDataTimedDuration/README.md @@ -1,20 +1,34 @@ # QuteDataTimedDuration -A duration of time, represented by a numerical value and a unit. Sometimes this includes a custom display string, for durations which cannot be represented using the normal structure. Examples: +A duration of time, represented by a numerical value and a unit. Sometimes this includes a custom display string, +for durations which cannot be represented using the normal structure. -- A duration of 3 minutes:
3 minutes
-- A duration of 1 turn:
until the end of your next turn
-- An unlimited duration:
unlimited
+Examples: + +- A duration of 3 minutes: `3 minutes` +- A duration of 1 turn: `until the end of your next turn` +- An unlimited duration: `unlimited` ## Attributes -[customDisplay](#customdisplay), [notes](#notes), [unit](#unit), [value](#value) +[customDisplay](#customdisplay), [formattedNotes](#formattednotes), [notes](#notes), [unit](#unit), [value](#value) -### value +### customDisplay + +The custom display used for this duration. + +### formattedNotes + +Returns a comma delimited string containing all notes. + +### notes -The quantity of time ### unit The unit that the quantity is measured in, as a [DurationUnit](DurationUnit.md) + +### value + +The quantity of time diff --git a/docs/templates/pf2e/QuteDeity/QuteDeityCleric.md b/docs/templates/pf2e/QuteDeity/QuteDeityCleric.md index 7ebf3ddd3..025534806 100644 --- a/docs/templates/pf2e/QuteDeity/QuteDeityCleric.md +++ b/docs/templates/pf2e/QuteDeity/QuteDeityCleric.md @@ -2,7 +2,9 @@ Pf2eTools cleric divine attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.actionType}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.actionType}`. ## Attributes diff --git a/docs/templates/pf2e/QuteDeity/QuteDivineAvatar.md b/docs/templates/pf2e/QuteDeity/QuteDivineAvatar.md index 859ff6dd6..154d7a730 100644 --- a/docs/templates/pf2e/QuteDeity/QuteDivineAvatar.md +++ b/docs/templates/pf2e/QuteDeity/QuteDivineAvatar.md @@ -2,7 +2,9 @@ Pf2eTools avatar attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.actionType}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.actionType}`. ## Attributes diff --git a/docs/templates/pf2e/QuteDeity/QuteDivineAvatarAbility.md b/docs/templates/pf2e/QuteDeity/QuteDivineAvatarAbility.md deleted file mode 100644 index bb6c4c128..000000000 --- a/docs/templates/pf2e/QuteDeity/QuteDivineAvatarAbility.md +++ /dev/null @@ -1,15 +0,0 @@ -# QuteDivineAvatarAbility - -Pf2eTools divine avatar ability attributes - -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.actionType}`. - -## Attributes - -[name](#name), [text](#text) - - -### name - - -### text diff --git a/docs/templates/pf2e/QuteDeity/QuteDivineAvatarAction.md b/docs/templates/pf2e/QuteDeity/QuteDivineAvatarAction.md deleted file mode 100644 index 8e3ddec00..000000000 --- a/docs/templates/pf2e/QuteDeity/QuteDivineAvatarAction.md +++ /dev/null @@ -1,30 +0,0 @@ -# QuteDivineAvatarAction - -Pf2eTools Divine avatar action attributes - -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.actionType}`. - -## Attributes - -[actionType](#actiontype), [activityType](#activitytype), [damage](#damage), [name](#name), [note](#note), [range](#range), [traits](#traits) - - -### actionType - - -### activityType - - -### damage - - -### name - - -### note - - -### range - - -### traits diff --git a/docs/templates/pf2e/QuteDeity/QuteDivineIntercession.md b/docs/templates/pf2e/QuteDeity/QuteDivineIntercession.md index d5f1321a6..b87ff762e 100644 --- a/docs/templates/pf2e/QuteDeity/QuteDivineIntercession.md +++ b/docs/templates/pf2e/QuteDeity/QuteDivineIntercession.md @@ -2,7 +2,9 @@ Pf2eTools divine intercession attributes. -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.actionType}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.actionType}`. ## Attributes diff --git a/docs/templates/pf2e/QuteDeity/README.md b/docs/templates/pf2e/QuteDeity/README.md index dac4ecca5..2c50f4ba3 100644 --- a/docs/templates/pf2e/QuteDeity/README.md +++ b/docs/templates/pf2e/QuteDeity/README.md @@ -2,15 +2,18 @@ Pf2eTools Deity attributes (`deity2md.txt`) -Deities are rendered both standalone and inline (as an admonition block). The default template can render both. It contains some special syntax to handle the inline case. +Deities are rendered both standalone and inline (as an admonition block). +The default template can render both. +It uses special syntax to handle the inline case. -Use `%%--` to mark the end of the preamble (frontmatter and other leading content only appropriate to the standalone case). +Use `%%--` to mark the end of the preamble (frontmatter and +other leading content only appropriate to the standalone case). Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [alignment](#alignment), [anathema](#anathema), [areasOfConcern](#areasofconcern), [avatar](#avatar), [category](#category), [cleric](#cleric), [edicts](#edicts), [followerAlignment](#followeralignment), [hasSections](#hassections), [intercession](#intercession), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[aliases](#aliases), [alignment](#alignment), [anathema](#anathema), [areasOfConcern](#areasofconcern), [avatar](#avatar), [category](#category), [cleric](#cleric), [edicts](#edicts), [followerAlignment](#followeralignment), [hasSections](#hassections), [intercession](#intercession), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### aliases @@ -59,6 +62,10 @@ Note name ### pantheon +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/QuteFeat.md b/docs/templates/pf2e/QuteFeat.md index 8428eb91c..d396e78fb 100644 --- a/docs/templates/pf2e/QuteFeat.md +++ b/docs/templates/pf2e/QuteFeat.md @@ -2,15 +2,18 @@ Pf2eTools Feat attributes (`feat2md.txt`) -Feats are rendered both standalone and inline (as an admonition block). The default template can render both. It contains some special syntax to handle the inline case. +Feats are rendered both standalone and inline (as an admonition block). +The default template can render both. +It uses special syntax to handle the inline case. -Use `%%--` to mark the end of the preamble (frontmatter and other leading content only appropriate to the standalone case). +Use `%%--` to mark the end of the preamble (frontmatter and +other leading content only appropriate to the standalone case). Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[access](#access), [activity](#activity), [aliases](#aliases), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasSections](#hassections), [labeledSource](#labeledsource), [leadsTo](#leadsto), [level](#level), [name](#name), [note](#note), [prerequisites](#prerequisites), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[access](#access), [activity](#activity), [aliases](#aliases), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasSections](#hassections), [labeledSource](#labeledsource), [leadsTo](#leadsto), [level](#level), [name](#name), [note](#note), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### access @@ -29,12 +32,15 @@ Aliases for this note ### embedded -True if this ability is embedded in another note (admonition block). The default template uses this flag to include a `title:` prefix for the admonition block: - `{#if resource.embedded }title: {#else}# {/if}{resource.name}` * +True if this ability is embedded in another note (admonition block). +The default template uses this flag to include a `title:` prefix for the admonition block: + +`{#if resource.embedded }title: {#else}# {/if}{resource.name}` * ### frequency -[QuteDataFrequency](QuteDataFrequency.md). How often this feat can be used/activated. Use directly to get a formatted string. +[QuteDataFrequency](QuteDataFrequency.md). +How often this feat can be used/activated. Use directly to get a formatted string. ### hasSections @@ -60,6 +66,10 @@ Note name ### prerequisites +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### requirements diff --git a/docs/templates/pf2e/QuteHazard.md b/docs/templates/pf2e/QuteHazard.md deleted file mode 100644 index 8ccccab70..000000000 --- a/docs/templates/pf2e/QuteHazard.md +++ /dev/null @@ -1,93 +0,0 @@ -# QuteHazard - -Pf2eTools Hazard attributes (`hazard2md.txt`) - -Hazards are rendered both standalone and inline (as an admonition block). The default template can render both. It contains some special syntax to handle the inline case. - -Use `%%--` to mark the end of the preamble (frontmatter and other leading content only appropriate to the standalone case). - -Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - -## Attributes - -[abilities](#abilities), [actions](#actions), [attacks](#attacks), [complexity](#complexity), [defenses](#defenses), [disable](#disable), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [perception](#perception), [reset](#reset), [routine](#routine), [routineAdmonition](#routineadmonition), [source](#source), [sourceAndPage](#sourceandpage), [stealth](#stealth), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) - - -### abilities - -The hazard's abilities, as a list of [QuteAbility](QuteAbility.md) - -### actions - -The hazard's actions, as a list of [QuteAbilityOrAffliction](QuteAbilityOrAffliction.md). - -Using the elements directly will give a default rendering, but if you want more control you can use `isAffliction` and `isAbility` to check whether it's an affliction or an ability. Example:
 {#each resource.actions} {#if it.isAffliction} **Affliction** {it} {#else if it.isAbility} **Ability** {it} {/if} {/each} 
- -### attacks - -The attacks available to the hazard, as a list of [QuteInlineAttack](QuteInlineAttack.md) - -### complexity - - -### defenses - - -### disable - - -### hasSections - -True if the content (text) contains sections - -### labeledSource - -Formatted string describing the content's source(s): `_Source: _` - -### level - - -### name - -Note name - -### perception - -The hazard's perception, as a [QuteDataGenericStat](QuteDataGenericStat.md) - -### reset - - -### routine - - -### routineAdmonition - - -### source - -String describing the content's source(s) - -### sourceAndPage - -Book sources as list of [SourceAndPage](../SourceAndPage.md) - -### stealth - -The hazard's stealth, as a [QuteHazardAttributes](QuteHazard/QuteHazardStealth.md) - -### tags - -Collected tags for inclusion in frontmatter - -### text - -Formatted text. For most templates, this is the bulk of the content. - -### traits - -Collection of traits (decorated links) - -### vaultPath - -Path to this note in the vault diff --git a/docs/templates/pf2e/QuteHazard/QuteHazardAttributes.md b/docs/templates/pf2e/QuteHazard/QuteHazardAttributes.md deleted file mode 100644 index bbe4c4cd9..000000000 --- a/docs/templates/pf2e/QuteHazard/QuteHazardAttributes.md +++ /dev/null @@ -1,26 +0,0 @@ -# QuteHazardAttributes - -Pf2eTools hazard attributes. - -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. - -## Attributes - -[bonus](#bonus), [dc](#dc), [minProf](#minprof), [notes](#notes) - - -### bonus - -Number. Bonus - -### dc - -Number. Difficulty class - -### minProf - -String. Minimum proficiency - -### notes - -Formatted string. Notes diff --git a/docs/templates/pf2e/QuteHazard/QuteHazardStealth.md b/docs/templates/pf2e/QuteHazard/QuteHazardStealth.md index 8ba32ac19..92568362c 100644 --- a/docs/templates/pf2e/QuteHazard/QuteHazardStealth.md +++ b/docs/templates/pf2e/QuteHazard/QuteHazardStealth.md @@ -2,16 +2,17 @@ Pf2eTools hazard attributes. -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. ## Attributes [dc](#dc), [minProf](#minprof), [notes](#notes), [value](#value) -### value +### dc -The hazard's Stealth bonus +The DC which must be passed to see the hazard ### minProf @@ -19,8 +20,9 @@ The minimum Perception proficiency required to be able to roll against the hazar ### notes -Any notes associated with the hazard's Stealth. Sometimes this includes other stats which may be rolled against the hazard's Stealth. +Any notes associated with the hazard's Stealth. Sometimes this includes other stats which may +be rolled against the hazard's Stealth. -### dc +### value -The DC which must be passed to see the hazard +The hazard's Stealth bonus diff --git a/docs/templates/pf2e/QuteHazard/README.md b/docs/templates/pf2e/QuteHazard/README.md index c7036a0b6..e192070ec 100644 --- a/docs/templates/pf2e/QuteHazard/README.md +++ b/docs/templates/pf2e/QuteHazard/README.md @@ -2,22 +2,50 @@ Pf2eTools Hazard attributes (`hazard2md.txt`) -Hazards are rendered both standalone and inline (as an admonition block). The default template can render both. It contains some special syntax to handle the inline case. +Hazards are rendered both standalone and inline (as an admonition block). +The default template can render both. +It uses special syntax to handle the inline case. -Use `%%--` to mark the end of the preamble (frontmatter and other leading content only appropriate to the standalone case). +Use `%%--` to mark the end of the preamble (frontmatter and +other leading content only appropriate to the standalone case). Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[abilities](#abilities), [actions](#actions), [complexity](#complexity), [defenses](#defenses), [disable](#disable), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [perception](#perception), [reset](#reset), [routine](#routine), [routineAdmonition](#routineadmonition), [source](#source), [sourceAndPage](#sourceandpage), [stealth](#stealth), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[abilities](#abilities), [actions](#actions), [attacks](#attacks), [complexity](#complexity), [defenses](#defenses), [disable](#disable), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [reset](#reset), [routine](#routine), [routineAdmonition](#routineadmonition), [source](#source), [sourceAndPage](#sourceandpage), [stealth](#stealth), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### abilities +The hazard's abilities, as a list of +[QuteAbility](../QuteAbility.md) ### actions +The hazard's actions, as a list of +[QuteAbilityOrAffliction](../QuteAbilityOrAffliction.md). + +Using the elements directly will give a default rendering, but if you want more +control you can use `isAffliction` and `isAbility` to check whether it's an affliction or an +ability. Example: + +```md +{#each resource.actions} +{#if it.isAffliction} + +**Affliction** {it} +{#else if it.isAbility} + +**Ability** {it} +{/if} +{/each} +``` + +### attacks + +The attacks available to the hazard, as a list of +[QuteInlineAttack](../QuteInlineAttack/README.md) ### complexity @@ -45,6 +73,12 @@ Note name ### perception +The hazard's perception, as a +[QuteDataGenericStat](../QuteDataGenericStat/README.md) + +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) ### reset @@ -65,6 +99,8 @@ Book sources as list of [SourceAndPage](../../SourceAndPage.md) ### stealth +The hazard's stealth, as a +[QuteHazardAttributes](QuteHazardStealth.md) ### tags diff --git a/docs/templates/pf2e/QuteInlineAffliction/QuteAfflictionStage.md b/docs/templates/pf2e/QuteInlineAffliction/QuteAfflictionStage.md deleted file mode 100644 index 6f64a70c1..000000000 --- a/docs/templates/pf2e/QuteInlineAffliction/QuteAfflictionStage.md +++ /dev/null @@ -1,16 +0,0 @@ -# QuteAfflictionStage - -Pf2eTools affliction stage attributes. - -## Attributes - -[duration](#duration), [text](#text) - - -### duration - -Formatted text. Affliction duration - -### text - -Formatted text. Affliction stage diff --git a/docs/templates/pf2e/QuteInlineAffliction/README.md b/docs/templates/pf2e/QuteInlineAffliction/README.md deleted file mode 100644 index 26dac7340..000000000 --- a/docs/templates/pf2e/QuteInlineAffliction/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# QuteInlineAffliction - -Pf2eTools Affliction attributes (inline/embedded, `inline-affliction2md.txt`) - -Extension of [Pf2eQuteNote](../Pf2eQuteNote.md) - -## Attributes - -[effect](#effect), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [maxDuration](#maxduration), [name](#name), [onset](#onset), [savingThrow](#savingthrow), [source](#source), [sourceAndPage](#sourceandpage), [stages](#stages), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) - - -### effect - -Formatted text. Affliction effect - -### hasSections - -True if the content (text) contains sections - -### labeledSource - -Formatted string describing the content's source(s): `_Source: _` - -### level - -Overall power, from 1 to 10. - -### maxDuration - -Formatted text. Maximum duration of the infliction. - -### name - -Note name - -### onset - -Formatted text. Maximum duration of the infliction. - -### savingThrow - -Formatted text. Savint throws. - -### source - -String describing the content's source(s) - -### sourceAndPage - -Book sources as list of [SourceAndPage](../../SourceAndPage.md) - -### stages - -Affliction stages: map of name to stage data as [QuteAfflictionStage](QuteAfflictionStage.md) - -### tags - -Collected tags for inclusion in frontmatter - -### text - -Formatted text. For most templates, this is the bulk of the content. - -### traits - -Collection of traits (decorated links) - -### vaultPath - -Path to this note in the vault diff --git a/docs/templates/pf2e/QuteInlineAttack.md b/docs/templates/pf2e/QuteInlineAttack.md deleted file mode 100644 index 8cd7d8cbf..000000000 --- a/docs/templates/pf2e/QuteInlineAttack.md +++ /dev/null @@ -1,59 +0,0 @@ -# QuteInlineAttack - -Pf2eTools Attack attributes (inline/embedded, `inline-attack2md.txt`) - -Extension of [Pf2eQuteNote](Pf2eQuteNote.md) - -## Attributes - -[activity](#activity), [attack](#attack), [damage](#damage), [hasSections](#hassections), [labeledSource](#labeledsource), [meleeOrRanged](#meleeorranged), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) - - -### activity - -Activity/Activation cost (as [QuteDataActivity](QuteDataActivity.md)) - -### attack - - -### damage - - -### hasSections - -True if the content (text) contains sections - -### labeledSource - -Formatted string describing the content's source(s): `_Source: _` - -### meleeOrRanged - - -### name - -Note name - -### source - -String describing the content's source(s) - -### sourceAndPage - -Book sources as list of [SourceAndPage](../SourceAndPage.md) - -### tags - -Collected tags for inclusion in frontmatter - -### text - -Formatted text. For most templates, this is the bulk of the content. - -### traits - -Collection of traits (decorated links) - -### vaultPath - -Path to this note in the vault diff --git a/docs/templates/pf2e/QuteInlineAttack/README.md b/docs/templates/pf2e/QuteInlineAttack/README.md index 9dee9d237..0295faf65 100644 --- a/docs/templates/pf2e/QuteInlineAttack/README.md +++ b/docs/templates/pf2e/QuteInlineAttack/README.md @@ -19,15 +19,19 @@ The to-hit bonus for the attack (integer) ### damage -Damage if the attack hits (formatted string), e.g. "1d8 bludgeoning plus grab". This will include damage types and non-multiline effects. +Damage if the attack hits (formatted string), e.g. "1d8 bludgeoning plus grab". This will include +damage types and non-multiline effects. ### damageTypes -The damage types caused by the attack. Will be included in either [damage](../QuteInlineAttack.md#damage) or in [multilineEffect](../QuteInlineAttack.md#multilineEffect). +The damage types caused by the attack. Will be included in either +[damage](#damage) or in +[multilineEffect](#multilineeffect). ### effects -Any additional effects associated with the attack e.g. grab (list of strings). Effects listed here may be repeated in [damage](../QuteInlineAttack.md#damage). +Any additional effects associated with the attack e.g. grab (list of strings). Effects listed here +may be repeated in [damage](#damage). ### multilineEffect diff --git a/docs/templates/pf2e/QuteItem/QuteItemActivate.md b/docs/templates/pf2e/QuteItem/QuteItemActivate.md index ba8d7944d..b2b07593c 100644 --- a/docs/templates/pf2e/QuteItem/QuteItemActivate.md +++ b/docs/templates/pf2e/QuteItem/QuteItemActivate.md @@ -2,7 +2,9 @@ Pf2eTools item activation attributes. -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.activate}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.activate}`. ## Attributes @@ -19,7 +21,8 @@ Formatted string. Components required to activate this item ### frequency -[QuteDataFrequency](../QuteDataFrequency.md). How often this item can be used/activated. Use directly to get a formatted string. +[QuteDataFrequency](../QuteDataFrequency.md). +How often this item can be used/activated. Use directly to get a formatted string. ### requirements diff --git a/docs/templates/pf2e/QuteItem/QuteItemArmorData.md b/docs/templates/pf2e/QuteItem/QuteItemArmorData.md index 504d35839..a1e332c9f 100644 --- a/docs/templates/pf2e/QuteItem/QuteItemArmorData.md +++ b/docs/templates/pf2e/QuteItem/QuteItemArmorData.md @@ -2,7 +2,9 @@ Pf2eTools item armor attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.armor}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.armor}`. ## Attributes diff --git a/docs/templates/pf2e/QuteItem/QuteItemShieldData.md b/docs/templates/pf2e/QuteItem/QuteItemShieldData.md index d454097dc..5211d8eea 100644 --- a/docs/templates/pf2e/QuteItem/QuteItemShieldData.md +++ b/docs/templates/pf2e/QuteItem/QuteItemShieldData.md @@ -2,7 +2,9 @@ Pf2eTools item shield attributes. When referenced directly, provides a default formatting, e.g. +```md **AC Bonus** +2; **Speed Penalty** —; **Hardness** 3; **HP (BT)** 12 (6) +``` ## Attributes @@ -11,11 +13,14 @@ Pf2eTools item shield attributes. When referenced directly, provides a default f ### ac -AC bonus for the shield, as [QuteDataArmorClass](../QuteDataArmorClass.md) (required) +AC bonus for the shield, as [QuteDataArmorClass](../QuteDataArmorClass.md) +(required) ### hpHardnessBt -HP, hardness, and broken threshold of the shield, as [QuteDataHpHardnessBt](../QuteDataHpHardnessBt.md) (required) +HP, hardness, and broken threshold of the shield, as +[QuteDataHpHardnessBt](../QuteDataHpHardnessBt/README.md) +(required) ### speedPenalty diff --git a/docs/templates/pf2e/QuteItem/QuteItemVariant.md b/docs/templates/pf2e/QuteItem/QuteItemVariant.md index 7a6990008..2632f9353 100644 --- a/docs/templates/pf2e/QuteItem/QuteItemVariant.md +++ b/docs/templates/pf2e/QuteItem/QuteItemVariant.md @@ -2,18 +2,24 @@ Pf2eTools item variant attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: - ``` - {#for variants in resource.variants} - {variants} - {/for} - ``` - or, using `{#each}` instead: - ``` - {#each resource.variants} - {it} - {/each} - ``` +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. + +To use it, reference it directly: + +```md +{#for variants in resource.variants} +{variants} +{/for} +``` + +or, using `{#each}` instead: + +```md +{#each resource.variants} +{it} +{/each} +``` ## Attributes diff --git a/docs/templates/pf2e/QuteItem/QuteItemWeaponData.md b/docs/templates/pf2e/QuteItem/QuteItemWeaponData.md index a5cc8ec09..8abc05c2d 100644 --- a/docs/templates/pf2e/QuteItem/QuteItemWeaponData.md +++ b/docs/templates/pf2e/QuteItem/QuteItemWeaponData.md @@ -2,18 +2,24 @@ Pf2eTools item weapon attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: - ``` - {#for weapons in resource.weapons} - {weapons} - {/for} - ``` - or, using `{#each}` instead: - ``` - {#each resource.weapons} - {it} - {/each} - ``` +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. + +To use it, reference it directly: + +```md +{#for weapons in resource.weapons} +{weapons} +{/for} +``` + +or, using `{#each}` instead: + +```md +{#each resource.weapons} +{it} +{/each} +``` ## Attributes diff --git a/docs/templates/pf2e/QuteItem/README.md b/docs/templates/pf2e/QuteItem/README.md index 76a9b9a16..dab5364b5 100644 --- a/docs/templates/pf2e/QuteItem/README.md +++ b/docs/templates/pf2e/QuteItem/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[access](#access), [activate](#activate), [aliases](#aliases), [ammunition](#ammunition), [armor](#armor), [category](#category), [contract](#contract), [craftReq](#craftreq), [duration](#duration), [group](#group), [hands](#hands), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [onset](#onset), [price](#price), [shield](#shield), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [usage](#usage), [variants](#variants), [vaultPath](#vaultpath), [weapons](#weapons) +[access](#access), [activate](#activate), [aliases](#aliases), [ammunition](#ammunition), [armor](#armor), [category](#category), [contract](#contract), [craftReq](#craftreq), [duration](#duration), [group](#group), [hands](#hands), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [onset](#onset), [price](#price), [reprintOf](#reprintof), [shield](#shield), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [usage](#usage), [variants](#variants), [vaultPath](#vaultpath), [weapons](#weapons) ### access @@ -77,6 +77,10 @@ Formatted string. Onset attributes Formatted string. Item price (pp, gp, sp, cp) +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### shield Item shield attributes as [QuteItemShieldData](QuteItemShieldData.md) diff --git a/docs/templates/pf2e/QuteRitual/QuteRitualCasting.md b/docs/templates/pf2e/QuteRitual/QuteRitualCasting.md index e70eadeb1..318c1a9ff 100644 --- a/docs/templates/pf2e/QuteRitual/QuteRitualCasting.md +++ b/docs/templates/pf2e/QuteRitual/QuteRitualCasting.md @@ -2,7 +2,9 @@ Pf2eTools ritual casting attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.casting}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.casting}`. ## Attributes @@ -15,7 +17,8 @@ Formatted string. Material cost of the spell ### duration -Duration to cast, as a [QuteDataDuration](../QuteDataDuration.md) which is either a [QuteDataActivity](../QuteDataActivity.md), or a [QuteDataTimedDuration](../QuteDataTimedDuration.md). +Duration to cast, as a [QuteDataDuration](../QuteDataDuration.md) which is either a [QuteDataActivity](../QuteDataActivity.md), or a +[QuteDataTimedDuration](../QuteDataTimedDuration/README.md). ### secondaryCasters diff --git a/docs/templates/pf2e/QuteRitual/QuteRitualChecks.md b/docs/templates/pf2e/QuteRitual/QuteRitualChecks.md index 32dc5b503..6b7d396ec 100644 --- a/docs/templates/pf2e/QuteRitual/QuteRitualChecks.md +++ b/docs/templates/pf2e/QuteRitual/QuteRitualChecks.md @@ -2,7 +2,9 @@ Pf2eTools ritual check attributes -This data object provides a default mechanism for creating a marked up string based on the attributes that are present. To use it, reference it directly: `{resource.checks}`. +This data object provides a default mechanism for creating +a marked up string based on the attributes that are present. +To use it, reference it directly: `{resource.checks}`. ## Attributes diff --git a/docs/templates/pf2e/QuteRitual/README.md b/docs/templates/pf2e/QuteRitual/README.md index f4c8fea12..de4bc4407 100644 --- a/docs/templates/pf2e/QuteRitual/README.md +++ b/docs/templates/pf2e/QuteRitual/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [casting](#casting), [checks](#checks), [duration](#duration), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [requirements](#requirements), [ritualType](#ritualtype), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [targeting](#targeting), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[aliases](#aliases), [casting](#casting), [checks](#checks), [duration](#duration), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [ritualType](#ritualtype), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [targeting](#targeting), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### aliases @@ -45,6 +45,10 @@ A spell’s overall power, from 1 to 10. Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### requirements Formatted text. Ritual requirements diff --git a/docs/templates/pf2e/QuteSpell/QuteSpellAmp.md b/docs/templates/pf2e/QuteSpell/QuteSpellAmp.md index 20298c9f1..b6ba35c3b 100644 --- a/docs/templates/pf2e/QuteSpell/QuteSpellAmp.md +++ b/docs/templates/pf2e/QuteSpell/QuteSpellAmp.md @@ -2,7 +2,8 @@ Pf2eTools spell Amp attributes -This attribute will render itself as labeled elements if you reference it directly: `{resource.amp}`. +This attribute will render itself as labeled elements +if you reference it directly: `{resource.amp}`. ## Attributes diff --git a/docs/templates/pf2e/QuteSpell/QuteSpellCasting.md b/docs/templates/pf2e/QuteSpell/QuteSpellCasting.md deleted file mode 100644 index 23c759f18..000000000 --- a/docs/templates/pf2e/QuteSpell/QuteSpellCasting.md +++ /dev/null @@ -1,30 +0,0 @@ -# QuteSpellCasting - -Pf2eTools spell casting attributes. - -This attribute will render itself as a list of labeled elements if you reference it directly: `{resource.casting}`. - -## Attributes - -[cast](#cast), [components](#components), [cost](#cost), [requirements](#requirements), [trigger](#trigger) - - -### cast - -Formatted action icon/link. Casting action - -### components - -Comma-separated list of required spell components (material, somatic, verbal, focus) - -### cost - -Formatted string. Material cost of the spell - -### requirements - -Formatted string. Spell cast requirements - -### trigger - -Formatted string. Spell activation trigger. diff --git a/docs/templates/pf2e/QuteSpell/QuteSpellDuration.md b/docs/templates/pf2e/QuteSpell/QuteSpellDuration.md index d8aa00a3e..921cd037c 100644 --- a/docs/templates/pf2e/QuteSpell/QuteSpellDuration.md +++ b/docs/templates/pf2e/QuteSpell/QuteSpellDuration.md @@ -1,15 +1,16 @@ # QuteSpellDuration -Details about the duration of the spell. Example default representations:
1 minute
sustained up to 1 minute
+Details about the duration of the spell. -## Attributes +Example default representations: -[dismissable](#dismissable), [duration](#duration), [sustained](#sustained) +- `1 minute` +- `sustained up to 1 minute` +## Attributes -### sustained +[dismissable](#dismissable), [duration](#duration), [sustained](#sustained) -Whether this is a sustained spell, boolean ### dismissable @@ -17,4 +18,8 @@ Whether this spell can be dismissed, boolean. Not included in the default repres ### duration -The duration of this spell, as a [QuteDataTimedDuration](../QuteDataTimedDuration.md). +The duration of this spell, as a [QuteDataTimedDuration](../QuteDataTimedDuration/README.md). + +### sustained + +Whether this is a sustained spell, boolean diff --git a/docs/templates/pf2e/QuteSpell/QuteSpellSave.md b/docs/templates/pf2e/QuteSpell/QuteSpellSave.md index fda3106dd..246521378 100644 --- a/docs/templates/pf2e/QuteSpell/QuteSpellSave.md +++ b/docs/templates/pf2e/QuteSpell/QuteSpellSave.md @@ -1,15 +1,16 @@ # QuteSpellSave -Details about the saving throw for a spell. Example default representations:
basic Reflex or Fortitude
basic Reflex, Fortitude, or Willpower
+Details about the saving throw for a spell. -## Attributes +Example default representations: -[basic](#basic), [hidden](#hidden), [saves](#saves) +- `basic Reflex or Fortitude` +- `basic Reflex, Fortitude, or Willpower` +## Attributes -### saves +[basic](#basic), [hidden](#hidden), [saves](#saves) -The saving throws that can be used for this spell (list of strings) ### basic @@ -17,4 +18,9 @@ True if this is a basic save (boolean) ### hidden -Whether this save should be hidden. This is sometimes true when it's a special save that is described in the text of the spell. +Whether this save should be hidden. This is sometimes true when it's a special save that is +described in the text of the spell. + +### saves + +The saving throws that can be used for this spell (list of strings) diff --git a/docs/templates/pf2e/QuteSpell/QuteSpellSaveDuration.md b/docs/templates/pf2e/QuteSpell/QuteSpellSaveDuration.md deleted file mode 100644 index b059aaabf..000000000 --- a/docs/templates/pf2e/QuteSpell/QuteSpellSaveDuration.md +++ /dev/null @@ -1,22 +0,0 @@ -# QuteSpellSaveDuration - -Pf2eTools spell save attributes. - -This attribute will render itself as labeled elements if you reference it directly: `{resource.saveDuration}`. - -## Attributes - -[basic](#basic), [duration](#duration), [savingThrow](#savingthrow) - - -### basic - -Boolean. True if this is a basic saving throw - -### duration - -Formatted string. Duration. - -### savingThrow - -Formatted string. Saving throw diff --git a/docs/templates/pf2e/QuteSpell/QuteSpellTarget.md b/docs/templates/pf2e/QuteSpell/QuteSpellTarget.md index 2cf42d145..264c23842 100644 --- a/docs/templates/pf2e/QuteSpell/QuteSpellTarget.md +++ b/docs/templates/pf2e/QuteSpell/QuteSpellTarget.md @@ -2,7 +2,8 @@ Pf2eTools spell target attributes. -This attribute will render itself as labeled elements if you reference it directly: `{resource.targeting}`. +This attribute will render itself as labeled elements +if you reference it directly: `{resource.targeting}`. ## Attributes @@ -15,7 +16,7 @@ Formatted string describing the spell area of effect ### range -The spell's range, as a [QuteDataRange](../QuteDataRange.md). +The spell's range, as a [QuteDataRange](../QuteDataRange/README.md). ### targets diff --git a/docs/templates/pf2e/QuteSpell/README.md b/docs/templates/pf2e/QuteSpell/README.md index eeef59894..bd23672d1 100644 --- a/docs/templates/pf2e/QuteSpell/README.md +++ b/docs/templates/pf2e/QuteSpell/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [amp](#amp), [castDuration](#castduration), [components](#components), [cost](#cost), [domains](#domains), [duration](#duration), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [requirements](#requirements), [save](#save), [source](#source), [sourceAndPage](#sourceandpage), [spellLists](#spelllists), [spellType](#spelltype), [subclass](#subclass), [tags](#tags), [targeting](#targeting), [text](#text), [traditions](#traditions), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[aliases](#aliases), [amp](#amp), [castDuration](#castduration), [components](#components), [cost](#cost), [domains](#domains), [duration](#duration), [formattedComponents](#formattedcomponents), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [save](#save), [source](#source), [sourceAndPage](#sourceandpage), [spellLists](#spelllists), [spellType](#spelltype), [subclass](#subclass), [tags](#tags), [targeting](#targeting), [text](#text), [traditions](#traditions), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### aliases @@ -19,11 +19,14 @@ Psi amp behavior as [QuteSpellAmp](QuteSpellAmp.md) ### castDuration -The time it takes to cast the spell, as a [QuteDataDuration](../QuteDataDuration.md) which is either a [QuteDataActivity](../QuteDataActivity.md) or a [QuteDataTimedDuration](../QuteDataTimedDuration.md). +The time it takes to cast the spell, as a [QuteDataDuration](../QuteDataDuration.md) which is either a [QuteDataActivity](../QuteDataActivity.md) +or a [QuteDataTimedDuration](../QuteDataTimedDuration/README.md). ### components -The required spell components as a list of formatted strings (maybe empty). Use [QuteSpell#formattedComponents()](../QuteSpell.md#formattedComponents()) to get a pre-formatted representation. +The required spell components as a list of formatted strings (maybe empty). Use +[QuteSpell#formattedComponents](#formattedcomponents) +to get a pre-formatted representation. ### cost @@ -35,7 +38,15 @@ List of spell domains (links) ### duration -Spell duration, as [QuteDataTimedDuration](../QuteDataTimedDuration.md) +Spell duration, as [QuteDataTimedDuration](../QuteDataTimedDuration/README.md) + +### formattedComponents + +The components required for the spell, as a formatted string. Example: + +```md +[somatic](#), [verbal](#) +``` ### hasSections @@ -57,6 +68,10 @@ A spell’s overall power, from 1 to 10. Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + ### requirements The requirements to cast the spell (optional) @@ -83,7 +98,8 @@ Type: spell, cantrip, or focus ### subclass -List of category (Bloodline or Mystery) to Subclass (Sorcerer or Oracle). Link to class (if present) as a list of [NamedText](../../NamedText.md). +List of category (Bloodline or Mystery) to Subclass (Sorcerer or Oracle). Link to class (if present) +as a list of [NamedText](../../NamedText.md). ### tags diff --git a/docs/templates/pf2e/QuteTrait.md b/docs/templates/pf2e/QuteTrait.md index 50c571c4c..ae1f85af9 100644 --- a/docs/templates/pf2e/QuteTrait.md +++ b/docs/templates/pf2e/QuteTrait.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [categories](#categories), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[aliases](#aliases), [categories](#categories), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### aliases @@ -29,6 +29,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/QuteTraitIndex.md b/docs/templates/pf2e/QuteTraitIndex.md index b78f02f8c..8008ad819 100644 --- a/docs/templates/pf2e/QuteTraitIndex.md +++ b/docs/templates/pf2e/QuteTraitIndex.md @@ -2,13 +2,15 @@ Pf2eTools Trait index attributes (`indexTrait.md`) -This replaces the index usually generated for folders. The default template for the trait consructs a list of links to traits grouped by category. +This replaces the index usually generated for folders. +The default template for the trait consructs a list of links to +traits grouped by category. Extension of [Pf2eQuteNote](Pf2eQuteNote.md) ## Attributes -[categoryLinks](#categorylinks), [categoryToTraits](#categorytotraits), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[categoryLinks](#categorylinks), [categoryToTraits](#categorytotraits), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### categoryLinks @@ -31,6 +33,10 @@ Formatted string describing the content's source(s): `_Source: _` Note name +### reprintOf + +List of content superceded by this note (as [Reprinted](../Reprinted.md)) + ### source String describing the content's source(s) diff --git a/docs/templates/pf2e/README.md b/docs/templates/pf2e/README.md index bb750368d..5e8de0fda 100644 --- a/docs/templates/pf2e/README.md +++ b/docs/templates/pf2e/README.md @@ -1,5 +1,10 @@ # Pf2eTools templates - Qute templates for generating content from Pf2eTools data. Pathfinder data uses a lot of inline and nested embedding, which creates additional template variants and some special behavior. + +Qute templates for generating content from Pf2eTools data. + +Pathfinder data uses a lot of inline and nested embedding, +which creates additional template variants and some special +behavior. ## References @@ -7,17 +12,58 @@ - [Pf2eQuteNote](Pf2eQuteNote.md): Attributes for notes that are generated from the Pf2eTools data. - [QuteAbility](QuteAbility.md): Pf2eTools Ability attributes (`ability2md.txt` or `inline-ability2md.txt`). - [QuteAction](QuteAction/README.md): Pf2eTools Action attributes (`action2md.txt`) -- [QuteAffliction](QuteAffliction.md): Pf2eTools Affliction attributes (inline/embedded, `inline-affliction2md.txt`) + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) +- [QuteAffliction](QuteAffliction/README.md): Pf2eTools Affliction attributes (inline/embedded, `inline-affliction2md.txt`) + +Extension of [Pf2eQuteNote](Pf2eQuteNote.md) - [QuteArchetype](QuteArchetype.md): Pf2eTools Archetype attributes (`archetype2md.txt`) + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - [QuteBackground](QuteBackground.md): Pf2eTools Background attributes (`background2md.txt`) + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - [QuteBook](QuteBook/README.md): Pf2eTools Book attributes (`book2md.txt`) + +Extension of [Pf2eQuteNote](Pf2eQuteNote.md) - [QuteCreature](QuteCreature/README.md): Pf2eTools Creature attributes (`creature2md.txt`) + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) +- [QuteDataActivity](QuteDataActivity.md): Pf2eTools activity attributes. +- [QuteDataArmorClass](QuteDataArmorClass.md): Pf2eTools armor class attributes. +- [QuteDataDefenses](QuteDataDefenses/README.md): Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard. +- [QuteDataFrequency](QuteDataFrequency.md): A description of a frequency e.g. +- [QuteDataHpHardnessBt](QuteDataHpHardnessBt/README.md): Hit Points, Hardness, and a broken threshold for hazards and shields. +- [QuteDataRange](QuteDataRange/README.md): A range with a given value and unit of measurement for that value. +- [QuteDataSpeed](QuteDataSpeed.md): Examples: + +- `10 feet, swim 20 feet (some note); some ability` +- `10 feet, swim 20 feet, some ability` +- [QuteDataTimedDuration](QuteDataTimedDuration/README.md): A duration of time, represented by a numerical value and a unit. - [QuteDeity](QuteDeity/README.md): Pf2eTools Deity attributes (`deity2md.txt`) + +Deities are rendered both standalone and inline (as an admonition block). - [QuteFeat](QuteFeat.md): Pf2eTools Feat attributes (`feat2md.txt`) -- [QuteHazard](QuteHazard.md): Pf2eTools Hazard attributes (`hazard2md.txt`) + +Feats are rendered both standalone and inline (as an admonition block). +- [QuteHazard](QuteHazard/README.md): Pf2eTools Hazard attributes (`hazard2md.txt`) + +Hazards are rendered both standalone and inline (as an admonition block). - [QuteInlineAttack](QuteInlineAttack/README.md): Pf2eTools Attack attributes (inline/embedded, `inline-attack2md.txt`) + +When used directly, renders according to `inline-attack2md.txt` - [QuteItem](QuteItem/README.md): Pf2eTools Item attributes + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - [QuteRitual](QuteRitual/README.md): Pf2eTools Ritual attributes (`ritual2md.txt`) + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - [QuteSpell](QuteSpell/README.md): Pf2eTools Spell attributes (`spell2md.txt`) + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - [QuteTrait](QuteTrait.md): Pf2eTools Trait attributes (`trait2md.txt`) + +Extension of [Pf2eQuteBase](Pf2eQuteBase.md) - [QuteTraitIndex](QuteTraitIndex.md): Pf2eTools Trait index attributes (`indexTrait.md`) + +This replaces the index usually generated for folders. diff --git a/examples/config/config.5e.json b/examples/config/config.5e.json index 35d3c8533..1ed84ade5 100644 --- a/examples/config/config.5e.json +++ b/examples/config/config.5e.json @@ -1,7 +1,19 @@ { - "from" : [ - "PHB" - ], + "sources" : { + "toolsRoot" : "local/5etools/data", + "reference" : [ + "DMG" + ], + "adventure" : [ + "LMoP" + ], + "book" : [ + "PHB" + ], + "homebrew" : [ + "homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json" + ] + }, "paths" : { "compendium" : "/compendium/", "rules" : "/compendium/rules/" @@ -20,6 +32,7 @@ "excludePattern" : [ "race|.*|dmg" ], + "reprintBehavior" : "newest", "template" : { "background" : "examples/templates/tools5e/images-background2md.txt" }, @@ -30,16 +43,5 @@ "internalRoot" : "local/path/for/remote/images", "copyInternal" : true, "copyExternal" : true - }, - "full-source" : { - "adventure" : [ - "LMoP" - ], - "book" : [ - "PHB" - ], - "homebrew" : [ - "homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json" - ] } } \ No newline at end of file diff --git a/examples/config/config.5e.yaml b/examples/config/config.5e.yaml index 0b5c6cb90..4df1575ad 100644 --- a/examples/config/config.5e.yaml +++ b/examples/config/config.5e.yaml @@ -1,6 +1,14 @@ --- -from: -- "PHB" +sources: + toolsRoot: "local/5etools/data" + reference: + - "DMG" + adventure: + - "LMoP" + book: + - "PHB" + homebrew: + - "homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json" paths: compendium: "/compendium/" rules: "/compendium/rules/" @@ -14,6 +22,7 @@ exclude: - "monster|expert|slw" excludePattern: - "race|.*|dmg" +reprintBehavior: "newest" template: background: "examples/templates/tools5e/images-background2md.txt" useDiceRoller: true @@ -23,10 +32,3 @@ images: internalRoot: "local/path/for/remote/images" copyInternal: true copyExternal: true -full-source: - adventure: - - "LMoP" - book: - - "PHB" - homebrew: - - "homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json" diff --git a/examples/config/config.pf2e.json b/examples/config/config.pf2e.json index e301914c5..0f283e98b 100644 --- a/examples/config/config.pf2e.json +++ b/examples/config/config.pf2e.json @@ -1,8 +1,14 @@ { - "from" : [ - "CRB", - "GMG" - ], + "sources" : { + "reference" : [ + "CRB", + "GMG" + ], + "book" : [ + "crb", + "gmg" + ] + }, "paths" : { "compendium" : "compendium/", "rules" : "compendium/rules/" @@ -16,16 +22,11 @@ "excludePattern" : [ "background|.*|lowg" ], + "reprintBehavior" : "newest", "template" : { "ability" : "../path/to/ability2md.txt" }, "useDiceRoller" : true, "tagPrefix" : "ttrpg-cli", - "images" : { }, - "full-source" : { - "book" : [ - "crb", - "gmg" - ] - } + "images" : { } } \ No newline at end of file diff --git a/examples/config/config.pf2e.yaml b/examples/config/config.pf2e.yaml index 7f43c12ee..e1684b4a3 100644 --- a/examples/config/config.pf2e.yaml +++ b/examples/config/config.pf2e.yaml @@ -1,7 +1,11 @@ --- -from: -- "CRB" -- "GMG" +sources: + reference: + - "CRB" + - "GMG" + book: + - "crb" + - "gmg" paths: compendium: "compendium/" rules: "compendium/rules/" @@ -11,13 +15,10 @@ exclude: - "background|insurgent|apg" excludePattern: - "background|.*|lowg" +reprintBehavior: "newest" template: ability: "../path/to/ability2md.txt" useDiceRoller: true tagPrefix: "ttrpg-cli" images: { } -full-source: - book: - - "crb" - - "gmg" diff --git a/examples/config/config.schema.json b/examples/config/config.schema.json index acd838036..4b2daa621 100644 --- a/examples/config/config.schema.json +++ b/examples/config/config.schema.json @@ -77,6 +77,12 @@ "type" : "string" } }, + "includePattern" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, "paths" : { "type" : "object", "properties" : { @@ -88,6 +94,42 @@ } } }, + "reprintBehavior" : { + "type" : "string", + "enum" : [ "newest", "edition", "all" ] + }, + "sources" : { + "type" : "object", + "properties" : { + "adventure" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "book" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "homebrew" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "reference" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "toolsRoot" : { + "type" : "string" + } + } + }, "tagPrefix" : { "type" : "string" }, diff --git a/pom.xml b/pom.xml index 47046c4c4..a3eefe489 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,7 @@ 3.0.7 76.1 4.37.0 + 1.0.3 uber-jar ttrpg-convert @@ -129,7 +130,7 @@ us.hebi.sass sass-cli-maven-plugin - 1.0.3 + ${hebi-sass.version} ${sass.version} @@ -269,22 +270,11 @@ maven-javadoc-plugin ${javadoc-plugin.version} - - - dev.ebullient.convert,dev.ebullient.convert.config,dev.ebullient.convert.io,dev.ebullient.convert.tools,dev.ebullient.convert.tools.dnd5e,dev.ebullient.convert.tools.pf2e dev.ebullient.convert.io.MarkdownDoclet ${project.basedir}/target/classes - ${project.basedir} - ${project.basedir} -d - javadoc + ${project.basedir}/docs/templates false diff --git a/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java b/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java index d06ffe9c2..2150098f7 100644 --- a/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java +++ b/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; @@ -16,6 +15,7 @@ import dev.ebullient.convert.config.TemplatePaths; import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.MarkdownWriter; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Templates; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.ToolsIndex; @@ -112,10 +112,6 @@ void setDatasource(String datasource) { @Option(names = { "-c", "--config" }, description = "Config file") Path configPath; - @Option(names = "-s", hidden = true, description = "Source Books%n " + - "Comma-separated list or multiple declarations (PHB,DMG,...); use ALL for all sources") - List source = Collections.emptyList(); - @Option(names = "--index", description = "Create index of keys that can be used to exclude entries") boolean writeIndex; @@ -157,14 +153,6 @@ public Integer call() { TtrpgConfig.init(tui, game); Configurator configurator = new Configurator(tui); - if (source.size() == 1 && source.get(0).contains(",")) { - String tmp = source.remove(0); - source = List.of(tmp.split(",")); - } - if (source.contains("ALL")) { - source = List.of("*"); - } - configurator.addSources(source); configurator.setTemplatePaths(templatePaths); if (configPath != null) { @@ -183,8 +171,8 @@ public Integer call() { CompendiumConfig config = TtrpgConfig.getConfig(); - tui.done("Finished reading config."); - tui.verbosef("Writing markdown to %s.\n", output); + tui.printlnf(Msg.OK, "Finished reading config."); + tui.progressf("Writing markdown to %s.\n", output); ToolsIndex index = ToolsIndex.createIndex(); Path toolsPath = null; @@ -194,13 +182,12 @@ public Integer call() { // ATM, both 5e and pf2e use the same general structure. // Marker files are in configData for (Path inputPath : input) { - tui.printlnf("⏱️ Reading %s", inputPath); + tui.progressf("Reading %s", inputPath); Path input = inputPath.toAbsolutePath(); if (input.toFile().isDirectory()) { boolean isTools = tui.readToolsDir(input, index::importTree); if (isTools) { // we found the tools directory toolsPath = input; - TtrpgConfig.setToolsPath(toolsPath); } else { // this is some other directory full of json allOk &= tui.readDirectory("", input, index::importTree); @@ -210,30 +197,27 @@ public Integer call() { } } - // Include extra books and adventures from config (relative to toolsPath) - if (allOk && toolsPath != null) { - for (String adventure : config.getAdventures()) { - allOk &= tui.readFile(toolsPath.resolve(adventure), TtrpgConfig.getFixes(adventure), index::importTree); - } - for (String book : config.getBooks()) { - allOk &= tui.readFile(toolsPath.resolve(book), TtrpgConfig.getFixes(book), index::importTree); - } + // We've read all user specified files and user config. + if (toolsPath == null) { + tui.errorf("❌ No tools directory found. Please specify the directory containing the data files."); + return ExitCode.USAGE; } - // Include additional standalone files from config (relative to current directory) - for (String brew : config.getHomebrew()) { - allOk &= tui.readFile(Path.of(brew), TtrpgConfig.getFixes(brew), index::importTree); + // Include extra books, adventures, and homebrew from config + if (allOk && toolsPath != null) { + allOk = index.resolveSources(toolsPath); } if (!allOk) { - tui.println("❌ errors reading data. Check the following: ", - "- Are you specifying the right game? (-g 5e OR -g pf2e),", - " Using " + TtrpgConfig.getConfig().datasource().shortName(), - "- Check error messages to see what files couldn't be read", - " For bulk conversion, specify the the /data directory"); + tui.warnf(""" + Unable to find or read data. Check the following: + - Are you specifying the right game (-g 5e OR -g pf2e)? + - Check error messages to see what files couldn't be read + """); return ExitCode.USAGE; } - tui.done("Finished reading data."); + tui.printlnf(Msg.OK, "Finished reading data."); + try { index.prepare(); @@ -242,25 +226,26 @@ public Integer call() { index.writeFullIndex(output.resolve("all-index.json")); index.writeFilteredIndex(output.resolve("src-index.json")); } catch (IOException e) { - tui.error(e, " Exception: " + e.getMessage()); + tui.errorf(e, "Exception: %s", e); allOk = false; } } - tui.println("💡 Writing files to " + output); + tui.progressf("💡 Writing files to %s", output); tpl.setCustomTemplates(config); MarkdownWriter writer = new MarkdownWriter(output, tpl, tui); index.markdownConverter(writer) .writeAll() - .writeNotesAndTables() .writeImages(); + + tui.printlnf(Msg.ALLDONE, "All done!"); } catch (Throwable e) { String message = e.getMessage(); if (message == null) { message = e.getClass().getSimpleName(); } - tui.errorf(e, "An error occurred: %s.%n%nRun with --debug for details.", message); + tui.errorf(e, "An error occurred: %s.%n%nRun again with --log to capture details.", message); allOk = false; } @@ -282,6 +267,8 @@ public int run(String... args) { .setCaseInsensitiveEnumValuesAllowed(true) .setExecutionStrategy(this::executionStrategy) .setParameterExceptionHandler(new ShortErrorMessageHandler()) + .setOut(Tui.streamToWriter(System.out)) + .setErr(Tui.streamToWriter(System.err)) .execute(args); } @@ -291,7 +278,7 @@ public int handleParseException(ParameterException ex, String[] args) { CommandSpec spec = cmd.getCommandSpec(); tui.init(spec, debug, verbose); - tui.error(ex, ex.getMessage()); + tui.errorf(ex, ex.getMessage()); UnmatchedArgumentException.printSuggestions(ex, cmd.getErr()); cmd.getErr().println(cmd.getHelp().fullSynopsis()); // normal text to error stream diff --git a/src/main/java/dev/ebullient/convert/StringUtil.java b/src/main/java/dev/ebullient/convert/StringUtil.java index 6d0f64122..89238dbce 100644 --- a/src/main/java/dev/ebullient/convert/StringUtil.java +++ b/src/main/java/dev/ebullient/convert/StringUtil.java @@ -33,6 +33,18 @@ public static String format(String formatString, Object val) { return val == null || (val instanceof String && ((String) val).isBlank()) ? "" : formatString.formatted(val); } + public static String valueOrDefault(String value, String fallback) { + return value == null || value.isEmpty() ? fallback : value; + } + + public static String uppercaseFirst(String value) { + return value == null || value.isEmpty() ? value : Character.toUpperCase(value.charAt(0)) + value.substring(1); + } + + public static boolean equal(Object o1, Object o2) { + return o1 == null ? o2 == null : o1.equals(o2); + } + /** * {@link #join(String, Collection)} but with the ability to accept varargs. * diff --git a/src/main/java/dev/ebullient/convert/VersionProvider.java b/src/main/java/dev/ebullient/convert/VersionProvider.java index f64bc1af5..b556649c6 100644 --- a/src/main/java/dev/ebullient/convert/VersionProvider.java +++ b/src/main/java/dev/ebullient/convert/VersionProvider.java @@ -15,8 +15,7 @@ public String[] getVersion() { properties.load(TtrpgConfig.class.getResourceAsStream("/git.properties")); return new String[] { "${COMMAND-FULL-NAME} version " + properties.getProperty("git.build.version"), - "Git commit: " + properties.get("git.commit.id.abbrev"), - "Build time: " + properties.get("git.build.time") + "Git commit: " + properties.get("git.commit.id.abbrev") }; } catch (IOException e) { return new String[] { "${COMMAND-FULL-NAME} version unknown " }; diff --git a/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java b/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java index 26adef475..f709974a8 100644 --- a/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java +++ b/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java @@ -2,34 +2,32 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.BiConsumer; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.config.TtrpgConfig.Fix; +import dev.ebullient.convert.config.UserConfig.ImageOptions; +import dev.ebullient.convert.config.UserConfig.VaultPaths; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.ParseState; -import io.quarkus.runtime.annotations.RegisterForReflection; public class CompendiumConfig { + public enum DiceRoller { disabled, disabledUsingFS, @@ -56,8 +54,12 @@ static DiceRoller fromAttributes(Boolean useDiceRoller, Boolean yamlStatblocks) final static Path CWD = Path.of("."); + @JsonIgnore final Tui tui; - final Datasource datasource; + + Datasource datasource; + + @JsonIgnore final ParseState parseState = new ParseState(); String tagPrefix = ""; @@ -65,6 +67,7 @@ static DiceRoller fromAttributes(Boolean useDiceRoller, Boolean yamlStatblocks) ImageOptions images; boolean allSources = false; DiceRoller useDiceRoller = DiceRoller.disabled; + ReprintBehavior reprintBehavior = ReprintBehavior.newest; final Set allowedSources = new HashSet<>(); final Set includedKeys = new HashSet<>(); final Set includedGroups = new HashSet<>(); @@ -76,9 +79,9 @@ static DiceRoller fromAttributes(Boolean useDiceRoller, Boolean yamlStatblocks) final Map customTemplates = new HashMap<>(); final Map sourceIdAlias = new HashMap<>(); - CompendiumConfig(Datasource src, Tui tui) { + CompendiumConfig(Datasource datasource, Tui tui) { + this.datasource = datasource; this.tui = tui; - this.datasource = src; } public ParseState parseState() { @@ -97,6 +100,10 @@ public DiceRoller useDiceRoller() { return useDiceRoller; } + public ReprintBehavior reprintBehavior() { + return reprintBehavior; + } + public boolean allSources() { return allSources; } @@ -109,6 +116,10 @@ public String getAllowedSourcePattern() { return allSources ? "([^|]+)" : "(" + String.join("|", allowedSources) + ")"; } + public boolean readSource(Path p, List fixes, BiConsumer callback) { + return tui.readFile(p, fixes, callback); + } + public boolean sourceIncluded(String source) { if (allSources) { return true; @@ -119,6 +130,16 @@ public boolean sourceIncluded(String source) { return allowedSources.contains(source.toLowerCase()); } + public boolean sourcesIncluded(List sources) { + if (allSources) { + return true; + } + if (sources == null || sources.isEmpty()) { + return false; + } + return sources.stream().anyMatch(x -> allowedSources.contains(x.toLowerCase())); + } + public boolean sourceIncluded(CompendiumSources source) { if (allSources) { return true; @@ -134,9 +155,6 @@ public Optional keyIsIncluded(String key) { excludedPatterns.stream().anyMatch(x -> x.matcher(key).matches())) { return Optional.of(false); } - if (allSources) { - return Optional.of(true); - } return Optional.empty(); } @@ -170,7 +188,7 @@ public String tagOfRaw(String tag) { return tagPrefix + tag; } - public List getBooks() { + public List resolveBooks() { // works for 5eTools and pf2eTools return books.stream() .map(b -> { @@ -185,7 +203,7 @@ public List getBooks() { .toList(); } - public List getAdventures() { + public List resolveAdventures() { // works for 5eTools and pf2eTools return adventures.stream() .map(a -> { @@ -200,7 +218,7 @@ public List getAdventures() { .toList(); } - public Collection getHomebrew() { + public Collection resolveHomebrew() { return Collections.unmodifiableCollection(homebrew); } @@ -314,33 +332,22 @@ public boolean readConfiguration(Path configPath) { } /** - * Reads contents of JsonNode. If TTRPG/Compendium - * configuration is present, it will create the - * CompendiumConfig for it, and set that on - * {@link TtrpgConfig} (as default, or with - * appropriate key). + * Reads contents of JsonNode. + * Will read and process user configuration keys if they are + * present. * * @param node */ public void readConfigIfPresent(JsonNode node) { - JsonNode ttrpgNode = ConfigKeys.ttrpg.get(node); - if (ttrpgNode != null) { - for (Iterator> i = ttrpgNode.fields(); i.hasNext();) { - Entry e = i.next(); - Datasource source = Datasource.matchDatasource(e.getKey()); - CompendiumConfig cfg = TtrpgConfig.getConfig(source); - readConfig(cfg, e.getValue()); - } - } else if (userConfigPresent(node)) { + if (userConfigPresent(node)) { CompendiumConfig cfg = TtrpgConfig.getConfig(); readConfig(cfg, node); } } private void readConfig(CompendiumConfig config, JsonNode node) { - InputConfig input = Tui.MAPPER.convertValue(node, InputConfig.class); + UserConfig input = Tui.MAPPER.convertValue(node, UserConfig.class); - config.addSources(input.from); if (input.useDiceRoller != null || input.yamlStatblocks != null) { config.useDiceRoller = DiceRoller.fromAttributes(input.useDiceRoller, input.yamlStatblocks); } @@ -350,9 +357,10 @@ private void readConfig(CompendiumConfig config, JsonNode node) { input.exclude.forEach(s -> config.excludedKeys.add(s.toLowerCase())); input.excludePattern.forEach(s -> config.addExcludePattern(s.toLowerCase())); - config.books.addAll(input.fullSource.book); - config.adventures.addAll(input.fullSource.adventure); - config.homebrew.addAll(input.fullSource.homebrew); + config.books.addAll(input.books()); + config.adventures.addAll(input.adventures()); + config.homebrew.addAll(input.homebrew()); + config.addSources(input.references()); config.images = new ImageOptions(config.images, input.images); config.paths = new PathAttributes(config.paths, input.paths); @@ -370,11 +378,13 @@ private void readConfig(CompendiumConfig config, JsonNode node) { tplPaths.verify(tui); config.customTemplates.putAll(tplPaths.customTemplates); } + + config.reprintBehavior = input.reprintBehavior; } } private static boolean userConfigPresent(JsonNode node) { - return Stream.of(ConfigKeys.values()) + return Stream.of(UserConfig.ConfigKeys.values()) .anyMatch((k) -> k.get(node) != null); } @@ -388,7 +398,7 @@ private static class PathAttributes { PathAttributes() { } - public PathAttributes(PathAttributes old, InputPaths paths) { + public PathAttributes(PathAttributes old, VaultPaths paths) { String root; if (paths.rules != null) { root = toRoot(paths.rules); @@ -428,132 +438,4 @@ private static String toVaultRoot(String root) { return root.replaceAll(" ", "%20"); } } - - private enum ConfigKeys { - useDiceRoller, - exclude, - excludePattern, - fallbackPaths(List.of("fallback-paths")), - from, - fullSource(List.of("convert", "full-source")), - images, - include, - includeGroups, - paths, - yamlStatblocks, - tagPrefix, - template, - ttrpg; - - final List aliases; - - ConfigKeys() { - aliases = List.of(); - } - - ConfigKeys(List aliases) { - this.aliases = aliases; - } - - JsonNode get(JsonNode node) { - JsonNode child = node.get(this.name()); - if (child == null) { - Optional y = aliases.stream() - .map(node::get) - .filter(Objects::nonNull) - .findFirst(); - return y.orElse(null); - } - return child; - } - } - - @RegisterForReflection - @JsonInclude(JsonInclude.Include.NON_EMPTY) - public static class InputConfig { - List from = new ArrayList<>(); - - @JsonAlias({ "convert" }) - @JsonProperty(value = "full-source") - FullSource fullSource = new FullSource(); - - InputPaths paths = new InputPaths(); - - List include = new ArrayList<>(); - - List includeGroup = new ArrayList<>(); - - List exclude = new ArrayList<>(); - - List excludePattern = new ArrayList<>(); - - Map template = new HashMap<>(); - - Boolean useDiceRoller = null; - Boolean yamlStatblocks = null; - - String tagPrefix = ""; - - ImageOptions images = new ImageOptions(); - } - - @RegisterForReflection - @JsonInclude(JsonInclude.Include.NON_EMPTY) - static class ImageOptions { - String internalRoot; - Boolean copyInternal; - Boolean copyExternal; - final Map fallbackPaths = new HashMap<>(); - - public ImageOptions() { - } - - public ImageOptions(ImageOptions images, ImageOptions images2) { - if (images != null) { - copyExternal = images.copyExternal; - copyInternal = images.copyInternal; - internalRoot = images.internalRoot; - fallbackPaths.putAll(images.fallbackPaths); - } - if (images2 != null) { - copyExternal = images2.copyExternal == null - ? copyExternal - : images2.copyExternal; - copyInternal = images2.copyInternal == null - ? copyInternal - : images2.copyInternal; - internalRoot = images2.internalRoot == null - ? internalRoot - : images2.internalRoot; - fallbackPaths.putAll(images2.fallbackPaths); - } - } - - public boolean copyExternal() { - return copyExternal != null && copyExternal; - } - - public boolean copyInternal() { - return copyInternal != null && copyInternal; - } - - public Map fallbackPaths() { - return Collections.unmodifiableMap(fallbackPaths); - } - } - - @RegisterForReflection - @JsonInclude(JsonInclude.Include.NON_EMPTY) - static class FullSource { - List adventure = new ArrayList<>(); - List book = new ArrayList<>(); - List homebrew = new ArrayList<>(); - } - - @RegisterForReflection - @JsonInclude(JsonInclude.Include.NON_EMPTY) - static class InputPaths { - String compendium; - String rules; - } } diff --git a/src/main/java/dev/ebullient/convert/config/ReprintBehavior.java b/src/main/java/dev/ebullient/convert/config/ReprintBehavior.java new file mode 100644 index 000000000..b6d32b875 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/config/ReprintBehavior.java @@ -0,0 +1,7 @@ +package dev.ebullient.convert.config; + +public enum ReprintBehavior { + newest, // Newest only + edition, // Follow reprints within an edition + all; // Ignore reprints +} diff --git a/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java b/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java index ca567b44b..179ac1ae3 100644 --- a/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java +++ b/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java @@ -18,7 +18,8 @@ import com.fasterxml.jackson.databind.node.NullNode; import dev.ebullient.convert.config.CompendiumConfig.Configurator; -import dev.ebullient.convert.config.CompendiumConfig.ImageOptions; +import dev.ebullient.convert.config.UserConfig.ImageOptions; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonNodeReader; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -27,30 +28,35 @@ public class TtrpgConfig { public static final String DEFAULT_IMG_ROOT = "imgRoot"; - static final Map globalConfig = new HashMap<>(); - static final Map userConfig = new HashMap<>(); static final Set missingSourceName = new HashSet<>(); - private static Datasource datasource = Datasource.tools5e; + private static Datasource datasource; + private static CompendiumConfig activeConfig = null; + private static DatasourceConfig datasourceConfig = null; private static Tui tui; private static ImageRoot internalImageRoot; private static Path toolsPath; + public static void init(Tui tui) { + init(tui, null); + } + public static void init(Tui tui, Datasource datasource) { - userConfig.clear(); TtrpgConfig.tui = tui; - TtrpgConfig.datasource = datasource; TtrpgConfig.internalImageRoot = null; TtrpgConfig.toolsPath = null; + TtrpgConfig.activeConfig = null; + TtrpgConfig.datasource = datasource == null ? Datasource.tools5e : datasource; + TtrpgConfig.datasourceConfig = new DatasourceConfig(); + TtrpgConfig.missingSourceName.clear(); readSystemConfig(); } public static CompendiumConfig getConfig() { - return getConfig(datasource == null ? Datasource.tools5e : datasource); - } - - public static CompendiumConfig getConfig(Datasource datasource) { - return userConfig.computeIfAbsent(datasource, (k) -> new CompendiumConfig(datasource, tui)); + if (activeConfig == null) { + activeConfig = new CompendiumConfig(TtrpgConfig.datasource, tui); + } + return activeConfig; } public static String getConstant(String key) { @@ -62,7 +68,7 @@ public static void setToolsPath(Path toolsPath) { } private static DatasourceConfig activeDSConfig() { - return globalConfig.computeIfAbsent(datasource, (k) -> new DatasourceConfig()); + return datasourceConfig; } public static List getFixes(String filepath) { @@ -70,13 +76,21 @@ public static List getFixes(String filepath) { } public static String sourceToLongName(String src) { - return activeDSConfig().abvToName.getOrDefault(sourceToAbbreviation(src).toLowerCase(), src); + String abbreviation = sourceToAbbreviation(src).toLowerCase(); + SourceReference ref = activeDSConfig().reference.get(abbreviation); + return ref == null ? src : ref.name; } public static String sourceToAbbreviation(String src) { return activeDSConfig().longToAbv.getOrDefault(src.toLowerCase(), src); } + public static String sourcePublicationDate(String src) { + String abbreviation = sourceToAbbreviation(src).toLowerCase(); + SourceReference ref = activeDSConfig().reference.get(abbreviation); + return ref == null || ref.date == null ? "1970-01-01" : ref.date; // utils.json: ascSortDateString + } + public static Collection getTemplateKeys() { return activeDSConfig().templateKeys; } @@ -131,7 +145,7 @@ private ImageRoot(String cfgRoot, ImageOptions options) { this.internalImageRoot = endWithSlash(imgPath.toString()); this.copyInternal = true; } - Tui.instance().printlnf("[ 🖼️ OK] Using %s as the source for remote images (copyInternal=%s)", + Tui.instance().infof("Using %s as the source for remote images (copyInternal=%s)", this.internalImageRoot, this.copyInternal); } } @@ -203,7 +217,7 @@ public static void checkKnown(Collection bookSources) { DatasourceConfig activeConfig = activeDSConfig(); bookSources.forEach(s -> { String check = s.toLowerCase(); - if (activeConfig.abvToName.containsKey(check)) { + if (activeConfig.reference.containsKey(check)) { return; } String alternate = activeConfig.longToAbv.get(check); @@ -211,7 +225,7 @@ public static void checkKnown(Collection bookSources) { return; } if (missingSourceName.add(check)) { - tui.warnf("Source %s is unknown", s); + tui.warnf(Msg.SOURCE, "Source %s is unknown", s); } }); } @@ -235,61 +249,60 @@ private static void readSystemConfig() { JsonNode node = Tui.readTreeFromResource("/convertData.json"); readSystemConfig(node); - node = Tui.readTreeFromResource("/sourceMap.json"); + node = Tui.readTreeFromResource("/sourceMap.yaml"); readSystemConfig(node); } // Global config: path mapping for missing images protected static void readSystemConfig(JsonNode node) { - DatasourceConfig config = globalConfig.computeIfAbsent(datasource, k -> new DatasourceConfig()); - if (datasource == Datasource.tools5e) { - JsonNode config5e = ConfigKeys.config5e.get(node); + JsonNode config5e = ConfigKeys.config5e.getFrom(node); if (config5e != null) { - JsonNode srdEntries = ConfigKeys.srdEntries.get(config5e); + JsonNode srdEntries = ConfigKeys.srdEntries.getFrom(config5e); if (srdEntries != null) { - config.data.put(ConfigKeys.srdEntries.name(), srdEntries); + datasourceConfig.data.put(ConfigKeys.srdEntries.name(), srdEntries); } - config.constants.putAll(ConfigKeys.constants.getAsMap(config5e)); - config.aliases.putAll(ConfigKeys.aliases.getAsMap(config5e)); - config.abvToName.putAll(ConfigKeys.abvToName.getAsKeyLowerMap(config5e)); - config.longToAbv.putAll(ConfigKeys.longToAbv.getAsKeyLowerMap(config5e)); - config.fallbackImagePaths.putAll(ConfigKeys.fallbackImage.getAsMap(config5e)); - config.markerFiles.addAll(ConfigKeys.markerFiles.getAsList(config5e)); - config.sources.addAll(ConfigKeys.sources.getAsList(config5e)); - config.indexes.putAll(ConfigKeys.indexes.getAsKeyLowerMap(config5e)); - config.templateKeys.addAll(ConfigKeys.templateKeys.getAsList(config5e)); - - Map> fixes = ConfigKeys.fixes.getAs(config5e, FIXES); - if (fixes != null) { - config.fixes.putAll(fixes); + JsonNode basicRules = ConfigKeys.basicRules.getFrom(config5e); + if (basicRules != null) { + datasourceConfig.data.put(ConfigKeys.basicRules.name(), basicRules); } + JsonNode freeRules2024 = ConfigKeys.freeRules2024.getFrom(config5e); + if (freeRules2024 != null) { + datasourceConfig.data.put(ConfigKeys.freeRules2024.name(), freeRules2024); + } + readCommonSystemConfig(config5e); } } if (datasource == Datasource.toolsPf2e) { - JsonNode configPf2e = ConfigKeys.configPf2e.get(node); + JsonNode configPf2e = ConfigKeys.configPf2e.getFrom(node); if (configPf2e != null) { - config.abvToName.putAll(ConfigKeys.abvToName.getAsKeyLowerMap(configPf2e)); - config.longToAbv.putAll(ConfigKeys.longToAbv.getAsKeyLowerMap(configPf2e)); - config.fallbackImagePaths.putAll(ConfigKeys.fallbackImage.getAsMap(configPf2e)); - config.markerFiles.addAll(ConfigKeys.markerFiles.getAsList(configPf2e)); - config.sources.addAll(ConfigKeys.sources.getAsList(configPf2e)); - config.indexes.putAll(ConfigKeys.indexes.getAsKeyLowerMap(configPf2e)); - config.templateKeys.addAll(ConfigKeys.templateKeys.getAsList(configPf2e)); - - Map> fixes = ConfigKeys.fixes.getAs(configPf2e, FIXES); - if (fixes != null) { - config.fixes.putAll(fixes); - } + readCommonSystemConfig(configPf2e); } } } + protected static void readCommonSystemConfig(JsonNode source) { + datasourceConfig.constants.putAll(ConfigKeys.constants.getAsMap(source)); + datasourceConfig.aliases.putAll(ConfigKeys.aliases.getAsMap(source)); + datasourceConfig.reference.putAll(ConfigKeys.reference.getAsKeyLowerRefMap(source)); + datasourceConfig.longToAbv.putAll(ConfigKeys.longToAbv.getAsKeyLowerMap(source)); + datasourceConfig.fallbackImagePaths.putAll(ConfigKeys.fallbackImage.getAsMap(source)); + datasourceConfig.markerFiles.addAll(ConfigKeys.markerFiles.getAsList(source)); + datasourceConfig.sources.addAll(ConfigKeys.sources.getAsList(source)); + datasourceConfig.indexes.putAll(ConfigKeys.indexes.getAsKeyLowerMap(source)); + datasourceConfig.templateKeys.addAll(ConfigKeys.templateKeys.getAsList(source)); + + Map> fixes = ConfigKeys.fixes.getAs(source, FIXES); + if (fixes != null) { + datasourceConfig.fixes.putAll(fixes); + } + } + static class DatasourceConfig { final Map data = new HashMap<>(); final Map constants = new HashMap<>(); final Map aliases = new HashMap<>(); - final Map abvToName = new HashMap<>(); + final Map reference = new HashMap<>(); final Map longToAbv = new HashMap<>(); final Map fallbackImagePaths = new HashMap<>(); final Map> fixes = new HashMap<>(); @@ -309,11 +322,11 @@ public List findFixesFor(String filepath) { public boolean addSource(String name, String abv, String longAbv) { String key = abv.toLowerCase(); - if (abvToName.containsKey(key)) { + if (reference.containsKey(key)) { tui.errorf("Duplicate source abbreviation %s for %s", abv, name); return false; } - abvToName.put(key, name); + reference.put(key, new SourceReference(name)); if (longAbv != null) { String longKey = longAbv.toLowerCase(); @@ -329,9 +342,26 @@ public boolean addSource(String name, String abv, String longAbv) { } } + public final static TypeReference> MAP_REFERENCE = new TypeReference<>() { + }; + public final static TypeReference>> FIXES = new TypeReference<>() { }; + @RegisterForReflection + static class SourceReference { + String name; + String type; + String date; + + SourceReference() { + } + + SourceReference(String name) { + this.name = name; + } + } + @RegisterForReflection public static class Fix { public String _comment; @@ -342,6 +372,8 @@ public static class Fix { enum ConfigKeys implements JsonNodeReader { aliases, abvToName, + basicRules, // 5e + freeRules2024, // 5e config5e, configPf2e, constants, @@ -352,13 +384,11 @@ enum ConfigKeys implements JsonNodeReader { longToAbv, markerFiles, properties, + reference, sources, srdEntries, - templateKeys; - - JsonNode get(JsonNode node) { - return node.get(this.name()); - } + templateKeys, + ; public T getAs(JsonNode node, TypeReference ref) { JsonNode obj = node.get(this.name()); @@ -384,6 +414,20 @@ Map getAsKeyLowerMap(JsonNode node) { return result; } + Map getAsKeyLowerRefMap(JsonNode node) { + JsonNode map = node.get(this.name()); + if (map == null) { + return Map.of(); + } + Map result = new HashMap<>(); + map.fields().forEachRemaining(e -> { + String key = e.getKey().toLowerCase(); + SourceReference ref = Tui.MAPPER.convertValue(e.getValue(), SourceReference.class); + result.put(key, ref); + }); + return result; + } + List getAsList(JsonNode node) { JsonNode list = node.get(this.name()); return list == null diff --git a/src/main/java/dev/ebullient/convert/config/UserConfig.java b/src/main/java/dev/ebullient/convert/config/UserConfig.java new file mode 100644 index 000000000..b4f750bac --- /dev/null +++ b/src/main/java/dev/ebullient/convert/config/UserConfig.java @@ -0,0 +1,202 @@ +package dev.ebullient.convert.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class UserConfig { + + Sources sources = new Sources(); + + @Deprecated + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + List from = new ArrayList<>(); + + @Deprecated + @JsonAlias({ "convert", "full-source" }) + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + FullSource fullSource = new FullSource(); + + VaultPaths paths = new VaultPaths(); + + List include = new ArrayList<>(); + + List includePattern = new ArrayList<>(); + + List includeGroup = new ArrayList<>(); + + List exclude = new ArrayList<>(); + + List excludePattern = new ArrayList<>(); + + ReprintBehavior reprintBehavior = ReprintBehavior.newest; + + Map template = new HashMap<>(); + + Boolean useDiceRoller = null; + Boolean yamlStatblocks = null; + + String tagPrefix = ""; + + ImageOptions images = new ImageOptions(); + + List references() { + List reference = new ArrayList<>(); + reference.addAll(sources.reference); + if (from != null) { + reference.addAll(from); + } + return reference; + } + + List books() { + List books = new ArrayList<>(); + books.addAll(sources.book); + if (fullSource != null) { + books.addAll(fullSource.book); + } + return books; + } + + List adventures() { + List adventures = new ArrayList<>(); + adventures.addAll(sources.adventure); + if (fullSource != null) { + adventures.addAll(fullSource.adventure); + } + return adventures; + } + + List homebrew() { + List homebrew = new ArrayList<>(); + homebrew.addAll(sources.homebrew); + if (fullSource != null) { + homebrew.addAll(fullSource.homebrew); + } + return homebrew; + } + + enum ConfigKeys { + useDiceRoller, + exclude, + excludePattern, + fallbackPaths(List.of("fallback-paths")), + from, + fullSource(List.of("convert", "full-source")), + images, + include, + includeGroups, + includePattern, + paths, + reprintBehavior, + sources, + yamlStatblocks, + tagPrefix, + template; + + final List aliases; + + ConfigKeys() { + aliases = List.of(); + } + + ConfigKeys(List aliases) { + this.aliases = aliases; + } + + JsonNode get(JsonNode node) { + JsonNode child = node.get(this.name()); + if (child == null) { + Optional y = aliases.stream() + .map(node::get) + .filter(Objects::nonNull) + .findFirst(); + return y.orElse(null); + } + return child; + } + } + + @RegisterForReflection + @JsonInclude(JsonInclude.Include.NON_EMPTY) + static class VaultPaths { + String compendium; + String rules; + } + + @RegisterForReflection + @JsonInclude(JsonInclude.Include.NON_EMPTY) + static class Sources { + String toolsRoot; + List reference = new ArrayList<>(); + List adventure = new ArrayList<>(); + List book = new ArrayList<>(); + List homebrew = new ArrayList<>(); + } + + @RegisterForReflection + @JsonInclude(JsonInclude.Include.NON_EMPTY) + static class FullSource { + List adventure = new ArrayList<>(); + List book = new ArrayList<>(); + List homebrew = new ArrayList<>(); + } + + @RegisterForReflection + @JsonInclude(JsonInclude.Include.NON_EMPTY) + static class ImageOptions { + String internalRoot; + Boolean copyInternal; + Boolean copyExternal; + final Map fallbackPaths = new HashMap<>(); + + public ImageOptions() { + } + + public ImageOptions(ImageOptions images, ImageOptions images2) { + if (images != null) { + copyExternal = images.copyExternal; + copyInternal = images.copyInternal; + internalRoot = images.internalRoot; + fallbackPaths.putAll(images.fallbackPaths); + } + if (images2 != null) { + copyExternal = images2.copyExternal == null + ? copyExternal + : images2.copyExternal; + copyInternal = images2.copyInternal == null + ? copyInternal + : images2.copyInternal; + internalRoot = images2.internalRoot == null + ? internalRoot + : images2.internalRoot; + fallbackPaths.putAll(images2.fallbackPaths); + } + } + + public boolean copyExternal() { + return copyExternal != null && copyExternal; + } + + public boolean copyInternal() { + return copyInternal != null && copyInternal; + } + + public Map fallbackPaths() { + return Collections.unmodifiableMap(fallbackPaths); + } + } +} diff --git a/src/main/java/dev/ebullient/convert/io/JavadocIgnore.java b/src/main/java/dev/ebullient/convert/io/JavadocIgnore.java new file mode 100644 index 000000000..68bddf7fe --- /dev/null +++ b/src/main/java/dev/ebullient/convert/io/JavadocIgnore.java @@ -0,0 +1,12 @@ +package dev.ebullient.convert.io; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface JavadocIgnore { + +} diff --git a/src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java b/src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java new file mode 100644 index 000000000..3d86cf3d6 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java @@ -0,0 +1,13 @@ +package dev.ebullient.convert.io; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.METHOD }) +public @interface JavadocVerbatim { + // include this method using the exact method or field name + +} diff --git a/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java b/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java index 85755d5d4..5b9b0a51e 100644 --- a/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java +++ b/src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java @@ -15,10 +15,19 @@ import java.util.Objects; import java.util.Set; import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.lang.model.SourceVersion; -import javax.lang.model.element.*; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -41,6 +50,8 @@ import jdk.javadoc.doclet.Reporter; public class MarkdownDoclet implements Doclet { + Pattern preformattedText = Pattern.compile("```|
");
+
     Reporter reporter;
     DocletEnvironment environment;
     Path outputDirectory;
@@ -65,7 +76,7 @@ public String getDescription() {
 
         @Override
         public Kind getKind() {
-            return Doclet.Option.Kind.STANDARD;
+            return Kind.OTHER;
         }
 
         @Override
@@ -112,7 +123,10 @@ public Set getSupportedOptions() {
     @Override
     public boolean run(DocletEnvironment environment) {
         try {
+            System.out.println("TTRPG Convert Cli Markdown Doclet: run begin");
+            System.out.println("target: " + targetDir.getValue());
             processFiles(environment);
+            System.out.println("TTRPG Convert Cli Markdown Doclet: run end");
         } catch (final Exception e) {
             reporter.print(Diagnostic.Kind.ERROR, e.getMessage());
             e.printStackTrace();
@@ -138,37 +152,54 @@ protected void processFiles(DocletEnvironment environment) throws IOException {
 
         Set elements = environment.getIncludedElements();
 
-        Map> innerClasses = ElementFilter.typesIn(elements).stream()
-                .filter(t -> t.getKind() != ElementKind.INTERFACE)
-                .filter(t -> t.getNestingKind() != NestingKind.TOP_LEVEL)
-                .filter(t -> !isExcluded(t))
-                .collect(Collectors.groupingBy(t -> (TypeElement) t.getEnclosingElement()));
-
-        for (TypeElement t : innerClasses.keySet()) {
-            String reference = t.getQualifiedName().toString();
-            classNameMapping.put(reference, reference + ".README");
-        }
+        // Find TOP_LEVEL elements that enclose (interesting) others
+        ElementFilter.typesIn(elements).stream()
+                .filter(t -> isQute(t)) // only include template-related classes
+                .filter(t -> !isIgnored(t)) // skip @JavadocIgnore and Builder classes
+                .filter(t -> t.getNestingKind() != NestingKind.TOP_LEVEL) // find nested elements
+                .filter(t -> t.getKind() != ElementKind.INTERFACE) // skip inner interfaces
+                .map(TypeElement::getEnclosingElement) // map to enclosing element
+                .distinct() // remove duplicates
+                .forEach(te -> {
+                    // Append "README" to the class name to generate a README file
+                    // inside the directory for GH-based documentation
+                    String reference = te.toString();
+                    classNameMapping.put(reference, reference + ".README");
+                });
 
         // Print package indexes (README.md)
         packages = ElementFilter.packagesIn(elements);
         for (PackageElement p : packages) {
-            writeReadmeFile(docTrees, p);
+            if (isQute(p) && !isIgnored(p)) {
+                writeReadmeFile(docTrees, p);
+            }
         }
 
         for (TypeElement t : ElementFilter.typesIn(elements)) {
-            if (t.getKind() == ElementKind.INTERFACE) {
+            if (!isQute(t) || isExcluded(t)) {
                 continue;
             }
+            String mapping = classNameMapping.get(t.getQualifiedName().toString());
+            System.out.println(
+                    t.getKind().toString().substring(0, 4)
+                            + "\t" + t.getQualifiedName()
+                            + (mapping == null ? "" : "\n\t-- " + mapping));
             writeReferenceFile(docTrees, t);
         }
     }
 
+    private void debugFile(String type, Name name, Path outFile) {
+        // String out = outFile.toString().replace(targetDir.getValue(), "");
+        // System.out.println(type + ", " + name.toString() + " --> " + out);
+    }
+
     protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOException {
         String name = t.getSimpleName().toString();
         if (name.contains("Builder")) {
             return;
         }
         Path outFile = getOutputFile(t);
+        debugFile("reference", t.getQualifiedName(), outFile);
         try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) {
             Aggregator aggregator = new Aggregator();
             aggregator.add("# " + name + "\n\n");
@@ -197,6 +228,7 @@ protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOExc
                     .collect(Collectors.joining(", ")));
             aggregator.add("\n\n");
 
+            Map> recordContent = new HashMap<>();
             if (t.getKind() == ElementKind.RECORD) {
                 // If it's a record, then we can't retrieve the attributes as Elements, so we have to parse them from
                 // the comment tree instead.
@@ -206,46 +238,68 @@ protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOExc
                         .map(param -> (ParamTree) param)
                         .filter(p -> !p.getName().toString().startsWith("_")) // fields with "_" prefix are internal
                         .forEach(param -> {
-                            aggregator.add("\n\n### " + param.getName() + "\n\n");
-                            aggregator.addAll(param.getDescription());
+                            recordContent.put(param.getName().toString(), param.getDescription());
                         });
-            } else {
-                for (Map.Entry entry : members.entrySet()) {
-                    aggregator.add("\n\n### " + entry.getKey() + "\n\n");
+            }
+
+            for (Map.Entry entry : members.entrySet()) {
+                aggregator.add("\n\n### " + entry.getKey() + "\n\n");
+                var content = recordContent.get(entry.getKey());
+                if (content != null) {
+                    aggregator.addAll(content);
+                } else {
                     aggregator.addFullBody(docTrees.getDocCommentTree(entry.getValue()));
                 }
             }
+
             out.println(aggregator);
+            out.flush();
         }
     }
 
     protected void processElement(DocTrees docTrees, Map members, Element e) {
         String name = e.getSimpleName().toString();
         ElementKind kind = e.getKind();
-        if (!e.getModifiers().stream().anyMatch(m -> m == Modifier.PUBLIC)
-                || e.getModifiers().stream().anyMatch(m -> m == Modifier.STATIC)) {
+
+        if (isIgnored(e)) {
+            // Return early if the element is annotated with @JavadocIgnore
             return;
         }
-        if (kind == ElementKind.METHOD) {
-            if (!name.startsWith("get") && !name.startsWith("is")) {
+
+        if (!isIncludedVerbatim(e)) {
+            // If the element is not annotated with @JavadocVerbatim,
+            // filter and format the element name
+
+            if (!e.getModifiers().stream().anyMatch(m -> m == Modifier.PUBLIC)
+                    || e.getModifiers().stream().anyMatch(m -> m == Modifier.STATIC)) {
+                // Skip non-public and static elements
                 return;
             }
-            if (e.getAnnotation(Deprecated.class) != null) {
+            if (kind == ElementKind.METHOD) {
+                if (!name.startsWith("get") && !name.startsWith("is")) {
+                    // Skip methods that don't start with "get" or "is"
+                    return;
+                }
+                if (e.getAnnotation(Deprecated.class) != null) {
+                    // Skip deprecated methods
+                    return;
+                }
+
+                name = name.replaceFirst("(get|is)", "");
+                name = name.substring(0, 1).toLowerCase() + name.substring(1);
+            } else if (!kind.isField() && kind != ElementKind.RECORD_COMPONENT) {
+                // Skip any other non-field elements
                 return;
             }
-        } else if (!kind.isField() && kind != ElementKind.RECORD_COMPONENT) {
-            return;
         }
 
-        if (kind == ElementKind.METHOD) {
-            name = name.replaceFirst("(get|is)", "");
-            name = name.substring(0, 1).toLowerCase() + name.substring(1);
-        }
         members.put(name, e);
     }
 
     void writeReadmeFile(DocTrees docTrees, PackageElement p) throws IOException {
         Path outFile = getOutputFile(p);
+        debugFile("readme", p.getQualifiedName(), outFile);
+
         try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) {
             Aggregator aggregator = new Aggregator();
 
@@ -255,10 +309,10 @@ void writeReadmeFile(DocTrees docTrees, PackageElement p) throws IOException {
             // Make list linking to package members
             Map members = new TreeMap<>();
             for (Element e : p.getEnclosedElements()) {
+                TypeElement te = (TypeElement) e;
                 if (isExcluded(e)) {
                     continue;
                 }
-                TypeElement te = (TypeElement) e;
                 if (te.getKind() == ElementKind.INTERFACE) {
                     continue;
                 }
@@ -283,14 +337,36 @@ void writeReadmeFile(DocTrees docTrees, PackageElement p) throws IOException {
                 aggregator.add(result);
             }
             out.println(aggregator.toString());
+            out.flush();
         }
     }
 
+    private boolean isQute(QualifiedNameable e) {
+        return e.getQualifiedName().toString().contains("qute");
+    }
+
+    boolean isIgnored(Element element) {
+        return element.getAnnotation(JavadocIgnore.class) != null
+                || element.getSimpleName().toString().contains("Builder");
+    }
+
+    boolean isIncludedVerbatim(Element element) {
+        return element.getAnnotation(JavadocVerbatim.class) != null;
+    }
+
     boolean isExcluded(Element element) {
-        ElementKind kind = element.getKind();
+        if (isIncludedVerbatim(element)) {
+            return false;
+        }
+
+        boolean excludeKind = switch (element.getKind()) {
+            case CLASS, INTERFACE, RECORD, ENUM -> false;
+            default -> true;
+        };
+
         return !environment.isIncluded(element)
-                || element.getSimpleName().toString().contains("Builder")
-                || (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE && kind != ElementKind.ENUM);
+                || isIgnored(element)
+                || excludeKind;
     }
 
     String getDescription(DocTrees docTrees, TypeElement te) {
@@ -327,11 +403,11 @@ static TypeElement getSuperclassElement(TypeElement typeElement) {
     }
 
     String qualifiedNameToPath(QualifiedNameable element) {
-        String reference = element.getQualifiedName().toString();
-        return qualifiedNameToPath(classNameMapping.getOrDefault(reference, reference));
+        return qualifiedNameToPath(element.getQualifiedName().toString());
     }
 
     String qualifiedNameToPath(String reference) {
+        reference = classNameMapping.getOrDefault(reference, reference);
         if (reference.endsWith("qute")) {
             reference += ".README";
         } else if (!isValidClass(reference.replace(".README", ""))) {
@@ -373,7 +449,17 @@ void addAll(List docTrees) {
         void add(DocTree docTree) {
             switch (docTree.getKind()) {
                 case TEXT:
-                    add(((TextTree) docTree).getBody().toString().replace("\n", ""));
+                    // Always remove single leading javadoc space
+                    String text = ((TextTree) docTree).getBody().toString()
+                            .replaceAll("\n ", "\n")
+                            .replaceAll("\n\n\n", "\n\n"); // consolidate extra lines
+
+                    Matcher m = preformattedText.matcher(text);
+                    if (!m.find()) {
+                        // if there isn't any pre-formatted text, remove any other leading whitespace
+                        text = text.replaceAll("\n +", "\n");
+                    }
+                    add(text);
                     break;
                 case CODE:
                 case LITERAL:
@@ -409,8 +495,18 @@ void add(DocTree docTree) {
                     reference = qualifiedNameToPath(reference);
                     if (!reference.startsWith("http")) {
                         Path target = outputDirectory.resolve(reference);
-                        Path relative = currentResource.getParent().relativize(target);
-                        reference = relative.toString();
+                        if (target.equals(currentResource)) {
+                            reference = "";
+                        } else {
+                            Path relative = currentResource.getParent().relativize(target);
+                            reference = relative.toString();
+                        }
+                        anchor = anchor
+                                .replaceFirst("^#(get|is)", "#")
+                                .replace("()", "").toLowerCase();
+                        label = label
+                                .replaceFirst("#(get|is)", "#")
+                                .replace("()", "");
                     }
                     add(String.format("[%s](%s%s)", label, reference, anchor));
                     break;
diff --git a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java
index ef1cc5fb1..7d7bc9133 100644
--- a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java
+++ b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java
@@ -93,7 +93,7 @@ public  void writeFiles(Path basePath, List elements) {
                     }
                 });
 
-        counts.forEach((k, v) -> tui.donef("Wrote %s %s files.", v, k));
+        counts.forEach((k, v) -> tui.printlnf(Msg.OK, "Wrote %s %s files.", v, k));
     }
 
      FileMap doWrite(FileMap fileMap, T qs, Map counts) {
@@ -131,7 +131,7 @@ public void writeNotes(Path dir, Collection notes, boolean compendium)
             writeNote(fd, fileName, n);
         }
 
-        tui.donef("Wrote %s notes to %s.",
+        tui.printlnf(Msg.OK, "Wrote %s notes to %s.",
                 notes.size(),
                 compendium ? "compendium" : "rules");
     }
diff --git a/src/main/java/dev/ebullient/convert/io/Msg.java b/src/main/java/dev/ebullient/convert/io/Msg.java
new file mode 100644
index 000000000..57682c2a5
--- /dev/null
+++ b/src/main/java/dev/ebullient/convert/io/Msg.java
@@ -0,0 +1,56 @@
+package dev.ebullient.convert.io;
+
+public enum Msg {
+    ALLDONE(Character.toString(0x1F389)), // 🎉
+    BREW(Character.toString(0x1F37A)), // 🍺
+    CLASSES(Character.toString(0x1F913)), // 🤓
+    DEBUG(Character.toString(0x1F527), "faint"), // 🔧
+    DECK(Character.toString(0x1F0CF)), // 🃏
+    DEITY(Character.toString(0x1F47C)), // 👼
+    ERR(Character.toString(0x1F6D1) + "  ERR|"), // 🛑
+    FEATURE(Character.toString(0x2B50)), // ⭐️
+    FEATURETYPE(Character.toString(0x1F31F)), // 🌟
+    FILTER(Character.toString(0x1F50D)), // 🔍
+    FOLDER(Character.toString(0x1F4C1)), // 📁
+    MULTIPLE(Character.toString(0x1F4DA)), // 📚
+    NOT_SET(Character.toString(0x1FAE5) + " "), // 🫥
+    OK(Character.toString(0x2705) + "   OK|"), // ✅
+    INFO(Character.toString(0x1F537) + " INFO|"), // 🔷
+    PROGRESS(Character.toString(0x23F3)), // ⏳
+    RACES(Character.toString(0x1F4D5)), // 📕
+    REPRINT(Character.toString(0x1F4F0)), // 📰
+    SOMEDAY(Character.toString(0x1F6A7)), // 🚧
+    SOURCE(Character.toString(0x1F4D8)), // 📘
+    TARGET(Character.toString(0x1F3AF)), // 🎯
+    UNKNOWN(Character.toString(0x1F47B)), // 👻
+    UNRESOLVED(Character.toString(0x1FAE3)), // 🫣
+    VERBOSE(Character.toString(0x1F539), "faint"),
+    WARN(Character.toString(0x1F538) + " WARN|"),
+    NOOP("");
+
+    final String prefix;
+    final String colorPrefix;
+
+    private Msg(String prefix) {
+        this.prefix = prefix + " ";
+        this.colorPrefix = null;
+    }
+
+    private Msg(String prefix, String color) {
+        this.prefix = prefix + " ";
+        this.colorPrefix = "@|%s %s".formatted(color, prefix);
+    }
+
+    public String color(String message) {
+        if (colorPrefix != null) {
+            return colorPrefix + message + "|@";
+        }
+        return wrap(message);
+    }
+
+    public String wrap(String message) {
+        return this == NOOP
+                ? message
+                : prefix + message;
+    }
+}
diff --git a/src/main/java/dev/ebullient/convert/io/NoStackTraceException.java b/src/main/java/dev/ebullient/convert/io/NoStackTraceException.java
new file mode 100644
index 000000000..10e81ce9c
--- /dev/null
+++ b/src/main/java/dev/ebullient/convert/io/NoStackTraceException.java
@@ -0,0 +1,26 @@
+package dev.ebullient.convert.io;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NoStackTraceException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public NoStackTraceException(Throwable cause) {
+        super(flattenMessage(cause));
+    }
+
+    @Override
+    public synchronized Throwable fillInStackTrace() {
+        return null;
+    }
+
+    private static String flattenMessage(Throwable cause) {
+        List sb = new ArrayList<>();
+        while (cause != null) {
+            sb.add(cause.toString());
+            cause = cause.getCause();
+        }
+        return String.join("\n", sb);
+    }
+}
diff --git a/src/main/java/dev/ebullient/convert/io/Templates.java b/src/main/java/dev/ebullient/convert/io/Templates.java
index 3ab6eeab0..d2c127cba 100644
--- a/src/main/java/dev/ebullient/convert/io/Templates.java
+++ b/src/main/java/dev/ebullient/convert/io/Templates.java
@@ -44,7 +44,7 @@ private Template customTemplateOrDefault(String id) throws RuntimeException {
         if (!engine.isTemplateLoaded(key)) {
             Path customPath = config.getCustomTemplate(id);
             if (customPath != null) {
-                tui.verbosef("📝 %s template: %s", id, customPath);
+                tui.infof("%25s: %s", id, customPath);
                 try {
                     Template template = engine.parse(Files.readString(customPath));
                     engine.putTemplate(key, template);
@@ -73,7 +73,7 @@ public String render(QuteBase resource) {
         } catch (TemplateException tex) {
             Throwable cause = tex.getCause();
             String message = cause != null ? cause.toString() : tex.toString();
-            tui.error(tex, message);
+            tui.errorf(tex, message);
             return "%% ERROR: " + message + " %%";
         }
     }
@@ -87,7 +87,7 @@ public String renderInlineEmbedded(QuteUtil resource) {
         } catch (TemplateException tex) {
             Throwable cause = tex.getCause();
             String message = cause != null ? cause.toString() : tex.toString();
-            tui.error(tex, message);
+            tui.errorf(tex, message);
             return "%% ERROR: " + message + " %%";
         }
     }
@@ -102,7 +102,7 @@ public String renderIndex(String name, Collection resources) {
         } catch (TemplateException tex) {
             Throwable cause = tex.getCause();
             String message = cause != null ? cause.toString() : tex.toString();
-            tui.error(tex, message);
+            tui.errorf(tex, message);
             return "%% ERROR: " + message + " %%";
         }
     }
@@ -121,7 +121,7 @@ public String renderCss(FontRef fontRef, InputStream data) throws IOException {
         } catch (TemplateException tex) {
             Throwable cause = tex.getCause();
             String message = cause != null ? cause.toString() : tex.toString();
-            tui.error(tex, message);
+            tui.errorf(tex, message);
             return "%% ERROR: " + message + " %%";
         }
     }
diff --git a/src/main/java/dev/ebullient/convert/io/Tui.java b/src/main/java/dev/ebullient/convert/io/Tui.java
index e7beb9bd3..f41e75b0a 100644
--- a/src/main/java/dev/ebullient/convert/io/Tui.java
+++ b/src/main/java/dev/ebullient/convert/io/Tui.java
@@ -5,16 +5,17 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.net.URI;
 import java.net.URL;
 import java.nio.channels.Channels;
 import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.Charset;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
@@ -41,8 +42,10 @@
 import com.fasterxml.jackson.core.util.DefaultIndenter;
 import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.MapperFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
+import com.fasterxml.jackson.databind.json.JsonMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactoryBuilder;
 import com.github.slugify.Slugify;
@@ -79,7 +82,13 @@ public static Tui instance() {
     public final static TypeReference>> MAP_STRING_LIST_STRING = new TypeReference<>() {
     };
 
-    public final static ObjectMapper MAPPER = initMapper(new ObjectMapper());
+    public final static PrintWriter streamToWriter(PrintStream stream) {
+        return new PrintWriter(stream, true, Charset.forName("UTF-8"));
+    }
+
+    public final static ObjectMapper MAPPER = initMapper(JsonMapper.builder()
+            .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
+            .build());
 
     private static Slugify slugify;
 
@@ -117,8 +126,9 @@ private static ObjectMapper yamlMapper() {
     }
 
     private static ObjectMapper initMapper(ObjectMapper mapper) {
-        mapper.setVisibility(VisibilityChecker.Std.defaultInstance()
-                .with(JsonAutoDetect.Visibility.ANY));
+        mapper.setSerializationInclusion(Include.NON_DEFAULT)
+                .setVisibility(VisibilityChecker.Std.defaultInstance()
+                        .with(JsonAutoDetect.Visibility.ANY));
         return mapper;
     }
 
@@ -180,7 +190,9 @@ public static String toAnchorTag(String x) {
     private Templates templates;
     private CommandLine commandLine;
     private boolean debug;
+    private boolean debugOrLog;
     private boolean verbose;
+    private boolean verboseOrLog;
     private Path output = Paths.get("");
     private final Set inputRoot = new TreeSet<>();
 
@@ -188,8 +200,8 @@ public Tui() {
         this.ansi = Help.Ansi.OFF;
         this.colors = Help.defaultColorScheme(ansi);
 
-        this.out = new PrintWriter(System.out);
-        this.err = new PrintWriter(System.err);
+        this.out = streamToWriter(System.out);
+        this.err = streamToWriter(System.err);
         this.debug = false;
         this.verbose = true;
 
@@ -209,8 +221,11 @@ public void init(CommandSpec spec, boolean debug, boolean verbose, boolean log)
             this.commandLine = spec.commandLine();
         }
 
-        this.debug = debug || log;
-        this.verbose = verbose;
+        this.debug = debug || picocliDebugEnabled;
+        this.debugOrLog = this.debug || log;
+        this.verbose = verbose || debug;
+        this.verboseOrLog = this.verbose || log;
+
         if (log) {
             Path p = Path.of("ttrpg-convert.out.txt");
             try {
@@ -250,103 +265,125 @@ public void flush() {
         }
     }
 
-    private void outLine(Text line) {
+    private void outLine(String text, Text line) {
         out.println(line);
         if (log != null) {
-            log.println(line.plainString());
+            log.println(text);
         }
     }
 
-    private void errLine(Text line) {
+    private void errLine(String text, Text line) {
         err.println(line);
         if (log != null) {
-            log.println(line.plainString());
+            log.println(text);
         }
     }
 
     public boolean isDebug() {
-        return debug || picocliDebugEnabled;
+        return debugOrLog;
     }
 
-    public void debugf(String format, Object... params) {
-        if (isDebug()) {
-            debug(String.format(format, params));
-        }
+    public boolean isVerbose() {
+        return verboseOrLog;
+    }
+
+    public void debugf(String output, Object... params) {
+        debugf(Msg.NOOP, output, params);
     }
 
-    public void debug(String output) {
-        if (isDebug()) {
-            outLine(ansi.new Text("@|faint 🔧 " + output + "|@", colors));
+    public void debugf(Msg msg, String output, Object... params) {
+        if (debugOrLog) {
+            output = format(msg.wrap(output), params);
+            if (debug) {
+                out.println(ansi.new Text(Msg.DEBUG.color(output), colors));
+            }
+            if (log != null) {
+                log.println(Msg.DEBUG.wrap(output));
+            }
         }
     }
 
-    public boolean isVerbose() {
-        return verbose;
+    public void progressf(String output, Object... params) {
+        verboseMsg(Msg.PROGRESS, output, params);
     }
 
-    public void verbosef(String format, Object... params) {
-        if (isVerbose()) {
-            verbose(String.format(format, params));
+    private void verboseMsg(Msg msgWrap, String output, Object... params) {
+        if (verboseOrLog) {
+            output = format(output, params);
+            if (verbose) {
+                out.println(ansi.new Text(msgWrap.color(output), colors));
+            }
+            if (log != null) {
+                log.println(msgWrap.wrap(output));
+            }
         }
     }
 
-    public void verbose(String output) {
-        if (isVerbose()) {
-            outLine(ansi.new Text("@|faint 🔹 " + output + "|@", colors));
+    public void logf(String output, Object... params) {
+        logf(Msg.NOOP, output, params);
+    }
+
+    public void logf(Msg msg, String output, Object... params) {
+        if (log != null) {
+            output = format(msg.wrap(output), params);
+            log.println(output);
         }
     }
 
-    public void warnf(String format, Object... params) {
-        warn(String.format(format, params));
+    public void infof(Msg msg, String output, Object... params) {
+        infof(msg.wrap(output), params);
     }
 
-    public void warn(String output) {
-        outLine(ansi.new Text("[🔸 WARN] " + output));
+    public void infof(String output, Object... params) {
+        output = format(Msg.INFO.wrap(output), params);
+        outLine(output, ansi.new Text(output));
     }
 
-    public void donef(String format, Object... params) {
-        done(String.format(format, params));
+    public void warnf(Msg msg, String output, Object... params) {
+        warnf(msg.wrap(output), params);
     }
 
-    public void done(String output) {
-        outLine(ansi.new Text("[ ✅  OK] " + output));
+    public void warnf(String output, Object... params) {
+        output = format(Msg.WARN.wrap(output), params);
+        outLine(output, ansi.new Text(output));
     }
 
-    public void printlnf(String format, Object... args) {
-        println(String.format(format, args));
+    public void printlnf(Msg msgType, String output, Object... params) {
+        output = format(msgType.wrap(output), params);
+        outLine(output, ansi.new Text(output));
     }
 
-    public void println(String output) {
-        outLine(ansi.new Text(output, colors));
-        flush();
+    public void errorf(String output, Object... params) {
+        errorf(null, Msg.NOOP, output, params);
     }
 
-    public void println(String... output) {
-        Arrays.stream(output).forEach(l -> outLine(ansi.new Text(l, colors)));
-        flush();
+    public void errorf(Msg msgType, String output, Object... params) {
+        errorf(null, msgType, output, params);
     }
 
-    public void errorf(String format, Object... args) {
-        error(null, String.format(format, args));
+    public void errorf(Throwable th, String output, Object... params) {
+        errorf(th, Msg.NOOP, output, params);
     }
 
-    public void errorf(Throwable th, String format, Object... args) {
-        error(th, String.format(format, args));
+    public void errorf(Throwable th, Msg msgType, String output, Object... params) {
+        output = format(msgType.wrap(output), params);
+        error(th, output);
     }
 
-    public void error(String errorMsg) {
-        error(null, errorMsg);
+    private void error(Throwable ex, String errorMsg) {
+        String message = Msg.ERR.wrap(errorMsg
+                .replace("java.nio.file.NoSuchFileException: ", "File not found: "));
+        errLine(message, colors.errorText(message));
+        if (ex != null && log != null) {
+            ex.printStackTrace(log);
+        }
     }
 
-    public void error(Throwable ex, String errorMsg) {
-        errLine(colors.errorText("[ 🛑 ERR] " + errorMsg));
-        if (ex != null && isDebug()) {
-            ex.printStackTrace(err);
-            if (log != null) {
-                ex.printStackTrace(log);
-            }
+    private String format(String output, Object... params) {
+        if (params != null && params.length > 0) {
+            return String.format(output, params);
         }
-        flush();
+        return output;
     }
 
     public void throwInvalidArgumentException(String message) {
@@ -372,12 +409,12 @@ public void copyFonts(Collection fonts) {
             Path targetPath = output.resolve(Path.of("css-snippets", slugify(fontRef.fontFamily) + ".css"));
             targetPath.getParent().toFile().mkdirs();
 
-            printlnf("⏱️ Generating CSS snippet for %s", fontRef.sourcePath);
+            progressf("Generating CSS snippet for %s", fontRef.sourcePath);
             if (fontRef.sourcePath.startsWith("http")) {
                 try (InputStream is = URI.create(fontRef.sourcePath.replace(" ", "%20")).toURL().openStream()) {
                     Files.writeString(targetPath, templates.renderCss(fontRef, is));
                 } catch (IOException e) {
-                    errorf(e, "Unable to copy font from %s to %s", fontRef.sourcePath, targetPath);
+                    errorf("Unable to copy font. %s", e);
                 }
             } else {
                 Optional resolvedSource = resolvePath(Path.of(fontRef.sourcePath));
@@ -388,7 +425,7 @@ public void copyFonts(Collection fonts) {
                 try (BufferedInputStream is = new BufferedInputStream(Files.newInputStream(resolvedSource.get()))) {
                     Files.writeString(targetPath, templates.renderCss(fontRef, is));
                 } catch (IOException e) {
-                    errorf(e, "Unable to copy font from %s to %s", fontRef.sourcePath, targetPath);
+                    errorf("Unable to copy font. %s", e);
                 }
             }
         }
@@ -418,7 +455,7 @@ public void copyImages(Collection images) {
             try {
                 Files.copy(image.sourcePath(), targetPath, StandardCopyOption.REPLACE_EXISTING);
             } catch (IOException e) {
-                errorf(e, "Unable to copy image from %s to %s (%s)", image.sourcePath(), image.targetFilePath(), e);
+                errorf("Unable to copy image. %s", e);
             }
         }
     }
@@ -431,7 +468,7 @@ private void copyImageResource(ImageRef image, Path targetPath) {
             InputStream in = TtrpgConfig.class.getResourceAsStream(sourcePath);
             Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);
         } catch (IOException e) {
-            errorf(e, "Unable to copy resource from %s to %s (%s)", sourcePath, image.targetFilePath(), e);
+            errorf("Unable to copy resource. %s", e);
         }
     }
 
@@ -456,13 +493,14 @@ private void copyRemoteImage(ImageRef image, Path targetPath) {
                 fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
             }
         } catch (IOException e) {
-            errorf(e, "Unable to copy remote image from %s to %s (%s)", url, image.targetFilePath(), e);
+            errorf("Unable to copy remote image (%s). ", url, e);
         }
     }
 
     public boolean readFile(Path p, List fixes, BiConsumer callback) {
         inputRoot.add(p.getParent().toAbsolutePath());
         try {
+            progressf("Reading %s", p);
             File f = p.toFile();
             String contents = Files.readString(p);
             for (Fix fix : fixes) {
@@ -470,7 +508,6 @@ public boolean readFile(Path p, List fixes, BiConsumer ca
             }
             JsonNode node = MAPPER.readTree(contents);
             callback.accept(f.getName(), node);
-            verbosef("🔖 Finished reading %s", p);
         } catch (IOException e) {
             errorf(e, "Unable to read source file at path %s (%s)", p, e.getMessage());
             return false;
@@ -479,7 +516,7 @@ public boolean readFile(Path p, List fixes, BiConsumer ca
     }
 
     public boolean readDirectory(String relative, Path dir, BiConsumer callback) {
-        debugf("📁 %s", dir);
+        debugf(Msg.FOLDER.wrap(dir.toString()));
 
         inputRoot.add(dir.toAbsolutePath());
 
@@ -590,10 +627,16 @@ public  T readJsonValue(JsonNode node, Class classTarget) {
 
     public static JsonNode readTreeFromResource(String resource) {
         try {
-            return Tui.MAPPER.readTree(TtrpgConfig.class.getResourceAsStream(resource));
+            return resource.endsWith(".yaml")
+                    ? Tui.yamlMapper().readTree(TtrpgConfig.class.getResourceAsStream(resource))
+                    : Tui.MAPPER.readTree(TtrpgConfig.class.getResourceAsStream(resource));
         } catch (IOException | IllegalArgumentException e) {
             Tui.instance.errorf(e, "Unable to read or parse required resource (%s): %s", resource, e.toString());
             return null;
         }
     }
+
+    public static String jsonStringify(Object o) {
+        return Tui.MAPPER.valueToTree(o).toPrettyString();
+    }
 }
diff --git a/src/main/java/dev/ebullient/convert/qute/ImageRef.java b/src/main/java/dev/ebullient/convert/qute/ImageRef.java
index 97f770769..400b5be9d 100644
--- a/src/main/java/dev/ebullient/convert/qute/ImageRef.java
+++ b/src/main/java/dev/ebullient/convert/qute/ImageRef.java
@@ -14,25 +14,19 @@
 /**
  * Create links to referenced images.
  *
- * 

* The general form of a markdown image link is: `![alt text](vaultPath "title")`. * You can also use anchors to position the image within the page, * which creates links that look like this: `![alt text](vaultPath#anchor "title")`. - *

* - *

Anchor Tags

+ * ## Anchor Tags * - *

* Anchor tags are used to position images within a page and are styled with CSS. Examples: - *

* - *
    - *
  • `center` centers the image and constrains its height.
  • - *
  • `gallery` constrains images within a gallery callout.
  • - *
  • `portrait` floats an image to the right.
  • - *
  • `symbol` floats Deity symbols to the right.
  • - *
  • `token` is a smaller image, also floated to the right. Used in statblocks.
  • - *
+ * - `center` centers the image and constrains its height. + * - `gallery` constrains images within a gallery callout. + * - `portrait` floats an image to the right. + * - `symbol` floats Deity symbols to the right. + * - `token` is a smaller image, also floated to the right. Used in statblocks. */ @TemplateData public class ImageRef { @@ -97,17 +91,17 @@ public String getEmbeddedLink(String anchor) { * Return an embedded markdown link to the image, using an optional * anchor tag to position the image in the page. * For example: `{resource.image.getEmbeddedLink("symbol")}` - *

+ * * If the title is longer than 50 characters: * `![{resource.shortTitle}]({resource.vaultPath}#anchor "{resource.title}")`, - *

- *

+ * + * * If the title is 50 characters or less: * `![{resource.title}]({resource.vaultPath}#anchor)`, - *

- *

+ * + * * Links will be generated using "center" as the anchor by default. - *

+ * */ public String getEmbeddedLink() { String anchor = "center"; diff --git a/src/main/java/dev/ebullient/convert/qute/NamedText.java b/src/main/java/dev/ebullient/convert/qute/NamedText.java index 46e3f3f2c..9a9ea9023 100644 --- a/src/main/java/dev/ebullient/convert/qute/NamedText.java +++ b/src/main/java/dev/ebullient/convert/qute/NamedText.java @@ -12,10 +12,7 @@ /** * Holder of a name or category and associated descriptive text. * - *

- * This attribute will render itself as labeled elements - * if you reference it directly. - *

+ * This attribute will render itself as labeled elements if you reference it directly. */ @TemplateData @RegisterForReflection diff --git a/src/main/java/dev/ebullient/convert/qute/QuteBase.java b/src/main/java/dev/ebullient/convert/qute/QuteBase.java index 8ac2065e2..6a8a6cd42 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteBase.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteBase.java @@ -10,10 +10,9 @@ /** * Defines attributes inherited by other Qute templates. - *

+ * * Notes created from {@code QuteBase} (or a derivative) will use a specific template * for the type. For example, {@code QuteBackground} will use {@code background2md.txt}. - *

*/ @TemplateData public class QuteBase implements QuteUtil { @@ -52,7 +51,7 @@ public String getLabeledSource() { return "_Source: " + sourceText + "_"; } - /** Book sources as list of {@link dev.ebullient.convert.qute.SourceAndPage SourceAndPage} */ + /** Book sources as list of {@link dev.ebullient.convert.qute.SourceAndPage} */ public Collection getSourceAndPage() { if (sources == null) { return List.of(); @@ -60,6 +59,14 @@ public Collection getSourceAndPage() { return sources.getSourceAndPage(); } + /** List of content superceded by this note (as {@link dev.ebullient.convert.qute.Reprinted}) */ + public Collection getReprintOf() { + if (sources == null) { + return List.of(); + } + return sources.getReprints(); + } + /** True if the content (text) contains sections */ public boolean getHasSections() { return text != null && !text.isEmpty() && text.contains("\n## "); diff --git a/src/main/java/dev/ebullient/convert/qute/QuteNote.java b/src/main/java/dev/ebullient/convert/qute/QuteNote.java index 2c922e42a..529c0886b 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteNote.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteNote.java @@ -9,10 +9,9 @@ /** * Common attributes for simple notes. THese attributes are more * often used by books, adventures, rules, etc. - *

+ * * Notes created from {@code QuteNote} (or a derivative) will look for a template * named {@code note2md.txt} by default. - *

*/ @TemplateData public class QuteNote extends QuteBase { diff --git a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java index d31955fe9..6968b9994 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java @@ -5,9 +5,11 @@ import java.util.List; import java.util.Map; +import dev.ebullient.convert.io.JavadocIgnore; import dev.ebullient.convert.tools.IndexType; import dev.ebullient.convert.tools.pf2e.Pf2eIndexType; +@JavadocIgnore public interface QuteUtil { default boolean isPresent(Map s) { return s != null && !s.isEmpty(); @@ -73,6 +75,7 @@ default IndexType indexType() { return Pf2eIndexType.syntheticGroup; } + @JavadocIgnore interface Renderable { /** Return this object rendered using its template. */ String render(); diff --git a/src/main/java/dev/ebullient/convert/qute/Reprinted.java b/src/main/java/dev/ebullient/convert/qute/Reprinted.java new file mode 100644 index 000000000..7e80de5f2 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/qute/Reprinted.java @@ -0,0 +1,13 @@ +package dev.ebullient.convert.qute; + +import io.quarkus.qute.TemplateData; + +/** + * A simple record to hold the name and source of a reprinted item. + * + * @param name Name of the reprinted item + * @param source Primary source of the reprinted item + */ +@TemplateData +public record Reprinted(String name, String source) { +} diff --git a/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java b/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java index b707044b0..014a39e35 100644 --- a/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java +++ b/src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java @@ -1,25 +1,49 @@ package dev.ebullient.convert.qute; +import static dev.ebullient.convert.StringUtil.pluralize; import static dev.ebullient.convert.StringUtil.toTitleCase; import java.util.Collection; import dev.ebullient.convert.StringUtil; +import dev.ebullient.convert.io.JavadocVerbatim; import io.quarkus.qute.TemplateExtension; +/** + * Qute template extensions for TTRPG data. + * + * Use these functions to help render TTRPG data in Qute templates. + */ @TemplateExtension public class TtrpgTemplateExtension { - /** Return the value formatted with a bonus with a +/- prefix */ + + /** Return the value formatted with a bonus with a +/- prefix. Example: `{perception.asBonus}` */ + @JavadocVerbatim static String asBonus(Integer value) { return String.format("%+d", value); } - /** Return the string capitalized */ + /** Return the string capitalized. Example: `{resource.name.capitalized}` */ + @JavadocVerbatim static String capitalized(String s) { return toTitleCase(s); } - /** Return the given object as a string, with a space prepended if it's non-empty and non-null. */ + /** + * Return the string pluralized based on the size of the collection. + * + * Example: `{resource.name.pluralized(resource.components)}` + */ + @JavadocVerbatim + static String pluralizeLabel(Collection collection, String s) { + return pluralize(s, collection.size(), true); + } + + /** + * Return the given object as a string, with a space prepended if it's non-empty and non-null. + * Example: `{resource.name.prefixSpace}` + */ + @JavadocVerbatim static String prefixSpace(Object obj) { if (obj == null) { return ""; @@ -28,8 +52,23 @@ static String prefixSpace(Object obj) { return s.isEmpty() ? "" : (" " + s); } - /** Return the given collection converted into a string and joined using {@code delim} */ - static String join(Collection collection, String delim) { - return StringUtil.join(delim, collection); + /** + * Return the given collection converted into a string and joined using the specified joiner. + * + * Example: `{resource.components.join(", ")}` + */ + @JavadocVerbatim + static String join(Collection collection, String joiner) { + return StringUtil.join(joiner, collection); + } + + /** + * Return the given list joined into a single string, using a different delimiter for the last element. + * + * Example: `{resource.components.joinConjunct(", ", " or ")}` + */ + @JavadocVerbatim + static String joinConjunct(Collection collection, String joiner, String lastjoiner) { + return StringUtil.joinConjunct(joiner, lastjoiner, collection.stream().map(o -> o.toString()).toList()); } } diff --git a/src/main/java/dev/ebullient/convert/qute/package-info.java b/src/main/java/dev/ebullient/convert/qute/package-info.java index 76e2cb1f4..a8fc5ce3e 100644 --- a/src/main/java/dev/ebullient/convert/qute/package-info.java +++ b/src/main/java/dev/ebullient/convert/qute/package-info.java @@ -1,25 +1,17 @@ /** *

Qute Template Reference

* - *

* The following pages describe attributes that you can use to customize * generated output in Qute templates. - *

* - *

* Use a {@code resource.} prefix to access these attributes unless otherwise noted. * For example, {@code resource.title}. - *

* - *

- * For more information about Qute, see the Qute guide. - *

+ * For more information about Qute, see the [Qute guide](https://quarkus.io/guides/qute). * - * + * - [Documentation for using templates with the CLI](../../examples/templates/README.md) + * - [5e Template Examples](../../examples/templates/tools5e/README.md) + * - {@link dev.ebullient.convert.tools.dnd5e.qute 5eTools template attributes} + * - {@link dev.ebullient.convert.tools.pf2e.qute Pf2eTools template attributes} */ package dev.ebullient.convert.qute; diff --git a/src/main/java/dev/ebullient/convert/tools/CompendiumSources.java b/src/main/java/dev/ebullient/convert/tools/CompendiumSources.java index 29623e191..870ffd26c 100644 --- a/src/main/java/dev/ebullient/convert/tools/CompendiumSources.java +++ b/src/main/java/dev/ebullient/convert/tools/CompendiumSources.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -10,6 +11,7 @@ import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.qute.Reprinted; import dev.ebullient.convert.qute.SourceAndPage; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; import io.quarkus.qute.TemplateData; @@ -23,16 +25,27 @@ public abstract class CompendiumSources { // sources will only appear once, iterate by insertion order protected final Set sources = new LinkedHashSet<>(); protected final Set bookRef = new LinkedHashSet<>(); - protected final String sourceText; + protected String sourceText; + + // Provides a list of sources that this is a reprint of + protected final Set reprintOf = new HashSet<>(); + // Source that this is a copy of + protected final JsonNode copyElement; public CompendiumSources(IndexType type, String key, JsonNode jsonElement) { this.type = type; this.key = key; this.name = findName(type, jsonElement); - this.sourceText = findSourceText(type, jsonElement); + // remember this: handling copies will remove the copy field from the element + // to avoid repeated processing + this.copyElement = Fields._copy.getFrom(jsonElement); + initSources(jsonElement); } public String getSourceText() { + if (sourceText == null) { + sourceText = findSourceText(type, findNode()); + } return sourceText; } @@ -41,75 +54,116 @@ public Collection getSources() { } /** Protected: used by Tags.addSourceTags(sources) */ - List primarySourceTag() { - return List.of( - String.format("compendium/src/%s/%s", - TtrpgConfig.getConfig().datasource().shortName(), - isSynthetic() ? "" : primarySource().toLowerCase())); + String primarySourceTag() { + return String.format("compendium/src/%s/%s", + TtrpgConfig.getConfig().datasource().shortName(), + isSynthetic() ? "" : primarySource().toLowerCase()); } public abstract JsonNode findNode(); protected abstract String findName(IndexType type, JsonNode jsonElement); - protected String findSourceText(IndexType type, JsonNode jsonElement) { - List srcText = new ArrayList<>(); - + protected void initSources(JsonNode jsonElement) { // add the primary source... SourceAndPage primary = new SourceAndPage(jsonElement); if (primary.source != null) { - srcText.add(primary.toString()); this.sources.add(primary.source); this.bookRef.add(primary); - } else { - this.sources.add(type.defaultSourceString()); + } else if (type.defaultSourceString() != null) { + // synthetic groups don't have a default source + String source = type.defaultSourceString(); + this.sources.add(source); + this.bookRef.add(new SourceAndPage(source, null)); } - JsonNode copyElement = Fields._copy.getFrom(jsonElement); - String copyOf = SourceField.name.getTextOrNull(copyElement); String copySrc = SourceField.source.getTextOrNull(copyElement); - String copiedFrom = Fields._copiedFrom.getTextOrNull(copyElement); - - if (copyOf != null) { - srcText.add(String.format("Derived from %s (%s)", copyOf, copySrc)); - } else if (copiedFrom != null) { - srcText.add(String.format("Derived from %s", copiedFrom)); - } - // find/add additional sources - if (Fields.additionalSources.existsIn(jsonElement)) { // Additional information from... - srcText.addAll(Fields.additionalSources.streamFrom(jsonElement) + if (Fields.additionalSources.existsIn(jsonElement)) { + // Additional information from... + Fields.additionalSources.streamFrom(jsonElement) .map(SourceAndPage::new) .filter(sp -> sp.source != null) .filter(sp -> !sp.source.equals(copySrc)) - .filter(sp -> datasourceFilter(sp.source)) // eliminate common sources, e.g. - .peek(this.bookRef::add) - .peek(sp -> this.sources.add(sp.source)) - .map(sp -> sp.toString()) - .collect(Collectors.toList())); + .forEach(sp -> { + this.bookRef.add(sp); + this.sources.add(sp.source); + }); } - if (Fields.otherSources.existsIn(jsonElement)) { // Also found in... - srcText.addAll(Fields.otherSources.streamFrom(jsonElement) + + if (Fields.otherSources.existsIn(jsonElement)) { + // Also found in... + // This can be overly generous.. only add other sources that + // are explicitly included in the configuration + Fields.otherSources.streamFrom(jsonElement) .map(SourceAndPage::new) .filter(sp -> sp.source != null) .filter(sp -> !sp.source.equals(copySrc)) - .filter(sp -> datasourceFilter(sp.source)) - .peek(this.bookRef::add) - .peek(sp -> this.sources.add(sp.source)) .filter(sp -> TtrpgConfig.getConfig().sourceIncluded(sp.source)) - .map(sp -> sp.toString()) - .collect(Collectors.toList())); + .forEach(sp -> { + this.bookRef.add(sp); + this.sources.add(sp.source); + }); } - - return String.join(", ", srcText); } - protected boolean datasourceFilter(String source) { - return true; + protected String findSourceText(IndexType type, JsonNode jsonElement) { + List srcText = new ArrayList<>(); + + final SourceAndPage primary = bookRef.iterator().next(); + List consolidated = bookRef.stream() + .reduce(new ArrayList<>(), (list, sp) -> { + if (list.isEmpty()) { + list.add(sp); + } else { + SourceAndPage existing = list.stream() + .filter(x -> x.source.equals(sp.source)) + .findFirst() + .orElse(null); + if (existing == null) { + list.add(sp); + } else if (existing.page != null) { + SourceAndPage replace = new SourceAndPage(existing.source, null); + list.remove(existing); + if (existing == primary) { + list.add(0, replace); + } else { + list.add(replace); + } + } + } + return list; + }, (a, b) -> { + a.addAll(b); + return a; + }); + + final SourceAndPage first = consolidated.iterator().next(); + if (first.source != null) { + srcText.add(first.toString()); + } + + String copyOf = SourceField.name.getTextOrNull(copyElement); + String copySrc = SourceField.source.getTextOrNull(copyElement); + String copiedFrom = Fields._copiedFrom.getTextOrNull(copyElement); + + if (copyOf != null) { + srcText.add(String.format("Derived from %s (%s)", copyOf, copySrc)); + } else if (copiedFrom != null) { + srcText.add(String.format("Derived from %s", copiedFrom)); + } + + // find/add additional sources + consolidated.stream() + .filter(sp -> sp != first && sp.source != null) + .filter(sp -> !sp.source.equals(copySrc)) + .forEach(sp -> srcText.add(sp.toString())); + + return String.join(", ", srcText); } public boolean isPrimarySource(String source) { - return source.equals(primarySource()); + return source.equalsIgnoreCase(primarySource()); } public String primarySource() { @@ -141,6 +195,16 @@ public IndexType getType() { return type; } + public Collection getSourceAndPage() { + return bookRef; + } + + public Collection getReprints() { + return reprintOf.stream() + .map(s -> new Reprinted(s.getName(), s.primarySource())) + .collect(Collectors.toList()); + } + @Override public String toString() { return "sources[" + key + ']'; @@ -150,6 +214,10 @@ public void checkKnown() { TtrpgConfig.checkKnown(this.sources); } + public void addReprint(CompendiumSources reprint) { + this.reprintOf.add(reprint); + } + /** Documents that have no primary source (compositions) */ protected boolean isSynthetic() { return false; @@ -161,8 +229,4 @@ protected enum Fields implements JsonNodeReader { additionalSources, otherSources, } - - public Collection getSourceAndPage() { - return bookRef; - } } diff --git a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java index d53ab6911..09264d58a 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java @@ -60,11 +60,44 @@ default String bonusOrNull(JsonNode x) { return (n >= 0 ? "+" : "") + n; } + /** + * Return the boolean value of the field in the node: + * - if the field is a boolean, return the value + * - if the field is a string, return true if the string is present and not 'false' + * + * @param source + * @param value + * @return + */ + default boolean coerceBooleanOrDefault(JsonNode source, boolean value) { + JsonNode result = getFrom(source); + if (result == null) { + return value; + } + if (result.isBoolean()) { + return result.asBoolean(); + } + if (result.isTextual()) { + return !result.asText().equalsIgnoreCase("false"); + } + return value; + } + default boolean booleanOrDefault(JsonNode source, boolean value) { JsonNode result = getFrom(source); return result == null ? value : result.asBoolean(value); } + default Double doubleOrDefault(JsonNode source, double value) { + JsonNode result = getFrom(source); + return result == null ? value : result.asDouble(); + } + + default Double doubleOrNull(JsonNode source) { + JsonNode result = getFrom(source); + return result == null ? null : result.asDouble(); + } + default boolean existsIn(JsonNode source) { if (source == null || source.isNull()) { return false; @@ -151,19 +184,6 @@ default JsonNode getFieldFrom(JsonNode source, JsonNodeReader field) { return targetNode.get(field.nodeName()); } - default Optional getIntFrom(JsonNode source) { - JsonNode result = getFrom(source); - return result == null || !result.isInt() ? Optional.empty() : Optional.of(result.asInt()); - } - - default int getIntOrThrow(JsonNode x) { - JsonNode result = getFrom(x); - if (result == null) { - throw new IllegalArgumentException("Missing int from " + this.nodeName()); - } - return result.asInt(); - } - default List getListOfStrings(JsonNode source, Tui tui) { JsonNode target = getFrom(source); if (target == null) { @@ -214,11 +234,29 @@ default Optional getTextFrom(JsonNode x) { return Optional.empty(); } + default Optional intFrom(JsonNode source) { + JsonNode result = getFrom(source); + return result == null || !result.isInt() ? Optional.empty() : Optional.of(result.asInt()); + } + default int intOrDefault(JsonNode source, int value) { JsonNode result = getFrom(source); return result == null || result.isNull() ? value : result.asInt(); } + default Integer intOrNull(JsonNode source) { + JsonNode result = getFrom(source); + return result == null || result.isNull() ? null : result.asInt(); + } + + default int intOrThrow(JsonNode x) { + JsonNode result = getFrom(x); + if (result == null) { + throw new IllegalArgumentException("Missing int from " + this.nodeName()); + } + return result.asInt(); + } + default String joinAndReplace(JsonNode source, JsonTextConverter replacer) { return joinAndReplace(source, replacer, ", "); } @@ -448,4 +486,15 @@ default void copy(JsonNode source, JsonNode target) { } ((ObjectNode) target).set(this.nodeName(), getFrom(source).deepCopy()); } + + /** Destructive! */ + default void link(JsonNode source, JsonNode target) { + if (source == null || target == null) { + return; + } + if (!source.has(this.nodeName())) { + return; + } + ((ObjectNode) target).set(this.nodeName(), getFrom(source)); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java b/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java index bf1ba8969..6af175aee 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonNodeReader.FieldValue; @@ -57,11 +58,11 @@ public JsonNode handleCopy(T type, JsonNode copyTo) { String copyFromKey = type.createKey(_copy); JsonNode copyFrom = getOriginNode(copyFromKey); if (copyToKey.equals(copyFromKey)) { - tui().errorf("Error (%s): Self-referencing copy. This is a data entry error. %s", copyToKey, _copy); + tui().errorf("(%s) Self-referencing copy. This is a data entry error.", copyToKey, _copy); return copyTo; } if (copyFrom == null) { - tui().errorf("Error (%s): Unable to find source for %s", copyToKey, copyFromKey); + tui().errorf("(%s): Unable to find source %s to copy from", copyToKey, copyFromKey); return copyTo; } // is the copy a copy? @@ -163,13 +164,15 @@ protected void doModProp( // Properties case setProp -> doSetProp(originKey, modInfo, prop, target); case setProps -> doSetProps(originKey, modInfo, prop, target); + case prefixSuffixStringProp -> doPrefixSuffixStringProp(originKey, modInfo, prop, target); // Arrays case prependArr, appendArr, replaceArr, replaceOrAppendArr, appendIfNotExistsArr, insertArr, removeArr -> doModArray(originKey, mode, modInfo, prop, target); // MATH case scalarAddProp -> doScalarAddProp(originKey, modInfo, prop, target); case scalarMultProp -> doScalarMultProp(originKey, modInfo, prop, target); - default -> tui().errorf("Error (%s): Unknown modification mode: %s", originKey, modInfo); + default -> tui().warnf(Msg.UNKNOWN, "(%s): Unknown modification mode: %s", + originKey, modInfo); } } @@ -179,7 +182,7 @@ protected void doModProp(String originKey, JsonNode modInfos, JsonNode copyFrom, if ("remove".equals(modInfo.asText()) && prop != null) { target.remove(prop); } else { - tui().errorf("Error(%s): Unknown text modification mode for %s: %s", originKey, prop, modInfo); + tui().warnf(Msg.UNKNOWN, "(%s): Unknown text modification mode for %s: %s", originKey, prop, modInfo); } } else { doModProp(originKey, modInfo, copyFrom, prop, target, ModFieldMode.getModMode(modInfo)); @@ -270,6 +273,32 @@ private void doSetProps(String originKey, JsonNode modInfo, String propPath, Obj } } + /** Set the target prop which corresponds to the prop in {@code modInfo} to the value from {@code modInfo}. */ + private void doPrefixSuffixStringProp(String originKey, JsonNode modInfo, String prop, ObjectNode target) { + // target.(prop . modinfo.prop) = modinfo.value + String propPath = MetaFields.prop.getTextOrEmpty(modInfo); + if (!"*".equals(prop)) { + // target.(prop . modinfo.prop) = modinfo.value + propPath = prop + "." + propPath; + } + String prefix = MetaFields.prefix.getTextOrEmpty(modInfo); + String suffix = MetaFields.suffix.getTextOrEmpty(modInfo); + + String[] path = splitLastPropPath(propPath); + ObjectNode targetRw = target.withObject(path[0]); + + // Verify that we're going to replace a string value + JsonNode targetValue = targetRw.get(path[1]); + if (targetValue == null || !targetValue.isTextual()) { + return; + } + // Update the string value, add prefix and suffix + targetRw.put(path[1], + prefix + + MetaFields.value.getTextOrEmpty(modInfo) + + suffix); + } + private String nodePath(String propPath) { return "/" + String.join("/", propPath.split("\\.")); } @@ -421,6 +450,8 @@ private void doModArray(String originKey, ModFieldMode mode, JsonNode modInfo, S return; } } + default -> { + } } ArrayNode targetArray = target.withArray(propPath); @@ -430,7 +461,7 @@ private void doModArray(String originKey, ModFieldMode mode, JsonNode modInfo, S case appendIfNotExistsArr -> appendIfNotExistsArr(targetArray, items); case insertArr -> insertIntoArray( targetArray, - MetaFields.index.getIntFrom(modInfo).filter(n -> n >= 0).orElse(targetArray.size()), + MetaFields.index.intFrom(modInfo).filter(n -> n >= 0).orElse(targetArray.size()), items); case removeArr -> removeFromArray(originKey, modInfo, prop, targetArray); case replaceArr -> replaceArray(originKey, modInfo, targetArray, items); @@ -440,7 +471,8 @@ private void doModArray(String originKey, ModFieldMode mode, JsonNode modInfo, S appendToArray(targetArray, items); } } - default -> tui().errorf("Error (%s): Unknown modification mode for property %s: %s", originKey, prop, modInfo); + default -> tui().warnf(Msg.UNKNOWN, "(%s): Unknown modification mode for property %s: %s", + originKey, prop, modInfo); } } @@ -540,7 +572,7 @@ protected boolean replaceArray(String originKey, JsonNode modInfo, ArrayNode tgt Pattern pattern = Pattern.compile("\\b" + MetaFields.regex.getTextOrEmpty(replace)); index = matchFirstIndexByName(originKey, tgtArray, pattern); } else { - tui().errorf("Error (%s): Unknown replace; %s", originKey, modInfo); + tui().warnf(Msg.UNKNOWN, "(%s): Unknown replace; %s", originKey, modInfo); return false; } @@ -619,6 +651,7 @@ public enum MetaFields implements JsonNodeReader { mode, names, overwrite, + prefix, prof_bonus, prop, props, @@ -629,9 +662,11 @@ public enum MetaFields implements JsonNodeReader { scalar, skills, str, + suffix, type, value, with, + ; } public enum TemplateVariable implements JsonNodeReader.FieldValue { @@ -672,6 +707,7 @@ public enum ModFieldMode implements JsonNodeReader.FieldValue { scalarMultProp, setProp, setProps, + prefixSuffixStringProp, addSenses, addSaves, diff --git a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java index 5c34c1b2f..f951017c9 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.function.BiFunction; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -22,15 +23,23 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import dev.ebullient.convert.config.CompendiumConfig; -import dev.ebullient.convert.config.CompendiumConfig.DiceRoller; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.QuteBase; import dev.ebullient.convert.qute.QuteUtil; +import dev.ebullient.convert.tools.ParseState.DiceFormulaState; public interface JsonTextConverter { - public static String DICE_FORMULA = "[ +d\\d-‒]+"; + public static String DICE_FORMULA = "[ +d\\d-]+"; public static String DICE_TABLE_HEADER = "\\| dice: \\d*d\\d+ \\|.*"; - Pattern footnotePattern = Pattern.compile("\\{@footnote ([^}]+)}"); + + static final Pattern dicePattern = Pattern.compile("\\{@(" + + "dice|autodice|damage|h |hit|d20|initiative|scaledice|scaledamage" + + ") ?([^}]+)}"); + static final Pattern dicePatternWithSpan = Pattern.compile("(.+)(]+>)(.+)()"); + static final Pattern footnotePattern = Pattern.compile("\\{@footnote ([^}]+)}"); + static final Pattern textAverageRoll = Pattern.compile(" (\\d+) \\((`dice:[^`]+text\\(([^)]+)\\)`)\\)"); + static final Pattern averageRoll = Pattern.compile(" (\\d+) \\(`(dice:[^`]+)` (\\([^)]+\\))\\)"); void appendToText(List inner, JsonNode target, String heading); @@ -89,94 +98,174 @@ default JsonNode objectIntersect(JsonNode a, JsonNode b) { return x; } - default String formatDice(String diceRoll) { - DiceRoller roller = cfg().useDiceRoller(); - boolean suppressInYaml = parseState().inTrait() && roller.useFantasyStatblocks(); + default String replaceWithDiceRoller(String input) { + Matcher m = dicePattern.matcher(input); + if (!m.find()) { + return input; + } + if (m.groupCount() < 2) { + tui().warnf(Msg.UNKNOWN, "Unknown/Invalid dice formula Input: %s", input); + return input; + } + + DiceFormulaState formulaState = parseState().diceFormulaState(); + + String tag = m.group(1); + String[] parts = m.group(2).split("\\|"); + + String rollString = parts[0].trim(); + String displayText = parts.length > 1 ? parts[1].trim() : null; + String scaleSkillName = parts.length > 2 ? parts[2].trim() : null; + + return switch (tag) { + case "d20", "h", "hit", "initiative" -> { + // {@d20 -4}, {@d20 -2 + PB}, + // {@d20 0|10}, {@d20 2|+2|Perception}, {@d20 -1|\u22121|Father Belderone} + // {@hit +7}, {@hit 6|+6|Slam}, {@hit 6|+6 bonus}, {@hit +3|+3 to hit} + // @initiative -- like @hit + String posGroup = "(? { + // damage of 2d6 or 3d6 at level 1: {@scaledamage 2d6;3d6|2-9|1d6} for each level beyond 2nd; + // roll 2d6 when using 1 psi point: {@scaledice 2d6|1,3,5,7,9|1d6|psi|extra amount} for each additional psi point spent + // format: {@scaledice 2d6;3d6|2-8,9|1d6|psi|display text} (or @scaledamage) + // [baseRoll, progression, addPerProgress, renderMode, displayText] + yield parts.length > 4 + ? formatDice(scaleSkillName, parts[4].trim(), formulaState, true, true) + : formatDice(scaleSkillName, codeString(scaleSkillName, formulaState), formulaState, true, false); + } + // {@dice 1d2-2+2d3+5} for regular dice rolls + // {@dice 1d6;2d6} for multiple options; + // {@dice 1d6 + #$prompt_number:min=1,title=Enter a Number!,default=123$#} for input prompts + // --> prompts will have been replaced with default value: {@dice 1d6 + lotsofstuff} + // {@dice 1d20+2|display text} + // {@dice 1d20+2|display text|rolled by name} + // {@damage 1d12+3} + // @autodice -- like @dice + default -> { + String[] alternatives = rollString.split(";"); + if (displayText == null && alternatives.length > 1) { + for (int i = 0; i < alternatives.length; i++) { + String coded = codeString(alternatives[i], formulaState); + alternatives[i] = formatDice(alternatives[i], coded, formulaState, true, false); + } + displayText = String.join(" or ", alternatives); + } + yield formatDice(rollString, displayText, formulaState, true, true); + } + }; + } - int pos = diceRoll.indexOf(";"); - if (pos >= 0) { - diceRoll = diceRoll.substring(0, pos); + default String formatDice(String diceRoll, String displayText, DiceFormulaState formulaState, boolean useAverage, + boolean appendFormula) { + if (diceRoll.contains(";")) { + return displayText; + } + if (diceRoll.contains(" `dice: 2d6|text(7)` (`2d6`) + DiceFormulaState formulaState = parseState().diceFormulaState(); String dtxt = parseState().inMarkdownTable() ? "\\\\|text(" : "|text("; - return text - .replaceAll("` \\((" + DICE_FORMULA + ")\\) to hit", "` ($1 to hit)") - .replaceAll(" (\\d+) \\(`dice:[^`]+` \\(`([^`]+)`\\)\\)", - " `dice:$2" + dtxt + "$1)` (`$2`)"); + + // 26 (`dice:1d20+8|noform|text(+8)`) --> `dice:1d20+8|noform|text(26)` (`+8`) + text = textAverageRoll.matcher(text).replaceAll((match) -> { + String replaceText = "(" + match.group(3) + ")"; + String avgValue = "(" + match.group(1) + ")"; + return " " + match.group(2).replace(replaceText, avgValue) + + " (" + codeString(match.group(3), formulaState) + ")"; + }); + + // 7 (`dice:1d6+4|noform|avg` (`1d6 + 4`)) --> `dice:1d6+4|noform|avg|text(7)` (`1d6 + 4`) + // 7 (`dice:2d6|noform|avg` (`2d6`)) --> `dice:2d6|noform|avg|text(7)` (`2d6`) + text = averageRoll.matcher(text).replaceAll((match) -> { + String dice = match.group(2) + dtxt + match.group(1) + ")"; + return " `" + dice + "` " + match.group(3); + }); + + return text; } /** Tokenizer: use a stack of StringBuilders to deal with nested tags */ @@ -629,11 +718,15 @@ enum SourceField implements JsonNodeReader { id, items, _meta, + isReprinted, name, note, page, + reprintedAs, source, - type; + tag, + type, + uid; final String nodeName; diff --git a/src/main/java/dev/ebullient/convert/tools/MarkdownConverter.java b/src/main/java/dev/ebullient/convert/tools/MarkdownConverter.java index 0c797f242..eb7cd7c86 100644 --- a/src/main/java/dev/ebullient/convert/tools/MarkdownConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/MarkdownConverter.java @@ -8,9 +8,7 @@ public interface MarkdownConverter { MarkdownConverter writeFiles(List types); - MarkdownConverter writeNotesAndTables(); - - MarkdownConverter writeFiles(IndexType feat); + MarkdownConverter writeFiles(IndexType type); MarkdownConverter writeImages(); } diff --git a/src/main/java/dev/ebullient/convert/tools/ParseState.java b/src/main/java/dev/ebullient/convert/tools/ParseState.java index 27224f442..ca80c6a68 100644 --- a/src/main/java/dev/ebullient/convert/tools/ParseState.java +++ b/src/main/java/dev/ebullient/convert/tools/ParseState.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.config.CompendiumConfig.DiceRoller; import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.SourceAndPage; @@ -381,4 +382,31 @@ public void popCitations(List footerEntries) { }); citations.clear(); } + + public DiceFormulaState diceFormulaState() { + return new DiceFormulaState(this); + } + + public static class DiceFormulaState { + public final DiceRoller roller; + public final boolean suppressInYaml; + + public DiceFormulaState(ParseState parseState) { + this.roller = TtrpgConfig.getConfig().useDiceRoller(); + this.suppressInYaml = parseState.inTrait() && roller.useFantasyStatblocks(); + } + + /** + * We can't use dice roller fomulas if the roller is disabled, or if we're + * in a YAML trait block. + */ + public boolean noRoller() { + return !roller.enabled() || suppressInYaml; + } + + /** In YAML blocks (traits), we avoid all formatting in dice formulas */ + public boolean plainText() { + return suppressInYaml; + } + } } diff --git a/src/main/java/dev/ebullient/convert/tools/Tags.java b/src/main/java/dev/ebullient/convert/tools/Tags.java index d58c3ba3d..6ec54e5c0 100644 --- a/src/main/java/dev/ebullient/convert/tools/Tags.java +++ b/src/main/java/dev/ebullient/convert/tools/Tags.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools; +import static dev.ebullient.convert.StringUtil.join; + +import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -21,18 +24,21 @@ public Tags(CompendiumSources sources) { public void addSourceTags(CompendiumSources sources) { if (sources != null) { - tags.addAll(sources.primarySourceTag()); + String sourceTag = config.tagOfRaw(sources.primarySourceTag()); + tags.add(sourceTag); } } /** Prepend configured prefix and slugify parts */ - public void addRaw(String first, String rawValue) { - tags.add(config.tagOfRaw(first + "/" + rawValue)); + public void addRaw(String... rawValues) { + String rawTag = config.tagOfRaw(join("/", List.of(rawValues))); + tags.add(rawTag); } /** Prepend configured prefix and slugify parts */ - public void add(String... tag) { - tags.add(config.tagOf(tag)); + public void add(String... segments) { + String tag = config.tagOf(segments); + tags.add(tag); } public Set build() { diff --git a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java index 8140ea4e2..b5e3af88e 100644 --- a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java @@ -52,6 +52,22 @@ default Path compendiumFilePath() { return cfg().compendiumFilePath(); } + default boolean resolveSources(Path toolsPath) { + TtrpgConfig.setToolsPath(toolsPath); + var allOk = true; + for (String adventure : cfg().resolveAdventures()) { + allOk &= cfg().readSource(toolsPath.resolve(adventure), TtrpgConfig.getFixes(adventure), this::importTree); + } + for (String book : cfg().resolveBooks()) { + allOk &= cfg().readSource(toolsPath.resolve(book), TtrpgConfig.getFixes(book), this::importTree); + } + // Include additional standalone files from config (relative to current directory) + for (String brew : cfg().resolveHomebrew()) { + allOk &= cfg().readSource(Path.of(brew), TtrpgConfig.getFixes(brew), this::importTree); + } + return allOk; + } + void prepare(); boolean notPrepared(); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java new file mode 100644 index 000000000..c82f3085e --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java @@ -0,0 +1,72 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static dev.ebullient.convert.StringUtil.isPresent; + +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.JsonTextConverter.SourceField; + +public record ItemMastery( + String name, + String indexKey, + String tag) { + + public String toString() { + return name + " Mastery"; + } + + public String linkify() { + return linkify(null); + } + + public String linkify(String linkText) { + Tools5eIndex index = Tools5eIndex.getInstance(); + linkText = isPresent(linkText) ? linkText : name; + + boolean included = isPresent(indexKey) + ? index.isIncluded(indexKey) + : index.customRulesIncluded(); + + return included + ? "[%s](%sitem-mastery.md#%s)".formatted( + linkText, index.rulesVaultRoot(), Tui.toAnchorTag(name)) + : linkText; + } + + public static final Comparator comparator = Comparator.comparing(ItemMastery::name); + private static final Map masteryMap = new HashMap<>(); + + public static ItemMastery fromKey(String key, Tools5eIndex index) { + String finalKey = index.getAliasOrDefault(key); + JsonNode node = index.getNode(finalKey); + return node == null ? null : fromNode(finalKey, node); + } + + public static ItemMastery fromNode(String key, JsonNode mastery) { + // Create the ItemType object once + return masteryMap.computeIfAbsent(key, k -> { + String name = SourceField.name.getTextOrEmpty(mastery); + if (name == null) { + throw new IllegalArgumentException("Unable to get name for Mastery: " + mastery.toPrettyString()); + } + + return new ItemMastery( + name, + key, + "item/mastery/" + Tui.slugify(name)); + }); + } + + public static List asLinks(Collection itemMasteries) { + return itemMasteries.stream() + .map(ItemMastery::linkify) + .toList(); + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java index 9bc3c7eba..ea0455ba7 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java @@ -1,233 +1,185 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.isPresent; + import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; -import io.quarkus.qute.TemplateData; -public interface ItemProperty { - static final Map propertyToLink = new HashMap<>(); +record ItemProperty( + String name, + String abbreviation, + String indexKey, + String sectionName, + String tag) { - Comparator comparator = Comparator.comparing(ItemProperty::value); + public String toString() { + return "Property: " + name; + } - String getMarkdownLink(Tools5eIndex index); + public String linkify() { + return linkify(null); + } - String tagValue(); + public String linkify(String linkText) { + Tools5eIndex index = Tools5eIndex.getInstance(); + linkText = isPresent(linkText) ? linkText : name; - String value(); + boolean included = isPresent(indexKey) + ? index.isIncluded(indexKey) + : index.customRulesIncluded(); - class CustomItemProperty implements ItemProperty { - final String name; - final String tag; - final String abbreviation; + return included + ? "[%s](%sitem-properties.md#%s)".formatted( + linkText, index.rulesVaultRoot(), + Tui.toAnchorTag(isPresent(sectionName) ? sectionName : name)) + : linkText; + } - public CustomItemProperty(JsonNode property) { - abbreviation = property.get("abbreviation").asText(); + public static final Comparator comparator = Comparator.comparing(ItemProperty::name); + public static final Map propertyMap = new HashMap<>(); + + public static final ItemProperty CURSED = ItemProperty.customProperty("Cursed", "Cursed Items", "="); + public static final ItemProperty SILVERED = ItemProperty.customProperty("Silvered", "Silvered Weapons", "="); + public static final ItemProperty POISON = ItemProperty.customProperty("Poison", "="); + + public static ItemProperty fromKey(String key, Tools5eIndex index) { + String finalKey = index.getAliasOrDefault(key); + JsonNode node = index.getNode(finalKey); + return node == null ? null : ItemProperty.fromNode(finalKey, node); + } + + public static ItemProperty fromNode(String key, JsonNode property) { + // Create the ItemType object once + return ItemProperty.propertyMap.computeIfAbsent(key, k -> { + String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(property); + String sectionName = null; String name = SourceField.name.getTextOrNull(property); if (name == null) { - JsonNode entries = SourceField.entries.existsIn(property) - ? SourceField.entries.getFrom(property) - : Tools5eFields.entriesTemplate.getFrom(property); - - if (entries != null && entries.size() > 0) { - JsonNode firstEntry = entries.get(0); - if (firstEntry.has("name")) { - name = firstEntry.get("name").asText(); - } + JsonNode firstEntry = SourceField.entries.getFirstFromArray(property); + if (firstEntry == null) { + firstEntry = Tools5eFields.entriesTemplate.getFirstFromArray(property); + } + if (firstEntry != null) { + name = SourceField.name.getTextOrNull(firstEntry); } - if (name == null) { + Tui.instance().warnf(Msg.NOT_SET.wrap("Name not found for property %s"), key); name = abbreviation; } + // we've fished it out. remember it. + SourceField.name.setIn(property, name); + } else if ("AF".equals(abbreviation)) { + // Special case for firearms to distinguish from regular ammunition + name = "Ammunition (Firearm)"; + } else if ("S".equals(abbreviation)) { + // Special, as the name, doesn't match the property/rules section + sectionName = "Special Weapons"; } - this.name = name; - this.tag = "property/" + Tui.slugify(abbreviation); - } - - public CustomItemProperty(String abbreviation) { - this.name = abbreviation; - this.tag = "property/" + Tui.slugify(abbreviation); - this.abbreviation = abbreviation; - } - - @Override - public String getMarkdownLink(Tools5eIndex index) { - return propertyToLink.computeIfAbsent(this, p -> { - List targets = index.elementsMatching(Tools5eIndexType.itemProperty, abbreviation.toLowerCase()); - if (targets.isEmpty() || targets.size() > 1) { - return name; - } - String key = Tools5eIndexType.itemProperty.createKey(targets.get(0)); - - return index.isIncluded(key) - ? String.format("[%s](%s)", name, - index.rulesVaultRoot() + "item-properties.md#" + index.toAnchorTag(name)) - : name; - }); - } - - @Override - public String tagValue() { - return tag; - } - - @Override - public String value() { - return name; - } + // item property tag w/ a few fixes + String tag = ItemTag.property.build(name + .toLowerCase() + .replace("extended reach", "reach/extended")); + + return new ItemProperty( + name, + abbreviation, + key, + sectionName, + tag); + }); } - @TemplateData - enum PropertyEnum implements ItemProperty { - AMMUNITION("Ammunition", "A", "property/ammunition"), - FINESSE("Finesse", "F", "property/finesse"), - HEAVY("Heavy", "H", "property/heavy"), - LIGHT("Light", "L", "property/light"), - LOADING("Loading", "LD", "property/loading"), - REACH("Reach", "R", "property/reach"), - EXTENDED_REACH("Extended Reach", "ER", "property/reach/extended"), - SPECIAL("Special", "S", "property/special"), - THROWN("Thrown", "T", "property/thrown"), - TWO_HANDED("Two-handed", "2H", "property/two-handed"), - VERSATILE("Versatile", "V", "property/versatile"), - MARTIAL("Martial", "M", "property/martial"), - SILVERED("Silvered", "-", "property/silvered"), - POISON("Poison", "=", "property/poison"), - CURSED("Cursed Item", "*", "property/cursed"), - - // Additional properties - AMMUNITION_FIREARM("Ammunition (Firearm)", "AF", "property/ammunition/firearm"), - BURST_FIRE("Burst Fire", "BF", "property/burst-fire"), - RELOAD("Reload", "RLD", "property/reload"), - - // Magic/Wondrous item attributes: tier - MAJOR("Major", "!", "tier/major"), - MINOR("Minor", "@", "tier/minor"), - - // Magic/Wondrous item attributes: rarity - VESTIGE("Vestige", "Vst", "property/vestige"), - COMMON("Common", "1", "rarity/common"), - UNCOMMON("Uncommon", "2", "rarity/uncommon"), - RARE("Rare", "3", "rarity/rare"), - VERY_RARE("Very Rare", "4", "rarity/very-rare"), - LEGENDARY("Legendary", "5", "rarity/legendary"), - ARTIFACT("Artifact", "6", "rarity/artifact"), - VARIES("varies", "7", "rarity/varies"), - RARITY_UNKNOWN("unknown", "8", "rarity/unknown"), - RARITY_UNK_MAGIC("unknown (magic)", "9", "rarity/unknown/magic"), - - // Magic/Wondrous item attributes: attunement - REQ_ATTUNEMENT("Requires Attunement", "#", "attunement/required"), - OPT_ATTUNEMENT("Optional Attunement", "$", "attunement/optional"); - - public final String longName; - private final String encodedValue; - private final String tagValue; - - PropertyEnum(String longName, String ev, String tagValue) { - this.longName = longName; - this.encodedValue = ev; - this.tagValue = tagValue; - } - - public static final List tierProperties = List.of(MAJOR, MINOR); - - public static final List rarityProperties = Stream.of(PropertyEnum.values()) - .filter(x -> x.ordinal() >= COMMON.ordinal() && x.ordinal() <= RARITY_UNK_MAGIC.ordinal()) - .collect(Collectors.toList()); - - private static final List knownProperties = Stream.of(PropertyEnum.values()) - .collect(Collectors.toList()); - - public static boolean mundaneProperty(ItemProperty p) { - return !tierProperties.contains(p) && !rarityProperties.contains(p); - } - - public static boolean homebrewProperty(ItemProperty p) { - return !knownProperties.contains(p); - } - - public String value() { - return longName.toLowerCase(); - } - - public String tagValue() { - return tagValue; - } - - public String getMarkdownLink(Tools5eIndex index) { - if (rarityProperties.contains(this)) { - return longName; - } - return propertyToLink.computeIfAbsent(this, p -> { - List targets = index.elementsMatching(Tools5eIndexType.itemProperty, encodedValue.toLowerCase()); - if (targets.isEmpty() || targets.size() > 1) { - return longName; - } - String key = Tools5eIndexType.itemProperty.createKey(targets.get(0)); - - return index.isIncluded(key) - ? String.format("[%s](%s)", longName, - index.rulesVaultRoot() + "item-properties.md#" + index.toAnchorTag(longName)) - : longName; - }); - } - - public static PropertyEnum fromValue(String v) { - if (v == null || v.isBlank()) { - return null; - } - String key = v.toLowerCase(); - for (PropertyEnum p : PropertyEnum.values()) { - if (p.longName.toLowerCase().equals(key)) { - return p; - } - } - Tui.instance().errorf("Invalid/Unknown property %s", v); - return null; - } + public static List asLinks(Collection properties) { + return properties.stream() + .map(ItemProperty::linkify) + .toList(); + } - public static PropertyEnum fromEncodedType(String v) { - if (v == null || v.isBlank()) { - return null; - } - for (PropertyEnum p : PropertyEnum.values()) { - if (p.encodedValue.equals(v) || p.longName.toLowerCase().equals(v.toLowerCase())) { - return p; - } - } - return null; - } + /** + * Invented properties. No relevance to source material, but useful for + * links to rules, e.g. Poison. + * + * @param name + * @param sectionName Section heading in rules + * @param abbreviation + */ + public static ItemProperty customProperty(String name, String sectionName, String abbreviation) { + return propertyMap.computeIfAbsent(sectionName, k -> { + return new ItemProperty( + name, + abbreviation, + "", + sectionName, + ItemTag.property.build(name)); + }); + } - public static void findAdditionalProperties(String name, ItemType type, Collection properties, - Predicate matches) { - if (type.isWeapon() && name.toLowerCase(Locale.ROOT).contains("silvered")) { - properties.add(SILVERED); - } - if (matches.test("^Curse: .*")) { - properties.add(PropertyEnum.CURSED); - } - if (matches.test( - "^(This poison is|This poison was|You can use the poison in|This poison must be harvested|A creature subjected to this poison|A creature that ingests this poison) .*")) { - properties.add(PropertyEnum.POISON); - } - if (matches.test(".*it is actually poison.*")) { - properties.add(PropertyEnum.POISON); - } - } + /** + * Invented properties. No relevance to source material, but useful for + * links to rules, e.g. Poison. + * + * @param name + * @param abbreviation + * @return + */ + public static ItemProperty customProperty(String name, String abbreviation) { + return ItemProperty.customProperty(name, name, abbreviation); } } + +// Parser.ITM_PROP_ABV__TWO_HANDED = "2H"; +// Parser.ITM_PROP_ABV__AMMUNITION = "A"; +// Parser.ITM_PROP_ABV__AMMUNITION_FUTURISTIC = "AF"; +// Parser.ITM_PROP_ABV__BURST_FIRE = "BF"; +// Parser.ITM_PROP_ABV__EXTENDED_REACH = "ER"; +// Parser.ITM_PROP_ABV__FINESSE = "F"; +// Parser.ITM_PROP_ABV__HEAVY = "H"; +// Parser.ITM_PROP_ABV__LIGHT = "L"; +// Parser.ITM_PROP_ABV__LOADING = "LD"; +// Parser.ITM_PROP_ABV__OTHER = "OTH"; +// Parser.ITM_PROP_ABV__REACH = "R"; +// Parser.ITM_PROP_ABV__RELOAD = "RLD"; +// Parser.ITM_PROP_ABV__SPECIAL = "S"; +// Parser.ITM_PROP_ABV__THROWN = "T"; +// Parser.ITM_PROP_ABV__VERSATILE = "V"; +// Parser.ITM_PROP_ABV__VESTIGE_OF_DIVERGENCE = "Vst"; + +// Parser.ITM_PROP__TWO_HANDED = "2H"; +// Parser.ITM_PROP__AMMUNITION = "A"; +// Parser.ITM_PROP__AMMUNITION_FUTURISTIC = "AF|DMG"; +// Parser.ITM_PROP__BURST_FIRE = "BF|DMG"; +// Parser.ITM_PROP__EXTENDED_REACH = "ER|TDCSR"; +// Parser.ITM_PROP__FINESSE = "F"; +// Parser.ITM_PROP__HEAVY = "H"; +// Parser.ITM_PROP__LIGHT = "L"; +// Parser.ITM_PROP__LOADING = "LD"; +// Parser.ITM_PROP__OTHER = "OTH"; +// Parser.ITM_PROP__REACH = "R"; +// Parser.ITM_PROP__RELOAD = "RLD|DMG"; +// Parser.ITM_PROP__SPECIAL = "S"; +// Parser.ITM_PROP__THROWN = "T"; +// Parser.ITM_PROP__VERSATILE = "V"; +// Parser.ITM_PROP__VESTIGE_OF_DIVERGENCE = "Vst|TDCSR"; + +// Parser.ITM_PROP__ODND_TWO_HANDED = "2H|XPHB"; +// Parser.ITM_PROP__ODND_AMMUNITION = "A|XPHB"; +// Parser.ITM_PROP__ODND_FINESSE = "F|XPHB"; +// Parser.ITM_PROP__ODND_HEAVY = "H|XPHB"; +// Parser.ITM_PROP__ODND_LIGHT = "L|XPHB"; +// Parser.ITM_PROP__ODND_LOADING = "LD|XPHB"; +// Parser.ITM_PROP__ODND_REACH = "R|XPHB"; +// Parser.ITM_PROP__ODND_THROWN = "T|XPHB"; +// Parser.ITM_PROP__ODND_VERSATILE = "V|XPHB"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java index c8bed6d6e..4947d23e3 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java @@ -1,255 +1,267 @@ package dev.ebullient.convert.tools.dnd5e; -import java.util.Collection; -import java.util.Locale; +import static dev.ebullient.convert.StringUtil.isPresent; + +import java.util.HashMap; +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; +import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; + +/** + * @param name Item type name. + * @param lowercaseName Item type name in lowercase (for comparison) + * @param abbreviation Item type abbreviation. + * @param link Markdown link to definition of this type or name if source is not included. + * @param group Item type group. Optional. + */ +public record ItemType( + String name, + String lowercaseName, + String abbreviation, + String indexKey, + ItemTypeGroup group) { + + public String toString() { + return "Type: " + name; + } -public interface ItemType { - - boolean isWeapon(); - - String getSpecializedType(); + public String linkify() { + return linkify(null); + } - String getItemTag(Collection itemProperties, Tui tui); + public String linkify(String linkText) { + Tools5eIndex index = Tools5eIndex.getInstance(); + linkText = isPresent(linkText) ? linkText : name; - class CustomItemType implements ItemType { - final String name; - final String lower; - final boolean error; + boolean included = isPresent(indexKey) + ? index.isIncluded(indexKey) + : index.customRulesIncluded(); - public CustomItemType(JsonNode typeNode) { - this.name = SourceField.name.getTextOrEmpty(typeNode); - this.lower = name.toLowerCase(Locale.ROOT); - this.error = false; - } + return included + ? "[%s](%sitem-types.md#%s)".formatted( + linkText, index.rulesVaultRoot(), Tui.toAnchorTag(name)) + : linkText; + } - public CustomItemType(String name) { - this.name = name; - this.lower = name.toLowerCase(Locale.ROOT); - this.error = true; - } + public static final Map typeMap = new HashMap<>(); - @Override - public boolean isWeapon() { - return lower.contains("weapon") - || lower.contains("firearm") - || lower.contains("explosive") - || lower.contains("ammo"); - } - - @Override - public String getItemTag(Collection itemProperties, Tui tui) { - if (lower.contains("armor")) { - return "armor/" + Tui.slugify(lower.replaceAll("\\s*armor\\s*", "")); - } - if (lower.contains("vehicle")) { - return "vehicle/" + Tui.slugify(lower.replaceAll("\\s*vehicle\\s*", "")); - } - if (lower.contains("wondrous")) { - return "wondrous/" + Tui.slugify(lower.replaceAll("\\s*wondrous( item)?\\s*", "")); - } + public static ItemType fromKey(String key, Tools5eIndex index) { + String finalKey = index.getAliasOrDefault(key); + JsonNode node = index.getNode(finalKey); + return node == null ? null : fromNode(finalKey, node); + } - if (lower.contains("ammunition")) { - return "weapon/" - + "ammunition/" + Tui.slugify(lower.replaceAll("\\s*ammunition\\s*", "")); - } - if (lower.contains("ammo")) { - return "weapon/" - + "ammunition/" + Tui.slugify(lower.replaceAll("\\s*ammo\\s*", "")); - } - if (lower.contains("explosive")) { - return "weapon/" - + "explosive/" + Tui.slugify(lower.replaceAll("\\s*explosive\\s*", "")); - } - if (lower.contains("firearm")) { - return "weapon/" - + (itemProperties.contains(ItemProperty.PropertyEnum.MARTIAL) ? "martial/" : "simple/") - + "firearm/" + Tui.slugify(lower.replaceAll("\\s*firearm\\s*", "")); - } - if (lower.contains("weapon")) { - return "weapon/" - + (itemProperties.contains(ItemProperty.PropertyEnum.MARTIAL) ? "martial/" : "simple/") - + Tui.slugify(lower.replaceAll("\\s*weapon\\s*", "")); + public static ItemType fromNode(String typeKey, JsonNode typeNode) { + // Create the ItemType object once + return typeMap.computeIfAbsent(typeKey, k -> { + String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(typeNode); + String name = SourceField.name.getTextOrEmpty(typeNode); + if (!isPresent(name)) { + Tui.instance().warnf(Msg.NOT_SET.wrap("Name not found for type %s"), typeKey); + name = abbreviation; } + name = fixName(abbreviation, name); + String lower = name.toLowerCase(); + ItemTypeGroup group = mapGroup(abbreviation, lower, typeNode); + + return new ItemType( + name, + lower, + abbreviation, + typeKey, + group); + }); + } - return "gear" - + (itemProperties.contains(ItemProperty.PropertyEnum.POISON) ? "/poison" : "") - + (itemProperties.contains(ItemProperty.PropertyEnum.CURSED) ? "/cursed" : "") - + "/" + Tui.slugify(lower.replaceAll("\\s*gear\\s*", "")); + public static String tagForType(ItemType type, Tui tui) { + String lower = type.lowercaseName(); + if (type.group() == ItemTypeGroup.armor) { + return ItemTag.armor.build(lower.replaceAll("\\s*armor\\s*", "")); } - - @Override - public String getSpecializedType() { - return name; + if (type.group() == ItemTypeGroup.shield) { + return ItemTag.shield.build(lower.replaceAll("\\s*shield\\s*", "")); } - } - - enum ItemEnum implements ItemType { - - LIGHT_ARMOR("Light Armor", "LA", ""), - MEDIUM_ARMOR("Medium Armor", "MA", ""), - HEAVY_ARMOR("Heavy Armor", "HA", ""), - SHIELD("Shield", "S", ""), - - MELEE_WEAPON("Melee Weapon", "M", ""), - EXPLOSIVE("Ranged Weapon", "EXP", "Explosive"), - RANGED_WEAPON("Ranged Weapon", "R", ""), - AMMUNITION("Ammunition", "A", ""), - AMMUNITION_FIREARM("Ammunition", "AF", "Ammunition (Firearm)"), - - ROD("Rod", "RD", ""), - STAFF("Staff", "ST", ""), - WAND("Wand", "WD", ""), - RING("Ring", "RG", ""), - POTION("Potion", "P", ""), - SCROLL("Scroll", "SC", ""), - ELDRITCH_MACHINE("Wondrous Item", "EM", "Eldritch Machine"), - GENERIC_VARIANT("Wondrous Item", "GV", "Generic Variant"), - MASTER_RUNE("Wondrous Item", "MR", "Master Rune"), - OTHER("Wondrous Item", "OTH", "Other"), - WONDROUS("Wondrous Item", "W", ""), - - ARTISANS_TOOLS("Adventuring Gear", "AT", "Artisan's Tools"), - FOOD("Adventuring Gear", "FD", "Food and Drink"), - GAMING_SET("Adventuring Gear", "GS", "Gaming Set"), - INSTRUMENT("Adventuring Gear", "INS", "Instrument"), - MOUNT("Adventuring Gear", "MNT", "Mount"), - SPELLCASTING_FOCUS("Adventuring Gear", "SCF", "Spellcasting Focus"), - TOOLS("Adventuring Gear", "T", "Tools"), - TACK("Adventuring Gear", "TAH", "Tack and Harness"), - TRADE_GOOD("Adventuring Gear", "TG", "Trade Good"), - GEAR("Adventuring Gear", "G", ""), - IDG("Adventuring Gear", "IDG", "Illegal Drug"), - - AIRSHIP("Vehicle", "AIR", "Airship, Vehicle (air)"), - SHIP("Vehicle", "SHP", "Ship, Vehicle (water)"), - SPELLJAMMER("Vehicle", "SPC", "Spelljammer, Vehicle (space)"), - VEHICLE("Vehicle", "VEH", "Vehicle (land)"), - - ART("Treasure", "$A", "Art object"), - COINAGE("Treasure", "$C", "Coinage"), - GEMSTONE("Treasure", "$G", "Gemstone"), - WEALTH("Treasure", "$", ""), - - UNKNOWN("Unknown", "", ""); - - private final String genericType; - private final String lower; - private final String encodedValue; - private final String specializedType; - - ItemEnum(String genericType, String encodedValue, String specializedType) { - this.genericType = genericType; - this.lower = genericType.toLowerCase(); - this.encodedValue = encodedValue; - this.specializedType = specializedType; + if (type.group() == ItemTypeGroup.vehicle) { + return ItemTag.vehicle.build(lower.replaceAll("\\s*vehicle\\s*", "")); } - - public String getSpecializedType() { - return specializedType.isBlank() ? genericType : specializedType; + if (type.group() == ItemTypeGroup.wondrous) { + return ItemTag.wondrous.build(lower.replaceAll("\\s*wondrous( item)?\\s*", "")); } - - public String value() { - return lower; + if (type.group() == ItemTypeGroup.weapon) { + return ItemTag.weapon.build("" + + (lower.contains("firearm") ? "firearm/" : "") + + (lower.contains("ammunition") || lower.contains("ammo") ? "ammunition/" : "") + + (lower.contains("explosive") ? "explosive/" : "") + + lower.replaceAll("\\s*(ammo|ammunition|explosive|firearm|weapon)\\s*", "")); } + return ItemTag.gear.build(lower.replaceAll("\\s*(adventuring|gear)\\s*", "")); + } - public static ItemEnum fromEncodedValue(String v) { - if (v == null || v.isBlank()) { - return null; - } - for (ItemEnum i : ItemEnum.values()) { - if (i.encodedValue.equals(v)) { - return i; - } - } - return null; - } + private static String fixName(String abbreviation, String name) { + return switch (abbreviation) { + case "AF" -> "Ammunition (Firearm)"; + case "AIR" -> "Airship, Vehicle (air)"; + case "SHP" -> "Ship, Vehicle (water)"; + case "SPC" -> "Spelljammer, Vehicle (space)"; + case "VEH" -> "Vehicle (land)"; + default -> name; + }; + } - public boolean isWeapon() { - int x = this.ordinal(); - return x >= MELEE_WEAPON.ordinal() && x <= AMMUNITION_FIREARM.ordinal(); + private static ItemTypeGroup mapGroup(String abbreviation, String lowercase, JsonNode itemType) { + if (abbreviation.contains("$") || lowercase.contains("treasure")) { + return ItemTypeGroup.treasure; } - - public boolean isArmor() { - int x = this.ordinal(); - return x >= LIGHT_ARMOR.ordinal() && x <= SHIELD.ordinal(); + if (lowercase.contains("ammunition")) { + return ItemTypeGroup.ammunition; } - - public boolean isGear() { - int x = this.ordinal(); - return x >= ARTISANS_TOOLS.ordinal() && x <= GEAR.ordinal(); + if (lowercase.contains("armor")) { + return ItemTypeGroup.armor; } - - public boolean isMoney() { - int x = this.ordinal(); - return x >= ART.ordinal() && x <= WEALTH.ordinal(); + if (lowercase.contains("shield")) { + return ItemTypeGroup.shield; } - - public boolean isVehicle() { - int x = this.ordinal(); - return x >= AIRSHIP.ordinal() && x <= VEHICLE.ordinal(); + if (lowercase.contains("vehicle")) { + return ItemTypeGroup.vehicle; } - - public boolean isWondrousItem() { - int x = this.ordinal(); - return x >= ROD.ordinal() && x <= WONDROUS.ordinal(); + if (lowercase.contains("weapon") + || lowercase.contains("explosive") + || lowercase.contains("firearm")) { + return ItemTypeGroup.weapon; } - - public String getItemTag(Collection properties, Tui tui) { - StringBuilder tag = new StringBuilder(); - if (isArmor()) { - tag.append("armor/"); - tag.append(lower.replace(" armor", "")); - } else if (isWeapon()) { - tag.append("weapon/"); - if (this == MELEE_WEAPON) { - tag.append(properties.contains(ItemProperty.PropertyEnum.MARTIAL) ? "martial/" : "simple/"); - tag.append("melee"); - } else if (this == RANGED_WEAPON) { - tag.append(properties.contains(ItemProperty.PropertyEnum.MARTIAL) ? "martial/" : "simple/"); - tag.append("ranged"); - } else if (this == EXPLOSIVE) { - tag.append("explosive"); - } else if (this == AMMUNITION) { - tag.append("ammunition"); - } else if (this == AMMUNITION_FIREARM) { - tag.append("ammunition/firearm"); - } - } else if (isVehicle()) { - tag.append("vehicle"); - if (this == SPELLJAMMER) { - tag.append("/spelljammer"); - } else if (this == AIRSHIP) { - tag.append("/airship"); - } else if (this == SHIP) { - tag.append("/ship"); - } - } else if (isGear()) { - tag.append("gear"); - if (!this.specializedType.isEmpty()) { - tag.append("/").append(Tui.slugify(this.specializedType)); - } - if (properties.contains(ItemProperty.PropertyEnum.POISON)) { - tag.append("/poison"); - } else if (properties.contains(ItemProperty.PropertyEnum.CURSED)) { - tag.append("/cursed"); - } - } else if (isWondrousItem()) { - tag.append("wondrous"); - if (this != WONDROUS) { - tag.append("/").append(Tui.slugify(genericType)); + if (lowercase.contains("wondrous")) { + return ItemTypeGroup.wondrous; + } + return switch (abbreviation) { + // generic variant, other + // potion, rod, ring, scroll, staff, wand, wondrous + case "GV", "MR", "OTH", "P", "RD", "RG", "SC", "ST", "WD", "W" -> ItemTypeGroup.wondrous; + // artisan tool, food & drink, gear, gaming set, illegal drug, musical instrument, + // mount, spellcasting focus, tools, tack & harness, trade good, + case "AT", "FD", "G", "GS", "IDG", "INS", "MNT", "SCF", "T", "TAH", "TG" -> ItemTypeGroup.gear; + default -> { + // Homebrew won't always have a type assigned. Poke around. + String nodeString = itemType.toString(); + if (nodeString.contains("this weapon")) { + yield ItemTypeGroup.weapon; } - } else if (isMoney()) { - tag.append("wealth"); - if (specializedType.length() > 0) { - tag.append("/").append(Tui.slugify(this.specializedType)); + if (lowercase.contains("magic") + || lowercase.contains("rune") + || nodeString.contains("magic")) { + yield ItemTypeGroup.wondrous; } + yield ItemTypeGroup.gear; } - return tag.toString(); - } + }; } } + +// Parser.ITM_TYP_ABV__TREASURE = "$"; +// Parser.ITM_TYP_ABV__TREASURE_ART_OBJECT = "$A"; +// Parser.ITM_TYP_ABV__TREASURE_COINAGE = "$C"; +// Parser.ITM_TYP_ABV__TREASURE_GEMSTONE = "$G"; +// Parser.ITM_TYP_ABV__AMMUNITION = "A"; +// Parser.ITM_TYP_ABV__AMMUNITION_FUTURISTIC = "AF"; +// Parser.ITM_TYP_ABV__VEHICLE_AIR = "AIR"; +// Parser.ITM_TYP_ABV__ARTISAN_TOOL = "AT"; +// Parser.ITM_TYP_ABV__EXPLOSIVE = "EXP"; +// Parser.ITM_TYP_ABV__FOOD_AND_DRINK = "FD"; +// Parser.ITM_TYP_ABV__ADVENTURING_GEAR = "G"; +// Parser.ITM_TYP_ABV__GAMING_SET = "GS"; +// Parser.ITM_TYP_ABV__GENERIC_VARIANT = "GV"; +// Parser.ITM_TYP_ABV__HEAVY_ARMOR = "HA"; +// Parser.ITM_TYP_ABV__ILLEGAL_DRUG = "IDG"; +// Parser.ITM_TYP_ABV__INSTRUMENT = "INS"; +// Parser.ITM_TYP_ABV__LIGHT_ARMOR = "LA"; +// Parser.ITM_TYP_ABV__MELEE_WEAPON = "M"; +// Parser.ITM_TYP_ABV__MEDIUM_ARMOR = "MA"; +// Parser.ITM_TYP_ABV__MOUNT = "MNT"; +// Parser.ITM_TYP_ABV__OTHER = "OTH"; +// Parser.ITM_TYP_ABV__POTION = "P"; +// Parser.ITM_TYP_ABV__RANGED_WEAPON = "R"; +// Parser.ITM_TYP_ABV__ROD = "RD"; +// Parser.ITM_TYP_ABV__RING = "RG"; +// Parser.ITM_TYP_ABV__SHIELD = "S"; +// Parser.ITM_TYP_ABV__SCROLL = "SC"; +// Parser.ITM_TYP_ABV__SPELLCASTING_FOCUS = "SCF"; +// Parser.ITM_TYP_ABV__VEHICLE_WATER = "SHP"; +// Parser.ITM_TYP_ABV__VEHICLE_SPACE = "SPC"; +// Parser.ITM_TYP_ABV__TOOL = "T"; +// Parser.ITM_TYP_ABV__TACK_AND_HARNESS = "TAH"; +// Parser.ITM_TYP_ABV__TRADE_GOOD = "TG"; +// Parser.ITM_TYP_ABV__VEHICLE_LAND = "VEH"; +// Parser.ITM_TYP_ABV__WAND = "WD"; + +// Parser.ITM_TYP__TREASURE = "$|DMG"; +// Parser.ITM_TYP__TREASURE_ART_OBJECT = "$A|DMG"; +// Parser.ITM_TYP__TREASURE_COINAGE = "$C"; +// Parser.ITM_TYP__TREASURE_GEMSTONE = "$G|DMG"; +// Parser.ITM_TYP__AMMUNITION = "A"; +// Parser.ITM_TYP__AMMUNITION_FUTURISTIC = "AF|DMG"; +// Parser.ITM_TYP__VEHICLE_AIR = "AIR|DMG"; +// Parser.ITM_TYP__ARTISAN_TOOL = "AT"; +// Parser.ITM_TYP__EXPLOSIVE = "EXP|DMG"; +// Parser.ITM_TYP__FOOD_AND_DRINK = "FD"; +// Parser.ITM_TYP__ADVENTURING_GEAR = "G"; +// Parser.ITM_TYP__GAMING_SET = "GS"; +// Parser.ITM_TYP__GENERIC_VARIANT = "GV|DMG"; +// Parser.ITM_TYP__HEAVY_ARMOR = "HA"; +// Parser.ITM_TYP__ILLEGAL_DRUG = "IDG|TDCSR"; +// Parser.ITM_TYP__INSTRUMENT = "INS"; +// Parser.ITM_TYP__LIGHT_ARMOR = "LA"; +// Parser.ITM_TYP__MELEE_WEAPON = "M"; +// Parser.ITM_TYP__MEDIUM_ARMOR = "MA"; +// Parser.ITM_TYP__MOUNT = "MNT"; +// Parser.ITM_TYP__OTHER = "OTH"; +// Parser.ITM_TYP__POTION = "P"; +// Parser.ITM_TYP__RANGED_WEAPON = "R"; +// Parser.ITM_TYP__ROD = "RD|DMG"; +// Parser.ITM_TYP__RING = "RG|DMG"; +// Parser.ITM_TYP__SHIELD = "S"; +// Parser.ITM_TYP__SCROLL = "SC|DMG"; +// Parser.ITM_TYP__SPELLCASTING_FOCUS = "SCF"; +// Parser.ITM_TYP__VEHICLE_WATER = "SHP"; +// Parser.ITM_TYP__VEHICLE_SPACE = "SPC|AAG"; +// Parser.ITM_TYP__TOOL = "T"; +// Parser.ITM_TYP__TACK_AND_HARNESS = "TAH"; +// Parser.ITM_TYP__TRADE_GOOD = "TG"; +// Parser.ITM_TYP__VEHICLE_LAND = "VEH"; +// Parser.ITM_TYP__WAND = "WD|DMG"; + +// Parser.ITM_TYP__ODND_TREASURE_ART_OBJECT = "$A|XDMG"; +// Parser.ITM_TYP__ODND_TREASURE_COINAGE = "$C|XPHB"; +// Parser.ITM_TYP__ODND_TREASURE_GEMSTONE = "$G|XDMG"; +// Parser.ITM_TYP__ODND_AMMUNITION = "A|XPHB"; +// Parser.ITM_TYP__ODND_AMMUNITION_FUTURISTIC = "AF|XDMG"; +// Parser.ITM_TYP__ODND_VEHICLE_AIR = "AIR|XPHB"; +// Parser.ITM_TYP__ODND_ARTISAN_TOOL = "AT|XPHB"; +// Parser.ITM_TYP__ODND_EXPLOSIVE = "EXP|XDMG"; +// Parser.ITM_TYP__ODND_FOOD_AND_DRINK = "FD|XPHB"; +// Parser.ITM_TYP__ODND_ADVENTURING_GEAR = "G|XPHB"; +// Parser.ITM_TYP__ODND_GAMING_SET = "GS|XPHB"; +// Parser.ITM_TYP__ODND_GENERIC_VARIANT = "GV|XDMG"; +// Parser.ITM_TYP__ODND_HEAVY_ARMOR = "HA|XPHB"; +// Parser.ITM_TYP__ODND_INSTRUMENT = "INS|XPHB"; +// Parser.ITM_TYP__ODND_LIGHT_ARMOR = "LA|XPHB"; +// Parser.ITM_TYP__ODND_MELEE_WEAPON = "M|XPHB"; +// Parser.ITM_TYP__ODND_MEDIUM_ARMOR = "MA|XPHB"; +// Parser.ITM_TYP__ODND_MOUNT = "MNT|XPHB"; +// Parser.ITM_TYP__ODND_POTION = "P|XPHB"; +// Parser.ITM_TYP__ODND_RANGED_WEAPON = "R|XPHB"; +// Parser.ITM_TYP__ODND_ROD = "RD|XDMG"; +// Parser.ITM_TYP__ODND_RING = "RG|XDMG"; +// Parser.ITM_TYP__ODND_SHIELD = "S|XPHB"; +// Parser.ITM_TYP__ODND_SCROLL = "SC|XPHB"; +// Parser.ITM_TYP__ODND_SPELLCASTING_FOCUS = "SCF|XPHB"; +// Parser.ITM_TYP__ODND_VEHICLE_WATER = "SHP|XPHB"; +// Parser.ITM_TYP__ODND_TOOL = "T|XPHB"; +// Parser.ITM_TYP__ODND_TACK_AND_HARNESS = "TAH|XPHB"; +// Parser.ITM_TYP__ODND_TRADE_GOOD = "TG|XDMG"; +// Parser.ITM_TYP__ODND_VEHICLE_LAND = "VEH|XPHB"; +// Parser.ITM_TYP__ODND_WAND = "WD|XDMG"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTypeGroup.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTypeGroup.java new file mode 100644 index 000000000..b856a28a7 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTypeGroup.java @@ -0,0 +1,20 @@ +package dev.ebullient.convert.tools.dnd5e; + +import io.quarkus.qute.TemplateData; + +@TemplateData +public enum ItemTypeGroup { + ammunition, + armor, + gear, + shield, + treasure, + vehicle, + weapon, + wondrous; + + public boolean hasGroup(ItemType type, ItemType typeAlt) { + return (type != null && this == type.group()) + || (typeAlt != null && this == typeAlt.group()); + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java new file mode 100644 index 000000000..4322a79fd --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java @@ -0,0 +1,109 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static dev.ebullient.convert.StringUtil.isPresent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.qute.ImageRef; +import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.dnd5e.qute.QuteBastion; +import dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Hireling; +import dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Space; +import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; + +public class Json2QuteBastion extends Json2QuteCommon { + + Map spaceMap = new HashMap<>(); + + Json2QuteBastion(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) { + super(index, type, jsonNode); + } + + @Override + protected Tools5eQuteBase buildQuteResource() { + Tags tags = new Tags(getSources()); + tags.add("bastion"); + + List fluffImages = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.facilityFluff, "##", fluffImages); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + + String type = BastionFields.facilityType.getTextOrThrow(rootNode); + + String prereqs = ""; + if (BastionFields.prerequisite.existsIn(rootNode)) { + prereqs = listPrerequisites(rootNode); + } else if (!"basic".equals(type)) { + prereqs = "None"; + } + + List hirelings = new ArrayList<>(); + for (JsonNode h : BastionFields.hirelings.iterateArrayFrom(rootNode)) { + hirelings.add(new Hireling( + BastionFields.exact.intOrNull(h), + BastionFields.min.intOrNull(h), + BastionFields.max.intOrNull(h), + spaceForName(BastionFields.space.getTextOrEmpty(h)))); + } + + List spaces = new ArrayList<>(); + for (JsonNode s : BastionFields.space.iterateArrayFrom(rootNode)) { + Space space = spaceForName(s.asText()); + if (space == null) { + // TODO: At some point, there will be a custom bastion space.. + tui().warnf(Msg.UNRESOLVED, "Bastion space %s not found (%s)", s, getSources().getKey()); + } else { + spaces.add(space); + } + } + + return new QuteBastion( + sources, + getName(), + getSourceText(getSources()), + hirelings, + BastionFields.level.getTextOrEmpty(rootNode), + BastionFields.orders.getListOfStrings(rootNode, tui()), + prereqs, + spaces, + type, + String.join("\n", text), + fluffImages, + tags); + } + + private Space spaceForName(String name) { + if (!isPresent(name)) { + return null; + } + if (spaceMap.isEmpty()) { + Space cramped = new Space("Cramped", 4, 500, 20); + Space roomy = new Space("Roomy", 16, 1000, 45, cramped); + Space vast = new Space("Vast", 36, 3000, 125, roomy); + spaceMap.put("cramped", cramped); + spaceMap.put("roomy", roomy); + spaceMap.put("vast", vast); + } + return spaceMap.get(name.toLowerCase()); + } + + enum BastionFields implements JsonNodeReader { + exact, + facilityType, + hirelings, + level, + min, + max, + orders, + space, + prerequisite, + fluffImages; + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java index eea64d13a..cca389a8e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java @@ -13,9 +13,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.OptionalFeatureType; +import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.qute.QuteClass; import dev.ebullient.convert.tools.dnd5e.qute.QuteSubclass; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -305,7 +306,7 @@ String buildStartMulticlassing() { maybeAddBlankLine(startMulticlass); JsonNode requirements = multiclassing.get("requirements"); if (requirements == null) { - tui().warnf("No requirements specified to multiclass %s: %s", getSources().getKey(), multiclassing); + tui().warnf(Msg.NOT_SET, "No requirements specified to multiclass %s: %s", getSources().getKey(), multiclassing); } else if (requirements.has("or")) { List options = new ArrayList<>(); requirements.get("or").get(0).fields().forEachRemaining(ability -> options.add(String.format("%s %s", @@ -368,7 +369,7 @@ void findClassFeatures(Tools5eIndexType type, JsonNode arrayElement, List text) { toAnchorTag(oft.title))); text.add("^list-" + slugify(oft.title)); } else { - tui().errorf("Can not find OptionalFeatureType for optional feature progression %s: %s", featureType, ofp); + tui().errorf( + Msg.UNRESOLVED, "Can not find optional feature type %s for progression. Source: %s", + featureType, ofp); } } } @@ -747,5 +750,7 @@ String columnValue(JsonNode c) { enum ClassFields implements JsonNodeReader { optionalfeatureProgression, + subclassSource, + subclassShortName, } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java index 8338a79fe..f8107c95e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java @@ -2,7 +2,6 @@ import static dev.ebullient.convert.StringUtil.isPresent; import static dev.ebullient.convert.StringUtil.joinConjunct; -import static dev.ebullient.convert.StringUtil.toTitleCase; import java.nio.file.Path; import java.text.Normalizer; @@ -24,6 +23,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import dev.ebullient.convert.StringUtil; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.NamedText; @@ -39,6 +39,11 @@ public class Json2QuteCommon implements JsonSource { static final Pattern featPattern = Pattern.compile("([^|]+)\\|?.*"); static final List SPEED_MODE = List.of("walk", "burrow", "climb", "fly", "swim"); static final List specialTraits = List.of("special equipment", "shapechanger"); + static final Map SCF_TYPE_TO_NAME = Map.of( + "arcane", "Arcane Focus", + "druid", "Druidic Focus", + "holy", "Holy Symbol"); + static final Comparator>> compareNumberStrings = Comparator .comparingInt(e -> Integer.parseInt(e.getKey())); @@ -101,12 +106,12 @@ public List getFluff(Tools5eIndexType fluffType, String heading, List getFluffImages(Tools5eIndexType fluffType) { List images = new ArrayList<>(); if (Tools5eFields.hasFluffImages.booleanOrDefault(rootNode, false)) { String fluffKey = fluffType.createKey(rootNode); - JsonNode fluffNode = index.getNode(fluffKey); + JsonNode fluffNode = index.getOrigin(fluffKey); if (fluffNode != null) { getImages(Tools5eFields.images.getFrom(fluffNode), images); } @@ -256,12 +261,44 @@ private String backgroundPrereq(JsonNode backgroundPrereq) { return joinConjunct(" or ", backgrounds); } - private String campaignPrereq(JsonNode campaignPrereq) { + private String replaceConjoinOr(JsonNode campaignPrereq, String suffix) { List cmpn = new ArrayList<>(); for (JsonNode p : iterableElements(campaignPrereq)) { replaceText(p.asText()); } - return joinConjunct(" or ", cmpn); + return joinConjunct(" or ", cmpn) + suffix; + } + + private String expertisePrereq(JsonNode expertisePrereq) { + // "prerequisite": [ + // { + // "expertise": [ + // { + // "skill": true + // } + // ] + // } + // ], + List expertise = new ArrayList<>(); + for (JsonNode p : iterableElements(expertisePrereq)) { + for (Entry prof : iterableFields(p)) { + switch (prof.getKey()) { + case "skill" -> { + if (prof.getValue().asBoolean()) { + expertise.add("Expertise in a skill"); + } else { + tui().warnf(Msg.UNKNOWN, "unknown expertise prereq value %s from %s / %s", + p.toString(), getSources().getKey(), parseState().getSource()); + } + } + default -> { + tui().warnf(Msg.UNKNOWN, "unknown expertise prereq type %s from %s / %s", + p.toString(), getSources().getKey(), parseState().getSource()); + } + } + } + } + return joinConjunct(" or ", expertise); } // "scion of the outer planes|ua2022wondersofthemultiverse|scion of the outer planes (good outer plane)" @@ -274,38 +311,15 @@ private String featPrereq(JsonNode featPrereq) { return joinConjunct(" or ", feats); } - private String featurePrereq(JsonNode featurePrereq) { - List features = new ArrayList<>(); - for (JsonNode p : iterableElements(featurePrereq)) { - replaceText(p.asText()); - } - return joinConjunct(" or ", features); - } - - private String groupPrereq(JsonNode groupPrereq) { - List grp = new ArrayList<>(); - for (JsonNode p : iterableElements(groupPrereq)) { - replaceText(toTitleCase(p.asText())); - } - return joinConjunct(" or ", grp); - } - - private String itemPrereq(JsonNode itemPrereq) { - List items = new ArrayList<>(); - for (JsonNode p : iterableElements(itemPrereq)) { - replaceText(p.asText()); - } - return joinConjunct(" or ", items); - } - private String itemTypePrereq(JsonNode itemTypePrereq) { List types = new ArrayList<>(); for (JsonNode p : iterableElements(itemTypePrereq)) { ItemType type = index.findItemType(p.asText(), getSources()); if (type != null) { - types.add(type.getSpecializedType()); + types.add(type.linkify()); } else { - tui().errorf("Unknown item type %s from %s", p, itemTypePrereq); + tui().warnf(Msg.UNKNOWN, "unknown item type prereq %s from %s / %s", + p.asText(), getSources().getKey(), parseState().getSource()); } } return joinConjunct(" and ", types); @@ -316,9 +330,10 @@ private String itemPropertyPrereq(JsonNode itemPropertyPrereq) { for (JsonNode p : iterableElements(itemPropertyPrereq)) { ItemProperty prop = index.findItemProperty(p.asText(), getSources()); if (prop != null) { - props.add(prop.getMarkdownLink(index)); + props.add(prop.linkify()); } else { - tui().errorf("Unknown item property %s", p); + tui().warnf(Msg.UNKNOWN, "unknown item property prereq %s from %s / %s", + p.asText(), getSources().getKey(), parseState().getSource()); } } return joinConjunct(" and ", props); @@ -328,7 +343,7 @@ private String itemPropertyPrereq(JsonNode itemPropertyPrereq) { // "level":{"level":1,"class":{"name":"Fighter","visible":true}}} private String levelPrereq(JsonNode levelPrereq) { if (levelPrereq.isArray()) - tui().error("levelPrereq: Array parameter"); + tui().errorf("levelPrereq: Array parameter"); if (levelPrereq.isNumber()) { return levelToText(levelPrereq.asText()); @@ -378,7 +393,8 @@ private String proficiencyPrereq(JsonNode profPrereq) { case "weaponGroup" -> profs.add(String.format("%s weapons", replaceText(prof.getValue().asText()))); default -> { - tui().errorf("Unknown proficiency prereq", p); + tui().warnf(Msg.UNKNOWN, "unknown proficiency prereq %s from %s / %s", + p.toString(), getSources().getKey(), parseState().getSource()); } } } @@ -404,6 +420,30 @@ private String racePrereq(JsonNode racePrereq) { return joinConjunct(" or ", races); } + private String scfPrereq(JsonNode scfPrereq) { + if (scfPrereq.isBoolean()) { + return replaceText("Ability to use a {@variantrule Spellcasting Focus|XPHB}"); + } + + List scfTypes = new ArrayList<>(); + for (JsonNode p : iterableElements(scfPrereq)) { + String type = p.asText(); + String name = SCF_TYPE_TO_NAME.getOrDefault(type, type); + String article = scfTypes.isEmpty() + ? articleFor(name) + " " + : ""; + + if (!name.equals(type)) { + name = replaceText("{@item " + name + "|XPHB}"); + } + + scfTypes.add("%s%s".formatted(article, name)); + } + + return replaceText("Ability to use %s as a {@variantrule Spellcasting Focus|XPHB}" + .formatted(joinConjunct(" or ", scfTypes))); + } + private List testBoolean(JsonNode node, String valueIfTrue) { return node.booleanValue() ? List.of(valueIfTrue) @@ -422,7 +462,8 @@ private String spellPrereq(JsonNode spellPrereq) { } else if ("x".equals(split[1])) { spells.add(replaceText(String.format("{@spell hex} spell or a warlock feature that curses", split[0]))); } else { - tui().errorf("Unknown spell prereq %s", p); + tui().warnf(Msg.UNKNOWN, "unknown spell prereq %s from %s / %s", + p.toString(), getSources().getKey(), parseState().getSource()); } } else { spells.add(replaceText(String.format("{@filter %s|spells|%s}", @@ -470,7 +511,8 @@ String listPrerequisites(JsonNode variantNode) { .map(x -> { PrereqFields field = fromString(x); if (field == PrereqFields.unknown) { - tui().errorf("Unknown prerequisite %s from %s", x, prerequisite); + tui().warnf(Msg.UNKNOWN, "Unexpected prerequisite %s (from %s / %s)", + x, prerequisite, getSources().getKey()); } return field; }) @@ -483,16 +525,18 @@ String listPrerequisites(JsonNode variantNode) { } JsonNode value = field.getFrom(prerequisite); - // TODO: blocklist? switch (field) { case ability -> values.add(abilityPrereq(value)); case alignment -> values.add(alignmentListToFull(value)); case background -> values.add(backgroundPrereq(value)); - case campaign -> values.add(campaignPrereq(value)); + case campaign -> values.add(replaceConjoinOr(value, " Campaign")); + case culture -> values.add(replaceConjoinOr(value, " Culture")); + case expertise -> values.add(expertisePrereq(value)); case feat -> values.add(featPrereq(value)); - case feature -> values.add(featurePrereq(value)); - case group -> values.add(groupPrereq(value)); - case item -> values.add(itemPrereq(value)); + case feature -> values.add(replaceConjoinOr(value, "")); + case optionalfeature -> values.add(replaceConjoinOr(value, "")); + case group -> values.add(replaceConjoinOr(value, " Group")); + case item -> values.add(replaceConjoinOr(value, "")); case itemProperty -> values.add(itemPropertyPrereq(value)); case itemType -> values.add(itemTypePrereq(value)); case level -> values.add(levelPrereq(value)); @@ -503,6 +547,7 @@ String listPrerequisites(JsonNode variantNode) { case proficiency -> values.add(proficiencyPrereq(value)); case race -> values.add(racePrereq(value)); case spell -> values.add(spellPrereq(value)); + case spellcastingFocus -> values.add(scfPrereq(value)); // --- Boolean values ---- case psionics -> values.addAll(testBoolean(value, replaceText("Psionic Talent feature or {@feat Wild Talent|UA2020PsionicOptionsRevisited} feat"))); @@ -517,7 +562,7 @@ String listPrerequisites(JsonNode variantNode) { // --- Other: Note ---- case note -> note = replaceText(value); default -> { - tui().errorf("Unknown prerequisite %s from %s", field.nodeName(), prerequisite); + tui().debugf(Msg.UNKNOWN, "Unexpected prerequisite %s (from %s)", field.nodeName(), prerequisite); } } } @@ -662,7 +707,7 @@ void findAc(AcHp acHp) { } else if (MonsterFields.special.existsIn(acNode)) { acHp.acText = MonsterFields.special.replaceTextFrom(acNode, this); } else { - tui().errorf("Unknown armor class in monster %s: %s", sources.getKey(), acNode.toPrettyString()); + tui().warnf(Msg.UNKNOWN, "Unknown armor class in monster %s: %s", sources.getKey(), acNode.toPrettyString()); } } @@ -849,8 +894,8 @@ void collectTraits(List traits, JsonNode array) { if (array == null || array.isNull()) { return; } else if (array.isObject()) { - tui().errorf("Unknown %s for %s: %s", array, sources.getKey(), array.toPrettyString()); - throw new IllegalArgumentException("Unknown field: " + getSources()); + tui().warnf(Msg.UNKNOWN, "Unknown %s for %s: %s", array, sources.getKey(), array.toPrettyString()); + return; } for (JsonNode e : iterableElements(array)) { String name = SourceField.name.replaceTextFrom(e, this) @@ -909,8 +954,8 @@ Collection sortedTraits(JsonNode arrayNode) { } return streamOf(arrayNode).sorted((a, b) -> { - Optional aSort = Tools5eFields.sort.getIntFrom(a); - Optional bSort = Tools5eFields.sort.getIntFrom(b); + Optional aSort = Tools5eFields.sort.intFrom(a); + Optional bSort = Tools5eFields.sort.intFrom(b); if (aSort.isPresent() && bSort.isPresent()) { return aSort.get().compareTo(bSort.get()); @@ -998,33 +1043,37 @@ enum VulnerabilityFields implements JsonNodeReader { // weighted (order matters) enum PrereqFields implements JsonNodeReader { - /* 1 */ level, - /* 2 */ pact, - /* 3 */ patron, - /* 4 */ spell, - /* 5 */ race, - /* 6 */ alignment, - /* 7 */ ability, - /* 8 */ proficiency, - /* 9 */ spellcasting, - /* 10 */ spellcasting2020, - /* 11 */ spellcastingFeature, - /* 12 */ spellcastingPrepared, - /* 13 */ psionics, - /* 14 */ feature, - /* 15 */ feat, - /* 16 */ background, - /* 17 */ item, - /* 18 */ itemType, - /* 19 */ itemProperty, - /* 20 */ campaign, - /* 21 */ group, - /* 22 */ other, - /* 23 */ otherSummary, + /* */ level, + /* */ pact, + /* */ patron, + /* */ spell, + /* */ race, + /* */ alignment, + /* */ ability, + /* */ proficiency, + /* */ expertise, + /* */ spellcasting, + /* */ spellcasting2020, + /* */ spellcastingFeature, + /* */ spellcastingPrepared, + /* */ spellcastingFocus, + /* */ psionics, + /* */ feature, + /* */ feat, + /* */ background, + /* */ item, + /* */ itemType, + /* */ itemProperty, + /* */ campaign, + /* */ culture, + /* */ group, + /* */ other, + /* */ otherSummary, choose, // inner field for spells displayEntry, // inner field for display note, // field alongside other fields prerequisite, // prereq field itself + optionalfeature, unknown // catcher for unknown attributes (see #fromString()) } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java index 1e5b24eb1..1ed193018 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java @@ -2,9 +2,7 @@ import java.util.ArrayList; import java.util.Comparator; -import java.util.HashMap; import java.util.List; -import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -230,6 +228,7 @@ private void appendItemProperties(List text, Tags tags) { final JsonNode srdEntries = TtrpgConfig.activeGlobalConfig("srdEntries").get("properties"); for (JsonNode srdEntry : iterableElements(srdEntries)) { + // FIXME: "edition" test for srd entries currentSources = Tools5eSources.findOrTemporary(srdEntry); boolean p2 = parseState().push(srdEntry); try { @@ -243,22 +242,18 @@ private void appendItemProperties(List text, Tags tags) { text.add(""); if (name.equals("General and Weapon Properties")) { - - Map properties = new HashMap<>(); - ArrayNode propertyEntries = srdEntry.withArray("entries"); - for (JsonNode x : iterableElements(propertyEntries)) { - properties.put(toAbbv(x), x); - } - - // All registered item properties. There could be overlaps - mergeEntries(properties, nodes); - - List sorted = new ArrayList<>(properties.values()); - sorted.sort(Comparator.comparing(SourceField.name::getTextOrEmpty)); + List sorted = nodes.stream() + .filter(this::propertyIncluded) + .sorted(Comparator.comparing(SourceField.name::getTextOrEmpty)) + .toList(); for (JsonNode property : sorted) { + String propName = SourceField.name.getTextOrEmpty(property); + if (propName.equalsIgnoreCase("special")) { + continue; + } maybeAddBlankLine(text); - text.add("### " + SourceField.name.getTextOrEmpty(property)); + text.add("### " + propName); if (!property.has("srd")) { text.add(getLabeledSource(property)); } @@ -273,30 +268,13 @@ private void appendItemProperties(List text, Tags tags) { } } - private String toAbbv(JsonNode x) { - return Tools5eFields.abbreviation.getTextOrDefault(x, - SourceField.name.getTextOrEmpty(x)).toLowerCase(); - } - - private void mergeEntries(Map properties, Iterable iterable) { - for (JsonNode x : iterable) { - if (propertyIncluded(x)) { - String abbv = toAbbv(x); - JsonNode old = properties.putIfAbsent(abbv, x); - if (old != null && SourceField.entries.existsIn(x) && !SourceField.entries.valueEquals(old, x)) { - tui().warnf("Duplicate item property with abbreviation %s from %s and %s", - abbv, SourceField.source.getTextOrEmpty(old), SourceField.source.getTextOrEmpty(x)); - tui().debugf("Old: %s", old.toString()); - tui().debugf("New: %s", x.toString()); - } - } - } - } - private boolean propertyIncluded(JsonNode x) { + Tools5eSources sources = Tools5eSources.findSources(x); + if (sources != null) { + return sources.includedByConfig(); + } String source = SourceField.source.getTextOrEmpty(x); - return booleanOrDefault(x, "srd", false) - || (!source.isEmpty() && index.sourceIncluded(source)); + return index.sourceIncluded(source); } enum ComposedTypeFields implements JsonNodeReader { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeck.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeck.java index 534887ec2..8a2e4d333 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeck.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeck.java @@ -35,9 +35,9 @@ protected Tools5eQuteBase buildQuteResource() { for (JsonNode cardRef : DeckFields.cards.iterateArrayFrom(rootNode)) { final String cardKey; if (cardRef.isTextual()) { - cardKey = Tools5eIndexType.card.fromRawKey(cardRef.asText()); + cardKey = Tools5eIndexType.card.fromTagReference(cardRef.asText()); } else if (cardRef.isObject()) { - cardKey = Tools5eIndexType.card.fromRawKey(DeckFields.uid.getTextOrThrow(cardRef)); + cardKey = Tools5eIndexType.card.fromTagReference(DeckFields.uid.getTextOrThrow(cardRef)); } else { cardKey = null; } @@ -66,7 +66,7 @@ public void appendCard(boolean hasCardArt, List cards, JsonNode cardNode) ImageRef face = hasCardArt ? getImage(DeckFields.face, cardNode) : null; String cardText = flattenToString(cardNode); String suit = DeckFields.suit.getTextOrEmpty(cardNode); - Optional value = DeckFields.value.getIntFrom(cardNode); + Optional value = DeckFields.value.intFrom(cardNode); String valueName = DeckFields.valueName.getTextOrEmpty(cardNode); String suitValue = null; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java index 9dc9a8b3c..579ba3729 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java @@ -1,14 +1,22 @@ package dev.ebullient.convert.tools.dnd5e; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.qute.ImageRef; +import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.Tuple; import dev.ebullient.convert.tools.dnd5e.qute.QuteDeity; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -39,14 +47,14 @@ protected Tools5eQuteBase buildQuteResource() { return new QuteDeity(sources, getName(), getSourceText(getSources()), - findAndReplace(rootNode, "altNames"), + DeityField.altNames.replaceTextFromList(rootNode, this), pantheon, dietyAlignment(), - replaceText(getTextOrEmpty(rootNode, "title")), - replaceText(getTextOrEmpty(rootNode, "category")), + replaceText(DeityField.title.getTextOrEmpty(rootNode)), + replaceText(DeityField.category.getTextOrEmpty(rootNode)), String.join(", ", domains), - replaceText(getTextOrEmpty(rootNode, "province")), - replaceText(getTextOrEmpty(rootNode, "symbol")), + replaceText(DeityField.province.getTextOrEmpty(rootNode)), + replaceText(DeityField.symbol.getTextOrEmpty(rootNode)), getSymbolImage(), getText("##"), tags); @@ -73,4 +81,60 @@ ImageRef getSymbolImage() { } return null; } + + public static Iterable findDeitiesToRemove(List allDeities) { + final Comparator byDate = Comparator + .comparing(k -> TtrpgConfig.sourcePublicationDate(k)); + + Function deityKey = n -> { + String reprintAlias = DeityField.reprintAlias.getTextOrNull(n.node); + if (reprintAlias == null) { + String pantheon = DeityField.pantheon.getTextOrEmpty(n.node); + String name = SourceField.name.getTextOrEmpty(n.node); + return name + "-" + pantheon; + } + return reprintAlias; + }; + + // Group by source + Map> deityBySource = allDeities.stream() + .collect(Collectors.groupingBy(t -> SourceField.source.getTextOrEmpty(t.node))); + + // Sort the sources by date, descending + List sourcesByDate = deityBySource.keySet().stream() + .sorted(byDate.reversed()) + .toList(); + + Map keepers = new HashMap<>(); + List keysToRemove = new ArrayList<>(); + // Iterate over groups of deities in order of publication. + // Keep the first deity of each name, add others to the remove pile. + for (String book : sourcesByDate) { + List deities = deityBySource.remove(book); + + if (keepers.isEmpty()) { // most recent bucket. Keep all. + deities.forEach(t -> keepers.put(deityKey.apply(t), t)); + continue; + } + for (Tuple deity : deities) { + String key = deityKey.apply(deity); + if (keepers.containsKey(key)) { + keysToRemove.add(deity.key); + } else { + keepers.put(key, deity); + } + } + } + return keysToRemove; + } + + enum DeityField implements JsonNodeReader { + altNames, + category, + pantheon, + province, + symbol, + title, + reprintAlias + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java index 2ef130448..3f5ba141d 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteFeat; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -16,6 +17,7 @@ protected Tools5eQuteBase buildQuteResource() { Tags tags = new Tags(getSources()); tags.add("feat"); + // TODO: update w/ category, ability, additionalSpells return new QuteFeat(sources, type.decoratedName(rootNode), getSourceText(sources), @@ -24,4 +26,11 @@ protected Tools5eQuteBase buildQuteResource() { getText("##"), tags); } + + enum FeatFields implements JsonNodeReader { + ability, + additionalSpells, + category, + ; + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java index e091ec527..e090c379f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java @@ -1,95 +1,77 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.joinConjunct; +import static dev.ebullient.convert.StringUtil.uppercaseFirst; + import java.util.ArrayList; -import java.util.Collection; +import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; -import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; -import dev.ebullient.convert.tools.dnd5e.ItemProperty.PropertyEnum; -import dev.ebullient.convert.tools.dnd5e.ItemType.ItemEnum; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.Tuple; import dev.ebullient.convert.tools.dnd5e.qute.QuteItem; import dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; public class Json2QuteItem extends Json2QuteCommon { - - final ItemType itemType; + static final List hiddenRarity = List.of("none", "unknown", "unknown (magic)", "varies"); Json2QuteItem(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) { super(index, type, jsonNode); - itemType = getItemType(); } @Override protected Tools5eQuteBase buildQuteResource() { - Set itemProperties = new TreeSet<>(ItemProperty.comparator); // stable order - - Variant rootVariant = createVariant(rootNode, itemProperties); - List fluffImages = new ArrayList<>(); - String text = itemText(itemProperties, fluffImages); - String detail = itemDetail(itemProperties); Tags tags = new Tags(getSources()); - tags.addRaw("item", itemType.getItemTag(itemProperties, tui())); - for (ItemProperty p : itemProperties) { - tags.addRaw("item", p.tagValue()); - } + Variant rootVariant = createVariant(rootNode, tags); List variants = new ArrayList<>(); - if (ItemFields._variants.existsIn(rootNode)) { - for (JsonNode variantNode : iterableElements(ItemFields._variants.getFrom(rootNode))) { - variants.add(createVariant(variantNode, new TreeSet<>(ItemProperty.comparator))); + if (ItemField._variants.existsIn(rootNode)) { + for (JsonNode variantNode : iterableElements(ItemField._variants.getFrom(rootNode))) { + Variant variant = createVariant(variantNode, tags); + variants.add(variant); } } + List fluffImages = new ArrayList<>(); + String text = itemText(fluffImages); + + if (type == Tools5eIndexType.itemGroup) { + List itemLinks = ItemField.items.linkifyListFrom(rootNode, Tools5eIndexType.item, this); + text = (text == null ? "" : text + "\n\n") + + "**Items in this group:**\n\n- " + String.join("\n- ", itemLinks); + } + return new QuteItem(sources, - rootVariant.name, getSourceText(sources), - itemType.getSpecializedType() + (detail.isBlank() ? "" : ", " + detail), - rootVariant.armorClass, - rootVariant.damage, - rootVariant.damage2h, - rootVariant.range, - rootVariant.properties, - rootVariant.strengthRequirement, - rootVariant.stealthPenalty, - rootVariant.cost, - rootVariant.costCp, - rootVariant.weight, - rootVariant.prerequisite, + rootVariant, text, fluffImages, variants, tags); } - private Variant createVariant(JsonNode variantNode, Set itemProperties) { - findProperties(itemProperties); + private Variant createVariant(JsonNode variantNode, Tags tags) { + ItemType itemType = getItemType(variantNode, ItemField.type); + ItemType itemTypeAlt = getItemType(variantNode, ItemField.typeAlt); - String properties = itemProperties.stream() - .filter(PropertyEnum::mundaneProperty) - .map(x -> x.getMarkdownLink(index)) - .collect(Collectors.joining(", ")); + Set itemProperties = new TreeSet<>(ItemProperty.comparator); + findProperties(variantNode, itemProperties, itemType, itemTypeAlt); - Integer strength = variantNode.has("strength") - ? variantNode.get("strength").asInt() - : null; - Double weight = variantNode.has("weight") - ? variantNode.get("weight").asDouble() - : null; - String range = variantNode.has("range") - ? variantNode.get("range").asText() - : null; + Set itemMasteries = new TreeSet<>(ItemMastery.comparator); + findMastery(variantNode, itemMasteries); String damage = null; String damage2h = null; @@ -103,43 +85,225 @@ private Variant createVariant(JsonNode variantNode, Set itemProper } } + String baseItemKey = ItemField.baseItem.getTextOrEmpty(variantNode); + String baseItem = linkify(Tools5eIndexType.item, baseItemKey); + boolean baseItemIncluded = false; + + boolean ammo = ItemField.ammo.booleanOrDefault(variantNode, false); + boolean cursed = ItemField.curse.booleanOrDefault(variantNode, false); + boolean firearm = ItemField.firearm.booleanOrDefault(variantNode, false); + boolean poison = ItemField.poison.booleanOrDefault(variantNode, false); + boolean staff = ItemField.staff.booleanOrDefault(variantNode, false); + boolean tattoo = ItemField.tattoo.booleanOrDefault(variantNode, false); + boolean wondrous = ItemField.wondrous.booleanOrDefault(variantNode, false); + + boolean focus = ItemField.focus.existsIn(variantNode) + || ItemField.scfType.existsIn(variantNode); + + String age = ItemField.age.getTextOrEmpty(variantNode); + String weaponCategory = ItemField.weaponCategory.getTextOrEmpty(variantNode); + + String attunement = attunement(variantNode); + String rarity = ItemField.rarity.getTextOrEmpty(variantNode); + String tier = ItemField.tier.getTextOrEmpty(variantNode); + + String poisonTypes = poison + ? joinConjunct(" or ", ItemField.poisonTypes.getListOfStrings(variantNode, tui())) + : null; + + // -- render.js ------------------------- + // const [typeListText, typeHtml, subTypeHtml] = Renderer.item.getHtmlAndTextTypes(item); + // Building typeDescription and subtypeDescription in a stable order + List typeDescription = new ArrayList<>(); + List subTypeDescription = new ArrayList<>(); + + if (wondrous) { + typeDescription.add("wondrous item" + (tattoo ? " (tattoo)" : "")); + if (tattoo) { + ItemTag.wondrous.add(tags, "tattoo"); + } + } + if (staff) { + typeDescription.add("staff"); + } + if (ammo) { + typeDescription.add("ammunition"); + } + if (isPresent(age)) { + ItemTag.age.add(tags, age); + subTypeDescription.add(age); + } + if (isPresent(weaponCategory)) { + ItemTag.weapon.add(tags, weaponCategory); + baseItemIncluded = isPresent(baseItem); + typeDescription.add("weapon" + + (baseItemIncluded ? " (" + baseItem + ")" : "")); + subTypeDescription.add(weaponCategory + " weapon"); + } + if (staff && (EncodedType.M.typeIn(itemType, itemTypeAlt))) { + // "M" --> Type: Melee weapon + // DMG p140: "Unless a staff's description says otherwise, a staff can be used as a quarterstaff." + subTypeDescription.add("melee weapon"); + } + if (itemType != null) { + tags.addRaw(ItemType.tagForType(itemType, tui())); + processType(itemType, typeDescription, subTypeDescription, baseItem, baseItemIncluded); + subTypeDescription.add(itemType.linkify()); + } + if (itemTypeAlt != null) { + tags.addRaw(ItemType.tagForType(itemTypeAlt, tui())); + processType(itemTypeAlt, typeDescription, subTypeDescription, baseItem, baseItemIncluded); + subTypeDescription.add(itemTypeAlt.linkify()); + } + if (firearm) { + subTypeDescription.add("firearm"); + } + if (poison) { + itemProperties.add(ItemProperty.POISON); + typeDescription.add("poison" + (isPresent(poisonTypes) ? " (" + poisonTypes + ")" : "")); + } + if (cursed) { + itemProperties.add(ItemProperty.CURSED); + typeDescription.add("cursed item"); + } + + // Begin creation of detail string; + // render.js getAttunementAndAttunementCatText(item); + // getTypeRarityAndAttunementText(item); + // getTypeRarityAndAttunementHtml + String detail = join(", ", typeDescription); + if ("other".equals(detail)) { + detail = ""; + } + + if (isPresent(tier)) { + ItemTag.tier.add(tags, tier); + detail += (detail.isBlank() ? "" : ", ") + tier; + } + if (isPresent(rarity)) { + ItemTag.rarity.add(tags, rarity + .replace("very rare", "very-rare") + .replaceAll("[()]", "") // unknown (magic) -> unknown magic + .split(" ")); + if (!hiddenRarity.contains(rarity)) { + detail += (detail.isBlank() ? "" : ", ") + rarity; + } + } + if (isPresent(attunement)) { + ItemTag.attunement.add(tags, + attunement.equals("optional") ? "optional" : "required"); + + detail += (detail.isBlank() ? "" : " ") + + switch (attunement) { + case "required" -> "(requires attunement)"; + case "optional" -> "(attunement optional)"; + default -> "(requires attunement " + attunement + ")"; + }; + } + return new Variant( itemName(variantNode), - armorClass(variantNode), + uppercaseFirst(detail), + uppercaseFirst(join(", ", subTypeDescription)), + baseItem, + itemType == null ? "" : itemType.name(), + itemTypeAlt == null ? "" : itemTypeAlt.name(), + ItemProperty.asLinks(itemProperties), + ItemMastery.asLinks(itemMasteries), + armorClass(variantNode, itemType, itemTypeAlt), + weaponCategory, damage, damage2h, - range, - properties, - strength, - booleanOrDefault(variantNode, "stealth", false), + ItemField.range.getTextOrNull(variantNode), + ItemField.strength.intOrNull(variantNode), + ItemField.stealth.booleanOrDefault(variantNode, false), + listPrerequisites(variantNode), + age, coinValue(variantNode), - ItemFields.value.getIntFrom(variantNode).orElse(null), // cpValue - weight, - listPrerequisites(variantNode)); + ItemField.value.intOrNull(variantNode), + ItemField.weight.doubleOrNull(variantNode), + rarity, + tier, + attunement, + ammo, + firearm, + cursed, + focus, + focus ? focusType(variantNode) : "", + poison, + poisonTypes, + staff, + tattoo, + wondrous); } - private String coinValue(JsonNode variantNode) { - if (variantNode.has("value")) { - return convertCurrency(variantNode.get("value").asInt()); + // render.js _getHtmlAndTextTypes_type + private void processType(ItemType type, + List typeDescription, List subTypeDescription, + String baseItem, boolean baseItemIncluded) { + + String allTypes = typeDescription.toString(); + String fullType = type.lowercaseName(); + + boolean isSubType = (type.group() == ItemTypeGroup.weapon && allTypes.contains("weapon")) + || (type.group() == ItemTypeGroup.armor && allTypes.contains("armor")); + List target = isSubType ? subTypeDescription : typeDescription; + + if (EncodedType.S.typeIn(type, null)) { + target.add("armor (" + linkify(Tools5eIndexType.item, "shield|phb") + ")"); + } else if (!baseItemIncluded && isPresent(baseItem)) { + target.add(fullType + " (" + baseItem + ")"); + } else if (EncodedType.GV.not(type)) { + target.add(fullType); + } + } + + private String attunement(JsonNode variantNode) { + // render.js -- getAttunementAndAttunementCatText + String attunement = ItemField.reqAttune.getTextOrEmpty(variantNode); + if (!isPresent(attunement)) { + attunement = ItemField.reqAttuneAlt.getTextOrEmpty(variantNode); + } + return switch (attunement) { + case "", "false" -> ""; + case "true" -> "required"; + case "optional" -> "optional"; + default -> replaceText(attunement); + }; + } + + private String focusType(JsonNode variantNode) { + List focusTypes = new ArrayList<>(); + JsonNode focusNode = ItemField.focus.getFrom(variantNode); + if (focusNode != null && focusNode.isArray()) { + focusNode.forEach(x -> focusTypes.add(linkifyClass(x.asText()))); } - return null; + String scfType = ItemField.scfType.getTextOrEmpty(variantNode); + return scfType + + (!isPresent(scfType) || focusTypes.isEmpty() ? "" : "; ") + + join(", ", focusTypes); + } + + private String coinValue(JsonNode variantNode) { + Integer value = ItemField.value.intOrNull(variantNode); + return value == null ? null : convertCurrency(value); } String itemName(JsonNode variantNode) { - JsonNode srd = variantNode.get("srd"); Tools5eSources vSources = Tools5eSources.findSources(variantNode); - if (srd != null) { + if (Tools5eSources.isSrd(variantNode)) { if (index().sourceIncluded(vSources.primarySource())) { return vSources.getName(); } - if (srd.isTextual()) { - return srd.asText(); + String srdName = Tools5eSources.srdName(variantNode); + if (srdName != null) { + return srdName; } } return vSources.getName(); } - String itemText(Collection propertyEnums, List imageRef) { + String itemText(List imageRef) { List text = getFluff(Tools5eIndexType.itemFluff, "##", imageRef); if (rootNode.has("entries")) { @@ -158,14 +322,12 @@ String itemText(Collection propertyEnums, List imageRef) } } } - PropertyEnum.findAdditionalProperties(getName(), - itemType, propertyEnums, s -> text.stream().anyMatch(l -> l.matches(s))); return text.isEmpty() ? null : String.join("\n", text); } void insertItemRefText(List text, String input) { - String finalKey = Tools5eIndexType.itemEntry.fromRawKey(input.replaceAll("\\{#itemEntry (.*)}", "$1")); + String finalKey = Tools5eIndexType.itemEntry.fromTagReference(input.replaceAll("\\{#itemEntry (.*)}", "$1")); if (finalKey == null || index.isExcluded(finalKey)) { return; } @@ -190,180 +352,141 @@ void insertItemRefText(List text, String input) { } } - String armorClass(JsonNode variantNode) { - if (!variantNode.has("ac")) { + String armorClass(JsonNode variantNode, ItemType type, ItemType typeAlt) { + String ac = ItemField.ac.getTextOrEmpty(variantNode); + if (!isPresent(ac)) { return null; } - - StringBuilder result = new StringBuilder(); - result.append(variantNode.get("ac").asText()); - // - If you wear light armor, you add your Dexterity modifier to the base number from your armor type to determine your Armor Class. - // - If you wear medium armor, you add your Dexterity modifier, to a maximum of +2, to the base number from your armor type to determine your Armor Class. - // - Heavy armor does not let you add your Dexterity modifier to your Armor Class, but it also does not penalize you if your Dexterity modifier is negative. - if (itemType == ItemEnum.LIGHT_ARMOR) { - result.append(" + DEX"); - } else if (itemType == ItemEnum.MEDIUM_ARMOR) { - result.append(" + DEX (max of +2)"); + // - If you wear light armor, you add your Dexterity modifier to the base number + // from your armor type to determine your Armor Class. + // - If you wear medium armor, you add your Dexterity modifier, to a maximum of +2, + // to the base number from your armor type to determine your Armor Class. + // - Heavy armor does not let you add your Dexterity modifier to your Armor Class, + // but it also does not penalize you if your Dexterity modifier is negative. + if (EncodedType.LA.typeIn(type, typeAlt)) { + ac += " + Dex modifier"; + } else if (EncodedType.MA.typeIn(type, typeAlt)) { + ac += " + Dex modifier (max of +2)"; } - return result.toString(); + return ac; } - ItemType getItemType() { - try { - String type = getTextOrDefault(rootNode, "type", ""); - if (type.isEmpty()) { - if (booleanOrDefault(rootNode, "staff", false)) { - return ItemEnum.STAFF; - } - if (booleanOrDefault(rootNode, "poison", false)) { - return ItemEnum.GEAR; - } - if (booleanOrDefault(rootNode, "wondrous", false) - || booleanOrDefault(rootNode, "sentient", false)) { - return ItemEnum.WONDROUS; - } - if (rootNode.has("rarity")) { - return ItemEnum.WONDROUS; - } - } - return index.findItemType(type, getSources()); - } catch (IllegalArgumentException e) { - tui().errorf(e, "Unable to parse text for item %s", getSources()); - throw e; - } + ItemType getItemType(JsonNode node, ItemField typeField) { + String fragment = typeField.getTextOrEmpty(node); + return index.findItemType(fragment, sources); } - void findProperties(Collection itemProperties) { - JsonNode property = rootNode.get("property"); - if (property != null && property.isArray()) { - for (JsonNode x : iterableElements(property)) { - ItemProperty prop = index.findItemProperty(x.asText(), getSources()); - if (prop != null) { - itemProperties.add(prop); + void findProperties(JsonNode variantNode, + Set itemProperties, ItemType type, ItemType typeAlt) { + + JsonNode propertyList = ItemField.property.getFrom(variantNode); + if (propertyList != null && propertyList.isArray()) { + // List of properties: abbreviation, or abbreviation|source + for (JsonNode x : iterableElements(propertyList)) { + ItemProperty p = index.findItemProperty(x.asText(), sources); + if (p != null) { + itemProperties.add(p); } } } - String category = getTextOrEmpty(rootNode, "weaponCategory"); - if ("martial".equals(category)) { - itemProperties.add(PropertyEnum.MARTIAL); - } - } - /** - * @param itemProperties Item properties -- ensure non-null & modifiable: side-effect, will set magic properties - * @return String containing formatted item text - */ - String itemDetail(Collection itemProperties) { - String tier = getTextOrDefault(rootNode, "tier", ""); - if (!tier.isEmpty()) { - ItemProperty p = index.findItemProperty(tier, getSources()); - if (p != null) { - itemProperties.add(p); - } + String lowerName = SourceField.name.getTextOrEmpty(variantNode).toLowerCase(); + if ((ItemTypeGroup.weapon.hasGroup(type, typeAlt)) && lowerName.contains("silvered")) { + // Add property to link to section on silvered weapons + itemProperties.add(ItemProperty.SILVERED); } - String rarity = rootNode.has("rarity") - ? rootNode.get("rarity").asText() - : ""; - if (!rarity.isEmpty() && !"none".equals(rarity)) { - ItemProperty p = index.findItemProperty(rarity, getSources()); - if (p != null) { - itemProperties.add(p); - } - } - String attunement = getTextOrDefault(rootNode, "reqAttune", ""); - String detail = createDetail(attunement, itemProperties); - return replaceText(detail); } - /** - * @param attunement blank if false, "true" for default string, "optional" if attunement is optional, or some other specific - * string - * @param properties Item properties -- ensure non-null & modifiable: side-effect, will set magic properties - * @return detail string - */ - String createDetail(String attunement, Collection properties) { - StringBuilder replacement = new StringBuilder(); - - PropertyEnum.tierProperties.forEach(p -> { - if (properties.contains(p)) { - if (replacement.length() > 0) { - replacement.append(", "); + void findMastery(JsonNode variantNode, Set itemMasteries) { + JsonNode masteryList = ItemField.mastery.getFrom(variantNode); + if (masteryList != null && masteryList.isArray()) { + for (JsonNode x : iterableElements(masteryList)) { + ItemMastery mastery = index.findItemMastery(x.asText(), sources); + if (mastery != null) { + itemMasteries.add(mastery); } - replacement.append(p.value()); } - }); - PropertyEnum.rarityProperties.forEach(p -> { - if (properties.contains(p)) { - if (replacement.length() > 0) { - replacement.append(", "); - } - replacement.append(p.value()); - } - }); + } + } - properties.stream().filter(PropertyEnum::homebrewProperty).forEach(p -> { - if (replacement.length() > 0) { - replacement.append(", "); - } - replacement.append(p.value()); - }); + enum ItemTag { + age, + armor, + attunement, + gear, + property, + rarity, + shield, + tier, + vehicle, + weapon, + wondrous, + ; + + void add(Tags tags, String... segments) { + tags.addRaw(build(segments)); + } - if (properties.contains(PropertyEnum.POISON)) { - if (replacement.length() > 0) { - replacement.append(", "); - } - replacement.append(PropertyEnum.POISON.value()); + String build(String... segments) { + return Stream.concat(Stream.of("item", name()), Arrays.stream(segments)) + .map(Tui::slugify) + .collect(Collectors.joining("/")); } - if (properties.contains(PropertyEnum.CURSED)) { - if (replacement.length() > 0) { - replacement.append(", "); - } - replacement.append(PropertyEnum.CURSED.value()); - } - - switch (attunement) { - case "": - case "false": - break; - case "true": - properties.add(PropertyEnum.REQ_ATTUNEMENT); - replacement.append(" (requires attunement)"); - break; - case "optional": - properties.add(PropertyEnum.OPT_ATTUNEMENT); - replacement.append(" (attunement optional)"); - break; - default: - properties.add(PropertyEnum.REQ_ATTUNEMENT); - replacement.append(" (requires attunement ") - .append(attunement).append(")"); - break; - } - return replacement.toString(); } - /** Update / replace item with variants (where appropriate) */ - public static List findGroupVariant(Tools5eIndex index, Tools5eIndexType type, - String key, JsonNode itemGroup, Tools5eJsonSourceCopier copier) { - - // Update type & key for the new item - final JsonNode item = copier.copyNode(itemGroup); - String newKey = Tools5eIndexType.item.createKey(item); - index.addAlias(key, newKey); - TtrpgValue.indexInputType.setIn(item, Tools5eIndexType.item.name()); - TtrpgValue.indexKey.setIn(item, newKey); - Tools5eSources.constructSources(item); - return List.of(new Tuple(newKey, item)); + enum EncodedType { + GV, // generic variant + LA, // light armor + MA, // medium armor + M, // melee weapon + S, // shield + ; + + boolean typeIn(ItemType type, ItemType typeAlt) { + return (type != null && name().equalsIgnoreCase(type.abbreviation())) + || (typeAlt != null && name().equalsIgnoreCase(typeAlt.abbreviation())); + } + + boolean not(ItemType type) { + return type == null || !name().equalsIgnoreCase(type.abbreviation()); + } } - enum ItemFields implements JsonNodeReader { + enum ItemField implements JsonNodeReader { _variants, + ac, + age, + ammo, + attunement, baseItem, + curse, + firearm, + focus, hasFluff, hasFluffImages, + items, + mastery, + packContents, + poison, + poisonTypes, property, + range, + rarity, + reqAttune, + reqAttuneAlt, + scfType, + sentient, + staff, + stealth, + strength, + tattoo, + tier, type, + typeAlt, value, + weaponCategory, weight, + wondrous, + ; } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java index 64c35b301..a05ef5bbe 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.NamedText; @@ -32,13 +33,9 @@ public class Json2QuteMonster extends Json2QuteCommon { public static boolean isNpc(JsonNode source) { - if (source.has("isNpc")) { - return source.get("isNpc").asBoolean(false); - } - if (source.has("isNamedCreature")) { - return source.get("isNamedCreature").asBoolean(false); - } - return false; + return MonsterFields.isNpc.booleanOrDefault(source, + MonsterFields.isNamedCreature.booleanOrDefault(source, + false)); } String creatureType; @@ -94,10 +91,11 @@ protected Tools5eQuteBase buildQuteResource() { legendaryGroupText = legendaryGroup(lgNode); legendaryGroupLink = linkifyType(Tools5eIndexType.legendaryGroup, lgKey, - lgSources.getName()); + lgSources.getName(), + sources.getKey()); } } else { - tui().debugf("Legendary group %s source excluded", lgKey); + tui().debugf(Msg.FILTER, "Legendary group source excluded: %s", lgKey); } } @@ -136,7 +134,7 @@ void findCreatureType() { : MonsterFields.creatureType.getFrom(rootNode); if (typeNode == null) { if (type == Tools5eIndexType.monster) { - tui().warn("Empty type for " + getSources()); + tui().warnf("Empty type for %s", getSources()); } return; } @@ -300,8 +298,8 @@ List monsterSpellcasting() { if (array == null || array.isNull()) { return null; } else if (array.isObject()) { - tui().errorf("Unknown spellcasting for %s: %s", sources.getKey(), array.toPrettyString()); - throw new IllegalArgumentException("Unknown spellcasting: " + getSources()); + tui().warnf(Msg.UNKNOWN, "Unknown spellcasting for %s: %s", sources.getKey(), array.toPrettyString()); + return null; } List casting = new ArrayList<>(); @@ -384,7 +382,8 @@ public String getImagePath() { return Tools5eQuteBase.monsterPath(isNpc, creatureType); } - public static List findMonsterVariants(Tools5eIndex index, Tools5eIndexType type, + public static List findMonsterVariants( + Tools5eIndex index, Tools5eIndexType type, String key, JsonNode jsonSource) { if (MonsterFields.summonedBySpellLevel.existsIn(jsonSource)) { return findConjuredMonsterVariants(index, type, key, jsonSource); @@ -423,6 +422,10 @@ public static List findMonsterVariants(Tools5eIndex index, Tools5eIndexTy } return variants; } + + if (key.contains("splugoth the returned") || key.contains("prophetess dran")) { + MonsterFields.isNpc.setIn(jsonSource, true); // Fix. + } return List.of(new Tuple(key, jsonSource)); } @@ -432,39 +435,55 @@ public static List findConjuredMonsterVariants(Tools5eIndex index, Tools5 int startLevel = MonsterFields.summonedBySpellLevel.intOrDefault(jsonSource, 0); String name = SourceField.name.getTextOrEmpty(jsonSource); - String hpString = jsonSource.get("hp").get("special").asText(); - String acString = jsonSource.get("ac").get(0).get("special").asText(); + String hpString = MonsterFields.special.getTextOrEmpty(MonsterFields.hp.getFrom(jsonSource)); + + JsonNode acNode = MonsterFields.ac.getFirstFromArray(jsonSource); + String acString = MonsterFields.special.existsIn(acNode) + ? MonsterFields.special.getTextOrEmpty(acNode) + : acNode.asText(); List variants = new ArrayList<>(); for (int i = startLevel; i < 10; i++) { - if (hpString.contains(" or ")) { + if (hpString.matches(" or \\d+") || + hpString.matches("(,\\s)?\\d+\\s\\([a-zA-Z ]+\\)")) { + String[] parts = {}; + String[] variantGroups = {}; // "50 (Demon only) or 40 (Devil only) or 60 (Yugoloth only) + 15 for each spell // level above 6th" // "30 (Ghostly and Putrid only) or 20 (Skeletal only) + 10 for each spell level // above 3rd" - String[] parts = hpString.split(" \\+ "); - String[] variantGroups = parts[0].split(" or "); + if (hpString.matches(" or \\d+")) { + parts = hpString.split(" \\+ "); + variantGroups = parts[0].split(" or "); + } else if (hpString.matches("(,\\s)?\\d+\\s\\([a-zA-Z0-9_ ]+\\)")) { + // 10 (Medium or smaller), 20 (Large), 40 (Huge) + variantGroups = hpString.split(",\\s"); + } for (String group : variantGroups) { Matcher m = variantPattern.matcher(group); if (m.find()) { String amount = m.group(1); String variant = m.group(2); + String hpText = amount; + if (parts.length > 1) { + hpText.concat(" + " + parts[1]); + } if (variant.contains(" and ")) { for (String v : variant.split(" and ")) { String variantName = String.format("%s (%s, %s-Level Spell)", name, v.replace(" only", ""), JsonSource.levelToString(i)); createVariant(index, variants, jsonSource, type, variantName, i, - amount + " + " + parts[1], acString); + hpText, acString); } } else { String variantName = String.format("%s (%s, %s-Level Spell)", name, variant.replace(" only", ""), JsonSource.levelToString(i)); createVariant(index, variants, jsonSource, type, variantName, i, - amount + " + " + parts[1], acString); + hpText, acString); } } else { - index.tui().errorf("Unknown HP variant from %s: %s", key, hpString); + index.tui().warnf(Msg.UNKNOWN, "Unknown HP variant from %s: %s", key, hpString); } } } else { @@ -482,10 +501,10 @@ static void createVariant(Tools5eIndex index, List variants, ConjuredMonster fixed = new ConjuredMonster(level, variantName, hpString, acString, jsonSource); ObjectNode adjustedSource = (ObjectNode) index.copyNode(jsonSource); - adjustedSource.set("original", adjustedSource.get("name")); - adjustedSource.replace("name", fixed.getName()); - adjustedSource.replace("ac", fixed.getAc()); - adjustedSource.replace("hp", fixed.getHp()); + MonsterFields.original.setIn(adjustedSource, SourceField.name.getFrom(adjustedSource)); + SourceField.name.setIn(adjustedSource, fixed.getName()); + MonsterFields.ac.setIn(adjustedSource, fixed.getAc()); + MonsterFields.hp.setIn(adjustedSource, fixed.getHp()); String newKey = type.createKey(adjustedSource); variants.add(new Tuple(newKey, adjustedSource)); @@ -556,6 +575,9 @@ public MonsterAC(int level, String acString) { } this.ac = value; this.from = armor == null ? null : new String[] { armor }; + } else if (acString.matches("\\d+")) { + this.ac = Integer.parseInt(acString); + this.from = new String[] {}; } else { throw new IllegalArgumentException("Unknown AC pattern: " + acString); } @@ -566,7 +588,7 @@ public MonsterAC(int level, String acString) { @JsonInclude(JsonInclude.Include.NON_NULL) public static class MonsterHp { static final Pattern hpPattern = Pattern - .compile("(\\d+) \\+ (\\d+) for each spell level above (\\d)(nd|rd|th|st) ?\\(?(.*?)?\\)?"); + .compile("(\\d+) \\+ (\\d+) for each spell level above (\\d) ?\\(?(.*?)?\\)?"); public final String special; public final String original; @@ -602,8 +624,14 @@ public MonsterHp(int level, String hpString, JsonNode jsonSource) { int scale = level - Integer.parseInt(m.group(3)); value += Integer.parseInt(m.group(2)) * scale; } else { - throw new IllegalArgumentException("Unknown HP pattern: " + hpString); + // TODO: 20 (Air only) or 30 (Land and Water only) + 5 for each spell level above 2 + // nothing we can do right now } + } else if (hpString.matches("^\\d+$")) { + value = Integer.parseInt(hpString); + } else if (hpString.matches("(\\d+\\s\\([a-zA-Z ]+\\),?\\s?)+")) { + // 10 (Medium or smaller), 20 (Large), 40 (Huge) + // nothing we can do right now } else { throw new IllegalArgumentException("Unknown HP pattern: " + hpString); } @@ -626,12 +654,12 @@ enum MonsterFields implements JsonNodeReader { headerEntries, hp, isNamedCreature, + isNpc, legendaryGroup, lower, original, save, senses, - shortName, skill, slots, special, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java index e50582eae..bd88a54eb 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.tools.Tags; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.OptionalFeatureType; +import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote; public class Json2QuteOptionalFeatureType extends Json2QuteCommon { @@ -28,25 +28,24 @@ public String getName() { @Override protected Tools5eQuteNote buildQuteNote() { - List nodes = optionalFeatures.nodes; + List featureKeys = optionalFeatures.features; + List nodes = featureKeys.stream() + .map(index::getAliasOrDefault) + .map(index::getNode) + .filter(x -> x != null) + .sorted(Comparator.comparing(SourceField.name::getTextOrEmpty)) + .toList(); + String key = super.sources.getKey(); if (nodes.isEmpty() || index().isExcluded(key)) { return null; } - nodes.sort(Comparator.comparing(SourceField.name::getTextOrEmpty)); Tags tags = new Tags(getSources()); List text = new ArrayList<>(); for (JsonNode entry : nodes) { - Tools5eSources featureSource = Tools5eSources.findSources(entry); - if (index().isExcluded(featureSource.getKey())) { - continue; - } - text.add("- " + linkify(Tools5eIndexType.optionalfeature, - featureSource.getName() - + "|" + featureSource.primarySource() - + "|" + decoratedUaName(featureSource.getName(), featureSource))); + text.add("- " + linkify(Tools5eIndexType.optfeature, Tools5eIndexType.optfeature.toTagReference(entry))); } if (text.isEmpty()) { return null; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java index d23a89d0e..150e8b165 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java @@ -10,7 +10,7 @@ import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.PsionicType.PsionicTypeEnum; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.HomebrewMetaTypes; +import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewMetaTypes; import dev.ebullient.convert.tools.dnd5e.qute.QutePsionic; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -82,8 +82,8 @@ String getModeName(JsonNode mode) { List amendWith = new ArrayList<>(); if (PsionicFields.cost.existsIn(mode)) { JsonNode cost = PsionicFields.cost.getFrom(mode); - int max = PsionicFields.max.getIntOrThrow(cost); - int min = PsionicFields.min.getIntOrThrow(cost); + int max = PsionicFields.max.intOrThrow(cost); + int min = PsionicFields.min.intOrThrow(cost); if (max == min) { amendWith.add(max + " psi"); } else { @@ -93,7 +93,7 @@ String getModeName(JsonNode mode) { if (PsionicFields.concentration.existsIn(mode)) { JsonNode concentration = PsionicFields.concentration.getFrom(mode); amendWith.add(String.format("conc., %s %s", - PsionicFields.duration.getIntOrThrow(concentration), + PsionicFields.duration.intOrThrow(concentration), PsionicFields.unit.getTextOrThrow(concentration))); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java index e6820cd50..83ac498c4 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java @@ -7,7 +7,6 @@ import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; -import dev.ebullient.convert.tools.dnd5e.ItemProperty.PropertyEnum; import dev.ebullient.convert.tools.dnd5e.qute.QuteReward; public class Json2QuteReward extends Json2QuteCommon { @@ -30,9 +29,9 @@ protected QuteReward buildQuteResource() { if (type != null) { details.add(type); } - PropertyEnum rarity = PropertyEnum.fromValue(RewardField.rarity.getTextOrNull(rootNode)); + String rarity = RewardField.rarity.getTextOrNull(rootNode); if (rarity != null) { - details.add(rarity.toString()); + details.add(rarity); } String detail = String.join(", ", details); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java index e7d1f4efd..97dc5f119 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java @@ -111,7 +111,7 @@ String spellDuration() { if (durations.size() > 1) { JsonNode ends = durations.get(1); result.append(", "); - String type = getTextOrEmpty(ends, "type"); + String type = SpellFields.type.getTextOrEmpty(ends); if ("timed".equals(type)) { result.append(" up to "); } @@ -121,7 +121,7 @@ String spellDuration() { } void addDuration(JsonNode element, StringBuilder result) { - String type = getTextOrEmpty(element, "type"); + String type = SpellFields.type.getTextOrEmpty(element); switch (type) { case "instant" -> result.append("Instantaneous"); case "permanent" -> { @@ -185,10 +185,11 @@ String spellCastingTime() { SpellFields.unit.getTextOrEmpty(time)); } + // FIXME: spell lists are pretty broken. Set indexedSpellClasses(Tags tags) { Collection list = index().classesForSpell(this.sources.getKey()); if (list == null) { - tui().debugf("No classes found for %s", this.sources.getKey()); + // tui().debugf("No classes found for %s", this.sources.getKey()); return new TreeSet<>(); } @@ -245,6 +246,7 @@ Set spellClasses(SpellSchool school, Tags tags) { } }); if (classes.contains("Wizard")) { + // FIXME. Spell schools are busted (PHB/XPHB for these two) if (school == SpellSchool.SchoolEnum.Abjuration || school == SpellSchool.SchoolEnum.Evocation) { String finalKey = Tools5eIndexType.getSubclassKey("Fighter", "PHB", "Eldritch Knight", "PHB"); if (index().isIncluded(finalKey)) { @@ -277,7 +279,7 @@ private String getSubclass(Tags tags, String className, String classSource, Stri String.format("%s (%s)", className, subclassName), subclassKey, Tools5eIndexType.classtype.getRelativePath(), - Tools5eQuteBase.getSubclassResource(subclassName, className, subclassSource)); + Tools5eQuteBase.getSubclassResource(subclassName, className, classSource, subclassSource)); } enum SpellFields implements JsonNodeReader { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteVehicle.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteVehicle.java index 52f25b7e4..e149cf35a 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteVehicle.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteVehicle.java @@ -100,11 +100,11 @@ private Collection getActions() { private ShipAcHp getAcHp(JsonNode node) { String cost = getCost(node); return new ShipAcHp(vehicleType.name(), - VehicleFields.ac.getIntFrom(node).orElse(null), + VehicleFields.ac.intOrNull(node), VehicleFields.acFrom.getTextOrNull(node), - VehicleFields.hp.getIntFrom(node).orElse(null), + VehicleFields.hp.intOrNull(node), VehicleFields.hpNote.getTextOrNull(node), - VehicleFields.dt.getIntFrom(node).orElse(null), + VehicleFields.dt.intOrNull(node), null, cost == null ? null : cost.toString()); } @@ -122,7 +122,7 @@ private String getCost(JsonNode node) { } private String convertCost(JsonNode node) { - Optional costCp = VehicleFields.cost.getIntFrom(node); + Optional costCp = VehicleFields.cost.intFrom(node); String note = VehicleFields.note.getTextOrNull(node); if (costCp.isPresent() || note != null) { return costCp.map(x -> convertCurrency(x)).orElse("\u23E4") + (note == null ? "" : " (" + note + ")"); @@ -137,7 +137,7 @@ private ShipCrewCargoPace shipCrewCargoPace() { String keelBeam = null; if (vehicleType == VehicleType.SHIP) { - shipPace = VehicleFields.pace.getIntFrom(rootNode).orElse(null); + shipPace = VehicleFields.pace.intOrNull(rootNode); } else if (vehicleType == VehicleType.SPELLJAMMER) { JsonNode speedNode = VehicleFields.speed.getFrom(rootNode); JsonNode paceNode = VehicleFields.pace.getFrom(rootNode); @@ -164,15 +164,15 @@ private ShipCrewCargoPace shipCrewCargoPace() { keelBeam = VehicleFields.dimensions.joinAndReplace(rootNode, this, " by "); } } else if (vehicleType == VehicleType.INFWAR) { - int dexMod = VehicleFields.dexMod.getIntFrom(rootNode).orElse(0); + int dexMod = VehicleFields.dexMod.intOrDefault(rootNode, 0); JsonNode hpNode = VehicleFields.hp.getFrom(rootNode); shipAcHp = new ShipAcHp(vehicleType.name(), dexMod == 0 ? 19 : 19 + dexMod, dexMod == 0 ? "" : "19 while motionless", - VehicleFields.hp.getIntFrom(hpNode).orElse(null), + VehicleFields.hp.intOrNull(hpNode), null, - VehicleFields.dt.getIntFrom(hpNode).orElse(null), - VehicleFields.mt.getIntFrom(hpNode).orElse(null), + VehicleFields.dt.intOrNull(hpNode), + VehicleFields.mt.intOrNull(hpNode), null); speedPace = speed(Tools5eFields.speed.getFrom(rootNode)); } else if (vehicleType == VehicleType.CREATURE || vehicleType == VehicleType.OBJECT) { @@ -261,7 +261,7 @@ void getShipSections(List sections) { } for (JsonNode node : VehicleFields.weapon.iterateArrayFrom(rootNode)) { String name = SourceField.name.replaceTextFrom(node, this); - Optional count = VehicleFields.count.getIntFrom(node); + Optional count = VehicleFields.count.intFrom(node); if (count.isPresent()) { name += " (" + count.get() + ")"; } @@ -299,8 +299,8 @@ void addMovementSpeed(List speed, JsonNode speedNode) { private void getSpelljammerSections(List sections) { for (JsonNode node : VehicleFields.weapon.iterateArrayFrom(rootNode)) { String name = SourceField.name.replaceTextFrom(node, this); - Optional count = VehicleFields.count.getIntFrom(node); - Optional crew = VehicleFields.crew.getIntFrom(node); + Optional count = VehicleFields.count.intFrom(node); + Optional crew = VehicleFields.crew.intFrom(node); boolean isMultiple = count.isPresent() && count.get() > 1; if (isMultiple) { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java index 961378421..a6c57b7c9 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.joinConjunct; import static java.util.Map.entry; import java.util.ArrayList; @@ -8,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -18,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.SourceAndPage; @@ -33,6 +36,7 @@ public interface JsonSource extends JsonTextReplacement { int CR_UNKNOWN = 100001; int CR_CUSTOM = 100000; + Pattern leadingNumber = Pattern.compile("(\\d+)(.*)"); default String getName() { return getSources() == null ? null : getSources().getName(); @@ -131,7 +135,7 @@ default void appendToText(List text, JsonNode node, String heading) { } else if (node.isObject()) { appendObjectToText(text, node, heading); } else { - tui().errorf("Unknown entry type in %s: %s", getSources(), node.toPrettyString()); + tui().debugf(Msg.UNKNOWN, "Unknown entry type in %s: %s", getSources(), node.toPrettyString()); } } finally { parseState().pop(pushed); // restore state @@ -199,8 +203,8 @@ default void appendObjectToText(List text, JsonNode node, String heading case tableGroup -> appendTableGroup(text, node, heading); case variant -> appendCallout("danger", "Variant", text, node); case variantInner, variantSub -> appendCallout("example", "Variant", text, node); - default -> tui().errorf("Unknown entry object type %s from %s: %s", type, getSources(), - node.toPrettyString()); + default -> tui().debugf(Msg.UNKNOWN, "Unknown entry object type %s from %s: %s", + type, getSources(), node.toPrettyString()); } // any entry/entries handled by type... return; @@ -228,27 +232,34 @@ default void appendObjectToText(List text, JsonNode node, String heading } default void appendAbility(AppendTypeValue type, List text, JsonNode entry) { + List abilities = Tools5eFields.attributes.streamFrom(entry) + .map(this::asAbilityEnum) + .toList(); + String ability = joinConjunct(" or ", abilities); + if (type == AppendTypeValue.abilityDc) { - text.add(String.format( - "**Spell save DC**: 8 + your proficiency bonus + your %s modifier", - asAbilityEnum(entry.withArray("attributes").get(0)))); + text.add(spanWrap("abilityDc", + "**Spell save DC**: 8 + your proficiency bonus + your %s modifier" + .formatted(ability))); } else if (type == AppendTypeValue.abilityAttackMod) { - text.add(String.format( - "**Spell attack modifier**: your proficiency bonus + your %s modifier", - asAbilityEnum(entry.withArray("attributes").get(0)))); + text.add(spanWrap("abilityAttackMod", + "**Spell attack modifier**: your proficiency bonus + your %s modifier" + .formatted(ability))); } else { // abilityGeneric - List abilities = new ArrayList<>(); - iterableElements(Tools5eFields.attributes.getFrom(entry)) - .forEach(x -> abilities.add(asAbilityEnum(x))); - List inner = new ArrayList<>(); - SourceField.name.appendUnlessEmptyFrom(entry, inner, this); - Tools5eFields.text.appendUnlessEmptyFrom(entry, inner); - inner.add(String.join(", ", abilities)); - inner.add("modifier"); + String name = SourceField.name.replaceTextFrom(entry, this); + if (isPresent(name)) { + inner.add("**" + name + ".**"); + } + if (Tools5eFields.text.existsIn(entry)) { + Tools5eFields.text.replaceTextFrom(entry, this); + } + if (!abilities.isEmpty()) { + inner.add(ability + " modifier"); + } maybeAddBlankLine(text); - text.add(String.join(" ", inner)); + text.add(spanWrap("abilityGeneric", String.join(" ", inner))); maybeAddBlankLine(text); } } @@ -259,10 +270,11 @@ default void appendAttack(List text, JsonNode entry) { String atkString = flattenToString(AttackFields.attackEntries.getFrom(entry), " "); String hitString = flattenToString(AttackFields.hitEntries.getFrom(entry), " "); - text.add(String.format("%s*%s:* %s *Hit:* %s", - isPresent(name) ? "***" + name + ".*** " : "", - "MW".equals(attackType) ? "Melee Weapon Attack" : "Ranged Weapon Attack", - atkString, hitString)); + text.add(spanWrap("attack", + "%s*%s:* %s *Hit:* %s".formatted( + isPresent(name) ? "***" + name + ".*** " : "", + "MW".equals(attackType) ? "Melee Weapon Attack" : "Ranged Weapon Attack", + atkString, hitString))); } default void appendCallout(String callout, String title, List text, JsonNode entry) { @@ -402,19 +414,19 @@ default void appendOptionalFeature(List text, JsonNode entry, String hea default void appendOptionalFeatureRef(List text, JsonNode entry) { String lookup = Tools5eFields.optionalfeature.getTextOrNull(entry); if (lookup == null) { - tui().warnf("Optional Feature not found in %s", entry); + tui().warnf(Msg.UNRESOLVED, "Optional Feature not found in %s", entry); return; // skipped or not found } String[] parts = lookup.split("\\|"); String nodeSource = parts.length > 1 && !parts[1].isBlank() ? parts[1] - : Tools5eIndexType.optionalfeature.defaultSourceString(); - String key = Tools5eIndexType.optionalfeature.createKey(lookup, nodeSource); + : Tools5eIndexType.optfeature.defaultSourceString(); + String key = Tools5eIndexType.optfeature.createKey(lookup, nodeSource); if (index().isIncluded(key)) { if (parseState().inList()) { - text.add(linkify(Tools5eIndexType.optionalfeature, lookup)); + text.add(linkify(Tools5eIndexType.optfeature, lookup)); } else { tui().errorf("TODO refOptionalfeature %s -> %s", - lookup, Tools5eIndexType.optionalfeature.fromRawKey(lookup)); + lookup, Tools5eIndexType.optfeature.fromTagReference(lookup)); } } } @@ -504,9 +516,9 @@ default void appendQuote(List text, JsonNode entry) { List quoteText = new ArrayList<>(); if (entry.has("by")) { String by = replaceText(Tools5eFields.by.getTextOrEmpty(entry)); - quoteText.add("[!quote]- A quote from " + by + " "); + quoteText.add("[!quote] A quote from " + by + " "); } else { - quoteText.add("[!quote]- "); + quoteText.add("[!quote] "); } appendToText(quoteText, SourceField.entries.getFrom(entry), null); @@ -520,7 +532,7 @@ default void appendStatblock(List text, JsonNode entry, String heading) String tagPropText = Tools5eFields.tag.getTextOrDefault(entry, Tools5eFields.prop.getTextOrEmpty(entry)); Tools5eIndexType type = Tools5eIndexType.fromText(tagPropText); if (type == null) { - tui().warnf("🚧 Unrecognized statblock type in %s", entry); + tui().debugf(Msg.SOMEDAY, "Unrecognized statblock type in %s", entry); return; } embedReference(text, entry, type, heading); @@ -530,7 +542,7 @@ default void appendStatblockInline(List text, JsonNode entry, String hea // For inline statblocks, we start with the dataType Tools5eIndexType type = Tools5eIndexType.fromText(Tools5eFields.dataType.getTextOrEmpty(entry)); if (type == null) { - tui().warnf("🚧 Unrecognized statblock dataType in %s", entry); + tui().debugf(Msg.SOMEDAY, "Unrecognized statblock dataType in %s", entry); return; } JsonNode data = Tools5eFields.data.getFrom(entry); @@ -565,7 +577,7 @@ default void appendStatblockInline(List text, JsonNode entry, String hea embedReference(text, data, type, heading); // embed note that will be present in the final output return; } else if (existingNode == null) { - Tools5eSources.constructSources(data); + Tools5eSources.constructSources(finalKey, data); } Tools5eQuteBase qs = null; @@ -620,7 +632,7 @@ default void embedReference(List text, JsonNode entry, Tools5eIndexType if (type == Tools5eIndexType.charoption) { // charoption is a special case, it is not a linkable type. - tui().warnf("🚧 charoption is not yet an embeddable type: %s", entry); + tui().debugf(Msg.SOMEDAY, "charoption is not yet an embeddable type: %s", entry); return; } @@ -636,7 +648,7 @@ default void embedReference(List text, JsonNode entry, Tools5eIndexType } } else { text.add(link); - tui().warnf("statblock entry did not resolve to a markdown link: %s", entry); + tui().warnf(Msg.UNRESOLVED, "unable to find statblock target: %s", entry); } } @@ -1026,6 +1038,12 @@ default String getSize(JsonNode value) { return "Unknown"; } + default String spanWrap(String cssClass, String text) { + return parseState().inTrait() + ? text + : "%s".formatted(cssClass, text); + } + default String sizeToString(String size) { return switch (size) { case "F" -> "Fine"; @@ -1079,6 +1097,17 @@ default String asModifier(int value) { return (value >= 0 ? "+" : "") + value; } + default String articleFor(String value) { + value = leadingNumber.matcher(value).replaceAll((m) -> { + return numberToText(Integer.parseInt(m.group(1))) + m.group(2); + }); + + return switch (value.toLowerCase().charAt(0)) { + case 'a', 'e', 'i', 'o', 'u' -> "an"; + default -> "a"; + }; + } + default String numberToText(int value) { int abs = Math.abs(value); if (abs >= 100) { @@ -1232,12 +1261,16 @@ enum Tools5eFields implements JsonNodeReader { by, className, classSource, + classFeatureKeys, // ELH: keys for related class/subclass features condition, // speed, ac count, cr, data, // statblock, statblockInline dataType, // statblockInline + deck, + edition, entriesTemplate, + familiar, featureType, fluff, group, @@ -1254,6 +1287,7 @@ enum Tools5eFields implements JsonNodeReader { prop, // statblock race, regionalEffects, // legendary group + shortName, size, sort, // monsters, vehicles (sorted traits) speed, @@ -1263,11 +1297,11 @@ enum Tools5eFields implements JsonNodeReader { subrace, tables, // for optfeature types tag, // statblock + template, text, tokenHref, tokenUrl, traitTags, - typeLookup, visible, xp, } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index 72818db37..0347bd08b 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.isPresent; + import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -15,10 +17,12 @@ import dev.ebullient.convert.config.CompendiumConfig; import dev.ebullient.convert.config.CompendiumConfig.DiceRoller; import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonTextConverter; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.OptionalFeatureType; +import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; +import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.Tools5eIndexType.IndexFields; import dev.ebullient.convert.tools.dnd5e.qute.AbilityScores; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -26,25 +30,27 @@ public interface JsonTextReplacement extends JsonTextConverter { static final Pattern FRACTIONAL = Pattern.compile("^(\\d+)?([⅛¼⅜½⅝¾⅞⅓⅔⅙⅚])?$"); static final Pattern linkifyPattern = Pattern.compile("\\{@(" - + "|action|background|card|class|condition|creature|deck|deity|disease" - + "|feat|hazard|item|itemMastery|legroup|object|psionic|race|reward" + + "|action|background|card|class|condition|creature|deck|deity|disease|facility" + + "|feat|hazard|item|itemMastery|itemProperty|itemType|legroup|object|psionic|race|reward" + "|sense|skill|spell|status|table|variantrule|vehicle" + "|optfeature|classFeature|subclassFeature|trap) ([^}]+)}"); - static final Pattern dicePattern = Pattern.compile("\\{@(dice|damage) ([^{}]+)}"); static final Pattern chancePattern = Pattern.compile("\\{@chance ([^}]+)}"); static final Pattern fontPattern = Pattern.compile("\\{@font ([^}]+)}"); static final Pattern homebrewPattern = Pattern.compile("\\{@homebrew ([^}]+)}"); static final Pattern linkTo5eImgRepo = Pattern.compile("\\{@5etoolsImg ([^}]+)}"); static final Pattern quickRefPattern = Pattern.compile("\\{@quickref ([^}]+)}"); - static final Pattern notePattern = Pattern.compile("\\{@note (\\*|Note:)?\\s?([^}]+)}"); + static final Pattern notePattern = Pattern.compile("\\{@(note|tip) ([^}]+)}"); static final Pattern footnotePattern = Pattern.compile("\\{@footnote ([^}]+)}"); static final Pattern abilitySavePattern = Pattern.compile("\\{@(ability|savingThrow) ([^}]+)}"); // {@ability str 20} + static final Pattern savingThrowPattern = Pattern.compile("\\{@actSave ([^}]+)}"); + static final Pattern attackPattern = Pattern.compile("\\{@atkr? ([^}]+)}"); static final Pattern skillCheckPattern = Pattern.compile("\\{@skillCheck ([^}]+)}"); // {@skillCheck animal_handling 5} static final Pattern optionalFeaturesFilter = Pattern.compile("\\{@filter ([^|}]+)\\|optionalfeatures\\|([^}]*)}"); static final Pattern featureTypePattern = Pattern.compile("(?:[Ff]eature )?[Tt]ype=([^|}]+)"); static final Pattern featureSourcePattern = Pattern.compile("source=([^|}]+)"); static final Pattern superscriptCitationPattern = Pattern.compile("\\{@(sup|cite) ([^}]+)}"); static final Pattern promptPattern = Pattern.compile("#\\$prompt_number(?::(.*?))?\\$#"); + static final String subclassFeatureMask = "subclassfeature\\|(.*)\\|.*?\\|.*?\\|.*?\\|.*?\\|(\\d+)\\|.*"; Tools5eIndex index(); @@ -79,7 +85,8 @@ default List findAndReplace(JsonNode jsonSource, String field, Function< return List.of(replaceText(node.asText())); } else if (node.isObject()) { throw new IllegalArgumentException( - String.format("Unexpected object node (expected array): %s (referenced from %s)", node, + "Unexpected object node (expected array): %s (referenced from %s)".formatted( + node, getSources())); } return streamOf(jsonSource.withArray(field)) @@ -97,7 +104,8 @@ default String joinAndReplace(JsonNode jsonSource, String field) { return node.asText(); } else if (node.isObject()) { throw new IllegalArgumentException( - String.format("Unexpected object node (expected array): %s (referenced from %s)", node, + "Unexpected object node (expected array): %s (referenced from %s)".formatted( + node, getSources())); } return joinAndReplace((ArrayNode) node); @@ -157,7 +165,7 @@ default String replacePromptStrings(String s) { } } prompts.sort(String::compareToIgnoreCase); - return String.format("[%s]", + return "[%s]".formatted( prompts.isEmpty() ? "" : " title='" + String.join(", ", prompts) + "'", title); }); @@ -166,31 +174,27 @@ default String replacePromptStrings(String s) { default String _replaceTokenText(String input, boolean nested) { String result = input; + // render.js this._renderString_renderTag try { result = replacePromptStrings(result); - // {@hit ..} and {@d20 ..} + // {@dice .. }, {@damage ..}{@hit ..}, {@d20 ..}, {@initiative ...}, {@scaledice..}, {@scaledamage..} result = replaceWithDiceRoller(result); - // Dice roller tags; {@dice 1d2-2+2d3+5} or {@damage ..} for regular dice rolls - // - {@dice 1d6;2d6} for multiple options; - // - {@dice 1d6 + #$prompt_number:min=1,title=Enter a Number!,default=123$#} for input prompts - // - {@dice 1d20+2|display text} and {@dice 1d20+2|display text|rolled by name} - result = dicePattern.matcher(result).replaceAll((match) -> { - String[] parts = match.group(2).split("\\|"); - if (parts.length > 1) { - return parts[1]; - } - return formatDice(parts[0]); - }); - result = chancePattern.matcher(result).replaceAll((match) -> { + // "Chance tags; similar to dice roller tags, but output success/failure. + // {@chance 50}; {@chance 50|display text}; {@chance 50|display text|rolled by name}; + // {@chance 50|display text|rolled by name|on success text}; + // {@chance 50|display text|rolled by name|on success text|on failure text}.", String[] parts = match.group(1).split("\\|"); - return parts[0] + "% chance"; + return parts.length > 1 + ? parts[1] + : parts[0] + " percent"; }); result = abilitySavePattern.matcher(result).replaceAll(this::replaceSkillOrAbility); result = skillCheckPattern.matcher(result).replaceAll(this::replaceSkillCheck); + result = savingThrowPattern.matcher(result).replaceAll(this::replaceSavingThrow); result = superscriptCitationPattern.matcher(result).replaceAll((match) -> { // {@sup {@cite Casting Times|FleeMortals|A}} @@ -243,7 +247,7 @@ default String _replaceTokenText(String input, boolean nested) { String imgRepo = TtrpgConfig.getConstant(TtrpgConfig.DEFAULT_IMG_ROOT); String url = ImageRef.Builder.fixUrl(imgRepo + (imgRepo.endsWith("/") ? "" : "/") + parts[1]); - return String.format("[%s](%s)", parts[0], url); + return "[%s](%s)".formatted(parts[0], url); }); result = linkifyPattern.matcher(result) @@ -263,20 +267,49 @@ default String _replaceTokenText(String input, boolean nested) { result = linkifyPattern.matcher(result) .replaceAll(this::linkify); - result = fontPattern.matcher(result) - .replaceAll((match) -> { - String[] parts = match.group(1).split("\\|"); - String fontFamily = Tools5eSources.getFontReference(parts[1]); - if (fontFamily != null) { - return String.format("%s", - fontFamily, parts[0]); - } - return parts[0]; - }); + result = fontPattern.matcher(result).replaceAll((match) -> { + String[] parts = match.group(1).split("\\|"); + String fontFamily = Tools5eSources.getFontReference(parts[1]); + if (fontFamily != null) { + return "%s".formatted( + fontFamily, parts[0]); + } + return parts[0]; + }); + + result = attackPattern.matcher(result).replaceAll((match) -> { + List type = new ArrayList<>(); + String method = ""; + // render.js Renderer.attackTagToFull + // const ptType = tags.includes("m") ? "Melee " : tags.includes("r") ? "Ranged " : tags.includes("g") ? "Magical " : tags.includes("a") ? "Area " : ""; + // const ptMethod = tags.includes("w") ? "Weapon " : tags.includes("s") ? "Spell " : tags.includes("p") ? "Power " : ""; + if (match.group(1).contains("m")) { + type.add("Melee "); + } + if (match.group(1).contains("r")) { + type.add("Ranged "); + } + if (match.group(1).contains("g")) { + type.add("Magical "); + } + if (match.group(1).contains("a")) { + type.add("Area "); + } + + if (match.group(1).contains("w")) { + method = "Weapon "; + } else if (match.group(1).contains("s")) { + method = "Spell "; + } else if (match.group(1).contains("p")) { + method = "Power "; + } + return "*%s%sAttack:*".formatted(String.join("or ", type), method); + }); try { result = result - .replace("{@hitYourSpellAttack}", "the summoner's spell attack modifier") + .replaceAll("\\{@hitYourSpellAttack ([^}]+)}", "$1") + .replaceAll("\\{@hitYourSpellAttack}", "the summoner's spell attack modifier") // "Internal links: {@5etools This Is Your Life|lifegen.html}", // "External links: {@link https://discord.gg/5etools} or {@link Discord|https://discord.gg/5etools}" .replaceAll("\\{@link ([^}|]+)\\|([^}]+)}", "$1 ($2)") // this must come first @@ -290,7 +323,6 @@ default String _replaceTokenText(String input, boolean nested) { .replaceAll("\\{@recharge}", "(Recharge 6)") .replaceAll("\\{@coinflip ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@coinflip}", "flip a coin") - .replaceAll("\\{@(scaledice|scaledamage) [^|]+\\|[^|]+\\|([^|}]+)[^}]*}", "`$2`") .replaceAll("\\{@filter ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@boon ([^|}]+)\\|([^|}]+)\\|[^|}]*}", "$2") .replaceAll("\\{@boon ([^|}]+)\\|[^}]*}", "$1") @@ -306,24 +338,13 @@ default String _replaceTokenText(String input, boolean nested) { .replaceAll("\\{@cult ([^|}]+)}", "$1") .replaceAll("\\{@language ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@book ([^}|]+)\\|?[^}]*}", "\"$1\"") - .replaceAll("\\{@h}", "*Hit:* ") - .replaceAll("\\{@m}", "*Miss:* ") - .replaceAll("\\{@atk a}", "*Area Attack:*") - .replaceAll("\\{@atk aw}", "*Area Weapon Attack:*") - .replaceAll("\\{@atk g}", "*Magical Attack:*") - .replaceAll("\\{@atk m}", "*Melee Attack:*") - .replaceAll("\\{@atk r}", "*Ranged Attack:*") - .replaceAll("\\{@atk mw}", "*Melee Weapon Attack:*") - .replaceAll("\\{@atk rw}", "*Ranged Weapon Attack:*") - .replaceAll("\\{@atk m, ?r}", "*Melee or Ranged Attack:*") - .replaceAll("\\{@atk mw\\|rw}", "*Melee or Ranged Weapon Attack:*") - .replaceAll("\\{@atk mw, ?rw}", "*Melee or Ranged Weapon Attack:*") - .replaceAll("\\{@atk mp}", "*Melee Power Attack:*") - .replaceAll("\\{@atk rp}", "*Ranged Power Attack:*") - .replaceAll("\\{@atk mp, ?rp}", "*Melee or Ranged Power Attack:*") - .replaceAll("\\{@atk ms}", "*Melee Spell Attack:*") - .replaceAll("\\{@atk rs}", "*Ranged Spell Attack:*") - .replaceAll("\\{@atk ms, ?rs}", "*Melee or Ranged Spell Attack:*") + .replaceAll("\\{@h}", "*Hit:* ") // render.js Renderer.tag + .replaceAll("\\{@m}", "*Miss:* ") // render.js Renderer.tag + .replaceAll("\\{@actSaveFail}", "*Failure:*") // render.js Renderer.tag + .replaceAll("\\{@actSaveSuccess}", "*Success:*") // render.js Renderer.tag + .replaceAll("\\{@actSaveSuccessOrFail}", "*Failure or Success:*") // render.js Renderer.tag + .replaceAll("\\{@actResponse}", "Response:") // render.js Renderer.tag + .replaceAll("\\{@actTrigger}", "Trigger:") // render.js Renderer.tag .replaceAll("\\{@spell\\s*}", "") // error in homebrew .replaceAll("\\{@color ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@style ([^|}]+)\\|?[^}]*}", "$1") @@ -360,25 +381,38 @@ default String _replaceTokenText(String input, boolean nested) { if (parts[0].contains("")) { // This already assumes what the footnote name will be // TODO: Note content is lost on this path at the moment - return String.format("%s", parts[0]); + return parts[0]; } if (parts.length > 2) { - return String.format("%s ^[%s, _%s_]", parts[0], parts[1], parts[2]); + return "%s ^[%s, _%s_]".formatted(parts[0], parts[1], parts[2]); } - return String.format("%s ^[%s]", parts[0], parts[1]); + return "%s ^[%s]".formatted(parts[0], parts[1]); }); result = notePattern.matcher(result).replaceAll((match) -> { - if (nested) { - return "***Note:** " + match.group(2).trim() + "*"; - } else { - List text = new ArrayList<>(); - text.add("> [!note]"); - for (String line : match.group(2).split("\n")) { - text.add("> " + line); + return switch (match.group(1)) { + case "note" -> { + // {@note This is a note} + if (nested) { + yield "**Note:** " + match.group(2).trim() + ""; + } else { + List text = new ArrayList<>(); + text.add("> [!note]"); + for (String line : match.group(2).split("\n")) { + text.add("> " + line); + } + yield String.join("\n", text); + } } - return String.join("\n", text); - } + case "tip" -> { + // {@tip tooltip tags|a note} + String[] parts = match.group(2).split("\\|"); + yield "%s".formatted(parts[1], parts[0]); + } + default -> { + yield match.group(0); + } + }; }); // after other replacements @@ -391,6 +425,14 @@ default String _replaceTokenText(String input, boolean nested) { return input; } + default String replaceSavingThrow(MatchResult match) { + // format: {@actSave dex} + String key = match.group(1); + SkillOrAbility ability = index().findSkillOrAbility(key, getSources()); + + return String.format("*%s Saving Throw:*", ability.value()); + } + default String replaceSkillOrAbility(MatchResult match) { // format: {@ability str 20} or {@ability str 20|Display Text} // or {@ability str 20|Display Text|Roll Name Text} @@ -421,7 +463,7 @@ default String replaceSkillOrAbility(MatchResult match) { mod = "`dice: d20" + mod + dtxt + mod + ")`"; } - return String.format("%s (%s)", text == null ? ability.value() : text, mod); + return "%s (%s)".formatted(text == null ? ability.value() : text, mod); } } @@ -447,7 +489,7 @@ default String replaceSkillCheck(MatchResult match) { dice = "`dice: d20" + dice + "|text(" + dice + ")`"; } - return String.format("%s (%s)", text, dice); + return "%s (%s)".formatted(text, dice); } default String linkifyRules(Tools5eIndexType type, String text, String rules) { @@ -456,16 +498,26 @@ default String linkifyRules(Tools5eIndexType type, String text, String rules) { // {@condition stunned|PHB|and optional link text added with another pipe}.", String[] parts = text.split("\\|"); - String heading = parts[0]; + String name = parts[0]; String source = parts.length > 1 ? parts[1] : "PHB"; - String linkText = parts.length > 2 ? parts[2] : heading; + String linkText = parts.length > 2 ? parts[2] : name; - String key = index().getAliasOrDefault(type.createKey(heading, source)); - if (index().isExcluded(key)) { + String aliasKey = index().getAliasOrDefault(type.createKey(name, source)); + if (index().isExcluded(aliasKey)) { return linkText; } - return String.format("[%s](%s%s.md#%s)", - linkText, index().rulesVaultRoot(), rules, toAnchorTag(heading)); + Tools5eIndexType aliasType = Tools5eIndexType.getTypeFromKey(aliasKey); + Tools5eSources rulesSources = Tools5eSources.findSources(aliasKey); + if (rulesSources != null) { + name = rulesSources.getName(); + } + if (aliasType != type) { + // we've changed types.. so we need to linkify based on the new type + // typically phb -> xphb changes + return linkify(aliasType, "%s|%s|%s".formatted(name, source, linkText)); + } + return "[%s](%s%s.md#%s)".formatted( + linkText, index().rulesVaultRoot(), rules, toAnchorTag(name)); } default String linkify(MatchResult match) { @@ -477,6 +529,9 @@ default String linkify(MatchResult match) { } default String linkify(Tools5eIndexType type, String s) { + if (!isPresent(s)) { + return ""; + } return switch (type) { // {@background Charlatan} assumes PHB by default, // {@background Anthropologist|toa} can have sources added with a pipe, @@ -526,11 +581,12 @@ default String linkify(Tools5eIndexType type, String s) { case background, feat, deck, + facility, hazard, item, legendaryGroup, object, - optionalfeature, + optfeature, race, reward, spell, @@ -544,7 +600,7 @@ default String linkify(Tools5eIndexType type, String s) { case disease -> linkifyRules(type, s, "diseases"); case sense -> linkifyRules(type, s, "senses"); case skill -> linkifyRules(type, s, "skills"); - case itemMastery -> linkifyRules(type, s, "item-mastery"); + case itemMastery, itemProperty, itemType -> linkifyItemAttribute(type, s); case monster -> linkifyCreature(s); case subclass, classtype -> linkifyClass(s); case deity -> linkifyDeity(s); @@ -553,7 +609,8 @@ default String linkify(Tools5eIndexType type, String s) { case subclassFeature -> linkifySubclassFeature(s); case variantrule -> linkifyVariant(s); default -> { - tui().errorf("Unknown type to linkify: %s from %s", type, s); + tui().debugf(Msg.UNKNOWN, "unknown tag/type {@%s %s} from %s", + type, s, parseState().getSource()); yield s; } }; @@ -561,7 +618,7 @@ default String linkify(Tools5eIndexType type, String s) { default String linkOrText(String linkText, String key, String dirName, String resourceName) { return index().isIncluded(key) - ? String.format("[%s](%s%s/%s.md)", + ? "[%s](%s%s/%s.md)".formatted( linkText, index().compendiumVaultRoot(), dirName, slugify(resourceName) .replace("-dmg-dmg", "-dmg")) // bad combo for some race names : linkText; @@ -573,16 +630,17 @@ default String linkifyType(Tools5eIndexType type, String match) { String linkText = parts.length > 2 ? parts[2] : parts[0]; String key = index().getAliasOrDefault(type.createKey(parts[0].trim(), source)); - return linkifyType(type, key, linkText); + return linkifyType(type, key, linkText, match); } - default String linkifyType(Tools5eIndexType type, String aliasKey, String linkText) { + default String linkifyType(Tools5eIndexType type, String aliasKey, String linkText, String match) { String dirName = type.getRelativePath(); - JsonNode jsonSource = index().getNode(aliasKey); + JsonNode jsonSource = index().getNode(aliasKey); // filtered if (jsonSource == null) { if (index().getOrigin(aliasKey) == null) { // sources can be excluded, that's fine.. but if this is something that doesn't exist at all.. - tui().debugf("🫣 Unable to create link, source for %s not found", aliasKey); + tui().debugf(Msg.UNRESOLVED, "unresolvable {@%s %s} as [%s] from %s", + type, match, aliasKey, parseState().getSource()); } return linkText; } @@ -605,7 +663,8 @@ default String linkifyCardType(String match) { return cardName; } String resource = Tools5eQuteBase.fixFileName(deckName, source, Tools5eIndexType.card); - return String.format("[%s](%s%s/%s.md#%s)", cardName, + return "[%s](%s%s/%s.md#%s)".formatted( + cardName, index().compendiumVaultRoot(), dirName, resource, cardName.replace(" ", "%20")); } @@ -660,7 +719,7 @@ default String linkifyClass(String match) { int second = key.indexOf('|', first + 1); subclass = key.substring(first + 1, second); return linkOrText(linkText, key, relativePath, - Tools5eQuteBase.getSubclassResource(subclass, className, subclassSource)); + Tools5eQuteBase.getSubclassResource(subclass, className, classSource, subclassSource)); } else { String key = index().getAliasOrDefault(Tools5eIndexType.classtype.createKey(className, classSource)); return linkOrText(linkText, key, relativePath, @@ -688,7 +747,7 @@ default String linkifyClassFeature(String match) { int pos = match.lastIndexOf("|"); match = match.substring(0, pos); } - String classFeatureKey = index().getAliasOrDefault(Tools5eIndexType.classfeature.fromRawKey(match)); + String classFeatureKey = index().getAliasOrDefault(Tools5eIndexType.classfeature.fromTagReference(match)); if (classFeatureKey == null || index().isExcluded(classFeatureKey)) { return linkText; } @@ -700,7 +759,8 @@ default String linkifyClassFeature(String match) { String resource = Tools5eQuteBase.getClassResource(className, classSource); String relativePath = Tools5eIndexType.classtype.getRelativePath(); - return String.format("[%s](%s%s/%s.md#%s)", linkText, + return "[%s](%s%s/%s.md#%s)".formatted( + linkText, index().compendiumVaultRoot(), relativePath, resource, toAnchorTag(headerName)); } @@ -741,42 +801,74 @@ default String linkifySubclassFeature(String match) { String[] parts = match.split("\\|"); String linkText = parts[0]; if (parts.length < 6) { - tui().errorf("Bad Subclass Feature Link {@subclassFeature %s} in %s", match, getSources()); + tui().warnf("Bad Subclass Feature Link {@subclassFeature %s} in %s", match, getSources()); return linkText; } - String className = parts[1]; - String classSource = parts[2].isBlank() ? "phb" : parts[2]; - String subclass = parts[3]; - String subclassSource = parts[4].isBlank() ? classSource : parts[4]; - String level = parts[5]; if (parts.length > 7) { linkText = parts[7]; + // trim optional display text int pos = match.lastIndexOf("|"); match = match.substring(0, pos); } - String classFeatureKey = index().getAliasOrDefault(Tools5eIndexType.subclassFeature.fromRawKey(match)); - if (classFeatureKey == null || index().isExcluded(classFeatureKey)) { + // Get the right subclass feature key + String featureKey = Tools5eIndexType.subclassFeature.fromTagReference(match); + featureKey = index().getAliasOrDefault(featureKey); + if (featureKey == null || index().isExcluded(featureKey)) { return linkText; } - // look up alias for subclass so link is correct, e.g. - // "subclass|redemption|paladin|phb|" : "subclass|oath of redemption|paladin|phb|", - // "subclass|twilight|cleric|phb|tce" : "subclass|twilight domain|cleric|phb|tce" - String subclassKey = index() - .getAliasOrDefault(Tools5eIndexType.getSubclassKey(className, classSource, subclass, subclassSource)); - int first = subclassKey.indexOf('|'); - subclass = subclassKey.substring(first + 1, subclassKey.indexOf('|', first + 1)); + // Find the subclass that will be emitted... + String subclassKey = Tools5eIndexType.subclass.fromChildKey(featureKey); - JsonNode featureJson = index().getNode(classFeatureKey); - Tools5eSources featureSources = Tools5eSources.findSources(classFeatureKey); - String headerName = decoratedFeatureTypeName(featureSources, featureJson) + " (Level " + level + ")"; + // look up alias for subclass so link is correct, but don't follow reprints + // "subclass|redemption|paladin|phb|" : "subclass|oath of redemption|paladin|phb|", + // "subclass|twilight|cleric|phb|tce" : "subclass|twilight domain|cleric|phb|tce" + subclassKey = index().getAliasOrDefault(subclassKey, false); + + JsonNode subclassNode = index().getNode(subclassKey); + if (subclassNode == null) { + // if the subclass was reprinted, the target file name will change (minimally) + subclassKey = index().getAliasOrDefault(subclassKey); + subclassNode = index().getNode(subclassKey); + if (subclassNode == null) { + tui().warnf(Msg.UNRESOLVED, "Subclass %s not found for {@subclassfeature %s} in %s", + subclassKey, match, getSources().getKey()); + return linkText; + } + // Examine new subclass node's features, to see if there is a match + // e.g. for "subclassfeature|primal companion|ranger|phb|beast master|phb|3|tce", + // consider "subclassfeature|primal companion|ranger|xphb|beast master|xphb|3|xphb" + String test = featureKey.replaceAll(subclassFeatureMask, "$1-$2"); + boolean found = false; + for (String fkey : Tools5eFields.classFeatureKeys.getListOfStrings(subclassNode, tui())) { + String compare = fkey.replaceAll(subclassFeatureMask, "$1-$2"); + if (test.equals(compare)) { + featureKey = fkey; + found = true; + break; + } + } + if (!found) { + tui().warnf(Msg.UNRESOLVED, "No equivalent subclass feature found for {@subclassfeature %s} in %s (from %s)", + match, subclassKey, getSources().getKey()); + return linkText; + } + } - String resource = slugify(Tools5eQuteBase.getSubclassResource(subclass, className, subclassSource)); + JsonNode featureJson = index().getNode(featureKey); + Tools5eSources featureSources = Tools5eSources.findSources(featureKey); + String level = Tools5eFields.level.getTextOrEmpty(featureJson); + String headerName = decoratedFeatureTypeName(featureSources, featureJson) + " (Level " + level + ")"; + String resource = slugify(Tools5eQuteBase.getSubclassResource( + SourceField.name.getTextOrEmpty(subclassNode), + IndexFields.className.getTextOrEmpty(subclassNode), + IndexFields.classSource.getTextOrEmpty(subclassNode), + SourceField.source.getTextOrEmpty(subclassNode))); String relativePath = Tools5eIndexType.classtype.getRelativePath(); - return String.format("[%s](%s%s/%s.md#%s)", + return "[%s](%s%s/%s.md#%s)".formatted( linkText, index().compendiumVaultRoot(), relativePath, resource, @@ -816,14 +908,48 @@ default String linkifyVariant(String variant) { String[] parts = variant.trim().split("\\|"); String source = parts.length > 1 ? parts[1] : Tools5eIndexType.variantrule.defaultSourceString(); if (!index().sourceIncluded(source)) { - return variant + " from " + TtrpgConfig.sourceToLongName(source); + return "%s".formatted(TtrpgConfig.sourceToLongName(source), parts[0]); } else { - return String.format("[%s](%svariant-rules/%s.md)", + return "[%s](%svariant-rules/%s.md)".formatted( parts[0], index().rulesVaultRoot(), Tools5eQuteBase.fixFileName(parts[0], source, Tools5eIndexType.variantrule)); } } + default String linkifyItemAttribute(Tools5eIndexType type, String s) { + String parts[] = s.split("\\|"); + String source = parts.length > 1 ? parts[1] : type.defaultSourceString(); + String linkText = parts.length > 2 ? parts[2] : parts[0]; + String lookup = "%s|%s".formatted(parts[0], source); + return switch (type) { + case itemType -> { + ItemType itemType = index().findItemType(lookup, getSources()); + if (itemType == null) { + tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); + yield s; + } + yield itemType.linkify(linkText); + } + case itemProperty -> { + ItemProperty itemProperty = index().findItemProperty(lookup, getSources()); + if (itemProperty == null) { + tui().warnf(Msg.UNRESOLVED, "Item property %s not found from %s", s, getSources().getKey()); + yield s; + } + yield itemProperty.linkify(linkText); + } + case itemMastery -> { + ItemMastery itemMastery = index().findItemMastery(lookup, getSources()); + if (itemMastery == null) { + tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); + yield s; + } + yield itemMastery.linkify(linkText); + } + default -> linkify(type, s); // should never happen + }; + } + default String handleCitation(String citationTag) { // Casting Times|FleeMortals|A // Casting Times|FleeMortals|{@sup A} @@ -847,7 +973,7 @@ default String handleCitation(String citationTag) { text.add(blockRef); } parseState().addCitation(key, String.join("\n", text)); - return String.format("[%s](#%s)", annotation, blockRef); + return "[%s](#%s)".formatted(annotation, blockRef); } default String decoratedUaName(String name, Tools5eSources sources) { @@ -860,7 +986,7 @@ default String decoratedUaName(String name, Tools5eSources sources) { default String getMonsterType(JsonNode node) { if (node == null || !node.has("type")) { - tui().warn("Monster: Empty type for " + getSources()); + tui().warnf("Monster: Empty type for %s", getSources()); return null; } JsonNode typeNode = node.get("type"); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java index 62a22a276..922f2ea73 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java @@ -23,8 +23,9 @@ import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; -import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemFields; +import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField; import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.Tuple; +import dev.ebullient.convert.tools.dnd5e.Tools5eSources.SourceAttributes; public class MagicVariant implements JsonSource { @@ -38,8 +39,21 @@ public class MagicVariant implements JsonSource { static final MagicVariant INSTANCE = new MagicVariant(); - /** Update generic variant item with inherited attributes (minimally source, required for key) */ - public void populateGenericVariant(final JsonNode variant) { + public static List findSpecificVariants(Tools5eIndex index, Tools5eIndexType type, + String key, JsonNode genericVariant, Tools5eJsonSourceCopier copier, + List baseItems) { + return INSTANCE.findVariants(index, type, key, genericVariant, copier, baseItems); + } + + public static void populateGenericVariant(final JsonNode variant) { + INSTANCE.populateVariant(variant); + } + + /** + * Update generic variant item with inherited attributes + * (minimally source, which is required to create the key) + */ + private void populateVariant(final JsonNode variant) { JsonNode inherits = MagicItemField.inherits.getFrom(variant); // for (const prop in genericVariant.inherits) { @@ -100,39 +114,46 @@ public void populateGenericVariant(final JsonNode variant) { } /** Update / replace item with variants (where appropriate) */ - public List findSpecificVariants(Tools5eIndex index, Tools5eIndexType type, - String key, JsonNode genericVariant, Tools5eJsonSourceCopier copier) { - // const specificVariants = Renderer.item._createSpecificVariants(baseItems, genericVariants); - // const outSpecificVariants = Renderer.item._enhanceItems(specificVariants); + private List findVariants(Tools5eIndex index, Tools5eIndexType type, + String key, JsonNode genericVariant, Tools5eJsonSourceCopier copier, + List baseItems) { List variants = new ArrayList<>(); - genericVariant = copyNode(genericVariant); + // baseItems.forEach((curBaseItem) => { + // .... + // genericVariants.forEach((curGenericVariant) => { + // if (!Renderer.item._createSpecificVariants_isEditionMatch({curBaseItem, curGenericVariant})) return; + // if (!Renderer.item._createSpecificVariants_hasRequiredProperty(curBaseItem, curGenericVariant)) return; + // if (Renderer.item._createSpecificVariants_hasExcludedProperty(curBaseItem, curGenericVariant)) return; + // genericAndSpecificVariants.push(Renderer.item._createSpecificVariants_createSpecificVariant(curBaseItem, curGenericVariant, opts)); + // }); + // }); + // .. + // We're looping the other way (variant is the outer loop / is passed in) + boolean spawnNewItems = key.contains(" (*)"); + String gvKey = Tools5eIndexType.item.createKey(genericVariant); - // Generic variants with (*) in the name have a single specific variant. - // Those will be replaced (See below) - if (!key.contains(" (*)")) { + if (!spawnNewItems) { // Add generic variant to the list of variants as a regular item - // (will have variants field, see) + // Variations will be added to this item. TtrpgValue.indexInputType.setIn(genericVariant, Tools5eIndexType.item.name()); variants.add(new Tuple(gvKey, genericVariant)); index.addAlias(key, gvKey); } - // Create specific variants - List baseItems = index.originNodesMatching(x -> TtrpgValue.indexBaseItem.booleanOrDefault(x, false)); for (JsonNode baseItem : baseItems) { - if (MagicItemField.packContents.existsIn(baseItem) - || !hasRequiredProperty(genericVariant, baseItem) - || hasExcludedProperty(genericVariant, baseItem)) { + if (ItemField.packContents.existsIn(baseItem) + || !editionMatch(baseItem, genericVariant) + || !hasRequiredProperty(baseItem, genericVariant) + || hasExcludedProperty(baseItem, genericVariant)) { continue; } - JsonNode specficVariant = createSpecificVariant(genericVariant, baseItem); + JsonNode specficVariant = createSpecificVariant(baseItem, genericVariant); if (specficVariant != null) { String newKey = Tools5eIndexType.item.createKey(specficVariant); TtrpgValue.indexInputType.setIn(specficVariant, Tools5eIndexType.item.name()); TtrpgValue.indexKey.setIn(specficVariant, newKey); - Tools5eSources.constructSources(specficVariant); - if (key.contains(" (*)")) { - // specific variant will replace single generic variant (Shield) as a regular item + Tools5eSources.constructSources(newKey, specficVariant); + if (spawnNewItems) { variants.add(new Tuple(newKey, specficVariant)); if (key.replace(" (*)", "").replace("magicvariant", "item").equals(newKey)) { index.addAlias(key, newKey); @@ -143,22 +164,68 @@ public List findSpecificVariants(Tools5eIndex index, Tools5eIndexType typ } else { // add variant to list of variants for this generic variant // magic variant remains in index as a magic variant - ItemFields._variants.ensureArrayIn(genericVariant).add(specficVariant); + ItemField._variants.ensureArrayIn(genericVariant).add(specficVariant); index.addAlias(newKey, gvKey); } } } - return variants; } - boolean hasRequiredProperty(JsonNode genericVariant, JsonNode baseItem) { + // @formatter:off + /** + * render.js -- _createSpecificVariants_isEditionMatch + * + * When creating specific variants, the application of "classic" and "one" editions + * goes by the following logic: + * + * | B. Item | Gen. Var | Apply | Example + * |----------|----------|-------|---------------------------------------- + * | null | null | X | "Fool's Blade|BMT" -> "Pitchfork|ToB3-Lairs" + * | classic | null | | "Fool's Blade|BMT" -> "Longsword|PHB" + * | one | null | X | "Fool's Blade|BMT" -> "Longsword|XPHB" + * | null | classic | X | "+1 Weapon|DMG" -> "Pitchfork|ToB3-Lairs" -- TODO(Future): consider cutting this, with a homebrew tag migration + * | classic | classic | X | "+1 Weapon|DMG" -> "Longsword|PHB" + * | one | classic | | "+1 Weapon|DMG" -> "Longsword|XPHB" + * | null | one | X | "+1 Weapon|XDMG" -> "Pitchfork|ToB3-Lairs" + * | classic | one | | "+1 Weapon|XDMG" -> "Longsword|PHB" + * | one | one | X | "+1 Weapon|XDMG" -> "Longsword|XPHB" + * + * This aims to minimize spamming near-duplicates, while preserving as many '14 items as possible. + */ + // @formatter:on + boolean editionMatch(JsonNode baseItem, JsonNode genericVariant) { + String baseItemEdition = Tools5eFields.edition.getTextOrNull(baseItem); + String variantEdition = Tools5eFields.edition.getTextOrNull(genericVariant); + if (baseItemEdition == null && variantEdition == null) { + return true; // ok: null -> null + } + if (baseItemEdition != null) { // variantEdition may be null + if (baseItemEdition.equalsIgnoreCase(variantEdition)) { + return true; // ok: classic -> classic, one -> one + } + if ("classic".equalsIgnoreCase(baseItemEdition)) { + return false; // nope: classic -> one or classic -> null + } + if ("one".equalsIgnoreCase(baseItemEdition)) { + // ok: one -> null; nope: one -> classic + return !"classic".equalsIgnoreCase(variantEdition); + } + } + // ok: null -> classic, null -> one + return true; + } + + /** + * render.js -- _createSpecificVariants_hasRequiredProperty + */ + boolean hasRequiredProperty(JsonNode baseItem, JsonNode genericVariant) { JsonNode variantRequires = MagicItemField.requires.getFrom(genericVariant); if (variantRequires == null) { return true; // all is well if there are no required properties defined } if (!variantRequires.isArray()) { - tui().errorf("Incorrectly specified magic variant requirements", genericVariant); + tui().errorf("Incorrect magic variant requirements: %s", genericVariant); return false; } // "requires": [ @@ -166,48 +233,61 @@ boolean hasRequiredProperty(JsonNode genericVariant, JsonNode baseItem) { // { "type": "S" }, // { "net": true } // ], - // return genericVariant.requires.some(req => Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, req, "every")); - return streamOf(variantRequires).anyMatch((r) -> { - return streamOfFieldNames(r).allMatch((name) -> testProperty(baseItem, name, r.get(name))); + // return genericVariant.requires.some(req => + // Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, req, "every")); + return streamOf(variantRequires).anyMatch(req -> { + if (req != null && !req.isObject()) { + tui().errorf("Incorrectly specified magic variant requirement in %s: %s", + TtrpgValue.indexKey.getFrom(genericVariant), req); + } + return matchesRequiresExcludes(baseItem, req, true); }); } - boolean hasExcludedProperty(JsonNode genericVariant, JsonNode baseItem) { + boolean hasExcludedProperty(JsonNode baseItem, JsonNode genericVariant) { JsonNode excludes = MagicItemField.excludes.getFrom(genericVariant); - if (excludes == null) { - // no excluded properties - return false; - } - if (!excludes.isObject()) { - tui().errorf("Incorrectly specified magic variant requirements", genericVariant); + if (excludes != null && !excludes.isObject()) { + tui().errorf("Incorrectly specified magic variant excludes in %s: %s", + TtrpgValue.indexKey.getFrom(genericVariant), excludes); return true; } // "excludes": { // "net": true // }, - // bail the first time you find an excluded property - return streamOfFieldNames(excludes).anyMatch((name) -> testProperty(baseItem, name, excludes.get(name))); + // return Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, genericVariant.excludes, "some"); + return matchesRequiresExcludes(baseItem, excludes, false); } - boolean testProperty(JsonNode baseItem, String reqKey, JsonNode reqValue) { - JsonNode customProperties = MagicItemField.customProperties.getFrom(baseItem); - JsonNode candidate = getProperty(baseItem, customProperties, reqKey); - if (candidate == null || candidate.isNull()) { + // _createSpecificVariants_isRequiresExcludesMatch + private boolean matchesRequiresExcludes(JsonNode candidate, JsonNode reqsOrExcludes, boolean matchAll) { + if (candidate == null || reqsOrExcludes == null) { return false; } + + var entries = reqsOrExcludes.properties().stream(); + + return matchAll + ? entries.allMatch(e -> testProperty(candidate, e.getKey(), e.getValue(), matchAll)) + : entries.anyMatch(e -> testProperty(candidate, e.getKey(), e.getValue(), matchAll)); + } + + private boolean testProperty(JsonNode candidate, String reqKey, JsonNode reqValue, boolean matchAll) { + JsonNode candidateValue = candidate.get(reqKey); + if (candidateValue == null || candidateValue.isNull()) { + return reqValue == null || reqValue.isNull(); + } if (reqValue.isArray()) { - return candidate.isArray() - ? streamOf(candidate).anyMatch((x) -> arrayContains(reqValue, x)) - : arrayContains(reqValue, candidate); + return candidateValue.isArray() + ? streamOf(candidateValue).anyMatch(it -> arrayContains(reqValue, it)) + : arrayContains(reqValue, candidateValue); } if (reqValue.isObject()) { - tui().errorf( - "Unsupported comparison for required property \"%s\"; Raise an issue containing this message. We need to look for %s in %s", - reqKey, reqValue, baseItem); + // recursion: chase required custom properties (e.g.) + return matchesRequiresExcludes(candidate.get(reqKey), reqValue, matchAll); } - return candidate.isArray() - ? arrayContains(candidate, reqValue) - : reqValue.equals(candidate); + return candidateValue.isArray() + ? arrayContains(candidateValue, reqValue) + : reqValue.equals(candidateValue); } boolean arrayContains(JsonNode array, JsonNode value) { @@ -316,37 +396,31 @@ private BiFunction tokenResolver(final JsonNode baseIte }; } - private JsonNode getProperty(JsonNode baseItem, JsonNode customProperties, String fieldName) { - JsonNode value = baseItem.get(fieldName); - if (value == null || value.isNull()) { - if (customProperties != null) { - value = customProperties.get(fieldName); - } - } - return value; - } - - private JsonNode createSpecificVariant(JsonNode genericVariant, JsonNode baseItem) { + private JsonNode createSpecificVariant(JsonNode baseItem, JsonNode genericVariant) { + JsonNode inherits = MagicItemField.inherits.getFrom(genericVariant); JsonNode specificVariant = copyNode(baseItem); + + // Remove base item flag TtrpgValue.indexBaseItem.removeFrom(specificVariant); // Magic variants apply their own SRD info; page info - Tools5eFields.basicRules.removeFrom(specificVariant); - Tools5eFields.srd.removeFrom(specificVariant); + SourceAttributes.basicRules.removeFrom(specificVariant); + SourceAttributes.freeRules2024.removeFrom(specificVariant); + SourceAttributes.srd.removeFrom(specificVariant); + SourceAttributes.srd52.removeFrom(specificVariant); SourceField.page.removeFrom(specificVariant); // Magic items do not inherit the value of the non-magical item - ItemFields.value.removeFrom(specificVariant); + ItemField.value.removeFrom(specificVariant); - // Remove fluff specifiers - ItemFields.hasFluff.removeFrom(specificVariant); - ItemFields.hasFluffImages.removeFrom(specificVariant); + // Reset or remove fluff specifiers based on generic variant + resetOrRemove(ItemField.hasFluff, genericVariant, specificVariant); + resetOrRemove(ItemField.hasFluffImages, genericVariant, specificVariant); - JsonNode inherits = MagicItemField.inherits.getFrom(genericVariant); - for (Entry property : iterableFields(inherits)) { + for (Entry property : inherits.properties()) { switch (property.getKey()) { case "barding" -> { - MagicItemField.bardingType.setIn(specificVariant, ItemFields.type.getFrom(baseItem)); + MagicItemField.bardingType.setIn(specificVariant, ItemField.type.getFrom(baseItem)); } case "entries" -> { JsonNode entries = resolveEntryAttributes(property.getValue(), @@ -367,11 +441,11 @@ private JsonNode createSpecificVariant(JsonNode genericVariant, JsonNode baseIte SourceField.name.setIn(specificVariant, p.matcher(name).replaceAll("")); } case "propertyAdd" -> { - ArrayNode itemProperty = ItemFields.property.ensureArrayIn(specificVariant); + ArrayNode itemProperty = ItemField.property.ensureArrayIn(specificVariant); index().copier.appendIfNotExistsArr(itemProperty, property.getValue()); } case "propertyRemove" -> { - ArrayNode itemProperty = ItemFields.property.ensureArrayIn(specificVariant); + ArrayNode itemProperty = ItemField.property.ensureArrayIn(specificVariant); index().copier.removeFromArr(itemProperty, property.getValue()); } case "valueExpression", "weightExpression" -> { @@ -396,10 +470,10 @@ private JsonNode createSpecificVariant(JsonNode genericVariant, JsonNode baseIte EvaluationValue result = expression.evaluate(); if (property.getKey() == "valueExpression") { IntNode value = IntNode.valueOf(result.getNumberValue().intValue()); - ItemFields.value.setIn(specificVariant, value); + ItemField.value.setIn(specificVariant, value); } else { DoubleNode value = DoubleNode.valueOf(result.getNumberValue().doubleValue()); - ItemFields.weight.setIn(specificVariant, value); + ItemField.weight.setIn(specificVariant, value); } } catch (EvaluationException | ParseException e) { tui().errorf(e, "Unable to parse %s: %s", property.getKey(), property.getValue()); @@ -418,9 +492,21 @@ private JsonNode createSpecificVariant(JsonNode genericVariant, JsonNode baseIte } } } + // TODO: + // Renderer.item._createSpecificVariants_mergeVulnerableResistImmune({specificVariant, inherits}); + return specificVariant; } + private void resetOrRemove(JsonNodeReader field, JsonNode source, JsonNode target) { + JsonNode value = field.getFrom(source); + if (value == null || value.isNull()) { + field.removeFrom(target); + } else { + field.setIn(target, value); + } + } + enum MagicItemField implements JsonNodeReader { armor, barding, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java new file mode 100644 index 000000000..654c19d34 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java @@ -0,0 +1,277 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import dev.ebullient.convert.config.CompendiumConfig; +import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; +import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; +import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewMetaTypes; +import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; + +public class OptionalFeatureIndex implements JsonSource { + private final Map optFeatureIndex = new HashMap<>(); + private final Tools5eIndex index; + + OptionalFeatureIndex(Tools5eIndex index) { + this.index = index; + } + + public void addOptionalFeature(String finalKey, JsonNode optFeatureNode, HomebrewMetaTypes homebrew) { + String lookup = null; + for (String ft : toListOfStrings(optFeatureNode.get("featureType"))) { + try { + boolean homebrewType = homebrew != null && homebrew.getOptionalFeatureType(ft) != null; + // scope the optional feature key (homebrew may conflict) + String featKey = (homebrewType ? ft + "-" + homebrew.jsonKey : ft).toLowerCase(); + + optFeatureIndex.computeIfAbsent(featKey, k -> new OptionalFeatureType(ft, k, homebrew, index())).add(finalKey); + lookup = lookup == null ? featKey : lookup; + } catch (IllegalArgumentException e) { + tui().errorf(e, "Unable to define optional feature"); + } + } + if (lookup != null) { + OftFields.oftLookup.setIn(optFeatureNode, lookup); + OftFields.oftIndexKey.setIn(optFeatureNode, optFeatureIndex.get(lookup).getKey()); + } + } + + public void amendSources(String key, JsonNode jsonSource, Tools5eHomebrewIndex homebrewIndex) { + Tools5eSources sources = Tools5eSources.findSources(key); + if (sources.getType() == Tools5eIndexType.optfeature) { + OptionalFeatureType oft = get(jsonSource); + if (oft == null) { + tui().warnf(Msg.UNRESOLVED, "OptionalFeatureType %s not found for %s", jsonSource, key); + } else { + oft.amendSources(sources); + } + } else { + for (JsonNode ofp : ClassFields.optionalfeatureProgression.iterateArrayFrom(jsonSource)) { + for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) { + // class/subclass source matters for homebrew scope (if necessary) + OptionalFeatureType oft = get(featureType, sources.primarySource(), homebrewIndex); + if (oft == null) { + tui().warnf(Msg.UNRESOLVED, "OptionalFeatureType %s not found for %s", + featureType, key); + continue; + } + oft.addConsumer(key); + oft.amendSources(sources); + } + } + } + } + + public OptionalFeatureType get(Tools5eIndexType type, String key) { + return switch (type) { + case optfeature -> { + JsonNode ofNode = index().getOrigin(key); + String oftKey = OftFields.oftIndexKey.getTextOrNull(ofNode); + JsonNode oftNode = index().getOrigin(oftKey); + yield get(oftNode); + } + case optionalFeatureTypes -> { + JsonNode node = index().getOrigin(key); + yield get(node); + } + default -> null; + }; + } + + public OptionalFeatureType get(JsonNode node) { + if (node == null) { + return null; + } + String lookup = OftFields.oftLookup.getTextOrNull(node); + return lookup == null ? null : optFeatureIndex.get(lookup); + } + + public OptionalFeatureType get(String ft, String source, Tools5eHomebrewIndex homebrewIndex) { + HomebrewMetaTypes metaTypes = homebrewIndex.getHomebrewMetaTypes(source); + String homebrewType = metaTypes == null + ? null + : metaTypes.getOptionalFeatureType(ft); + + OptionalFeatureType oft = optFeatureIndex.get(ft.toLowerCase()); + if (homebrewType != null) { + String homebrewScoped = ft + "-" + metaTypes.jsonKey; + OptionalFeatureType homebrewOft = optFeatureIndex.get(homebrewScoped.toLowerCase()); + return homebrewOft == null + ? oft + : homebrewOft; + } + return oft; + } + + public void clear() { + optFeatureIndex.clear(); + } + + public Map getMap() { + return optFeatureIndex; + } + + /** + * This is included in all-index.json + */ + static class OptionalFeatureType { + final String lookupKey; + final String featureTypeKey; + final String abbreviation; + final HomebrewMetaTypes homebrewMeta; + final String title; + final String source; + final List features = new ArrayList<>(); + final List consumers = new ArrayList<>(); + + @JsonIgnore + final ObjectNode featureTypeNode; + + OptionalFeatureType(String abbreviation, String scopedAbv, HomebrewMetaTypes homebrewMeta, Tools5eIndex index) { + this.abbreviation = abbreviation; + this.lookupKey = scopedAbv; + this.homebrewMeta = homebrewMeta; + String tmpTitle = null; + if (homebrewMeta != null) { + tmpTitle = homebrewMeta.getOptionalFeatureType(abbreviation); + } + if (tmpTitle == null) { + tmpTitle = switch (abbreviation) { + case "AI" -> "Artificer Infusion"; + case "ED" -> "Elemental Discipline"; + case "EI" -> "Eldritch Invocation"; + case "MM" -> "Metamagic"; + case "MV" -> "Maneuver"; + case "MV:B" -> "Maneuver, Battle Master"; + case "MV:C2-UA" -> "Maneuver, Cavalier V2 (UA)"; + case "AS:V1-UA" -> "Arcane Shot, V1 (UA)"; + case "AS:V2-UA" -> "Arcane Shot, V2 (UA)"; + case "AS" -> "Arcane Shot"; + case "OTH" -> "Other"; + case "FS:F" -> "Fighting Style, Fighter"; + case "FS:B" -> "Fighting Style, Bard"; + case "FS:P" -> "Fighting Style, Paladin"; + case "FS:R" -> "Fighting Style, Ranger"; + case "PB" -> "Pact Boon"; + case "OR" -> "Onomancy Resonant"; + case "RN" -> "Rune Knight Rune"; + case "AF" -> "Alchemical Formula"; + case "TT" -> "Traveler's Trick"; + default -> null; + }; + } + if (tmpTitle == null) { + index.tui().warnf(Msg.NOT_SET.wrap("Missing title for OptionalFeatureType in %s from %s"), + abbreviation, + homebrewMeta == null ? "unknown/core" : homebrewMeta.filename); + tmpTitle = abbreviation; + } + title = tmpTitle; + source = getSource(homebrewMeta); + + featureTypeNode = Tui.MAPPER.createObjectNode(); + featureTypeNode.put("name", scopedAbv); + featureTypeNode.put("source", source); + OftFields.oftLookup.setIn(featureTypeNode, lookupKey); + + if (inSRD(abbreviation)) { + featureTypeNode.put("srd", true); + } + // KNOCK-ON: Add to index + index.addToIndex(Tools5eIndexType.optionalFeatureTypes, featureTypeNode); + featureTypeKey = TtrpgValue.indexKey.getTextOrThrow(featureTypeNode); + Tools5eSources.constructSources(featureTypeKey, featureTypeNode); + } + + public void amendSources(Tools5eSources otherSources) { + // Update sources from those of a consuming/using class or subclass + // Optional features will always add to sources of types + Tools5eSources mySources = Tools5eSources.findSources(featureTypeNode); + if (otherSources.getType() == Tools5eIndexType.optfeature + || otherSources.contains(mySources)) { + mySources.amendSources(otherSources); + } + } + + public void addConsumer(String key) { + consumers.add(key); + } + + public void add(String key) { + features.add(key); + } + + public String getFilename() { + return "list-" + Tools5eQuteBase.fixFileName(title, source, Tools5eIndexType.optionalFeatureTypes); + } + + public Tools5eSources getSources() { + return Tools5eSources.findSources(featureTypeKey); + } + + public boolean inUse() { + return features.stream() + .map(k -> Tools5eSources.includedByConfig(k)) + .reduce(Boolean::logicalOr) + .orElse(false); + } + + private String getSource(HomebrewMetaTypes homebrewMeta) { + if (homebrewMeta != null) { + return homebrewMeta.jsonKey; + } + return switch (abbreviation) { + case "AF" -> "UAA"; + case "AI", "RN" -> "TCE"; + case "AS", "FS:B" -> "XGE"; + case "AS:V1-UA" -> "UAF"; + case "AS:V2-UA" -> "UARSC"; + case "MV:C2-UA" -> "UARCO"; + case "OR" -> "UACDW"; + default -> "PHB"; + }; + } + + private boolean inSRD(String abbreviation) { + return switch (abbreviation) { + case "EI", "FS:F", "FS:R", "FS:P", "MM", "PB" -> true; + default -> false; + }; + } + + @JsonIgnore + String getKey() { + return featureTypeKey; + } + } + + @Override + public CompendiumConfig cfg() { + return index.config; + } + + @Override + public Tools5eIndex index() { + return index; + } + + @Override + public Tools5eSources getSources() { + return null; + } + + enum OftFields implements JsonNodeReader { + oftLookup, + oftIndexKey, + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java new file mode 100644 index 000000000..56dd5011e --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java @@ -0,0 +1,285 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static dev.ebullient.convert.StringUtil.isPresent; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.config.CompendiumConfig; +import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.dnd5e.PsionicType.CustomPsionicType; +import dev.ebullient.convert.tools.dnd5e.SkillOrAbility.CustomSkillOrAbility; +import dev.ebullient.convert.tools.dnd5e.SpellSchool.CustomSpellSchool; + +public class Tools5eHomebrewIndex implements JsonSource { + + private final Map homebrewMetaTypes = new HashMap<>(); + private final Tools5eIndex index; + + Tools5eHomebrewIndex(Tools5eIndex index) { + this.index = index; + } + + public void importBrew(Consumer processHomebrewTree) { + for (HomebrewMetaTypes homebrew : homebrewMetaTypes.values()) { + processHomebrewTree.accept(homebrew); + } + } + + public HomebrewMetaTypes getHomebrewMetaTypes(Tools5eSources sources) { + return homebrewMetaTypes.get(sources.primarySource()); + } + + public HomebrewMetaTypes getHomebrewMetaTypes(String source) { + return homebrewMetaTypes.get(source); + } + + public SkillOrAbility findHomebrewSkillOrAbility(String key, Tools5eSources sources) { + HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); + if (meta != null) { + return meta.getSkillType(key); + } + return null; + } + + public SpellSchool findHomebrewSpellSchool(String abbreviation, Tools5eSources sources) { + HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); + if (meta != null) { + return meta.getSpellSchool(abbreviation); + } + return null; + } + + public ItemType findHomebrewType(String fragment, Tools5eSources sources) { + HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); + if (meta != null) { + JsonNode homebrewNode = meta.getItemProperty(fragment); + if (homebrewNode != null) { + String key = Tools5eIndexType.itemType.createKey(homebrewNode); + return ItemType.fromNode(key, homebrewNode); + } + } + return null; + } + + public ItemMastery findHomebrewMastery(String fragment, Tools5eSources sources) { + HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); + if (meta != null) { + JsonNode homebrewNode = meta.getItemMastery(fragment); + if (homebrewNode != null) { + String key = Tools5eIndexType.itemMastery.createKey(homebrewNode); + return ItemMastery.fromNode(key, homebrewNode); + } + } + return null; + } + + public ItemProperty findHomebrewProperty(String fragment, Tools5eSources sources) { + HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); + if (meta != null) { + JsonNode homebrewNode = meta.getItemProperty(fragment); + if (homebrewNode != null) { + String key = Tools5eIndexType.itemProperty.createKey(homebrewNode); + return ItemProperty.fromNode(key, homebrewNode); + } + } + return null; + } + + public void clear() { + homebrewMetaTypes.clear(); + } + + public boolean addHomebrewSourcesIfPresent(String filename, JsonNode node) { + JsonNode sources = SourceField._meta.getFieldFrom(node, HomebrewFields.sources); + if (sources == null || sources.size() == 0) { + return false; + } + // TODO include homebrew date + String json = HomebrewFields.json.getTextOrNull(sources.get(0)); + if (json == null) { + tui().errorf("Source does not define json id: %s", sources.get(0)); + return false; + } + TtrpgConfig.includeAdditionalSource(json); + + HomebrewMetaTypes metaTypes = new HomebrewMetaTypes(json, filename, node); + for (JsonNode source : iterableElements(sources)) { + String fullName = HomebrewFields.full.getTextOrEmpty(source); + String abbreviation = HomebrewFields.abbreviation.getTextOrEmpty(source); + json = HomebrewFields.json.getTextOrEmpty(source); + if (fullName == null) { + tui().warnf(Msg.BREW, "Homebrew source %s missing full name: %s", json, fullName); + } + // add homebrew to known sources + if (TtrpgConfig.addHomebrewSource(fullName, json, abbreviation)) { + // one homebrew file may include multiple sources, the same mapping applies to + // all + HomebrewMetaTypes old = homebrewMetaTypes.put(json, metaTypes); + if (old != null) { + tui().errorf(Msg.BREW, "Shared homebrew id: %s and %s", old.filename, metaTypes.filename); + } + } else { + tui().errorf(Msg.BREW, "Skipping homebrew id %s from %s; duplicate source id", json, metaTypes.filename); + } + } + + JsonNode featureTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.optionalFeatureTypes); + JsonNode spellSchools = SourceField._meta.getFieldFrom(node, HomebrewFields.spellSchools); + JsonNode psionicTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.psionicTypes); + JsonNode skillTypes = HomebrewFields.skill.getFrom(node); + if (featureTypes != null || spellSchools != null || psionicTypes != null || skillTypes != null) { + for (Entry entry : iterableFields(featureTypes)) { + metaTypes.setOptionalFeatureType(entry.getKey(), entry.getValue().asText()); + } + // ignoring short names for spell schools and psionic types + for (Entry entry : iterableFields(spellSchools)) { + metaTypes.setSpellSchool(entry.getKey(), + new CustomSpellSchool(HomebrewFields.full.getTextOrEmpty(entry.getValue()))); + } + for (Entry entry : iterableFields(psionicTypes)) { + metaTypes.setPsionicType(entry.getKey(), + tui().readJsonValue(entry.getValue(), CustomPsionicType.class)); + } + for (JsonNode skill : iterableElements(skillTypes)) { + String skillName = SourceField.name.getTextOrEmpty(skill); + if (skillName == null) { + tui().warnf(Msg.BREW, "Homebrew skill type missing name: %s", skill); + continue; + } + metaTypes.setSkillType(skillName, skill); + } + } + Tools5eSources.addFonts(SourceField._meta.getFrom(node), HomebrewFields.fonts); + return true; + } + + static class HomebrewMetaTypes { + final String jsonKey; + final String filename; + final JsonNode homebrewNode; + // name, long name + final Map optionalFeatureTypes = new HashMap<>(); + final Map psionicTypes = new HashMap<>(); + final Map skillOrAbility = new HashMap<>(); + final Map spellSchoolTypes = new HashMap<>(); + final Map itemTypes = new HashMap<>(); + final Map itemProperties = new HashMap<>(); + final Map itemMastery = new HashMap<>(); + + HomebrewMetaTypes(String jsonKey, String filename, JsonNode homebrewNode) { + this.jsonKey = jsonKey; + this.filename = filename; + this.homebrewNode = homebrewNode; + } + + public String getOptionalFeatureType(String key) { + return optionalFeatureTypes.get(key.toLowerCase()); + } + + public void setOptionalFeatureType(String key, String value) { + optionalFeatureTypes.put(key.toLowerCase(), value); + } + + public PsionicType getPsionicType(String key) { + return psionicTypes.get(key.toLowerCase()); + } + + public void setPsionicType(String key, PsionicType value) { + psionicTypes.put(key.toLowerCase(), value); + } + + public SkillOrAbility getSkillType(String key) { + return skillOrAbility.get(key.toLowerCase()); + } + + public void setSkillType(String key, JsonNode skill) { + skillOrAbility.put(key.toLowerCase(), new CustomSkillOrAbility(skill)); + } + + public SpellSchool getSpellSchool(String key) { + return spellSchoolTypes.get(key.toLowerCase()); + } + + public void setSpellSchool(String key, CustomSpellSchool value) { + spellSchoolTypes.put(key.toLowerCase(), value); + } + + public JsonNode getItemType(String abbreviation) { + return itemTypes.get(abbreviation); + } + + public JsonNode getItemProperty(String abbreviation) { + return itemProperties.get(abbreviation); + } + + public JsonNode getItemMastery(String name) { + return itemMastery.get(name); + } + + public void addElement(Tools5eIndexType type, String key, JsonNode value) { + String name = SourceField.name.getTextOrEmpty(value); + String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(value); + switch (type) { + case itemMastery -> { + if (isPresent(name)) { + itemMastery.put(name, value); + } else { + Tui.instance().errorf(Msg.BREW, "Missing name in %s", key); + } + } + case itemProperty -> { + if (isPresent(abbreviation)) { + itemProperties.put(abbreviation, value); + } else { + Tui.instance().errorf(Msg.BREW, "Missing abbreviation in %s", key); + } + } + case itemType -> { + if (isPresent(abbreviation)) { + itemTypes.put(abbreviation, value); + } else { + Tui.instance().errorf(Msg.BREW, "Missing abbreviation in %s", key); + } + } + default -> { + } // no-op + } + } + } + + enum HomebrewFields implements JsonNodeReader { + abbreviation, + fonts, + full, + json, + optionalFeatureTypes, + psionicTypes, + skill, + sources, + spellSchools, + spellDistanceUnits + } + + @Override + public CompendiumConfig cfg() { + return index.config; + } + + @Override + public Tools5eIndex index() { + return index; + } + + @Override + public Tools5eSources getSources() { + return null; + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 17ab66fac..4c86aa003 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -6,7 +6,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -14,8 +13,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.function.BiConsumer; import java.util.function.Function; -import java.util.regex.Pattern; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; @@ -23,23 +22,21 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import dev.ebullient.convert.config.CompendiumConfig; +import dev.ebullient.convert.config.ReprintBehavior; import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.MarkdownWriter; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.SourceAndPage; -import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.MarkdownConverter; import dev.ebullient.convert.tools.ToolsIndex; -import dev.ebullient.convert.tools.dnd5e.ItemProperty.CustomItemProperty; -import dev.ebullient.convert.tools.dnd5e.ItemProperty.PropertyEnum; -import dev.ebullient.convert.tools.dnd5e.ItemType.CustomItemType; -import dev.ebullient.convert.tools.dnd5e.ItemType.ItemEnum; -import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; +import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField; import dev.ebullient.convert.tools.dnd5e.Json2QuteRace.RaceFields; -import dev.ebullient.convert.tools.dnd5e.PsionicType.CustomPsionicType; +import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.SkillOrAbility.CustomSkillOrAbility; import dev.ebullient.convert.tools.dnd5e.SpellSchool.CustomSpellSchool; -import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; +import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewFields; +import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewMetaTypes; public class Tools5eIndex implements JsonSource, ToolsIndex { private static Tools5eIndex instance; @@ -58,47 +55,78 @@ public static Tools5eIndex getInstance() { final CompendiumConfig config; + // Initialization private final Map nodeIndex = new HashMap<>(); - private Map variantIndex = null; - private Map filteredIndex = null; - - private final Map optFeatureIndex = new HashMap<>(); - private final Map homebrewMetaTypes = new HashMap<>(); private final Map> subraceIndex = new HashMap<>(); private final Map> tableIndex = new HashMap<>(); + + private Map filteredIndex = null; + private final Map aliases = new HashMap<>(); - private final Map classRoot = new HashMap<>(); - private final Map> spellClassIndex = new HashMap<>(); + private final Map reprints = new HashMap<>(); + private final Map subraceMap = new HashMap<>(); private final Map nameToLink = new HashMap<>(); + private final Map> spellClassIndex = new HashMap<>(); + private final Set srdKeys = new HashSet<>(); - private final Set familiarKeys = new HashSet<>(); final Tools5eJsonSourceCopier copier = new Tools5eJsonSourceCopier(this); - - Pattern classFeaturePattern; - Pattern subclassFeaturePattern; + final OptionalFeatureIndex optFeatureIndex = new OptionalFeatureIndex(this); + final Tools5eHomebrewIndex homebrewIndex = new Tools5eHomebrewIndex(this); // index state - HomebrewMetaTypes homebrew = null; + volatile HomebrewMetaTypes homebrew = null; public Tools5eIndex(CompendiumConfig config) { this.config = config; instance = this; } + public Tools5eIndex importTree(String filename, JsonNode node) { + if (!node.isObject() || homebrewIndex.addHomebrewSourcesIfPresent(filename, node)) { + // defer reading contents of homebrew until after we've indexed the rest + // see prepare() / importHomebrewTree() + return this; + } + + // user configuration + config.readConfigurationIfPresent(node); + + // Index content types + indexTypes(filename, node); + + return this; + } + + private void importHomebrewTree(HomebrewMetaTypes homebrew) { + this.homebrew = homebrew; + try { + // Index content types + indexTypes(homebrew.filename, homebrew.homebrewNode); + Tools5eIndexType.adventureData.withArrayFrom(homebrew.homebrewNode, this::addToIndex); // homebrew + Tools5eIndexType.bookData.withArrayFrom(homebrew.homebrewNode, this::addToIndex); // homebrew + } finally { + this.homebrew = null; + } + } + private void indexTypes(String filename, JsonNode node) { // Reference/Internal Types Tools5eIndexType.backgroundFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.classFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.conditionFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.facilityFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.featFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.itemFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.monsterFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.objectFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.optionalfeatureFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.raceFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.spellFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.trapFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.vehicleFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.language.withArrayFrom(node, this::addToIndex); @@ -113,6 +141,9 @@ private void indexTypes(String filename, JsonNode node) { Tools5eIndexType.subrace.withArrayFrom(node, this::addToSubraceIndex); Tools5eIndexType.monsterTemplate.withArrayFrom(node, this::addToIndex); + + // Class-scoped resources (if the class is left out, the resource is not included) + Tools5eIndexType.subclass.withArrayFrom(node, this::addToIndex); Tools5eIndexType.classfeature.withArrayFrom(node, "classFeature", this::addToIndex); Tools5eIndexType.subclassFeature.withArrayFrom(node, "subclassFeature", this::addToIndex); @@ -132,7 +163,7 @@ private void indexTypes(String filename, JsonNode node) { Tools5eIndexType.psionic.withArrayFrom(node, this::addToIndex); Tools5eIndexType.legendaryGroup.withArrayFrom(node, this::addToIndex); - Tools5eIndexType.optionalfeature.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.optfeature.withArrayFrom(node, "optionalfeature", this::addToIndex); // tables @@ -145,9 +176,10 @@ private void indexTypes(String filename, JsonNode node) { Tools5eIndexType.classtype.withArrayFrom(node, "class", this::addToIndex); Tools5eIndexType.deck.withArrayFrom(node, this::addToIndex); Tools5eIndexType.deity.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.facility.withArrayFrom(node, this::addToIndex); Tools5eIndexType.feat.withArrayFrom(node, this::addToIndex); Tools5eIndexType.hazard.withArrayFrom(node, this::addToIndex); - Tools5eIndexType.item.withArrayFrom(node, "baseitem", this::addToIndex); + Tools5eIndexType.item.withArrayFrom(node, "baseitem", this::addBaseItemToIndex); Tools5eIndexType.item.withArrayFrom(node, this::addToIndex); Tools5eIndexType.monster.withArrayFrom(node, this::addToIndex); Tools5eIndexType.object.withArrayFrom(node, this::addToIndex); @@ -173,99 +205,6 @@ private void indexTypes(String filename, JsonNode node) { } } - public Tools5eIndex importTree(String filename, JsonNode node) { - if (!node.isObject() || addHomebrewSourcesIfPresent(filename, node)) { - return this; - } - // user configuration - config.readConfigurationIfPresent(node); - - // Index content types - indexTypes(filename, node); - - // base items are special: add an additional flag - Tools5eIndexType.item.withArrayFrom(node, "baseitem", (type, x) -> { - TtrpgValue.indexBaseItem.setIn(x, BooleanNode.TRUE); - }); - - return this; - } - - private void importHomebrewTree(HomebrewMetaTypes homebrew) { - this.homebrew = homebrew; - try { - // Index content types - indexTypes(homebrew.filename, homebrew.homebrewNode); - Tools5eIndexType.adventureData.withArrayFrom(homebrew.homebrewNode, this::addToIndex); // homebrew - Tools5eIndexType.bookData.withArrayFrom(homebrew.homebrewNode, this::addToIndex); // homebrew - } finally { - this.homebrew = null; - } - } - - private boolean addHomebrewSourcesIfPresent(String filename, JsonNode node) { - JsonNode sources = SourceField._meta.getFieldFrom(node, HomebrewFields.sources); - if (sources == null || sources.size() == 0) { - return false; - } - String json = HomebrewFields.json.getTextOrNull(sources.get(0)); - if (json == null) { - tui().errorf("Source does not define json id: %s", sources.get(0)); - return false; - } - TtrpgConfig.includeAdditionalSource(json); - - HomebrewMetaTypes metaTypes = new HomebrewMetaTypes(json, filename, node); - for (JsonNode source : iterableElements(sources)) { - String fullName = HomebrewFields.full.getTextOrEmpty(source); - String abbreviation = HomebrewFields.abbreviation.getTextOrEmpty(source); - json = HomebrewFields.json.getTextOrEmpty(source); - if (fullName == null) { - tui().warnf("Homebrew source %s missing full name: %s", json, fullName); - } - // add homebrew to known sources - if (TtrpgConfig.addHomebrewSource(fullName, json, abbreviation)) { - // one homebrew file may include multiple sources, the same mapping applies to - // all - HomebrewMetaTypes old = homebrewMetaTypes.put(json, metaTypes); - if (old != null) { - tui().errorf("Shared homebrew id: %s and %s", old.filename, metaTypes.filename); - } - } else { - tui().errorf("🍺 Skipping homebrew id %s from %s; duplicate source id", json, metaTypes.filename); - } - } - - JsonNode featureTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.optionalFeatureTypes); - JsonNode spellSchools = SourceField._meta.getFieldFrom(node, HomebrewFields.spellSchools); - JsonNode psionicTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.psionicTypes); - JsonNode skillTypes = HomebrewFields.skill.getFrom(node); - if (featureTypes != null || spellSchools != null || psionicTypes != null || skillTypes != null) { - for (Entry entry : iterableFields(featureTypes)) { - metaTypes.setOptionalFeatureType(entry.getKey(), entry.getValue().asText()); - } - // ignoring short names for spell schools and psionic types - for (Entry entry : iterableFields(spellSchools)) { - metaTypes.setSpellSchool(entry.getKey(), - new CustomSpellSchool(HomebrewFields.full.getTextOrEmpty(entry.getValue()))); - } - for (Entry entry : iterableFields(psionicTypes)) { - metaTypes.setPsionicType(entry.getKey(), - tui().readJsonValue(entry.getValue(), CustomPsionicType.class)); - } - for (JsonNode skill : iterableElements(skillTypes)) { - String skillName = SourceField.name.getTextOrEmpty(skill); - if (skillName == null) { - tui().warnf("Homebrew skill type missing name: %s", skill); - continue; - } - metaTypes.setSkillType(skillName, skill); - } - } - Tools5eSources.addFonts(SourceField._meta.getFrom(node), HomebrewFields.fonts); - return true; - } - void addToSubraceIndex(Tools5eIndexType type, JsonNode node) { String raceName = RaceFields.raceName.getTextOrThrow(node); String raceSource = RaceFields.raceSource.getTextOrThrow(node); @@ -274,7 +213,12 @@ void addToSubraceIndex(Tools5eIndexType type, JsonNode node) { } void addMagicVariantToIndex(Tools5eIndexType type, JsonNode node) { - MagicVariant.INSTANCE.populateGenericVariant(node); + MagicVariant.populateGenericVariant(node); + addToIndex(type, node); + } + + void addBaseItemToIndex(Tools5eIndexType type, JsonNode node) { + TtrpgValue.indexBaseItem.setIn(node, BooleanNode.TRUE); addToIndex(type, node); } @@ -286,128 +230,78 @@ void addToIndex(Tools5eIndexType type, JsonNode node) { nodeIndex.put(key, node); TtrpgValue.indexInputType.setIn(node, type.name()); TtrpgValue.indexKey.setIn(node, key); + + // Homebrew files are ingested in a lump: + // if homebrew is set, then we're reading a homebrew file TtrpgValue.isHomebrew.setIn(node, homebrew != null); + if (homebrew != null) { + homebrew.addElement(type, key, node); + } - if (type == Tools5eIndexType.classtype - && !booleanOrDefault(node, "isReprinted", false)) { - String[] parts = key.split("\\|"); - if (!parts[2].contains("ua")) { - String lookupKey = String.format("%s|%s|", parts[0], parts[1]); - classRoot.put(lookupKey, key); + switch (type) { + case optfeature -> { + // add while we're ingesting (homebrew or not) + optFeatureIndex.addOptionalFeature(key, node, homebrew); } - } else if (type == Tools5eIndexType.itemProperty && homebrew != null) { - // lookup by abbreviation - String[] parts = key.split("\\|"); - homebrew.itemProperties.put(parts[1].toLowerCase(), new CustomItemProperty(node)); - } else if (type == Tools5eIndexType.itemType && homebrew != null) { - // lookup by abbreviation - String[] parts = key.split("\\|"); - if (SourceField.name.existsIn(node)) { - homebrew.itemTypes.put(parts[1].toLowerCase(), new CustomItemType(node)); - } else { - tui().errorf("Item type %s does not specify name: %s", key, node); + case subclass -> { + // add alias with subclass shortname + String lookupKey = Tools5eIndexType.getSubclassKey( + Tools5eFields.className.getTextOrEmpty(node), + Tools5eFields.classSource.getTextOrEmpty(node), + Tools5eFields.shortName.getTextOrEmpty(node), + SourceField.source.getTextOrEmpty(node)); + addAlias(lookupKey, key); } - } else if (type == Tools5eIndexType.optionalfeature) { - String lookup = null; - for (String ft : toListOfStrings(node.get("featureType"))) { - try { - boolean homebrewType = homebrew != null && homebrew.getOptionalFeatureType(ft) != null; - // scope the optional feature key (homebrew may conflict) - String featKey = (homebrewType - ? ft + "-" + homebrew.jsonKey - : ft).toLowerCase(); - - optFeatureIndex.computeIfAbsent(featKey, k -> new OptionalFeatureType(ft, k, homebrew, index())).add(node); - lookup = lookup == null ? featKey : lookup; - } catch (IllegalArgumentException e) { - tui().error(e, "Unable to define optional feature"); + case table, tableGroup -> { + SourceAndPage sp = new SourceAndPage(node); + tableIndex.computeIfAbsent(sp, k -> new ArrayList<>()).add(node); + } + case language -> { + if (HomebrewFields.fonts.existsIn(node)) { + Tools5eSources.addFonts(node, HomebrewFields.fonts); } } - if (lookup != null) { - ((ObjectNode) node).put("typeLookup", lookup); + case adventure, book -> { + String id = SourceField.id.getTextOrEmpty(node); + String source = SourceField.source.getTextOrEmpty(node); + if (!id.equals(source) && type == Tools5eIndexType.book) { + // adventures can be subdivided from books. Don't map source/id for those + TtrpgConfig.sourceToIdMapping(source, id); + } } - } else if (type == Tools5eIndexType.subclass) { - String lookupKey = Tools5eIndexType.getSubclassKey( - getTextOrEmpty(node, "className"), getTextOrEmpty(node, "classSource"), - getTextOrEmpty(node, "shortName"), getTextOrEmpty(node, "source")); - // add subclass to alias. Referenced from spells - addAlias(lookupKey, key); - } else if (type == Tools5eIndexType.table || type == Tools5eIndexType.tableGroup) { - SourceAndPage sp = new SourceAndPage(node); - tableIndex.computeIfAbsent(sp, k -> new ArrayList<>()).add(node); - } else if (type == Tools5eIndexType.language && HomebrewFields.fonts.existsIn(node)) { - Tools5eSources.addFonts(node, HomebrewFields.fonts); - } else if (type == Tools5eIndexType.book || type == Tools5eIndexType.adventure) { - String id = SourceField.id.getTextOrEmpty(node); - String source = SourceField.source.getTextOrEmpty(node); - if (!id.equals(source) && type == Tools5eIndexType.book) { - // adventures can be subdivided from books. Don't map source/id for those - TtrpgConfig.sourceToIdMapping(source, id); + case itemGroup -> { + addAlias(key.replace("itemgroup|", "item|"), key); } - } - addSrdEntry(key, node); - if (node.has("familiar")) { - familiarKeys.add(key); - } - } - - void addSrdEntry(String key, JsonNode node) { - if (node.has("srd")) { - JsonNode srd = node.get("srd"); - if (srd.isTextual()) { - String srdKey = key.replace(SourceField.name.getTextOrThrow(node).toLowerCase(), - Tools5eFields.srd.getTextOrThrow(node).toLowerCase()); - addAlias(srdKey, key); - srdKeys.add(srdKey); - } else { - srdKeys.add(key); + default -> { } } - } - - void addAlias(String key, String alias) { - if (key.equals(alias)) { - return; - } - String old = aliases.putIfAbsent(key, alias); - if (old != null && !old.equals(alias)) { - tui().errorf("Oops! Duplicate simple key: %s -> %s", key, alias); - } - } - List getAliasesTo(String targetKey) { - return aliases.entrySet().stream() - .filter(e -> e.getValue().equals(targetKey)) - .map(Entry::getKey) - .collect(Collectors.toList()); - } - - void setClassFeaturePatterns() { - String allowed = config.getAllowedSourcePattern(); - classFeaturePattern = Pattern.compile(classFeature_1 + allowed + classFeature_2 + allowed + "?"); - subclassFeaturePattern = Pattern - .compile(subclassFeature_1 + allowed + subclassFeature_2 + allowed + subclassFeature_3 + allowed + "?"); + addSrdEntry(key, node); } public void prepare() { - if (variantIndex != null || filteredIndex != null) { + if (filteredIndex != null) { return; } + + tui().progressf("Adding default aliases"); + // Add missing/frequently-used aliases TtrpgConfig.addDefaultAliases(aliases); - // Find subrace variants (add to index) - findRaceVariants(); - + tui().progressf("Importing homebrew sources"); // Properly import homebrew sources - for (HomebrewMetaTypes homebrew : homebrewMetaTypes.values()) { - importHomebrewTree(homebrew); - } + homebrewIndex.importBrew(this::importHomebrewTree); - variantIndex = new HashMap<>(); + tui().debugf("Preparing index using configuration:\n%s", Tui.jsonStringify(config)); - setClassFeaturePatterns(); + tui().progressf("Adding subraces (2014)"); + // Add subraces to index + defineSubraces(); + tui().progressf("Resolving copies and link sources"); + + // For each node: handle copies, link sources for (Entry entry : nodeIndex.entrySet()) { String key = entry.getKey(); JsonNode jsonSource = entry.getValue(); @@ -415,81 +309,123 @@ public void prepare() { // check for / manage copies first. Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); jsonSource = copier.handleCopy(type, jsonSource); - entry.setValue(jsonSource); // update with resolved copy + // Pre-creation of sources.. if (type == Tools5eIndexType.adventureData || type == Tools5eIndexType.bookData) { - copySources(type, jsonSource); + // changes name and things used when constructing sources + linkSources(type, jsonSource); } - TtrpgValue.indexKey.setIn(jsonSource, key); - Tools5eSources sources = Tools5eSources.constructSources(jsonSource); + Tools5eSources.constructSources(key, jsonSource); + entry.setValue(jsonSource); // update with resolved copy - if (type == Tools5eIndexType.monsterTemplate || - type == Tools5eIndexType.deity) { - // traits (templates) groups are pulled in my monsters - // deities are a hot mess - continue; + // Post-creation of sources.. + switch (type) { + case classtype, subclass -> optFeatureIndex.amendSources(key, jsonSource, homebrewIndex); + case optfeature -> optFeatureIndex.amendSources(key, jsonSource, homebrewIndex); + case classfeature -> { + String classKey = Tools5eIndexType.classtype.fromChildKey(key); + JsonNode classNode = nodeIndex.get(classKey); + if (classNode != null) { + JsonNode featureKeys = Tools5eFields.classFeatureKeys.ensureArrayIn(classNode).add(key); + Tools5eFields.classFeatureKeys.setIn(jsonSource, featureKeys); + } + } + case subclassFeature -> { + // don't follow reprints, just go from shortname to subclass name + String scKey = Tools5eIndexType.subclass.fromChildKey(key); + scKey = getAliasOrDefault(scKey, false); + JsonNode scNode = nodeIndex.get(scKey); + if (scNode != null) { + JsonNode featureKeys = Tools5eFields.classFeatureKeys.ensureArrayIn(scNode).add(key); + Tools5eFields.classFeatureKeys.setIn(jsonSource, featureKeys); + } + } + default -> { + } } + } // end for each entry - // Find variants - List variants = findVariants(key, jsonSource); - if (variants.size() > 1) { - tui().debugf("%s variants found for %s", variants.size(), key); - } - variants.forEach(v -> { - if (variants.size() > 1) { - tui().debugf("\t%s", v.key); - } - // store unique key / construct sources for variants - TtrpgValue.indexKey.setIn(v.node, v.key); - Tools5eSources.constructSources(v.node); + filteredIndex = new HashMap<>(nodeIndex.size()); - JsonNode old = variantIndex.put(v.key, v.node); - if (old != null && !old.equals(v.node)) { - tui().errorf("Duplicate key: %s%nold: %s%nnew: %s", v.key, old, v.node); - } - }); + tui().progressf("Applying source filters"); - if (type == Tools5eIndexType.classtype || type == Tools5eIndexType.subclass) { - for (JsonNode ofp : iterableElements(ClassFields.optionalfeatureProgression.getFrom(jsonSource))) { - for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) { - OptionalFeatureType oft = getOptionalFeatureType(featureType, sources.primarySource()); - if (oft != null) { - oft.appendSources(sources); - } - } - } + BiConsumer logThis = (msgType, msg) -> { + if (msgType == Msg.TARGET) { + tui().debugf(msgType, msg); + } else { + tui().logf(msgType, msg); } - } + }; - // Find/Merge deities (this will also exclude based on sources) - List deities = findDeities(nodeIndex.entrySet().stream() - .filter(e -> Tools5eIndexType.getTypeFromKey(e.getKey()) == Tools5eIndexType.deity) - .map(e -> new Tuple(e.getKey(), e.getValue(), - String.format("%s-%s", - e.getValue().get("name").asText(), - e.getValue().get("pantheon").asText()))) - .collect(Collectors.toList())); - deities.forEach(v -> { - JsonNode old = variantIndex.put(v.key, v.node); - if (old != null) { - tui().errorf("Duplicate key: %s", v.key); + // Apply include/exclude rules & source filters + for (var e : nodeIndex.entrySet()) { + String key = e.getKey(); + Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); + Tools5eSources sources = Tools5eSources.findSources(key); + Msg msgType = sources.filterRuleApplied() ? Msg.TARGET : Msg.FILTER; + + if (type.isFluffType()) { + // no-op + } else if (type.isDependentType() && msgType != Msg.TARGET) { + // keep dependent types unless there is a specific rule + filteredIndex.put(key, e.getValue()); + } else if (sources.includedByConfig()) { + filteredIndex.put(key, e.getValue()); + logThis.accept(msgType, "(KEEP) " + key); + } else if (type.isOutputType()) { + logThis.accept(msgType, "(drop) " + key); } - }); + } + + // Remove reprints based on included sources (and reprint behavior) + // If someone includes MM, but not MPMM, you want the MM version + tui().progressf("Resolving reprints"); + filteredIndex.entrySet().removeIf(e -> isReprinted(e.getKey(), e.getValue())); - // Exclude items after we've created variants and handled copies - filteredIndex = new TreeMap<>(); - variantIndex.entrySet().stream() - .filter(e -> !isReprinted(e.getKey(), e.getValue())) - .filter(e -> keyIsIncluded(e.getKey(), e.getValue())) - .forEach(e -> filteredIndex.put(e.getKey(), e.getValue())); + // Follow inclusion of certain types to remove additional related elements + tui().progressf("Removing dependent and dangling resources"); + filteredIndex.keySet().removeIf(k -> otherwiseExcluded(k)); - // And finally, create an index of classes/subclasses/feats for spells - // based on included sources & avaiable spells. - buildSpellSourceIndex(); + // After we've removed reprints and otherwise excluded items, + // let's generate variants for monsters and magic items + + tui().progressf("Populating variants"); + + // Find remaining/included base items + List baseItems = filteredIndex.values().stream() + .filter(n -> TtrpgValue.indexBaseItem.booleanOrDefault(n, false)) + .filter(n -> !ItemField.packContents.existsIn(n)) + .toList(); + + // Find variant nodes (magic items, monsters) + List variantNodes = filteredIndex.entrySet().stream() + .filter(e -> Tools5eIndexType.getTypeFromKey(e.getKey()).hasVariants()) + .flatMap(e -> findVariants(e.getKey(), e.getValue(), baseItems).stream()) + .toList(); + + // Add the variants back into the index, which may replace the original + variantNodes.forEach(t -> filteredIndex.put(t.key, t.node)); + + // Deities have their own glorious reprint mess, which we only need to deal with + // when we aren't hoarding all the things. + if (config.reprintBehavior() != ReprintBehavior.all) { + tui().progressf("Dealing with deities"); + + List allDeities = filteredIndex.entrySet().stream() + .filter(e -> Tools5eIndexType.getTypeFromKey(e.getKey()) == Tools5eIndexType.deity) + .map(e -> new Tuple(e.getKey(), e.getValue())) + .toList(); + + // Remove deities that should be removed (superceded) + Json2QuteDeity.findDeitiesToRemove(allDeities).forEach(k -> { + tui().logf(Msg.DEITY, "(drop | superseded) %s", k); + filteredIndex.remove(k); + }); + } } - public void findRaceVariants() { + private void defineSubraces() { for (Entry> entry : subraceIndex.entrySet()) { String raceKey = entry.getKey(); JsonNode jsonSource = nodeIndex.get(raceKey); @@ -500,7 +436,7 @@ public void findRaceVariants() { Json2QuteRace.prepareBaseRace(this, jsonSource, inputSubraces); if (inputSubraces.size() > 1) { - tui().debugf("%s subraces found for %s", inputSubraces.size(), raceKey); + tui().logf(Msg.RACES, "%s subraces found for %s", inputSubraces.size(), raceKey); } for (JsonNode sr : inputSubraces) { @@ -526,8 +462,10 @@ public void findRaceVariants() { lookupKey = String.format("race|%s (%s)|%s", parts[2], parts[1], source).toLowerCase(); } } - addAlias(lookupKey, srKey); - tui().debugf("\t%s :: %s", lookupKey, srKey); + // lookups from race to subrace are necessary, but can conflict with reprints/aliases + // keep them separate (still used in getAliasOrDefault) + subraceMap.put(lookupKey, srKey); + tui().logf(Msg.RACES, "\t%s :: %s", lookupKey, srKey); } Json2QuteRace.updateBaseRace(this, jsonSource, inputSubraces, subraces); @@ -535,183 +473,173 @@ public void findRaceVariants() { subraceIndex.clear(); } - private List findDeities(List allDeities) { - List reverseOrder = List.of("dsotdq", "erlw", "mtf", "vgm", "scag", "dmg", "phb"); - List result = new ArrayList<>(); - Map> booklist = new HashMap<>(); - - // We have to build the reprint index ourselves in print order. - // If it isn't in one of the printed sources that matters, it goes right to the outbox - // otherwise, we put it in buckets by source (with aliases) - for (Tuple t : allDeities) { - if (keyIsIncluded(t.key, t.node)) { - String src = t.getSource().toLowerCase(); - if (reverseOrder.contains(src)) { - booklist.computeIfAbsent(src, k -> new ArrayList<>()).add(t); - } else { - result.add(t); - } - } - } - - // Now go through buckets in reverse order. - Map reprintIndex = new HashMap<>(); - for (String book : reverseOrder) { - List deities = booklist.remove(book); - if (deities == null || deities.isEmpty()) { - continue; - } - if (reprintIndex.isEmpty()) { // most recent bucket. Keep all. - deities.forEach(t -> reprintIndex.put(t.getName(), t)); - continue; - } - - for (Tuple t : deities) { - String lookup = t.node.has("reprintAlias") - ? t.node.get("reprintAlias").asText() - : t.getName(); - - if (reprintIndex.containsKey(lookup)) { - // skip it. It has already been reprinted as a new thing - } else { - reprintIndex.put(lookup, t); - } - } - } - result.addAll(reprintIndex.values()); - return result; - } - - List findVariants(String key, JsonNode jsonSource) { + List findVariants(String key, JsonNode jsonSource, List baseItems) { Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); - if (type == Tools5eIndexType.itemGroup) { - return Json2QuteItem.findGroupVariant(instance, type, key, jsonSource, copier); - } else if (type == Tools5eIndexType.magicvariant) { - return MagicVariant.INSTANCE.findSpecificVariants(this, type, key, jsonSource, copier); + if (type == Tools5eIndexType.magicvariant) { + return MagicVariant.findSpecificVariants(this, type, key, jsonSource, copier, baseItems); } else if (type == Tools5eIndexType.monster) { return Json2QuteMonster.findMonsterVariants(this, type, key, jsonSource); - } else if (key.contains("splugoth the returned") || key.contains("prophetess dran")) { - // Fix. - ObjectNode copy = (ObjectNode) copier.copyNode(jsonSource); - copy.put("isNpc", true); - return List.of(new Tuple(key, copy)); } return List.of(new Tuple(key, jsonSource)); } - private void buildSpellSourceIndex() { - // index key in resources/convertData.json - JsonNode indexNode = TtrpgConfig.readIndex("spell-source"); - if (indexNode.isNull()) { - return; + /** + * Behavior of looking for reprints is changed by reprint behavior defined in configuration. + * The default is "newest", which will always collapse reprints into the newest source. + * + * @param finalKey + * @param jsonSource + * @return + */ + private boolean isReprinted(String finalKey, JsonNode jsonSource) { + // This method assumes that excluded sources are already filtered out + if (config.reprintBehavior() == ReprintBehavior.all) { + return false; // ignore reprints and include everything ;) } - // structure is interesting: - // "spell source" -> "spell" -> ("class"|"subclass"|"feat") (walk your way through key construction.. ) - for (Entry sourceSpellMap : iterableFields(indexNode)) { - String spellSource = sourceSpellMap.getKey(); - for (Entry spellAssociation : iterableFields(sourceSpellMap.getValue())) { - String spellName = spellAssociation.getKey(); - String spellKey = Tools5eIndexType.spell.createKey(spellName, spellSource); - if (!filteredIndex.containsKey(spellKey)) { - // Spell is excluded - continue; - } - Set spellClassList = spellClassIndex.computeIfAbsent(spellKey, k -> new HashSet<>()); - JsonNode spellMap = spellAssociation.getValue(); - for (Entry sourceClassMap : iterableFields(spellMap.get("class"))) { - String classSource = sourceClassMap.getKey(); - for (String className : iterableFieldNames(sourceClassMap.getValue())) { - String classKey = index() - .getAliasOrDefault(Tools5eIndexType.classtype.createKey(className, classSource)); - if (isIncluded(classKey)) { - spellClassList.add(classKey); - } + Tools5eSources sources = Tools5eSources.findSources(jsonSource); + if (sources.filterRuleApplied()) { + return false; // keep because a rule says so + } + + if (SourceField.reprintedAs.existsIn(jsonSource)) { + // This was reprinted in one or more other sources. + // If any of those sources have been included, then skip this one + // in favor of the (newer) reprint. + // "reprintedAs": [ "Deep Gnome|MPMM" ] + // "reprintedAs": [ + // { + // "uid": "Unarmed Strike|XPHB", + // "tag": "variantrule" + // } + // ], + for (JsonNode reprintedAs : SourceField.reprintedAs.iterateArrayFrom(jsonSource)) { + String rawKey = reprintedAs.isObject() + ? SourceField.uid.getTextOrThrow(reprintedAs) + : reprintedAs.asText(); + + Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(finalKey); + if (reprintedAs.isObject()) { + String tag = SourceField.tag.getTextOrNull(reprintedAs); + if (tag != null) { + type = Tools5eIndexType.fromText(tag); } } - for (Entry sourceClassSubclassMap : iterableFields(spellMap.get("subclass"))) { - String classSource = sourceClassSubclassMap.getKey(); // PHB, XGE, etc - if (!sourceIncluded(classSource)) { - // skip it + + // Reprints can also be reprints; follow the alias/reprint chain + String reprintKey = getAliasOrDefault(type.fromTagReference(rawKey)); + JsonNode reprint = nodeIndex.get(reprintKey); + if (reprint == null) { + if (type == Tools5eIndexType.subrace) { + reprintKey = getAliasOrDefault(Tools5eIndexType.race.fromTagReference(rawKey)); + reprint = nodeIndex.get(reprintKey); + } + if (reprint == null) { + tui().warnf(Msg.UNRESOLVED, "%s: unresolved reprint source %s", finalKey, reprintKey); continue; } - for (Entry classMap : iterableFields(sourceClassSubclassMap.getValue())) { - String className = classMap.getKey(); // Bard, Cleric, etc - for (Entry sourceSubclassMap : iterableFields(classMap.getValue())) { - String subclassSource = sourceSubclassMap.getKey(); // PHB, XGE, etc - if (!sourceIncluded(subclassSource)) { - // skip it - continue; - } - for (Entry subclassMap : iterableFields(sourceSubclassMap.getValue())) { - String subclassName = subclassMap.getKey(); // College of Lore, etc - String subclassKey = index() - .getAliasOrDefault(Tools5eIndexType.getSubclassKey( - className, classSource, subclassName, subclassSource)); - if (isIncluded(subclassKey)) { - spellClassList.add(subclassKey); - } - } + } + + Tools5eSources reprintSources = Tools5eSources.findSources(reprint); + if (reprintSources.includedByConfig()) { + if (config.reprintBehavior() == ReprintBehavior.edition) { + // Only follow the reprint chain if it's in the same edition + String sourceEdition = sources.edition(); + String reprintEdition = reprintSources.edition(); + if (reprintEdition != null && sourceEdition != null && !reprintEdition.equals(sourceEdition)) { + tui().logf(Msg.REPRINT, "(SKIP | edition) %s: ignoring reprint as %s", + finalKey, reprintKey); + continue; } } + // Otherwise, we have a "newer" reprint that should be used instead + tui().logf(Msg.REPRINT, "(drop | reprinted) %s ==> %s", finalKey, reprintKey); + // 1) create an alias mapping the old key to the reprinted key + reprints.put(finalKey, reprintKey); + // 2) add the sources of the reprint to the sources of the original (for later linking) + reprintSources.addReprint(sources); + return true; } } } + if (SourceField.isReprinted.booleanOrDefault(jsonSource, false)) { + tui().logf(Msg.REPRINT, "(drop | isReprint) %s", finalKey); + return true; // the reprint will be used instead of this one. + } + return false; // keep } - boolean isReprinted(String finalKey, JsonNode jsonSource) { - if (jsonSource.has("reprintedAs")) { - // "reprintedAs": [ "Deep Gnome|MPMM" ] - // If any reprinted source is included, skip this in favor of the reprint - for (Iterator i = jsonSource.withArray("reprintedAs").elements(); i.hasNext();) { - String reprint = i.next().asText(); - String[] ra = reprint.split("\\|"); - if (sourceIncluded(ra[1])) { - Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(finalKey); - String primarySource = jsonSource.get("source").asText().toLowerCase(); - String reprintKey = type + "|" + reprint.toLowerCase(); - if (type == Tools5eIndexType.subrace && !variantIndex.containsKey(reprintKey)) { - reprintKey = Tools5eIndexType.race + "|" + reprint.toLowerCase(); - if (!variantIndex.containsKey(reprintKey)) { - reprintKey = finalKey.replace(primarySource, ra[1]).toLowerCase(); - } - } - if (!variantIndex.containsKey(reprintKey)) { - reprintKey = aliases.get(reprintKey); - if (reprintKey == null) { - tui().errorf("Unable to find reprint of %s: %s", finalKey, reprint); - return false; - } - } - tui().debugf("📰 Skipping %s; Reprinted as %s", finalKey, reprintKey); - // the reprint will be used instead (exclude this one) - // include an alias mapping the old key to the reprinted key - addAlias(finalKey, reprintKey); - return true; - } + /** + * Filter sub-resources based on the inclusion of the parent resource. + * + * @return true if resource has a parent, and that parent is excluded + */ + private boolean otherwiseExcluded(String key) { + // If a class is excluded, specific classfeatures, optional features, + // subclasses, and subclassfeatures should also be removed + // (unless a specific rule says otherwise). + Tools5eSources sources = Tools5eSources.findSources(key); + if (sources.filterRuleApplied()) { + return false; // keep because a rule says so (we already logged these) + } + + Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); + return switch (type) { + case card -> removeIfParentExcluded(key, Tools5eIndexType.deck, Msg.DECK); + case classfeature, subclassFeature -> removeIfParentExcluded(key, Tools5eIndexType.classtype, + Msg.CLASSES); + case optfeature, optionalFeatureTypes -> removeUnusedOptionalFeatures(type, key); + case subclass -> !sources.includedByConfig() || removeIfParentExcluded(key, Tools5eIndexType.classtype, + Msg.CLASSES); + case subrace -> !sources.includedByConfig() || removeIfParentExcluded(key, Tools5eIndexType.race, Msg.RACES); + default -> false; // does not have a parent + }; + } + + private boolean removeIfParentExcluded(String key, Tools5eIndexType parentType, Msg msg) { + String parentKey = parentType.fromChildKey(key); + Tools5eSources parentSources = Tools5eSources.findSources(parentKey); + if (parentSources == null) { + tui().warnf(Msg.UNRESOLVED, "%35s :: unresolved parent of [%s]", parentKey, key); + // allow for corrections (aliases), not reprints + parentKey = getAliasOrDefault(parentKey, false); + parentSources = Tools5eSources.findSources(parentKey); + if (parentSources == null) { + return true; // has a parent, it is missing (dangling resource) } } - // This true/false flag tends to comes from UA resources (when printed in official source) - if (booleanOrDefault(jsonSource, "isReprinted", false)) { - tui().debugf("🗞️ Skipping %s (has been reprinted)", finalKey); - if (finalKey.startsWith("classtype")) { - String[] parts = finalKey.split("\\|"); - String lookupKey = String.format("%s|%s|", parts[0], parts[1]); - String reprintKey = classRoot.get(lookupKey); - if (reprintKey == null) { - lookupKey = String.format("%s|%s|", parts[0], parts[1].replaceAll("\\s*\\(.*", "")); - reprintKey = classRoot.get(lookupKey); - } - if (reprintKey != null) { - addAlias(finalKey, reprintKey); - } + boolean included = parentSources.includedByConfig(); + if (!included) { + tui().debugf(msg, "(drop) %43s :: %s", parentKey, key); + } + return !included; + } + + private boolean removeUnusedOptionalFeatures(Tools5eIndexType type, String key) { + OptionalFeatureType oft = optFeatureIndex.get(type, key); + Tools5eSources oftSources = oft.getSources(); + + // the feature type sources are amended by consuming classes/subclasses + boolean included = oft.inUse() && oftSources.includedByConfig(); + var msgType = Msg.FEATURETYPE; + + if (included && type == Tools5eIndexType.optfeature) { + msgType = Msg.FEATURE; + // If an optional feature (rather than a type), + // and the optional feature source is different from the parent source, + // then we need to see if the feature source is included + Tools5eSources ofSources = Tools5eSources.findSources(key); + if (!ofSources.primarySource().equals(oftSources.primarySource())) { + included = ofSources.includedByConfig(); } - return true; // the reprint will be used instead of this one. } - return false; + if (!included) { + tui().debugf(msgType, "(drop) %43s :: %s", oft.getKey(), key); + } + return !included; } public boolean notPrepared() { - return filteredIndex == null || variantIndex == null; + return filteredIndex == null; } public List classElementsMatching(Tools5eIndexType type, String className, String classSource) { @@ -733,24 +661,68 @@ private List nodesMatching(String pattern) { .collect(Collectors.toList()); } - public String getAlias(String key) { - return aliases.get(key); + private void addSrdEntry(String key, JsonNode node) { + if (Tools5eSources.isSrd(node)) { + String srdName = Tools5eSources.srdName(node); + if (srdName != null) { + // If there is a generic/SRD name, replace the specific name in the key + String srdKey = key.replace(SourceField.name.getTextOrThrow(node).toLowerCase(), + srdName.toLowerCase()); + // Add an alias for the srd/generic-form of the name + addAlias(srdKey, key); + srdKeys.add(srdKey); + } else { + srdKeys.add(key); + } + } + } + + void addAlias(String key, String alias) { + if (key.equals(alias)) { + return; + } + String old = aliases.putIfAbsent(key, alias); + if (old != null && !old.equals(alias)) { + tui().warnf("Oops! Duplicate simple key: %s; old: %s; new: %s", key, old, alias); + } + } + + public List getAliasesFor(String targetKey) { + return aliases.entrySet().stream() + .filter(e -> e.getValue().equals(targetKey)) + .map(Entry::getKey) + .collect(Collectors.toList()); } public String getAliasOrDefault(String key) { + return getAliasOrDefault(key, true); + } + + public String getAliasOrDefault(String key, boolean includeReprints) { String previous; String value = key; do { previous = value; - value = aliases.getOrDefault(previous, previous); + + // race -> possible subrace alias + String alias = value.startsWith("race") + ? subraceMap.get(previous) + : null; + + if (includeReprints) { + String reprint = reprints.get(alias == null ? previous : alias); + if (reprint != null) { + alias = reprint; + } + } + + value = alias == null + ? aliases.getOrDefault(previous, previous) + : alias; } while (!value.equals(previous)); return value; } - public boolean isHomebrew() { - return homebrew != null; - } - /** * For subclasses, class features, and subclass features, * cross references come directly from the class definition @@ -760,59 +732,81 @@ public boolean isHomebrew() { * @return referenced JsonNode or null */ public JsonNode getNode(String finalKey) { - if (finalKey == null) { + if (finalKey == null || finalKey.isEmpty()) { return null; } return filteredIndex.get(finalKey); } - public ItemProperty findItemProperty(String abbreviation, Tools5eSources sources) { - if (abbreviation == null || abbreviation.isEmpty()) { + public ItemProperty findItemProperty(String fragment, Tools5eSources sources) { + if (fragment == null || fragment.isEmpty()) { return null; } - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); - ItemProperty prop = PropertyEnum.fromEncodedType(abbreviation); - if (prop == null && meta != null) { - prop = meta.getItemProperty(abbreviation); + if (fragment.contains("|")) { + String finalKey = Tools5eIndexType.itemProperty.fromTagReference(fragment); + return ItemProperty.fromKey(finalKey, this); } - if (prop == null) { - tui().errorf("Unknown property %s for %s", abbreviation, sources); - return new CustomItemProperty(abbreviation); + + // We could have a default property (phb), or we could have a homebrew property + ItemProperty property = homebrewIndex.findHomebrewProperty(fragment, sources); + if (property == null) { + // Then we'll try the default source + String key = Tools5eIndexType.itemProperty.fromTagReference(fragment); + return ItemProperty.fromKey(key, this); } - return prop; + return property; } - public ItemType findItemType(String abbreviation, Tools5eSources sources) { - if (abbreviation == null || abbreviation.isEmpty()) { + public ItemType findItemType(String fragment, Tools5eSources sources) { + if (fragment == null || fragment.isEmpty()) { return null; } - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); - ItemType itemType = ItemEnum.fromEncodedValue(abbreviation); - if (itemType == null && meta != null) { - itemType = meta.getItemType(abbreviation); + if (fragment.contains("|")) { + String finalKey = Tools5eIndexType.itemType.fromTagReference(fragment); + return ItemType.fromKey(finalKey, this); } - if (itemType == null) { - tui().errorf("Unknown item type %s", abbreviation); - return new CustomItemType(abbreviation); + // We could have a default property (phb), or we could have a homebrew property + ItemType type = homebrewIndex.findHomebrewType(fragment, sources); + if (type == null) { + // Then we'll try the default source + String key = Tools5eIndexType.itemType.fromTagReference(fragment); + return ItemType.fromKey(key, this); } - return itemType; + return type; + } + + public ItemMastery findItemMastery(String fragment, Tools5eSources sources) { + if (fragment == null || fragment.isEmpty()) { + return null; + } + if (fragment.contains("|")) { + String finalKey = Tools5eIndexType.itemMastery.fromTagReference(fragment); + return ItemMastery.fromKey(finalKey, this); + } + // We could have a default property, or we could have a homebrew property + ItemMastery mastery = homebrewIndex.findHomebrewMastery(fragment, sources); + if (mastery == null) { + // Then we'll try the default source + String key = Tools5eIndexType.itemMastery.fromTagReference(fragment); + return ItemMastery.fromKey(key, this); + } + return mastery; } public HomebrewMetaTypes getHomebrewMetaTypes(Tools5eSources sources) { - return homebrewMetaTypes.get(sources.primarySource()); + return homebrewIndex.getHomebrewMetaTypes(sources); } public SkillOrAbility findSkillOrAbility(String key, Tools5eSources sources) { if (key == null || key.isEmpty()) { return null; } - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); SkillOrAbility skill = SkillOrAbility.fromTextValue(key); - if (skill == null && meta != null) { - skill = meta.getSkillType(key); + if (skill == null) { + skill = homebrewIndex.findHomebrewSkillOrAbility(key, sources); } if (skill == null) { - tui().errorf("Unknown skill or ability %s in %s", key, sources); + tui().warnf(Msg.UNKNOWN, "Unknown skill or ability %s in %s", key, sources); return new CustomSkillOrAbility(key); } return skill; @@ -822,13 +816,12 @@ public SpellSchool findSpellSchool(String abbreviation, Tools5eSources sources) if (abbreviation == null || abbreviation.isEmpty()) { return null; } - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); SpellSchool school = SpellSchool.fromEncodedValue(abbreviation); - if (school == null && meta != null) { - school = meta.getSpellSchool(abbreviation); + if (school == null) { + school = homebrewIndex.findHomebrewSpellSchool(abbreviation, sources); } if (school == null) { - tui().errorf("Unknown spell school %s in %s", abbreviation, sources); + tui().warnf(Msg.UNKNOWN, "Unknown spell school %s in %s", abbreviation, sources); return new CustomSpellSchool(abbreviation); } return school; @@ -868,64 +861,57 @@ public List originNodesMatching(Function filter) { public JsonNode getOriginNoFallback(String finalKey) { JsonNode result = nodeIndex.get(finalKey); - if (result == null) { - result = variantIndex.get(finalKey); - } return result; } public JsonNode getOrigin(String finalKey) { JsonNode result = nodeIndex.get(finalKey); if (result == null) { - result = variantIndex.get(finalKey); - } - if (result == null) { - List target = variantIndex.keySet().stream() + List target = nodeIndex.keySet().stream() .filter(k -> k.startsWith(finalKey)) .collect(Collectors.toList()); if (target.size() == 1) { String lookup = target.get(0); - addAlias(finalKey, lookup); result = nodeIndex.get(lookup); } else if (target.size() > 1) { List reduce = target.stream() .filter(x -> !x.matches(".*\\|ua[^|]*$")) .filter(x -> !x.contains("|dmg")) + .filter(x -> isIncluded(x)) .distinct() .collect(Collectors.toList()); if (reduce.size() > 1) { - tui().debugf("Found several elements for %s: %s", finalKey, reduce); + tui().debugf(Msg.MULTIPLE, "Found several elements for %s: %s", + finalKey, reduce); return null; } else if (reduce.size() == 1) { String lookup = reduce.get(0); result = nodeIndex.get(lookup); - addAlias(finalKey, lookup); } } } return result; } - public JsonNode getOrigin(Tools5eIndexType type, String name, String source) { - String key = type.createKey(name, source); - return getOrigin(key); - } - - public JsonNode getOrigin(Tools5eIndexType type, JsonNode x) { - if (x == null) { - return null; - } - String key = type.createKey(x); - return getOrigin(key); - } - public String linkifyByName(Tools5eIndexType type, String name) { String prefix = String.format("%s|%s|", type, name).toLowerCase(); return nameToLink.computeIfAbsent(prefix, p -> { - List target = variantIndex.keySet().stream() - .filter(k -> k.startsWith(prefix)) - .collect(Collectors.toList()); + // Akin to getAliasOrDefault, but we have to filter by prefix + List target = List.of(); + + if (type == Tools5eIndexType.subrace || type == Tools5eIndexType.race) { + target = subraceMap.keySet().stream() + .filter(k -> k.startsWith(prefix)) + .collect(Collectors.toList()); + } + + if (target.isEmpty()) { + target = reprints.keySet().stream() + .filter(k -> k.startsWith(prefix)) + .collect(Collectors.toList()); + } + if (target.isEmpty()) { target = aliases.keySet().stream() .filter(k -> k.startsWith(prefix)) @@ -933,16 +919,24 @@ public String linkifyByName(Tools5eIndexType type, String name) { } if (target.isEmpty()) { - tui().debugf("🫥 Did not find element for \"%s\" using [%s]", name, prefix); + target = nodeIndex.keySet().stream() + .filter(k -> k.startsWith(prefix)) + .collect(Collectors.toList()); + } + + if (target.isEmpty()) { + tui().debugf(Msg.UNRESOLVED, "unresolved element for \"%s\" using [%s]", name, prefix); return name; } else if (target.size() > 1) { List reduce = target.stream() .filter(x -> !x.matches(".*\\|ua[^|]*$")) .map(x -> getAliasOrDefault(x)) + .filter(x -> isIncluded(x)) .distinct() .collect(Collectors.toList()); if (reduce.size() > 1) { - tui().debugf("Found several elements for %s using [%s]: %s", name, prefix, target); + tui().debugf(Msg.MULTIPLE, "Found several elements for %s using [%s]: %s", + name, prefix, target); return name; } else if (reduce.size() == 1) { target = reduce; @@ -955,6 +949,18 @@ public String linkifyByName(Tools5eIndexType type, String name) { }); } + public boolean customRulesIncluded() { + // The biggest hack of all time (not really). + // I have some custom content for types/property/mastery that + // should be included, but only if: + // 1. No content is included (srdOnly) + // 2. Some combination of basic rules and/or phb/dmg is included + return srdOnly() || + config.sourcesIncluded(List.of( + "srd", "basicRules", "phb", "dmg", + "srd52", "freerules2024", "xphb", "xdmg")); + } + public boolean srdOnly() { return config.noSources(); } @@ -963,36 +969,9 @@ public boolean sourceIncluded(String source) { return config.sourceIncluded(source); } - private boolean keyIsIncluded(String key, JsonNode node) { - - // Check against include/exclude rules (config: included/excluded/all) - Optional rulesAllow = config.keyIsIncluded(key); - if (rulesAllow.isPresent()) { - return rulesAllow.get(); - } - if (config.noSources()) { - return srdKeys.contains(key); - } - - // Special case for class features (match against constructed patterns) - if (key.contains("classfeature|")) { - String featureKey = key.replace("||", "|phb|"); - return classFeaturePattern.matcher(featureKey).matches() || subclassFeaturePattern.matcher(featureKey).matches(); - } - // Familiars - if (key.startsWith("monster|") - && config.groupIsIncluded("familiars") - && familiarKeys.contains(key)) { - return true; - } - - Tools5eSources sources = Tools5eSources.findSources(key); - return cfg().sourceIncluded(sources) || srdKeys.contains(key); - } - - boolean isIncluded(String key) { - String alias = getAlias(key); - return filteredIndex.containsKey(key) || (alias != null && filteredIndex.containsKey(alias)); + public boolean isIncluded(String key) { + String alias = getAliasOrDefault(key); + return filteredIndex.containsKey(key) || filteredIndex.containsKey(alias); } public boolean isExcluded(String key) { @@ -1017,7 +996,7 @@ public Set> includedEntries() { public JsonNode resolveClassFeatureNode(String finalKey) { JsonNode featureNode = getOrigin(finalKey); if (featureNode == null) { - tui().debugf("%s not found", finalKey); + tui().debugf(Msg.UNRESOLVED, "unresolved class feature %s", finalKey); return null; // skip this } return resolveClassFeatureNode(finalKey, featureNode); @@ -1032,27 +1011,15 @@ public Collection classesForSpell(String spellKey) { return spellClassIndex.get(spellKey); } - public OptionalFeatureType getOptionalFeatureType(JsonNode node) { - String lookup = Tools5eFields.typeLookup.getTextOrDefault(node, SourceField.name.getTextOrEmpty(node)); - return optFeatureIndex.get(lookup); + public OptionalFeatureType getOptionalFeatureType(JsonNode optfeatureNode) { + return optFeatureIndex.get(optfeatureNode); } public OptionalFeatureType getOptionalFeatureType(String ft, String source) { if (ft == null) { return null; } - HomebrewMetaTypes metaTypes = homebrewMetaTypes.get(source); - boolean homebrewType = metaTypes != null && metaTypes.getOptionalFeatureType(ft) != null; - - OptionalFeatureType oft = optFeatureIndex.get(ft.toLowerCase()); - if (homebrewType) { - String homebrewScoped = ft + "-" + metaTypes.jsonKey; - OptionalFeatureType homebrewOft = optFeatureIndex.get(homebrewScoped.toLowerCase()); - return homebrewOft == null - ? oft - : homebrewOft; - } - return oft; + return optFeatureIndex.get(ft, source, homebrewIndex); } @Override @@ -1061,9 +1028,12 @@ public void writeFullIndex(Path outputFile) throws IOException { throw new IllegalStateException("Index must be prepared before writing indexes"); } Map allKeys = new TreeMap<>(); - allKeys.put("keys", new TreeSet<>(variantIndex.keySet())); + allKeys.put("keys", new TreeSet<>(nodeIndex.keySet())); allKeys.put("mapping", new TreeMap<>(aliases)); + allKeys.put("reprints", new TreeMap<>(reprints)); allKeys.put("srdKeys", new TreeSet<>(srdKeys)); + allKeys.put("subraceMap", new TreeMap<>(subraceMap)); + allKeys.put("optionalFeatures", optFeatureIndex.getMap()); tui().writeJsonFile(outputFile, allKeys); } @@ -1072,33 +1042,36 @@ public void writeFilteredIndex(Path outputFile) throws IOException { if (notPrepared()) { throw new IllegalStateException("Index must be prepared before writing files"); } - tui().writeJsonFile(outputFile, Map.of("keys", filteredIndex.keySet())); + tui().writeJsonFile(outputFile, Map.of("keys", new TreeSet<>(filteredIndex.keySet()))); } @Override public JsonNode getAdventure(String id) { String finalKey = Tools5eIndexType.adventure.createKey("adventure", id); - return getOrigin(finalKey); + return getNode(finalKey); // filtered } @Override public JsonNode getBook(String id) { String finalKey = Tools5eIndexType.book.createKey("book", id); - return getOrigin(finalKey); + return getNode(finalKey); // filtered } - void copySources(Tools5eIndexType type, JsonNode dataNode) { + void linkSources(Tools5eIndexType type, JsonNode dataNode) { String id = dataNode.get("id").asText(); - JsonNode fromNode = type == Tools5eIndexType.adventureData - ? getAdventure(id) - : getBook(id); + + String finalKey = type == Tools5eIndexType.adventureData + ? Tools5eIndexType.adventure.createKey("adventure", id) + : Tools5eIndexType.book.createKey("book", id); + + JsonNode fromNode = getOrigin(finalKey); // Adventures and Books have metadata in a different entry. - SourceField.name.copy(fromNode, dataNode); - SourceField.source.copy(fromNode, dataNode); - SourceField.page.copy(fromNode, dataNode); - Tools5eFields.otherSources.copy(fromNode, dataNode); - Tools5eFields.additionalSources.copy(fromNode, dataNode); + SourceField.name.link(fromNode, dataNode); + SourceField.source.link(fromNode, dataNode); + SourceField.page.link(fromNode, dataNode); + Tools5eFields.otherSources.link(fromNode, dataNode); + Tools5eFields.additionalSources.link(fromNode, dataNode); } @Override @@ -1125,30 +1098,27 @@ public void cleanup() { if (instance == this) { instance = null; } - clearCollection(this.familiarKeys); - clearCollection(this.srdKeys); - clearMap(this.aliases); - clearMap(this.classRoot); - clearMap(this.filteredIndex); - clearMap(this.homebrewMetaTypes); - clearMap(this.nodeIndex); - clearMap(this.optFeatureIndex); - clearMap(this.spellClassIndex); - clearMap(this.subraceIndex); - clearMap(this.tableIndex); - clearMap(this.variantIndex); - } + nodeIndex.clear(); + subraceIndex.clear(); + tableIndex.clear(); - private void clearCollection(Collection collection) { - if (collection != null) { - collection.clear(); + if (filteredIndex != null) { + filteredIndex.clear(); } - } - private void clearMap(Map map) { - if (map != null) { - map.clear(); - } + aliases.clear(); + reprints.clear(); + subraceMap.clear(); + nameToLink.clear(); + + spellClassIndex.clear(); + srdKeys.clear(); + + optFeatureIndex.clear(); + homebrewIndex.clear(); + + // affiliated sources cache, too + Tools5eSources.clear(); } static class Tuple { @@ -1183,186 +1153,4 @@ public String getSource() { return source; } } - - static class OptionalFeatureType { - final String lookupKey; - final String abbreviation; - final HomebrewMetaTypes homebrewMeta; - final String title; - final String source; - final ObjectNode featureTypeNode; - final List nodes = new ArrayList<>(); - - OptionalFeatureType(String abbreviation, String scopedAbv, HomebrewMetaTypes homebrewMeta, Tools5eIndex index) { - this.abbreviation = abbreviation; - this.lookupKey = scopedAbv; - this.homebrewMeta = homebrewMeta; - String tmpTitle = null; - if (homebrewMeta != null) { - tmpTitle = homebrewMeta.getOptionalFeatureType(abbreviation); - } - if (tmpTitle == null) { - tmpTitle = switch (abbreviation) { - case "AI" -> "Artificer Infusion"; - case "ED" -> "Elemental Discipline"; - case "EI" -> "Eldritch Invocation"; - case "MM" -> "Metamagic"; - case "MV" -> "Maneuver"; - case "MV:B" -> "Maneuver, Battle Master"; - case "MV:C2-UA" -> "Maneuver, Cavalier V2 (UA)"; - case "AS:V1-UA" -> "Arcane Shot, V1 (UA)"; - case "AS:V2-UA" -> "Arcane Shot, V2 (UA)"; - case "AS" -> "Arcane Shot"; - case "OTH" -> "Other"; - case "FS:F" -> "Fighting Style, Fighter"; - case "FS:B" -> "Fighting Style, Bard"; - case "FS:P" -> "Fighting Style, Paladin"; - case "FS:R" -> "Fighting Style, Ranger"; - case "PB" -> "Pact Boon"; - case "OR" -> "Onomancy Resonant"; - case "RN" -> "Rune Knight Rune"; - case "AF" -> "Alchemical Formula"; - default -> null; - }; - } - if (tmpTitle == null) { - index.tui().warnf("Could not find title for OptionalFeatureType: %s from %s", - abbreviation, homebrewMeta == null ? "unknown/core" : homebrewMeta.filename); - tmpTitle = abbreviation; - } - title = tmpTitle; - source = getSource(homebrewMeta); - - featureTypeNode = Tui.MAPPER.createObjectNode(); - featureTypeNode.put("name", scopedAbv); - featureTypeNode.put("source", source); - if (inSRD(abbreviation)) { - featureTypeNode.put("srd", true); - } - index.addToIndex(Tools5eIndexType.optionalFeatureTypes, featureTypeNode); - } - - public void appendSources(Tools5eSources otherSources) { - // Update sources from those of a consuming/using class or subclass - Tools5eSources mySources = Tools5eSources.constructSources(featureTypeNode); - if (otherSources.contains(mySources)) { - mySources.amendSources(otherSources); - } - } - - public void add(JsonNode node) { - nodes.add(node); - } - - public String getFilename() { - return "list-" + Tools5eQuteBase.fixFileName(title, source, Tools5eIndexType.optionalFeatureTypes); - } - - private String getSource(HomebrewMetaTypes homebrewMeta) { - if (homebrewMeta != null) { - return homebrewMeta.jsonKey; - } - return switch (abbreviation) { - case "AF" -> "UAA"; - case "AI", "RN" -> "TCE"; - case "AS", "FS:B" -> "XGE"; - case "AS:V1-UA" -> "UAF"; - case "AS:V2-UA" -> "UARSC"; - case "MV:C2-UA" -> "UARCO"; - case "OR" -> "UACDW"; - default -> "PHB"; - }; - } - - private boolean inSRD(String abbreviation) { - return switch (abbreviation) { - case "EI", "FS:F", "FS:R", "FS:P", "MM", "PB" -> true; - default -> false; - }; - } - - String getKey() { - return TtrpgValue.indexKey.getTextOrNull(featureTypeNode); - } - } - - static class HomebrewMetaTypes { - final String jsonKey; - final String filename; - final JsonNode homebrewNode; - // name, long name - final Map optionalFeatureTypes = new HashMap<>(); - final Map psionicTypes = new HashMap<>(); - final Map skillOrAbility = new HashMap<>(); - final Map spellSchoolTypes = new HashMap<>(); - final Map itemTypes = new HashMap<>(); - final Map itemProperties = new HashMap<>(); - - HomebrewMetaTypes(String jsonKey, String filename, JsonNode homebrewNode) { - this.jsonKey = jsonKey; - this.filename = filename; - this.homebrewNode = homebrewNode; - } - - public String getOptionalFeatureType(String key) { - return optionalFeatureTypes.get(key.toLowerCase()); - } - - public void setOptionalFeatureType(String key, String value) { - optionalFeatureTypes.put(key.toLowerCase(), value); - } - - public PsionicType getPsionicType(String key) { - return psionicTypes.get(key.toLowerCase()); - } - - public void setPsionicType(String key, PsionicType value) { - psionicTypes.put(key.toLowerCase(), value); - } - - public SkillOrAbility getSkillType(String key) { - return skillOrAbility.get(key.toLowerCase()); - } - - public void setSkillType(String key, JsonNode skill) { - skillOrAbility.put(key.toLowerCase(), new CustomSkillOrAbility(skill)); - } - - public SpellSchool getSpellSchool(String key) { - return spellSchoolTypes.get(key.toLowerCase()); - } - - public void setSpellSchool(String key, CustomSpellSchool value) { - spellSchoolTypes.put(key.toLowerCase(), value); - } - - public ItemType getItemType(String key) { - return itemTypes.get(key.toLowerCase()); - } - - public void setItemType(String key, CustomItemType value) { - itemTypes.put(key.toLowerCase(), value); - } - - public ItemProperty getItemProperty(String key) { - return itemProperties.get(key.toLowerCase()); - } - - public void setItemProperty(String key, CustomItemProperty value) { - itemProperties.put(key.toLowerCase(), value); - } - } - - enum HomebrewFields implements JsonNodeReader { - abbreviation, - fonts, - full, - json, - optionalFeatureTypes, - psionicTypes, - skill, - sources, - spellSchools, - spellDistanceUnits - } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java index 488ff6b4c..79a44b53f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.valueOrDefault; + import java.util.function.BiConsumer; import java.util.stream.Stream; @@ -10,8 +12,6 @@ import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; -import dev.ebullient.convert.tools.dnd5e.Json2QuteDeck.DeckFields; -import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; public enum Tools5eIndexType implements IndexType, JsonNodeReader { action, @@ -27,7 +27,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { charoptionFluff, citation, classtype("class"), - classFluff, // not really a thing. + classFluff, classfeature, condition, conditionFluff, @@ -35,6 +35,8 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { disease, deity, deck, + facility("bastion"), + facilityFluff, feat, featFluff, hazard, @@ -43,9 +45,9 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { itemFluff, itemGroup, itemMastery, + itemProperty, itemType, itemTypeAdditionalEntries, - itemProperty, language, legendaryGroup, magicvariant, @@ -55,14 +57,16 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { monsterTemplate, object, objectFluff, - optionalfeature, + optfeature, optionalFeatureTypes, // homebrew + optionalfeatureFluff, psionic, psionicTypes, // homebrew race, raceFeature, raceFluff, reward, + rewardFluff, sense, skill, spell, @@ -75,6 +79,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { table, tableGroup, trap, + trapFluff, variantrule, vehicle, vehicleFluff, @@ -103,8 +108,8 @@ public static Tools5eIndexType fromText(String name) { if ("creature".equalsIgnoreCase(name)) { return monster; } - if ("optfeature".equalsIgnoreCase(name)) { - return optionalfeature; + if ("optionalfeature".equalsIgnoreCase(name)) { + return optfeature; } if ("legroup".equalsIgnoreCase(name)) { return legendaryGroup; @@ -115,6 +120,9 @@ public static Tools5eIndexType fromText(String name) { } public static Tools5eIndexType getTypeFromKey(String key) { + if (key == null || key.isEmpty()) { + return null; + } String typeKey = key.substring(0, key.indexOf("|")); return fromText(typeKey); } @@ -134,28 +142,30 @@ public String createKey(JsonNode x) { id).toLowerCase(); } else if (this == itemTypeAdditionalEntries) { return createKey( - Tools5eFields.appliesTo.getTextOrEmpty(x), + IndexFields.appliesTo.getTextOrEmpty(x), SourceField.source.getTextOrEmpty(x)); } String name = SourceField.name.getTextOrEmpty(x).trim(); String source = SourceField.source.getTextOrEmpty(x).trim(); + // With introduction of XPHB, etc., we are going to be explicit about sources + // links will be adjusted to add assumed sources switch (this) { case classfeature -> { String classSource = IndexFields.classSource.getTextOrDefault(x, "phb"); - return String.format("%s|%s|%s|%s|%s%s", + return "%s|%s|%s|%s|%s|%s".formatted( this.name(), name, IndexFields.className.getTextOrEmpty(x), - "phb".equalsIgnoreCase(classSource) ? "" : classSource, + classSource, IndexFields.level.getTextOrEmpty(x), - source.equalsIgnoreCase(classSource) ? "" : "|" + source) + source) .toLowerCase(); } case card -> { - String set = DeckFields.set.getTextOrThrow(x).trim(); - return String.format("%s|%s|%s|%s", + String set = IndexFields.set.getTextOrThrow(x).trim(); + return "%s|%s|%s|%s".formatted( this.name(), name, set, @@ -163,7 +173,7 @@ public String createKey(JsonNode x) { .toLowerCase(); } case deity -> { - return String.format("%s|%s|%s|%s", + return "%s|%s|%s|%s".formatted( this.name(), name, IndexFields.pantheon.getTextOrEmpty(x).trim(), @@ -171,61 +181,63 @@ public String createKey(JsonNode x) { .toLowerCase(); } case itemType, itemProperty -> { + source = SourceField.source.getTextOrDefault(x, "phb"); String abbreviation = IndexFields.abbreviation.getTextOrDefault(x, name).trim(); - return String.format("%s|%s|%s", + return "%s|%s|%s".formatted( this.name(), abbreviation, source) .toLowerCase(); } case itemEntry -> { - return String.format("%s|%s%s", + return "%s|%s|%s".formatted( this.name(), name, - "dmg".equalsIgnoreCase(source) ? "" : "|" + source) + source) .toLowerCase(); } - case optionalfeature -> { - return String.format("%s|%s%s", + case optfeature -> { + return "%s|%s|%s".formatted( this.name(), name, - "phb".equalsIgnoreCase(source) ? "" : "|" + source) + source) .toLowerCase(); } case subclass -> { - String classSource = IndexFields.classSource.getTextOrDefault(x, "PHB"); + String classSource = IndexFields.classSource.getTextOrDefault(x, "phb"); String scSource = SourceField.source.getTextOrDefault(x, classSource); // subclass|subclassName|className|classSource|subclassSource - return String.format("%s|%s|%s|%s|%s", + return "%s|%s|%s|%s|%s".formatted( this.name(), name, IndexFields.className.getTextOrEmpty(x).trim(), classSource, - scSource.equalsIgnoreCase(classSource) ? "" : scSource) + scSource) .toLowerCase(); } case subclassFeature -> { - String classSource = IndexFields.classSource.getTextOrDefault(x, "PHB"); - String scSource = IndexFields.subclassSource.getTextOrDefault(x, "PHB"); - return String.format("%s|%s|%s|%s|%s|%s|%s%s", + String classSource = IndexFields.classSource.getTextOrDefault(x, "phb"); + String scSource = IndexFields.subclassSource.getTextOrDefault(x, "phb"); + // scFeature|className|classSource|subclassShortName|subclassSource|level|source + return "%s|%s|%s|%s|%s|%s|%s|%s".formatted( this.name(), name, IndexFields.className.getTextOrEmpty(x).trim(), - "phb".equalsIgnoreCase(classSource) ? "" : classSource, + classSource, IndexFields.subclassShortName.getTextOrEmpty(x).trim(), - "phb".equalsIgnoreCase(scSource) ? "" : scSource, + scSource, IndexFields.level.getTextOrEmpty(x), - source.equalsIgnoreCase(scSource) ? "" : "|" + source) + source) .toLowerCase(); } case subrace -> { - String raceSource = IndexFields.raceSource.getTextOrDefault(x, "PHB"); - return String.format("%s|%s|%s|%s%s", + String raceSource = IndexFields.raceSource.getTextOrDefault(x, "phb"); + return "%s|%s|%s|%s|%s".formatted( this.name(), name, IndexFields.raceName.getTextOrEmpty(x).trim(), raceSource, - source.equalsIgnoreCase(raceSource) ? "" : "|" + source) + source) .toLowerCase(); } default -> { @@ -241,80 +253,114 @@ public String createKey(String name, String source) { if (this == book || this == adventure || this == bookData || this == adventureData) { return String.format("%s|%s-%s", this.name(), name, source).toLowerCase(); } - if (this == optionalfeature) { - // "optionalfeature|agonizing blast", - // "optionalfeature|alchemical acid|uaartificer", - return String.format("%s|%s%s", - Tools5eIndexType.optionalfeature, - name, - "phb".equalsIgnoreCase(source) ? "" : "|" + source) - .toLowerCase(); - } return String.format("%s|%s|%s", this.name(), name, source).toLowerCase(); } - public String fromRawKey(String crossRef) { - if (this.equals(subclassFeature)) { - String[] parts = crossRef.trim().split("\s?\\|\\s?"); - // 0 name, - // 1 IndexFields.className.getTextOrEmpty(x), - // 2 "phb".equalsIgnoreCase(classSource) ? "" : classSource, - // 3 IndexFields.subclassShortName.getTextOrEmpty(x), - // 4 "phb".equalsIgnoreCase(scSource) ? "" : scSource, - // 5 IndexFields.level.getTextOrEmpty(x), - // 6 source.equalsIgnoreCase(scSource) ? "" : "|" + source) - if (parts.length < 6) { - Tui.instance().errorf("Badly formed Subclass Feature key (not enough segments): %s", crossRef); - return null; - } - - String featureSource = parts.length > 6 ? parts[6] : parts[4]; - return getSubclassFeatureKey(parts[0], featureSource, parts[1], parts[2], parts[3], parts[4], - parts[5]); + public String fromTagReference(String crossRef) { + if (crossRef == null || crossRef.isEmpty()) { + return null; } - if (this.equals(classfeature)) { - String[] parts = crossRef.trim().split("\s?\\|\\s?"); - // 0 name, - // 1 IndexFields.className.getTextOrEmpty(x), - // 2 "phb".equalsIgnoreCase(classSource) ? "" : classSource, - // 3 IndexFields.level.getTextOrEmpty(x), - // 4 source.equalsIgnoreCase(classSource) ? "" : "|" + source) - if (parts.length < 4) { - Tui.instance().errorf("Badly formed Class Feature key (not enough segments): %s", crossRef); - return null; + String[] parts = crossRef.trim().split("\s?\\|\\s?"); + return switch (this) { + case card -> { + // 0 name, + // 1 set, + // 2 source + yield String.format("%s|%s|%s|%s", + this.name(), + parts[0].trim(), + parts[1].trim(), + parts.length > 2 ? parts[2] : defaultSourceString()) + .toLowerCase(); } - String featureSource = parts.length > 4 ? parts[4] : parts[2]; - return getClassFeatureKey(parts[0], featureSource, parts[1], parts[2], parts[3]); - } - if (this.equals(card)) { - String[] parts = crossRef.trim().split("\s?\\|\\s?"); - // 0 name, - // 1 set, - // 2 source - return String.format("%s|%s|%s|%s", - this.name(), - parts[0].trim(), - parts[1].trim(), - parts.length > 2 ? parts[2] : defaultSourceString()) - .toLowerCase(); - } - - return String.format("%s|%s", this.name(), crossRef).toLowerCase(); + case classfeature -> { + // 0 name, + // 1 IndexFields.className.getTextOrEmpty(x), + // 2 classSource || "phb", + // 3 IndexFields.level.getTextOrEmpty(x), + // 4 source || classSource + if (parts.length < 4) { + Tui.instance().errorf("Badly formed Class Feature key (not enough segments): %s", crossRef); + yield null; + } + String classSource = valueOrDefault(parts[2], "phb"); + String featureSource = parts.length > 4 ? parts[4] : classSource; + yield getClassFeatureKey( + parts[0], featureSource, + parts[1], classSource, + parts[3]); + } + case itemMastery, itemProperty, itemType -> { + // utils.js: itemType.unpackUid, itemProperty.unpackUid + String source = parts.length > 1 ? parts[1] : defaultSourceString(); + yield "%s|%s|%s".formatted(this.name(), parts[0], source).toLowerCase(); + } + case subclassFeature -> { + // 0 name, + // 1 IndexFields.className.getTextOrEmpty(x), + // 2 classSource || "phb", + // 3 IndexFields.subclassShortName.getTextOrEmpty(x), + // 4 subClassSource || "phb", + // 5 IndexFields.level.getTextOrEmpty(x), + // 6 source || subClassSource + if (parts.length < 6) { + Tui.instance().errorf("Badly formed Subclass Feature key (not enough segments): %s", crossRef); + yield null; + } + String classSource = valueOrDefault(parts[2], "phb"); + String subClassSource = valueOrDefault(parts[4], "phb"); + String featureSource = parts.length > 6 ? parts[6] : subClassSource; + yield getSubclassFeatureKey( + parts[0], featureSource, + parts[1], classSource, + parts[3], subClassSource, + parts[5]); + } + default -> "%s|%s".formatted(this.name(), crossRef).toLowerCase(); + }; } - public String linkify(JsonSource convert, JsonNode entry) { + public String toTagReference(JsonNode entry) { + String linkText = this.decoratedName(entry); String name = SourceField.name.getTextOrEmpty(entry); String source = SourceField.source.getTextOrEmpty(entry); + return switch (this) { - case subclass -> convert.linkify(this, Tools5eIndexType.getSubclassTextReference( - Tools5eFields.className.getTextOrEmpty(entry), - Tools5eFields.classSource.getTextOrEmpty(entry), - name, source, name)); - default -> convert.linkify(this, name + "|" + source + "|" + this.decoratedName(entry)); + // {@card Donjon|Deck of Several Things|LLK} + case card -> "%s|%s|%s".formatted( + name, + IndexFields.deck.getTextOrEmpty(entry), + source); + // {@class Fighter|phb|Samurai|Samurai|xge} + case subclass -> Tools5eIndexType.getSubclassTextReference( + IndexFields.className.getTextOrEmpty(entry), + IndexFields.classSource.getTextOrEmpty(entry), + name, source, linkText); + // {@subclassFeature Blessed Strikes|Cleric|PHB|Twilight|TCE|8|TCE} + case subclassFeature -> "%s|%s|%s|%s|%s|%s|%s|%s".formatted( + name, + IndexFields.className.getTextOrEmpty(entry), + IndexFields.classSource.getTextOrEmpty(entry), + IndexFields.subclassShortName.getTextOrEmpty(entry), + IndexFields.subclassSource.getTextOrEmpty(entry), + IndexFields.level.getTextOrEmpty(entry), + source, + linkText); + // {@itemType abv|source|linkText} + case itemProperty, itemType -> "%s|%s|%s".formatted( + IndexFields.abbreviation.getTextOrEmpty(entry), + source, linkText); + // {@feat name|source|linkText} + default -> "%s|%s|%s".formatted(name, source, linkText); }; } + public String linkify(JsonSource convert, JsonNode entry) { + String reference = toTagReference(entry); + return convert.linkify(this, reference); + } + public String decoratedName(JsonNode entry) { String name = SourceField.name.getTextOrEmpty(entry); switch (this) { @@ -335,7 +381,7 @@ public String decoratedName(JsonNode entry) { public String decoratedName(String name, JsonNode entry) { Tools5eSources sources = Tools5eSources.findOrTemporary(entry); if (sources.isPrimarySource("DMG") - && !sources.type.defaultSourceString().equals("DMG") + && !sources.type.defaultSourceString().equalsIgnoreCase("DMG") && !name.contains("(DMG)")) { return name + " (DMG)"; } @@ -344,59 +390,82 @@ public String decoratedName(String name, JsonNode entry) { public static String getSubclassKey(String className, String classSource, String subclassName, String subclassSource) { if (classSource == null || classSource.isEmpty()) { - // phb stays in the subclass text reference (match allowed sources) + // phb remains in the subclass text reference (match allowed sources) classSource = "phb"; } - return String.format("%s|%s|%s|%s|%s", + return "%s|%s|%s|%s|%s".formatted( Tools5eIndexType.subclass, subclassName, className, classSource, - classSource.equalsIgnoreCase(subclassSource) ? "" : subclassSource) + subclassSource) .toLowerCase(); } public static String getSubclassTextReference(String className, String classSource, String subclassName, String subclassSource, String text) { if (classSource == null || classSource.isEmpty()) { - // phb stays in the subclass text reference (match allowed sources) + // phb remains in the subclass text reference (match allowed sources) classSource = "phb"; } // {@class Fighter|phb|Samurai|Samurai|xge} - return String.format("%s|%s|%s|%s|%s", + return "%s|%s|%s|%s|%s".formatted( className, classSource, - text == null ? subclassName : text, + valueOrDefault(text, subclassName), subclassName, - classSource.equalsIgnoreCase(subclassSource) ? "" : subclassSource); + subclassSource); } public static String getClassFeatureKey(String name, String featureSource, String className, String classSource, String level) { - return String.format("%s|%s|%s|%s|%s%s", + return "%s|%s|%s|%s|%s|%s".formatted( Tools5eIndexType.classfeature, name, className, - "phb".equalsIgnoreCase(classSource) ? "" : classSource, + classSource, level, - featureSource.equalsIgnoreCase(classSource) ? "" : "|" + featureSource) + featureSource) .toLowerCase(); } public static String getSubclassFeatureKey(String name, String featureSource, String className, String classSource, String scShortName, String scSource, String level) { - return String.format("%s|%s|%s|%s|%s|%s|%s%s", + return "%s|%s|%s|%s|%s|%s|%s|%s".formatted( Tools5eIndexType.subclassFeature, name, className, - "phb".equalsIgnoreCase(classSource) ? "" : classSource, + classSource, scShortName, - "phb".equalsIgnoreCase(scSource) ? "" : scSource, + scSource, level, - featureSource.equalsIgnoreCase(scSource) ? "" : "|" + featureSource) + featureSource) .toLowerCase(); } + public String fromChildKey(String key) { + if (key == null || key.isEmpty()) { + return null; + } + return switch (this) { + case deck, classtype, race -> { + String[] parts = key.trim().split("\s?\\|\\s?"); + // card|cardName|deckName|source + // classfeature|cfName|className|classSource|level|cfSource + // subclass|scName|className|classSource|scSource + // subclassfeature|scfName|className|classSource|subclassShortName|scSource|level|scfSource + // subrace|subraceName|raceName|raceSource|subraceSource + yield parts.length < 4 ? null : "%s|%s|%s".formatted(this, parts[2], parts[3]); + } + case subclass -> { + String[] parts = key.trim().split("\s?\\|\\s?"); + // subclassfeature|scfName|className|classSource|subclassShortName|scSource|level|scfSource + yield parts.length < 6 ? null : "%s|%s|%s|%s|%s".formatted(this, parts[4], parts[2], parts[3], parts[5]); + } + default -> null; + }; + } + public boolean multiNode() { return switch (this) { case action, @@ -420,15 +489,14 @@ public boolean writeFile() { classtype, deck, deity, + facility, feat, hazard, item, - legendaryGroup, - magicvariant, + itemGroup, monster, object, - optionalfeature, - optionalFeatureTypes, + optfeature, psionic, race, reward, @@ -486,10 +554,15 @@ public String getRelativePath() { case bookData -> "books"; case card, deck -> "decks"; case deity -> "deities"; + case facility -> "bastions"; + case item, itemGroup -> "items"; + case itemType -> "item-types"; + case itemMastery -> "item-mastery"; + case itemProperty -> "item-properties"; case legendaryGroup -> "bestiary/legendary-group"; case magicvariant -> "items"; case monster -> "bestiary"; - case optionalfeature, optionalFeatureTypes -> "optional-features"; + case optfeature, optionalFeatureTypes -> "optional-features"; case race, subrace -> "races"; case subclass, classtype -> "classes"; case table, tableGroup -> "tables"; @@ -505,6 +578,7 @@ public String vaultRoot(Tools5eIndex index) { : index.rulesVaultRoot(); } + // render.js -- Tag* public String defaultSourceString() { return switch (this) { case card, @@ -524,27 +598,79 @@ public String defaultSourceString() { monster, monsterfeatures -> "MM"; - case vehicle, vehicleUpgrade -> "GoS"; case boon, cult -> "MTF"; - case psionic -> "UATheMysticClass"; case charoption -> "MOT"; - case syntheticGroup -> null; + case facility -> "XDMG"; + case itemMastery -> "XPHB"; case itemTypeAdditionalEntries -> "XGE"; + case psionic -> "UATheMysticClass"; + case syntheticGroup -> null; + case vehicle, vehicleUpgrade -> "GoS"; default -> "PHB"; }; } + boolean hasVariants() { + return switch (this) { + case magicvariant, monster -> true; + default -> false; + }; + } + + boolean isFluffType() { + return switch (this) { + case backgroundFluff, + facilityFluff, + classFluff, + conditionFluff, + featFluff, + itemFluff, + monsterFluff, + objectFluff, + optionalfeatureFluff, + raceFluff, + rewardFluff, + trapFluff, + vehicleFluff -> + true; + default -> false; + }; + } + + boolean isDependentType() { + // These types are not directly filtered. + // Special rules are applied after the parent item is filtered + return switch (this) { + case card, + classfeature, + optfeature, + optionalFeatureTypes, + subclass, + subclassFeature, + subrace -> + true; + default -> false; + }; + } + + boolean isOutputType() { + return useQuteNote() || writeFile(); + } + enum IndexFields implements JsonNodeReader { abbreviation, + appliesTo, className, classSource, + deck, featureType, level, pantheon, raceName, raceSource, + set, subclassShortName, - subclassSource + subclassSource, } public void withArrayFrom(JsonNode node, BiConsumer callback) { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java index 9f9605953..91df54991 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.tools.JsonCopyException; import dev.ebullient.convert.tools.JsonSourceCopier; import dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterFields; @@ -206,7 +207,7 @@ protected JsonNode mergeNodes(Tools5eIndexType type, String originKey, JsonNode JsonNode templateNode = getOriginNode(templateKey); if (templateNode == null) { - tui().warn("Unable to find traits for " + templateKey); + tui().warnf(Msg.NOT_SET.wrap("Unable to find traits for %s"), templateKey); continue; } else { if (!MetaFields._mod.nestedExistsIn(MetaFields.apply, templateNode)) { @@ -625,7 +626,7 @@ void doAddSenses(String originKey, JsonNode modInfo, JsonNode copyFrom, ObjectNo for (JsonNode modSense : iterableElements(modSenses)) { boolean found = false; String modType = MetaFields.type.getTextOrThrow(modSense); - int modRange = MetaFields.range.getIntOrThrow(modSense); + int modRange = MetaFields.range.intOrThrow(modSense); Pattern p = Pattern.compile(modType + " (\\d+)", Pattern.CASE_INSENSITIVE); for (int i = 0; i < senses.size(); i++) { Matcher m = p.matcher(senses.get(i).asText()); @@ -656,7 +657,7 @@ void doAddSkills(String originKey, JsonNode modInfo, ObjectNode target) { int abilityMod = getAbilityModNumber(abilityScore); // mode: 1 = proficient; 2 = expert - int mode = MetaFields.mode.getIntOrThrow(entry.getValue()); + int mode = MetaFields.mode.intOrThrow(entry.getValue()); int total = mode * pb + abilityMod; if (allSkills.has(modSkill)) { @@ -672,7 +673,7 @@ void doAddSkills(String originKey, JsonNode modInfo, ObjectNode target) { private String getShortName(JsonNode target, boolean isTitleCase) { String name = SourceField.name.getTextOrEmpty(target); - JsonNode shortName = MonsterFields.shortName.getFrom(target); + JsonNode shortName = Tools5eFields.shortName.getFrom(target); boolean isNamedCreature = MonsterFields.isNamedCreature.booleanOrDefault(target, false); String prefix = isNamedCreature ? "" diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java index b1202c3b5..aa0b299e9 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java @@ -5,17 +5,16 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.stream.Collectors; -import java.util.stream.Stream; import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.io.MarkdownWriter; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.qute.QuteBase; import dev.ebullient.convert.qute.QuteNote; import dev.ebullient.convert.tools.IndexType; import dev.ebullient.convert.tools.MarkdownConverter; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.OptionalFeatureType; +import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote; public class Tools5eMarkdownConverter implements MarkdownConverter { @@ -28,19 +27,11 @@ public Tools5eMarkdownConverter(Tools5eIndex index, MarkdownWriter writer) { } public Tools5eMarkdownConverter writeAll() { - return writeFiles(Stream.of(Tools5eIndexType.values()) - .filter(Tools5eIndexType::writeFile) - .collect(Collectors.toList())); - } - - public Tools5eMarkdownConverter writeNotesAndTables() { - return writeFiles(Stream.of(Tools5eIndexType.values()) - .filter(x -> !x.writeFile()) - .filter(Tools5eIndexType::useQuteNote) - .collect(Collectors.toList())); + return writeFiles(List.of(Tools5eIndexType.values())); } public Tools5eMarkdownConverter writeImages() { + index.tui().progressf("Writing images and fonts"); index.tui().copyImages(Tools5eSources.getImages()); index.tui().copyFonts(Tools5eSources.getFonts()); return this; @@ -56,10 +47,12 @@ public Tools5eMarkdownConverter writeFiles(List types) { } if (types != null) { writeQuteBaseFiles(types.stream() - .filter(x -> !((Tools5eIndexType) x).useQuteNote()) + .map(x -> (Tools5eIndexType) x) + .filter(x -> x.writeFile()) .toList()); writeQuteNoteFiles(types.stream() - .filter(x -> ((Tools5eIndexType) x).useQuteNote()) + .map(x -> (Tools5eIndexType) x) + .filter(x -> x.isOutputType() && x.useQuteNote()) .toList()); } return this; @@ -69,6 +62,7 @@ private void writeQuteBaseFiles(List types) { if (types.isEmpty()) { return; } + index.tui().progressf("Converting data: %s", types); List compendium = new ArrayList<>(); List rules = new ArrayList<>(); @@ -108,12 +102,13 @@ private QuteBase json2qute(Tools5eIndexType type, JsonNode jsonSource) { case background -> new Json2QuteBackground(index, type, jsonSource).build(); case deck -> new Json2QuteDeck(index, type, jsonSource).build(); case deity -> new Json2QuteDeity(index, type, jsonSource).build(); + case facility -> new Json2QuteBastion(index, type, jsonSource).build(); case feat -> new Json2QuteFeat(index, type, jsonSource).build(); case hazard, trap -> new Json2QuteHazard(index, type, jsonSource).build(); - case item -> new Json2QuteItem(index, type, jsonSource).build(); + case item, itemGroup -> new Json2QuteItem(index, type, jsonSource).build(); case monster -> new Json2QuteMonster(index, type, jsonSource).build(); case object -> new Json2QuteObject(index, type, jsonSource).build(); - case optionalfeature -> new Json2QuteOptionalFeature(index, type, jsonSource).build(); + case optfeature -> new Json2QuteOptionalFeature(index, type, jsonSource).build(); case psionic -> new Json2QutePsionicTalent(index, type, jsonSource).build(); case race, subrace -> new Json2QuteRace(index, type, jsonSource).build(); case reward -> new Json2QuteReward(index, type, jsonSource).build(); @@ -124,6 +119,11 @@ private QuteBase json2qute(Tools5eIndexType type, JsonNode jsonSource) { } private void writeQuteNoteFiles(List types) { + if (types.isEmpty()) { + return; + } + index.tui().progressf("Converting data: %s", types); + final String vrDir = Tools5eIndexType.variantrule.getRelativePath(); List compendium = new ArrayList<>(); @@ -156,7 +156,7 @@ private void writeQuteNoteFiles(List types) { } else if (index.isIncluded(metadataKey)) { compendium.addAll(new Json2QuteBook(index, nodeType, metadata, node).buildBook()); } else { - index.tui().debugf("%s is excluded", metadataKey); + index.tui().debugf(Msg.FILTER, "%s is excluded", metadataKey); } } case status, condition -> { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java index 5a13011f5..e2d9dca84 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -12,6 +13,7 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.config.CompendiumConfig; import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.FontRef; import dev.ebullient.convert.io.Tui; @@ -33,8 +35,45 @@ public class Tools5eSources extends CompendiumSources { private static final Map imageSourceToRef = new HashMap<>(); private static final Map fontSourceToRef = new HashMap<>(); private static final Map> keyToInlineNotes = new HashMap<>(); + private static final Set basicRulesKeys = new HashSet<>(); + private static final Set freeRulesKeys = new HashSet<>(); + + private static boolean isBasicRules(String key, JsonNode jsonElement) { + if (basicRulesKeys.isEmpty()) { + final JsonNode basicRules = TtrpgConfig.activeGlobalConfig("basicRules"); + basicRules.forEach(node -> basicRulesKeys.add(node.asText())); + } + return SourceAttributes.basicRules.coerceBooleanOrDefault(jsonElement, false) + || basicRulesKeys.contains(key); + } + + private static boolean isFreeRules2024(String key, JsonNode jsonElement) { + if (freeRulesKeys.isEmpty()) { + final JsonNode freeRules = TtrpgConfig.activeGlobalConfig("freeRules2024"); + freeRules.forEach(node -> freeRulesKeys.add(node.asText())); + } + return SourceAttributes.freeRules2024.coerceBooleanOrDefault(jsonElement, false) + || freeRulesKeys.contains(key); + } + + public static boolean includedByConfig(String key) { + Tools5eSources sources = findSources(key); + return sources != null && sources.includedByConfig(); + } + + public static boolean excludedByConfig(String key) { + return !includedByConfig(key); + } + + public static boolean filterRuleApplied(String key) { + Tools5eSources sources = findSources(key); + return sources != null && sources.filterRule; + } public static Tools5eSources findSources(String key) { + if (key == null) { + return null; + } return keyToSources.get(key); } @@ -43,15 +82,12 @@ public static Tools5eSources findSources(JsonNode node) { return keyToSources.get(key); } - public static Tools5eSources constructSources(JsonNode node) { + public static Tools5eSources constructSources(String key, JsonNode node) { if (node == null) { throw new IllegalArgumentException("Must pass a JsonNode"); } - String key = TtrpgValue.indexKey.getTextOrEmpty(node); - if (key == null) { - throw new IllegalArgumentException("Node has not been indexed (no key)"); - } Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); + TtrpgValue.indexKey.setIn(node, key); return keyToSources.computeIfAbsent(key, k -> { Tools5eSources s = new Tools5eSources(type, key, node); s.checkKnown(); @@ -68,10 +104,12 @@ public static Tools5eSources findOrTemporary(JsonNode node) { type = SourceField.source.existsIn(node) ? Tools5eIndexType.reference : Tools5eIndexType.syntheticGroup; + TtrpgValue.indexInputType.setIn(node, type.name()); } String key = TtrpgValue.indexKey.getTextOrNull(node); if (key == null) { key = type.createKey(node); + TtrpgValue.indexKey.setIn(node, key); } Tools5eSources sources = findSources(key); return sources == null @@ -109,7 +147,7 @@ public static void addFonts(JsonNode source, JsonNodeReader field) { } } - static void addFont(String fontFamily, String fontString) { + public static void addFont(String fontFamily, String fontString) { FontRef ref = FontRef.of(fontFamily, fontString); if (ref == null) { Tui.instance().warnf("Font '%s' is invalid, empty, or not found", fontString); @@ -121,7 +159,7 @@ static void addFont(String fontFamily, String fontString) { } } - static void addFont(String fontString) { + public static void addFont(String fontString) { String fontFamily = FontRef.fontFamily(fontString); addFont(fontFamily, fontString); } @@ -136,15 +174,86 @@ public static String getFontReference(String fontString) { return fontFamily; } + public static boolean isSrd(JsonNode node) { + return SourceAttributes.srd.coerceBooleanOrDefault(node, false) + || SourceAttributes.srd52.coerceBooleanOrDefault(node, false); + } + + /** Return the srd name or null */ + public static String srdName(JsonNode node) { + String name = SourceAttributes.srd52.getTextOrDefault(node, SourceAttributes.srd.getTextOrNull(node)); + return "true".equalsIgnoreCase(name) ? null : name; + } + final boolean srd; final boolean basicRules; + final boolean srd52; + final boolean freeRules2024; final Tools5eIndexType type; + final String edition; + + boolean filterRule; + boolean cfgIncluded; private Tools5eSources(Tools5eIndexType type, String key, JsonNode jsonElement) { super(type, key, jsonElement); this.type = type; - this.basicRules = jsonElement.has("basicRules") && jsonElement.get("basicRules").asBoolean(false); - this.srd = jsonElement.has("srd") && jsonElement.get("srd").asBoolean(false); + this.basicRules = isBasicRules(key, jsonElement); + this.freeRules2024 = isFreeRules2024(key, jsonElement); + this.srd = SourceAttributes.srd.coerceBooleanOrDefault(jsonElement, false); + this.srd52 = SourceAttributes.srd52.coerceBooleanOrDefault(jsonElement, false); + this.edition = SourceAttributes.edition.getTextOrEmpty(jsonElement); + testSourceRules(); + } + + /** + * Is this included by configuration (source list, include/exclude rules)? + * Content may be suppressed for other reasons (reprints) + */ + public boolean includedByConfig() { + return cfgIncluded; + } + + /** + * Was this targeted by an include/exclude rule? + */ + public boolean filterRuleApplied() { + return filterRule; + } + + private void testSourceRules() { + CompendiumConfig config = TtrpgConfig.getConfig(); + Optional rulesSpecify = config.keyIsIncluded(key); + this.filterRule = rulesSpecify.isPresent(); + this.cfgIncluded = testSourceRules(config, rulesSpecify); + } + + /** + * Test if this source is included by the configuration + */ + private boolean testSourceRules(CompendiumConfig config, Optional rulesSpecify) { + if (rulesSpecify.isPresent()) { + return rulesSpecify.get(); + } + if (config.allSources()) { + return true; + } + if (config.noSources()) { + return this.srd || this.basicRules || this.srd52 || this.freeRules2024; + } + if (type == Tools5eIndexType.background) { + // backgrounds don't nest. Check only primary source + return config.sourceIncluded(this.primarySource()) + || (config.sourceIncluded("srd") && this.srd) + || (config.sourceIncluded("srd52") && this.srd52) + || (config.sourceIncluded("basicrules") && this.basicRules) + || (config.sourceIncluded("freerules2024") && this.freeRules2024); + } + return config.sourceIncluded(this) + || (config.sourceIncluded("srd") && this.srd) + || (config.sourceIncluded("srd52") && this.srd52) + || (config.sourceIncluded("basicrules") && this.basicRules) + || (config.sourceIncluded("freerules2024") && this.freeRules2024); } @Override @@ -157,15 +266,34 @@ protected boolean isSynthetic() { return type == Tools5eIndexType.syntheticGroup; } + public String edition() { + return edition; + } + + public boolean isClassic() { + return "classic".equalsIgnoreCase(edition); + } + public String getSourceText(boolean useSrd) { if (useSrd) { - return "SRD / Basic Rules"; + List bits = new ArrayList<>(); + if (srd) { + bits.add("SRD 5.1"); + } else if (srd52) { + bits.add("SRD 5.2"); + } + if (freeRules2024) { + bits.add("the Free Rules (2024)"); + } else if (basicRules) { + bits.add("the Basic Rules (2014)"); + } + return String.join(" and ", bits); } return sourceText; } public JsonNode findNode() { - return Tools5eIndex.getInstance().getNode(this.key); + return Tools5eIndex.getInstance().getOrigin(this.key); } protected String findName(IndexType type, JsonNode jsonElement) { @@ -185,34 +313,47 @@ protected String findSourceText(IndexType type, JsonNode jsonElement) { } String srcText = super.findSourceText(type, jsonElement); - boolean basicRules = jsonElement.has("basicRules") && jsonElement.get("basicRules").asBoolean(false); - String value = jsonElement.has("srd") ? jsonElement.get("srd").asText() : null; - boolean srd = !(value == null || "false".equals(value)); - String srdValue = srd && !"true".equals(value) ? " (as '" + value + "')" : ""; - - String srdBasic = ""; - if (srd && basicRules) { - srdBasic = "Available in the SRD and the Basic Rules" + srdValue + "."; - } else if (srd) { - srdBasic = "Available in the SRD" + srdValue + "."; - } else if (basicRules) { - srdBasic = "Available in the Basic Rules" + srdValue + "."; + JsonNode basicRules = jsonElement.get("basicRules"); + JsonNode freeRules2024 = jsonElement.get("freeRules2024"); + JsonNode srd52 = jsonElement.get("srd52"); + JsonNode srd = jsonElement.get("srd"); + + String srdText = ""; + if (srd52 != null) { + srdText = "the SRD"; + if (srd52.isTextual()) { + srdText += " (as \"" + srd52.asText() + "\")"; + } + } else if (srd != null) { + srdText = "the SRD"; + if (srd.isTextual()) { + srdText += " (as \"" + srd.asText() + "\")"; + } + } + + String basicRulesText = ""; + if (freeRules2024 != null) { + basicRulesText = "the Free Rules (2024)"; + if (freeRules2024.isTextual()) { + basicRulesText += " (as \"" + freeRules2024.asText() + "\")"; + } + } else if (basicRules != null) { + basicRulesText = "the Basic Rules (2014)"; + if (basicRules.isTextual()) { + basicRulesText += " (as \"" + basicRules.asText() + "\")"; + } } String sourceText = String.join(", ", srcText); - if (srdBasic.isBlank()) { + if (srdText.isBlank() && basicRulesText.isBlank()) { return sourceText; } + String srdBasic = "Available in " + srdText + (srdText.isEmpty() ? "" : " and ") + basicRulesText; return sourceText.isEmpty() ? srdBasic : sourceText + ". " + srdBasic; } - @Override - protected boolean datasourceFilter(String source) { - return !List.of("phb", "mm", "dmg").contains(source.toLowerCase()); - } - public Optional uaSource() { Optional source = sources.stream().filter(x -> x.contains("UA") && !x.equals("UAWGE")).findFirst(); return source.map(TtrpgConfig::sourceToAbbreviation); @@ -292,11 +433,16 @@ public ImageRef buildImageRef(Tools5eIndex index, JsonMediaHref mediaHref, Strin public void amendSources(Tools5eSources otherSources) { this.sources.addAll(otherSources.sources); this.bookRef.addAll(otherSources.bookRef); + testSourceRules(); } @Override public boolean includedBy(Set sources) { return super.includedBy(sources) || + (this.basicRules && sources.contains("basicrules")) || + (this.srd && sources.contains("srd")) || + (this.srd52 && sources.contains("srd52")) || + (this.freeRules2024 && sources.contains("freerules2024")) || (TtrpgConfig.getConfig().noSources() && (this.srd || this.basicRules)); } @@ -304,4 +450,26 @@ public boolean contains(Tools5eSources sources) { Collection sourcesList = sources.getSources(); return this.sources.stream().anyMatch(sourcesList::contains); } + + public enum SourceAttributes implements JsonNodeReader { + srd, + basicRules, + srd52, + freeRules2024, + edition; + } + + public static void clear() { + keyToSources.clear(); + imageSourceToRef.clear(); + fontSourceToRef.clear(); + keyToInlineNotes.clear(); + basicRulesKeys.clear(); + freeRulesKeys.clear(); + } + + public static boolean isClassicEdition(JsonNode baseItem) { + String edition = SourceAttributes.edition.getTextOrDefault(baseItem, ""); + return "classic".equalsIgnoreCase(edition); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AbilityScores.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AbilityScores.java index 1d734035b..caf86ba00 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AbilityScores.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AbilityScores.java @@ -4,14 +4,14 @@ /** * 5eTools Ability Score attributes. - *

+ * * Used to describe a monster, object or vehicle's ability scores. - *

- *

+ * * If referenced as a unit (ignoring inner attributes), it will render ability scores as - * a `|` separated list of values, in `STR,DEX,CON,INT,WIS,CHA` order, for example:
+ * a `|` separated list of values, in `STR,DEX,CON,INT,WIS,CHA` order. + * + * For example: * `10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)`. - *

*/ @TemplateData public class AbilityScores { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AcHp.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AcHp.java index a5e44f56a..57a329464 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AcHp.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AcHp.java @@ -8,11 +8,10 @@ /** * 5eTools armor class and hit points attributes - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly. - *

*/ @TemplateData public class AcHp implements QuteUtil { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/ImmuneResist.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/ImmuneResist.java index ffcf905d5..6e98c05b2 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/ImmuneResist.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/ImmuneResist.java @@ -8,11 +8,10 @@ /** * 5eTools vulnerabilities, resistances, immunities, and condition immunities - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly. - *

*/ @TemplateData public class ImmuneResist implements QuteUtil { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java index 6ed1cfe3c..17bcccced 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java @@ -9,9 +9,8 @@ /** * 5eTools background attributes ({@code background2md.txt}). - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteBackground extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java new file mode 100644 index 000000000..70cde921b --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java @@ -0,0 +1,147 @@ +package dev.ebullient.convert.tools.dnd5e.qute; + +import static dev.ebullient.convert.StringUtil.joinConjunct; +import static dev.ebullient.convert.StringUtil.toTitleCase; + +import java.util.ArrayList; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; +import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.dnd5e.Tools5eSources; +import io.quarkus.qute.TemplateData; + +/** + * 5eTools background attributes ({@code bastion2md.txt}). + * + * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. + */ +@TemplateData +public class QuteBastion extends Tools5eQuteBase { + /** + * List of possible hirelings this bastion can have (as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Hireling}, + * optional) + */ + public final List hirelings; + /** Bastion level (optional) */ + public final String level; + /** Bastion orders (optional) */ + public final List orders; + /** + * List of possible spaces this bastion can occupy (as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Space}, + * optional) + */ + public final List space; + /** Type */ + public final String type; + /** Formatted text listing other prerequisite conditions (optional) */ + public final String prerequisite; + /** List of images for this bastion (as {@link dev.ebullient.convert.qute.ImageRef}, optional) */ + public final List fluffImages; + + public QuteBastion(Tools5eSources sources, String name, String source, + List hirelings, String level, List orders, + String prerequisite, List space, String type, + String text, List images, Tags tags) { + super(sources, name, source, text, tags); + this.fluffImages = images; // optional + this.hirelings = hirelings; // optional + this.level = level; // optional + this.orders = orders; // optional + this.prerequisite = prerequisite; // optional + this.space = space; // optional + this.type = type; + } + + /** Hirelings as a descriptive string (if hirelings is present) */ + public String getHirelingDescription() { + if (hirelings == null) { + return ""; + } + List all = new ArrayList<>(); + for (Hireling h : hirelings) { + all.add(h.getDescription()); + } + return joinConjunct(" or ", all); + } + + /** Space as a descriptive string (if space is present) */ + public String getSpaceDescription() { + if (space == null) { + return ""; + } + List all = new ArrayList<>(); + for (Space s : space) { + all.add(s.getDescription(type, getName())); + } + return joinConjunct(" or ", all); + } + + /** + * Hireling information. Either exact or min must be present. + * + * @param exact Exact number of hirelings (either exact or min) + * @param min Minimum number of hirelings (either exact or min) + * @param max Maximum number of hirelings (optional) + * @param space Size of bastion space required for these hirelings (optional) + */ + @TemplateData + public record Hireling( + Integer exact, + Integer min, + Integer max, + Space space) { + + /** Formatted string description of the hirelings for a Bastion */ + public String getDescription() { + String spaceTxt = space == null ? "" : " (%s)".formatted(toTitleCase(space.name())); + // Either min or exact must be present + if (exact != null) { + return "%s%s".formatted(exact, spaceTxt); + } else if (min != null && max != null) { + return "%s-%s%s".formatted(min, max, spaceTxt); + } else if (min != null) { + return "%s+%s".formatted(min, spaceTxt); + } + return ""; + } + } + + /** + * @param name Name of this size/space + * @param squares Maximum number of 5-foot squares a bastion this size can occupy + * @param cost Cost (GP) of building a bastion of this size + * @param time Time to construct a bastion of this size + * @param prevSpace Previous space to enlarge from (optional) + */ + @TemplateData + public record Space( + String name, + Integer squares, + Integer cost, + Integer time, + Space prevSpace) { + + public Space(String name, Integer squares, Integer cost, Integer time) { + this(name, squares, cost, time, null); + } + + /** Formatted string description of the space required for (or occupied by) a Bastion */ + public String getDescription(String type, String facilityName) { + List more = new ArrayList<>(); + if (squares > 0) { + more.add(squares + " sq"); + } + if ("basic".equalsIgnoreCase(type)) { + String txtCost = cost + " GP and " + time + " days to add"; + if (prevSpace != null) { + txtCost += ", or %s GP and %s days to enlarge from a %s %s".formatted( + cost - prevSpace.cost, time - prevSpace.time, prevSpace.name(), facilityName); + } + more.add("%s GP, %s days ^[%s]".formatted( + cost, time, txtCost)); + } + return name() + (more.isEmpty() ? "" : " (%s)".formatted(String.join("; ", more))); + } + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java index 664ce5164..793615413 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java @@ -6,9 +6,8 @@ /** * 5eTools class attributes ({@code class2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteClass extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java index 64ac62795..47095e475 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java @@ -11,9 +11,8 @@ /** * 5eTools deck attributes ({@code deck2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteDeck extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java index 84cb95c59..7a40cd9a2 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java @@ -9,9 +9,8 @@ /** * 5eTools deity attributes ({@code deity2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteDeity extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java index 1d7004259..84d02cf21 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java @@ -6,9 +6,8 @@ /** * 5eTools feat and optional feat attributes ({@code feat2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteFeat extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java index a386c6e87..573791d27 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java @@ -6,9 +6,8 @@ /** * 5eTools hazard attributes ({@code hazard2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteHazard extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java index cb1e1c1ab..b17e23e98 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import static dev.ebullient.convert.StringUtil.join; + import java.util.List; import java.util.stream.Collectors; @@ -8,67 +10,102 @@ import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; -import io.quarkus.runtime.annotations.RegisterForReflection; /** * 5eTools item attributes ({@code item2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteItem extends Tools5eQuteBase { + /** Detailed information about this item as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant} */ + public final Variant rootVariant; + + /** List of images for this item as {@link dev.ebullient.convert.qute.ImageRef} */ + public final List fluffImages; + /** List of magic item variants as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant}. Optional. */ + public final List variants; + + public QuteItem(Tools5eSources sources, String source, + Variant rootVariant, String text, List images, + List variants, Tags tags) { + super(sources, rootVariant.name, source, text, tags); + withTemplate("item2md.txt"); + + this.rootVariant = rootVariant; + this.fluffImages = images == null ? List.of() : images; + this.variants = variants == null ? List.of() : variants; + } + /** Formatted string of item details. Will include some combination of tier, rarity, category, and attunement */ - public final String detail; + public String getDetail() { + return rootVariant.detail(); + } + + /** Formatted string of additional item attributes. Optional. */ + public String getSubtypeString() { + return rootVariant.subtypeString(); + } + + /** Formatted string listing item's properties (with links to rules if the source is present) */ + public String getProperties() { + return rootVariant.getProperties(); + } + + /** Formatted string listing applicable item mastery (with links to rules if the source is present) */ + public String getMastery() { + return rootVariant.getMastery(); + } + /** Changes to armor class provided by the item, if applicable */ - public final String armorClass; + public String getArmorClass() { + return rootVariant.armorClass; + } + /** One-handed Damage string, if applicable. Contains dice formula and damage type */ - public final String damage; + public String getDamage() { + return rootVariant.damage; + } + /** Two-handed Damage string, if applicable. Contains dice formula and damage type */ - public final String damage2h; + public String getDamage2h() { + return rootVariant.damage2h; + } + /** Item's range, if applicable */ - public final String range; - /** Formatted string listing item's properties (with links to rules if the source is present) */ - public final String properties; + public String getRange() { + return rootVariant.range; + } + /** Strength requirement as a numerical value, if applicable */ - public final Integer strengthRequirement; + public Integer getStrengthRequirement() { + return rootVariant.strengthRequirement; + } + /** True if the item imposes a stealth penalty, if applicable */ - public final boolean stealthPenalty; - /** Cost of the item (gp, sp, cp). Usually missing for magic items. */ - public final String cost; - /** Cost of the item (cp) as number. Usually missing for magic items. */ - public final Integer costCp; - /** Weight of the item (pounds) as a decimal value */ - public final Double weight; - /** List of images for this item (as {@link dev.ebullient.convert.qute.ImageRef}) */ - public final List fluffImages; + public boolean getStealthPenalty() { + return rootVariant.stealthPenalty; + } + /** Formatted text listing other prerequisite conditions (optional) */ - public final String prerequisite; - /** List of magic item variants (as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant}, optional) */ - public final List variants; + public String getPrerequisite() { + return rootVariant.prerequisite; + } - public QuteItem(Tools5eSources sources, String name, String source, String detail, - String armorClass, String damage, String damage2h, - String range, String properties, Integer strengthRequirement, boolean stealthPenalty, - String costGp, Integer costCp, Double weightLbs, String prerequisite, - String text, List images, List variants, Tags tags) { - super(sources, name, source, text, tags); - - this.detail = detail; - this.armorClass = armorClass; - this.damage = damage; - this.damage2h = damage2h; - this.range = range; - this.properties = properties; - this.strengthRequirement = strengthRequirement; - this.stealthPenalty = stealthPenalty; - this.cost = costGp; - this.costCp = costCp; - this.weight = weightLbs; - this.fluffImages = images == null ? List.of() : images; - this.prerequisite = prerequisite; // optional - this.variants = variants == null ? List.of() : variants; + /** Cost of the item (gp, sp, cp). Optional. */ + public String getCost() { + return rootVariant.cost; + } + + /** Cost of the item (cp) as number. Optional. */ + public Integer getCostCp() { + return rootVariant.costCp; + } + + /** Weight of the item (pounds) as a decimal value */ + public Double getWeight() { + return rootVariant.weight; } /** @@ -97,49 +134,92 @@ public String getVariantSectionLinks() { .collect(Collectors.joining("\n")); } + /** + * @param name Name of the variant. + * @param detail Formatted string of item details. Will include some combination of tier, rarity, category, and attunement + * @param subtypeString Item subtype string. Optional. + * @param baseItem Markdown link to base item. Optional. + * @param type Item type + * @param typeAlt Alternate item type. Optional. + * @param propertiesList List of item's properties (with links to rules if the source is present). + * @param masteryList List of item mastery that apply to this item. + * + * @param armorClass Changes to armor class provided by the item. Optional. + * @param weaponCategory Weapon category. Optional. One of: "simple", "martial". + * @param damage One-handed Damage string. Contains dice formula and damage type. Optional. + * @param damage2h Two-handed Damage string. Contains dice formula and damage type. Optional. + * @param range Item's range. Optional. + * @param strengthRequirement Strength requirement as a numerical value. Optional. + * @param stealthPenalty True if the item imposes a stealth penalty. Optional. + * @param prerequisite Formatted text listing other prerequisite conditions. Optional. + * + * @param age Age/Era of item. Optional. Known values: futuristic, industrial, modern, renaissance, victorian. + * @param cost Cost of the item (gp, sp, cp). Usually missing for magic items. + * @param costCp Cost of the item (cp) as number. Usually missing for magic items. + * @param weight Weight of the item (pounds) as a decimal value. + * @param rarity Item rarity. Optional. One of: "none": mundane items; "unknown (magic)": miscellaneous magical items; + * "unknown": miscellaneous mundane items; "varies": item groups or magic variants. + * @param tier Item tier. Optional. One of: "minor", "major". + * @param attunement Attunement requirements. Optional. One of: required, optional, prerequisites/conditions (implies + * required). + * + * @param ammo True if this is ammunition + * @param cursed True if this is a cursed item + * @param firearm True if this is a firearm + * @param focus True if this is a spellcasting focus. + * @param focusType Spellcasting focus type. Optional. One of: "arcane", "druid", "holy", and/or a list of required classes. + * @param poison True if this is a poison. + * @param poisonTypes Poison type(s). Optional. + * @param staff True if this is a staff + * @param tattoo True if this is a tattoo + * @param wondrous True if this is a wondrous item + */ @TemplateData - @RegisterForReflection - public static class Variant { - /** Name of the variant */ - public final String name; - /** Changes to armor class provided by the item, if applicable */ - public final String armorClass; - /** One-handed Damage string, if applicable. Contains dice formula and damage type */ - public final String damage; - /** Two-handed Damage string, if applicable. Contains dice formula and damage type */ - public final String damage2h; - /** Item's range, if applicable */ - public final String range; + public static record Variant( + String name, + String detail, + String subtypeString, + String baseItem, + String type, + String typeAlt, + List propertiesList, + List masteryList, + // --- + String armorClass, + String weaponCategory, + String damage, + String damage2h, + String range, + Integer strengthRequirement, + boolean stealthPenalty, + String prerequisite, + // --- + String age, + String cost, + Integer costCp, + Double weight, + String rarity, + String tier, + String attunement, + // --- + boolean ammo, + boolean cursed, + boolean firearm, + boolean focus, + String focusType, + boolean poison, + String poisonTypes, + boolean staff, + boolean tattoo, + boolean wondrous) { /** Formatted string listing item's properties (with links to rules if the source is present) */ - public final String properties; - /** Strength requirement as a numerical value, if applicable */ - public final Integer strengthRequirement; - /** True if the item imposes a stealth penalty, if applicable */ - public final boolean stealthPenalty; - /** Cost of the item (gp, sp, cp). Usually missing for magic items. */ - public final String cost; - /** Cost of the item (cp) as number. Usually missing for magic items. */ - public final Integer costCp; - /** Weight of the item (pounds) as a decimal value */ - public final Double weight; - /** Formatted text listing other prerequisite conditions (optional) */ - public final String prerequisite; - - public Variant(String name, String armorClass, String damage, String damage2h, - String range, String properties, Integer strengthRequirement, boolean stealthPenalty, - String cost, Integer costCp, Double weightLbs, String prerequisite) { - this.name = name; - this.armorClass = armorClass; - this.damage = damage; - this.damage2h = damage2h; - this.range = range; - this.properties = properties; - this.strengthRequirement = strengthRequirement; - this.stealthPenalty = stealthPenalty; - this.cost = cost; - this.costCp = costCp; - this.weight = weightLbs; - this.prerequisite = prerequisite; // optional + public String getProperties() { + return join(", ", propertiesList); + } + + /** Formatted string listing applicable item mastery (with links to rules if the source is present) */ + public String getMastery() { + return join(", ", masteryList); } } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java index 6a07868db..7d1e64e7c 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java @@ -18,9 +18,8 @@ /** * 5eTools creature attributes ({@code monster2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteMonster extends Tools5eQuteBase { @@ -346,22 +345,25 @@ Collection spellcastingToTraits() { /** * 5eTools creature spellcasting attributes. - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. - * To use it, reference it directly:
- * ```
- * {#for spellcasting in resource.spellcasting}
- * {spellcasting}
- * {/for}
- * ```
- * or, using `{#each}` instead:
- * ```
- * {#each resource.spellcasting}
- * {it}
- * {/each}
+ * + * To use it, reference it directly: + * + * ```md + * {#for spellcasting in resource.spellcasting} + * {spellcasting} + * {/for} + * ``` + * + * or, using `{#each}` instead: + * + * ```md + * {#each resource.spellcasting} + * {it} + * {/each} * ``` - *

*/ @TemplateData @RegisterForReflection diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java index 1086be33f..d0d122312 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java @@ -14,9 +14,8 @@ /** * 5eTools object attributes ({@code object2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteObject extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java index b9cfb2157..1adf6cc6f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java @@ -9,9 +9,8 @@ /** * 5eTools psionic talent attributes ({@code psionic2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QutePsionic extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java index 7e23281f4..895b079c1 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java @@ -9,9 +9,8 @@ /** * 5eTools race attributes ({@code race2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteRace extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java index a8f6c808a..104bbfafc 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java @@ -6,9 +6,8 @@ /** * 5eTools reward attributes ({@code reward2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteReward extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java index 0fe7f2336..62dcc85a4 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java @@ -10,9 +10,8 @@ /** * 5eTools spell attributes ({@code spell2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteSpell extends Tools5eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java index 569b7eb72..d98f7e6bf 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java @@ -6,9 +6,8 @@ /** * 5eTools subclass attributes ({@code subclass2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteSubclass extends Tools5eQuteBase { @@ -43,7 +42,9 @@ public QuteSubclass(Tools5eSources sources, @Override public String targetFile() { - return Tools5eQuteBase.getSubclassResource(name, parentClass, sources.primarySource()); + return Tools5eQuteBase.getSubclassResource(name, + parentClass, parentClassSource, + sources.primarySource()); } @Override diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java index 7869c44e4..57c650e5f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java @@ -15,14 +15,12 @@ /** * 5eTools vehicle attributes ({@code vehicle2md.txt}) - *

+ * * Several different types of vehicle use this template, including: * Ship, spelljammer, infernal war manchie, objects and creatures. * They can have very different properties. Treat most as optional. - *

- *

+ * * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}. - *

*/ @TemplateData public class QuteVehicle extends Tools5eQuteBase { @@ -113,16 +111,17 @@ public boolean getIsObject() { /** * 5eTools Ship crew, cargo, and pace attributes - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. - * To use it, reference it directly:
- * ```
- * {#if resource.shipCrewCargoPace}
- * {resource.shipCrewCargoPace}
- * {/if}
- * ```
- *

+ * + * To use it, reference it directly: + * + * ```md + * {#if resource.shipCrewCargoPace} + * {resource.shipCrewCargoPace} + * {/if} + * ``` */ @TemplateData public static class ShipCrewCargoPace implements QuteUtil { @@ -223,11 +222,11 @@ public String toString() { /** * 5eTools vehicle armor class and hit points attributes - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly. - *

+ * */ @TemplateData public static class ShipAcHp extends AcHp { @@ -285,11 +284,11 @@ public String toString() { /** * 5eTools vehicle sections - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly. - *

+ * */ @TemplateData public static class ShipSection implements QuteUtil { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java index 486b0cc12..31cd42c8b 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java @@ -9,6 +9,7 @@ import dev.ebullient.convert.qute.QuteBase; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; import dev.ebullient.convert.tools.dnd5e.Tools5eIndexType; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; @@ -16,10 +17,9 @@ /** * Attributes for notes that are generated from the 5eTools data. * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteBase}. - *

+ * * Notes created from {@code Tools5eQuteBase} will use a specific template * for the type. For example, {@code QuteBackground} will use {@code background2md.txt}. - *

*/ @TemplateData public class Tools5eQuteBase extends QuteBase { @@ -39,7 +39,10 @@ public static String fixFileName(String name, Tools5eSources sources) { return switch (type) { case background -> fixFileName(type.decoratedName(node), primarySource, type); case deity -> Tui.slugify(getDeityResourceName(name, primarySource, node.get("pantheon").asText())); - case subclass -> getSubclassResource(name, node.get("className").asText(), primarySource); + case subclass -> getSubclassResource(name, + Tools5eFields.className.getTextOrEmpty(node), + Tools5eFields.classSource.getTextOrEmpty(node), + primarySource); default -> fixFileName(name, primarySource, type); }; } @@ -78,9 +81,17 @@ public static String getClassResource(String className, String classSource) { return fixFileName(className, classSource, Tools5eIndexType.classtype); } - public static String getSubclassResource(String subclass, String parentClass, String subclassSource) { + public static String getSubclassResource(String subclass, String parentClass, String classSource, String subclassSource) { + String parentFile = Tui.slugify(parentClass); + if ("xphb".equalsIgnoreCase(classSource)) { + // For the most part, all subclasses are derived from the basic classes. + // There wasn't really a need to include the class source in the file name. + // However, the XPHB has created duplicates of all of the base classes. + // So if the parent class is from the XPHB, we need to include that in the file name. + parentFile += "-xphb"; + } return fixFileName( - Tui.slugify(parentClass) + "-" + Tui.slugify(subclass), + parentFile + "-" + Tui.slugify(subclass), subclassSource, Tools5eIndexType.subclass); } @@ -89,9 +100,13 @@ public static String getDeityResourceName(String name, String source, String pan String suffix = ""; switch (pantheon.toLowerCase()) { case "exandria" -> { - if (!source.equalsIgnoreCase("egw")) { - suffix = "-" + Tui.slugify(source); - } + suffix = source.equalsIgnoreCase("egw") ? "" : ("-" + Tui.slugify(source)); + } + case "dragonlance" -> { + suffix = source.equalsIgnoreCase("dsotdq") ? "" : ("-" + Tui.slugify(source)); + } + default -> { + suffix = sourceIfNotDefault(source, Tools5eIndexType.deity); } } return pantheon + "-" + name + suffix; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteNote.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteNote.java index 4cead642a..75882ab4f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteNote.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteNote.java @@ -13,9 +13,8 @@ /** * Attributes for notes that are generated from the 5eTools data. * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteNote}. - *

+ * * Notes created from {@code Tools5eQuteNote} will use the {@code note2md.txt} template. - *

*/ @TemplateData public class Tools5eQuteNote extends QuteNote { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java index 184b03f46..3d2b73765 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java @@ -112,7 +112,7 @@ private static QuteAffliction createAffliction( Tags tags = new Tags(sources); Collection traits = convert.collectTraitsFrom(node, tags); - Optional afflictionLevel = level.getIntFrom(node).map(Objects::toString); + Optional afflictionLevel = level.intFrom(node).map(Objects::toString); afflictionLevel.ifPresent(lv -> tags.add("affliction", "level", lv)); String temptedCurseText = temptedCurse.transformTextFrom(node, "\n", convert); @@ -157,10 +157,10 @@ private static QuteAffliction createAffliction( // fields are present savingThrow.getTextFrom(dataNode) .or(() -> DC.getTextFrom(dataNode)) - .or(() -> DC.getIntFrom(dataNode).map(Objects::toString)) + .or(() -> DC.intFrom(dataNode).map(Objects::toString)) .map(StringUtil::isPresent) .map(unused -> new QuteAffliction.QuteAfflictionSave( - DC.getIntFrom(dataNode).orElse(null), + DC.intOrNull(dataNode), savingThrow.getTextFrom(dataNode) .map(s -> s.contains(" ") ? s : toTitleCase(s)).orElse(null), DC.getTextFrom(dataNode).map(convert::replaceText).orElse(null))) diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java index 59aabde8e..97f14bcfa 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java @@ -94,8 +94,8 @@ private static QuteCreature create(JsonNode node, JsonSource convert) { traits, alias.replaceTextFromList(node, convert), description.replaceTextFrom(node, convert), - level.getIntFrom(node).orElse(null), - perception.getObjectFrom(node).map(std::getIntOrThrow).orElse(null), + level.intOrNull(node), + perception.getObjectFrom(node).map(std::intOrThrow).orElse(null), defenses.getDefensesFrom(node, convert), languages.getLanguagesFrom(node, convert), skills.getSkillsFrom(node, convert), @@ -163,7 +163,7 @@ private List getSensesFrom(JsonNode source, JsonSour .map(n -> new QuteCreature.CreatureSense( Pf2eCreatureSense.name.getTextFrom(n).map(convert::replaceText).orElseThrow(), Pf2eCreatureSense.type.getTextFrom(n).map(convert::replaceText).orElse(null), - Pf2eCreatureSense.range.getIntFrom(n).orElse(null))) + Pf2eCreatureSense.range.intOrNull(n))) .toList(); } @@ -237,10 +237,10 @@ enum Pf2eCreatureSpellcasting implements Pf2eJsonNodeReader { private static QuteCreature.CreatureRitualCasting getRitual(JsonNode source, JsonSource convert) { return new QuteCreature.CreatureRitualCasting( tradition.getEnumValueFrom(source, QuteCreature.SpellcastingTradition.class), - DC.getIntFrom(source).orElse(null), + DC.intOrNull(source), rituals.streamFrom(source) .collect(Collectors.toMap( - n -> level.getIntFrom(n).orElse(null), + n -> level.intOrNull(n), n -> Stream.of(Pf2eCreatureSpellReference.getSpellReference(n, convert)), Stream::concat)) .entrySet().stream() @@ -253,9 +253,9 @@ private static QuteCreature.CreatureSpellcasting getSpellcasting(JsonNode source name.getTextOrNull(source), type.getEnumValueFrom(source, QuteCreature.SpellcastingPreparation.class), tradition.getEnumValueFrom(source, QuteCreature.SpellcastingTradition.class), - fp.getIntFrom(source).orElse(null), - attack.getIntFrom(source).orElse(null), - DC.getIntFrom(source).orElse(null), + fp.intOrNull(source), + attack.intOrNull(source), + DC.intOrNull(source), note.replaceTextFromList(source, convert), entry.getSpellsFrom(source, convert), constant.getSpellsFrom(entry.getFromOrEmptyObjectNode(source), convert)); @@ -265,8 +265,8 @@ private List getSpellsFrom(JsonNode source, JsonSou return streamPropsExcluding(source, constant) .map(e -> new QuteCreature.CreatureSpells( Integer.valueOf(e.getKey()), - level.getIntFrom(e.getValue()).orElse(null), - slots.getIntFrom(e.getValue()).orElse(null), + level.intOrNull(e.getValue()), + slots.intOrNull(e.getValue()), spells.streamFrom(e.getValue()) .map(n -> Pf2eCreatureSpellReference.getSpellReference(n, convert)) .toList())) @@ -294,7 +294,7 @@ private static QuteCreature.CreatureSpellReference getSpellReference(JsonNode no amount.getTextFrom(node) .filter(s -> s.equalsIgnoreCase("at will")) .map(unused -> 0) - .or(() -> amount.getIntFrom(node)) + .or(() -> amount.intFrom(node)) .orElse(1), notes.replaceTextFromList(node, convert)); } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java index d234d33ab..d63967031 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java @@ -148,7 +148,7 @@ QuteDeity.QuteDivineAvatar buildAvatar(Tags tags) { "ignore {@quickref difficult terrain||3|terrain} and {@quickref greater difficult terrain||3|terrain}")); } - avatar.shield = Pf2eDeity.shield.getIntFrom(avatarNode) + avatar.shield = Pf2eDeity.shield.intFrom(avatarNode) .map("shield (%d Hardness, can't be damaged)"::formatted).orElse(null); avatar.attacks = Stream.concat( diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java index 7a1e265b1..07fa0e798 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java @@ -64,15 +64,15 @@ enum Pf2eHazardAttribute implements Pf2eJsonNodeReader { static QuteHazard.QuteHazardStealth buildStealth(JsonNode node, JsonTextConverter convert) { return new QuteHazard.QuteHazardStealth( - bonus.getIntFrom(node).orElse(null), - dc.getIntFrom(node).orElse(null), + bonus.intOrNull(node), + dc.intOrNull(node), minProf.getTextOrNull(node), notes.getTextFrom(node).map(convert::replaceText).orElse(null)); } static QuteDataGenericStat.SimpleStat buildPerception(JsonNode node, JsonTextConverter convert) { return new QuteDataGenericStat.SimpleStat( - bonus.getIntOrThrow(node), + bonus.intOrThrow(node), notes.getTextFrom(node).map(convert::replaceText).orElse(null)); } } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java index bf4ed172b..39750df07 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java @@ -106,12 +106,12 @@ private QuteItemShieldData getShieldData() { return shieldNode == null ? null : new QuteItemShieldData( new QuteDataArmorClass( - Pf2eItem.ac.getIntOrThrow(shieldNode), - Pf2eItem.ac2.getIntFrom(shieldNode).orElse(null)), + Pf2eItem.ac.intOrThrow(shieldNode), + Pf2eItem.ac2.intOrNull(shieldNode)), new QuteDataHpHardnessBt( - new QuteDataHpHardnessBt.HpStat(Pf2eItem.hp.getIntOrThrow(shieldNode)), - new SimpleStat(Pf2eItem.hardness.getIntOrThrow(shieldNode)), - Pf2eItem.bt.getIntOrThrow(shieldNode)), + new QuteDataHpHardnessBt.HpStat(Pf2eItem.hp.intOrThrow(shieldNode)), + new SimpleStat(Pf2eItem.hardness.intOrThrow(shieldNode)), + Pf2eItem.bt.intOrThrow(shieldNode)), penalty(Pf2eItem.speedPen.getTextOrEmpty(shieldNode), " ft.")); } @@ -122,7 +122,7 @@ private QuteItemArmorData getArmorData() { } QuteItemArmorData armorData = new QuteItemArmorData(); - Pf2eItem.ac.getIntFrom(armorDataNode).ifPresent(ac -> armorData.ac = new QuteDataArmorClass(ac)); + Pf2eItem.ac.intFrom(armorDataNode).ifPresent(ac -> armorData.ac = new QuteDataArmorClass(ac)); armorData.dexCap = Pf2eItem.dexCap.bonusOrNull(armorDataNode); armorData.strength = Pf2eItem.str.getTextOrDefault(armorDataNode, "—"); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java index 58f71e69f..79ac45ff8 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.JsonNodeReader.FieldValue; @@ -67,7 +68,7 @@ default void appendToText(List text, JsonNode node, String heading) { } else if (node.isObject()) { appendObjectToText(text, node, heading); } else { - tui().errorf("Unknown entry type in %s: %s", getSources(), node.toPrettyString()); + tui().warnf(Msg.UNKNOWN, "Unknown entry type in %s: %s", getSources(), node.toPrettyString()); } } finally { parseState().pop(pushed); // restore state @@ -533,7 +534,7 @@ default void embedData(List text, JsonNode dataNode) { text.addAll(inner); return; } else if (dataType == null) { - tui().errorf("Unknown data type %s from: %s", tag, dataNode.toString()); + tui().warnf(Msg.UNKNOWN, "Unknown data type %s from: %s", tag, dataNode.toString()); return; } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java index 190f4ea07..8d890ec47 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.config.CompendiumConfig; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.JsonNodeReader.FieldValue; @@ -48,7 +49,6 @@ enum Field implements Pf2eJsonNodeReader { Pattern asPattern = Pattern.compile("\\{@as ([^}]+)}"); Pattern runeItemPattern = Pattern.compile("\\{@runeItem ([^}]+)}"); - Pattern dicePattern = Pattern.compile("\\{@(dice|damage) ([^}]+)}"); Pattern chancePattern = Pattern.compile("\\{@chance ([^}]+)}"); Pattern notePattern = Pattern.compile("\\{@note (\\*|Note:)?\\s?([^}]+)}"); Pattern quickRefPattern = Pattern.compile("\\{@quickref ([^}]+)}"); @@ -90,14 +90,6 @@ default String _replaceTokenText(String input, boolean nested) { } result = replaceWithDiceRoller(result); // {@hit ..} and {@d20 ..} - result = dicePattern.matcher(result) - .replaceAll((match) -> { - String[] parts = match.group(2).split("\\|"); - if (parts.length > 1) { - return parts[1]; - } - return formatDice(parts[0]); - }); result = chancePattern.matcher(result) .replaceAll((match) -> match.group(1) + "% chance"); @@ -146,17 +138,13 @@ default String _replaceTokenText(String input, boolean nested) { .replaceAll("\\{@reward ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@dc ([^}]+)}", "DC $1") .replaceAll("\\{@flatDC ([^}]+)}", "$1") - .replaceAll("\\{@d20 ([^}]+?)}", "$1") .replaceAll("\\{@recharge ([^}]+?)}", "(Recharge $1-6)") .replaceAll("\\{@recharge}", "(Recharge 6)") - .replaceAll("\\{@(scaledice|scaledamage) [^|]+\\|[^|]+\\|([^|}]+)[^}]*}", "$2") .replaceAll("\\{@filter ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@cult ([^|}]+)\\|([^|}]+)\\|[^|}]*}", "$2") .replaceAll("\\{@cult ([^|}]+)\\|[^}]*}", "$1") .replaceAll("\\{@language ([^|}]+)\\|?[^}]*}", "$1") - .replaceAll("\\{@variantrule ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@book ([^}|]+)\\|?[^}]*}", "\"$1\"") - .replaceAll("\\{@hit ([^}<]+)}", "+$1") .replaceAll("\\{@h}", "Hit: ") .replaceAll("\\{@c ([^}]+?)}", "$1") .replaceAll("\\{@center ([^}]+?)}", "$1") @@ -249,7 +237,7 @@ default String linkifyRuneItem(MatchResult match) { default String linkify(MatchResult match) { Pf2eIndexType targetType = Pf2eIndexType.fromText(match.group(1)); if (targetType == null) { - tui().errorf("Unknown type to linkify: %s", match.group(0)); + tui().warnf(Msg.UNKNOWN, "Unknown type to linkify: %s", match.group(0)); return match.group(0); } return linkify(targetType, match.group(2)); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java index cd86bb5ab..b15b01e21 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java @@ -266,6 +266,9 @@ boolean keyIsIncluded(String key, JsonNode node) { if (rulesAllow.isPresent()) { return rulesAllow.get(); } + if (config.allSources()) { + return true; + } if (CORE_RULES_KEY.equals(key)) { // include core rules unless turned off return true; } diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java index 431d1645d..bd2a46cc0 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java @@ -148,6 +148,7 @@ public String relativeRepositoryRoot(Pf2eIndex index) { } public Pf2eQuteBase convertJson2QuteBase(Pf2eIndex index, JsonNode node) { + // also update #isOutputType return switch (this) { case action -> new Json2QuteAction(index, node).build(); case archetype -> new Json2QuteArchetype(index, node).build(); @@ -179,20 +180,65 @@ public boolean checkCopiesAndReprints() { } public boolean useQuteNote() { + // also update #isOutputType return switch (this) { - case ability, affliction, curse, disease, condition, domain, skill, table -> true; // QuteNote-based + case ability, + affliction, + book, + condition, + curse, + disease, + domain, + skill, + table -> + true; // QuteNote-based default -> false; }; } public boolean useCompendiumBase() { return switch (this) { - case ability, action, book, condition, trait, table, variantrule -> false; // use rules + case ability, + action, + book, + condition, + trait, + table, + variantrule -> + false; // use rules default -> true; // use compendium }; } + public boolean isOutputType() { + return switch (this) { + case ability, + action, + affliction, + archetype, + background, + book, + condition, + creature, + curse, + deity, + disease, + domain, + feat, + hazard, + item, + ritual, + skill, + spell, + table, + trait -> + true; + default -> false; + }; + } + public String relativePath() { + // also update #isOutputType return switch (this) { // Simple suffix subdir (rules or compendium) case action, feat, spell, table, trait, variantrule -> this.name() + 's'; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java index a3ffe8c9f..a4db54aad 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java @@ -164,7 +164,7 @@ enum Pf2eSpeed implements Pf2eJsonNodeReader { /** Read a {@link QuteDataSpeed} from the {@code source} node. */ private static QuteDataSpeed getSpeed(JsonNode source, JsonSource convert) { return new QuteDataSpeed( - walk.getIntFrom(source).orElse(null), + walk.intOrNull(source), convert.streamPropsExcluding(source, speedNote, abilities) .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().asInt())), speedNote.getTextFrom(source) @@ -224,7 +224,7 @@ private static QuteDataFrequency getFrequency(JsonNode node, JsonSource convert) } return new QuteDataFrequency( // This should usually be an integer, but some entries deviate from the schema and use a word - number.getIntFrom(node).orElseGet(() -> { + number.intFrom(node).orElseGet(() -> { // Try to coerce the word back into a number, and otherwise log an error and give 0 String freqString = number.getTextOrThrow(node).trim(); if (freqString.equalsIgnoreCase("once")) { @@ -233,7 +233,7 @@ private static QuteDataFrequency getFrequency(JsonNode node, JsonSource convert) convert.tui().errorf("Got unexpected frequency value \"%s\"", freqString); return 0; }), - interval.getIntFrom(node).orElse(null), + interval.intOrNull(node), unit.getTextFrom(node).orElseGet(() -> customUnit.getTextOrThrow(node)), recurs.booleanOrDefault(node, false), overcharge.booleanOrDefault(node, false)); @@ -301,7 +301,7 @@ private static QuteDataDefenses getDefenses(JsonNode source, JsonSource convert) return new QuteDataDefenses( ac.getObjectFrom(source) .map(acNode -> new QuteDataArmorClass( - std.getIntOrThrow(acNode), + std.intOrThrow(acNode), ac.streamPropsExcluding(source, note, abilities, notes, std) .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().asInt())), (note.existsIn(acNode) ? note : notes).replaceTextFromList(acNode, convert), @@ -426,7 +426,7 @@ private static Map getHpMapFromArray( return convert.streamOf(convert.ensureArray(source)).collect(Collectors.toMap( n -> Pf2eHpStat.name.getTextFrom(n).map(StringUtil::toTitleCase).orElse(std.name()), n -> new QuteDataHpHardnessBt.HpStat( - hp.getIntOrThrow(n), + hp.intOrThrow(n), notes.replaceTextFromList(n, convert), abilities.replaceTextFromList(n, convert)))); } @@ -472,9 +472,9 @@ enum Pf2eNameAmountNote implements Pf2eJsonNodeReader { private static Collector> mappedStatCollector( JsonSource convert) { return Collectors.toMap( - name::getTextOrThrow, + n -> name.replaceTextFrom(n, convert), n -> new QuteDataGenericStat.SimpleStat( - amount.getIntFrom(n).orElse(null), + amount.intOrNull(n), note.replaceTextFrom(n, convert))); } } @@ -520,7 +520,7 @@ private static QuteDataActivity getActivity(JsonNode node, JsonSource convert) { String actionType = unit.getTextOrNull(node); Pf2eActivity activity = switch (actionType) { case "single", "action", "free", "reaction" -> - Pf2eActivity.toActivity(actionType, number.getIntOrThrow(node)); + Pf2eActivity.toActivity(actionType, number.intOrThrow(node)); case "varies" -> Pf2eActivity.varies; case "day", "minute", "hour", "round" -> Pf2eActivity.timed; default -> null; @@ -537,7 +537,7 @@ private static QuteDataActivity getActivity(JsonNode node, JsonSource convert) { .orElse(""); return activity.toQuteActivity( - convert, activity == Pf2eActivity.timed ? join(" ", number.getIntOrThrow(node), actionType, extra) : extra); + convert, activity == Pf2eActivity.timed ? join(" ", number.intOrThrow(node), actionType, extra) : extra); } /** @@ -552,7 +552,7 @@ private static QuteDataActivity getActivity(JsonNode node, JsonSource convert) { */ private static QuteDataDuration getDuration(JsonNode node, JsonSource convert) { QuteDataTimedDuration timedDuration = new QuteDataTimedDuration( - number.getIntFrom(node).orElse(1), + number.intFrom(node).orElse(1), unit.getEnumValueFrom(node, QuteDataTimedDuration.DurationUnit.class), entry.replaceTextFrom(node, convert)); // Prioritize using a custom display string if we have one @@ -593,8 +593,8 @@ private static QuteDataRange getRange(JsonNode source, JsonSource convert) { .or(() -> type.getRangeUnitFrom(node)) .or(() -> entry.getRangeUnitFrom(node)) // sometimes the entry is the unit .orElse(null); - Integer rangeValue = number.getIntFrom(node) - .or(() -> amount.getIntFrom(node)) + Integer rangeValue = number.intFrom(node) + .or(() -> amount.intFrom(node)) .orElse(null); String entryText = entry.replaceTextFrom(node, convert); if (rangeValue == null && rangeUnit == null && !isPresent(entryText)) { @@ -679,7 +679,7 @@ public static QuteInlineAttack getAttack(JsonNode node, JsonSource convert) { Optional.ofNullable(activity.getActivityFrom(node, convert)) .orElse(Pf2eActivity.single.toQuteActivity(convert, "")), QuteInlineAttack.AttackRangeType.valueOf(range.getTextOrDefault(node, "Melee").toUpperCase()), - attack.getIntFrom(node).orElse(null), + attack.intOrNull(node), formattedDamage, types.replaceTextFromList(node, convert), convert.collectTraitsFrom(node, null), @@ -728,7 +728,7 @@ public static QuteDataNamedBonus getNamedBonus( return new QuteDataNamedBonus( displayName, - std.getIntOrThrow(source), + std.intOrThrow(source), convert.streamPropsExcluding(source, std, note) .collect(Collectors.toMap(e -> convert.replaceText(e.getKey()), e -> e.getValue().asInt())), note.getTextFrom(source).map(convert::replaceText).map(List::of) diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java index 9a9ec01fd..569eda6b9 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java @@ -48,7 +48,8 @@ public Pf2eMarkdown writeFiles(List types) { if (types == null) { } else { writePf2eQuteBase(types.stream() - .filter(x -> !((Pf2eIndexType) x).useQuteNote()) + .map(x -> (Pf2eIndexType) x) + .filter(x -> x.isOutputType() && !x.useQuteNote()) .collect(Collectors.toList())); writeNotesAndTables(types.stream() .filter(x -> ((Pf2eIndexType) x).useQuteNote()) @@ -58,9 +59,10 @@ public Pf2eMarkdown writeFiles(List types) { } private void writePf2eQuteBase(List types) { - if (types != null && types.isEmpty()) { + if (types == null || types.isEmpty()) { return; } + index.tui().progressf("Converting data: %s", types); List compendium = new ArrayList<>(); List rules = new ArrayList<>(); @@ -85,15 +87,11 @@ private void writePf2eQuteBase(List types) { writer.writeFiles(index.rulesFilePath(), rules); } - @Override - public Pf2eMarkdown writeNotesAndTables() { - return writeNotesAndTables(null); - } - private Pf2eMarkdown writeNotesAndTables(List types) { - if (types != null && types.isEmpty()) { + if (types == null || types.isEmpty()) { return this; } + index.tui().progressf("Converting data: %s", types); List compendium = new ArrayList<>(); List rules = new ArrayList<>(); @@ -113,7 +111,7 @@ private Pf2eMarkdown writeNotesAndTables(List types) { case affliction, curse, disease -> compendium.add(new Json2QuteAffliction(index, type, node).buildNote()); case book -> { - index.tui().printlnf("📖 Looking at book: %s", e.getKey()); + index.tui().progressf("book %s", e.getKey()); JsonNode data = index.getIncludedNode(key.replace("book|", "data|")); if (data == null) { index.tui().errorf("No data for %s", key); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteBase.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteBase.java index b248b20ad..87a8ee701 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteBase.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteBase.java @@ -11,10 +11,9 @@ /** * Attributes for notes that are generated from the Pf2eTools data. * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteBase QuteBase}. - *

+ * * Notes created from {@code Pf2eQuteBase} will use a specific template * for the type. For example, {@code QuteBackground} will use {@code background2md.txt}. - *

*/ @TemplateData public class Pf2eQuteBase extends QuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteNote.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteNote.java index c0a194aab..b11f3c3ce 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteNote.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteNote.java @@ -11,10 +11,9 @@ /** * Attributes for notes that are generated from the Pf2eTools data. * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteNote QuteNote}. - *

+ * * Notes created from {@code Pf2eQuteNote} will use the {@code note2md.txt} template * unless otherwise noted. Folder index notes use {@code index2md.txt}. - *

*/ @TemplateData public class Pf2eQuteNote extends QuteNote { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java index 0cdfea648..b26591dd3 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java @@ -13,18 +13,15 @@ /** * Pf2eTools Ability attributes ({@code ability2md.txt} or {@code inline-ability2md.txt}). - *

+ * * Abilities are rendered both standalone and inline (as an admonition block). * The default template can render both. It contains some special syntax to handle * the inline case. - *

- *

+ * * Use `%%--` to mark the end of the preamble (frontmatter and * other leading content only appropriate to the standalone case). - *

- *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote} - *

*/ @TemplateData public final class QuteAbility extends Pf2eQuteNote implements QuteUtil.Renderable, QuteAbilityOrAffliction { @@ -33,7 +30,7 @@ public final class QuteAbility extends Pf2eQuteNote implements QuteUtil.Renderab public final String reference; /** * Collection of trait links. Use `{#for}` or `{#each}` to iterate over the collection. - * See traitList or bareTraitList. + * See [traitList](#traitlist) or [bareTraitList](#baretraitlist). */ public final Collection traits; /** {@link QuteDataRange}. The targeting range for this ability. */ diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbilityOrAffliction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbilityOrAffliction.java index b9393f732..62b4da5f2 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbilityOrAffliction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbilityOrAffliction.java @@ -7,11 +7,9 @@ * A union type which is either a {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbility QuteAbility} * or a {@link dev.ebullient.convert.tools.pf2e.qute.QuteAffliction QuteAffliction}. * - *

* Use {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction#isAbility() isAbility()} * and {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction#isAffliction() isAffliction()} * to tell whether it's an ability or an affliction. - *

*/ public sealed interface QuteAbilityOrAffliction extends QuteUtil permits QuteAbility, QuteAffliction { /** Returns true if this object is a {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbility QuteAbility} */ diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java index 6b19ebbf2..7397fe053 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java @@ -10,9 +10,8 @@ /** * Pf2eTools Action attributes ({@code action2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteAction extends Pf2eQuteBase { @@ -70,11 +69,11 @@ public boolean isItem() { /** * Pf2eTools Action type attributes. * - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference this attribute directly: `{resource.actionType}`. - *

+ * */ @TemplateData public static class ActionType { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java index 3e7e44e46..25068da6e 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java @@ -16,9 +16,8 @@ /** * Pf2eTools Affliction attributes (inline/embedded, {@code inline-affliction2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote} - *

*/ @TemplateData public final class QuteAffliction extends Pf2eQuteNote implements QuteUtil.Renderable, QuteAbilityOrAffliction { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java index 8fad44e3f..53ec98b96 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java @@ -9,9 +9,8 @@ /** * Pf2eTools Archetype attributes ({@code archetype2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteArchetype extends Pf2eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBackground.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBackground.java index 800c3f84b..826aa9de0 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBackground.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBackground.java @@ -8,9 +8,8 @@ /** * Pf2eTools Background attributes ({@code background2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteBackground extends Pf2eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBook.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBook.java index 2a5eda144..062562e71 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBook.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBook.java @@ -9,9 +9,8 @@ /** * Pf2eTools Book attributes ({@code book2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote} - *

*/ @TemplateData public class QuteBook extends Pf2eQuteNote { @@ -49,11 +48,11 @@ public String template() { /** * Pf2eTools book information * - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.actionType}`. - *

+ * */ @TemplateData public static class BookInfo { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java index 2df41779c..b24c918eb 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import dev.ebullient.convert.StringUtil; +import dev.ebullient.convert.io.JavadocVerbatim; import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.pf2e.Pf2eSources; @@ -21,9 +22,8 @@ /** * Pf2eTools Creature attributes ({@code creature2md.txt}) - *

+ * * Extension of {@link Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteCreature extends Pf2eQuteBase { @@ -99,10 +99,7 @@ public QuteCreature( /** * The languages and language features known by a creature. Example default output: - * - *
- * Common, Sylvan; telepathy 100ft; knows any language the summoner does - *
+ * `Common, Sylvan; telepathy 100ft; knows any language the summoner does` * * @param languages Languages known (optional) * @param notes Language-related notes (optional) @@ -123,9 +120,9 @@ public String toString() { /** * A creature's skill information. Example default output: * - *
+ * ```md * Athletics +10, Cult Lore +10 (lore on their cult), Stealth +10 (+12 in forests); Some skill note - *
+ * ``` * * @param skills Skill bonuses for the creature, as a list of * {@link QuteDataGenericStat.QuteDataNamedBonus QuteDataNamedBonus} @@ -143,10 +140,7 @@ public String toString() { } /** - * A creature's senses. Example default output: - *
- * tremorsense (imprecise) 20ft - *
+ * A creature's senses. Example default output: `tremorsense (imprecise) 20ft` * * @param name The name of the sense (required, string) * @param type The type of the sense - e.g. precise, imprecise (optional, string) @@ -241,6 +235,7 @@ public record CreatureSpellcasting( * The name for this set of spells. This is either the custom name, or derived from the tradition and * preparation - e.g. "Occult Prepared Spells", or "Divine Innate Spells". */ + @JavadocVerbatim public String name() { return customName != null && !customName.isBlank() ? customName @@ -250,10 +245,11 @@ public String name() { /** * Stats for this kind of spellcasting, including the DC, attack bonus, and any focus points. * - *
+ * ```md * DC 20, attack +25, 2 Focus Points - *
+ * ``` */ + @JavadocVerbatim public String formattedStats() { return join(", ", format("DC %d", dc), @@ -264,12 +260,14 @@ public String formattedStats() { /** * A collection of spells with some additional information. - *
- * Cantrips (9th) daze, shadow siphon (acid only) (×2) - *
- *
- * 4th confusion, phantasmal killer (2 slots) - *
+ * + * ```md + * **Cantrips (9th)** [daze](#), [shadow siphon](#) (acid only) (×2) + * ``` + * + * ```md + * **4th** [confusion](#), [phantasmal killer](#) (2 slots) + * ``` * * @param knownRank The rank that these spells are known at (0 for cantrips). May be absent for rituals. * @param cantripRank The rank that these spells are auto-heightened to. Present only for cantrips. @@ -311,9 +309,10 @@ public String toString() { /** * A spell known by the creature. - *
- * shadow siphon (acid only) (×2) - *
+ * + * ```md + * [shadow siphon](#) (acid only) (×2) + * ``` * * @param name The name of the spell * @param link A formatted link to the spell's note, or just the spell's name if we couldn't get a link. diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java index 8f2d043f3..7e72e158d 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java @@ -11,10 +11,13 @@ import io.quarkus.qute.TemplateData; /** - * Pf2eTools armor class attributes. Default representation example: - *

- * AC 15 (10 with mage armor) note ability - *

+ * Pf2eTools armor class attributes. + * + * Default representation example: + * + * ```md + * **AC** 15 (10 with mage armor) note ability + * ``` * * @param value The AC value * @param alternateValues Alternate AC values as a map of (condition, AC value) diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java index ef6bb45e5..5fd5abd1d 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java @@ -15,17 +15,21 @@ import io.quarkus.qute.TemplateData; /** - * Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard. Example: - *
    - *
  • AC 23 (33 with mage armor); Fort +15, Ref +12, Will +10
  • - *
  • - * Floor Hardness 18, Floor HP 72 (BT 36); - * Channel Hardness 12, Channel HP 48 (BT24 ) to destroy a channel gate; - * Immunities critical hits; - * Resistances precision damage; - * Weaknesses bludgeoning damage - *
  • - *
+ * Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard. + * + * Example: + * + * ```md + * **AC** 23 (33 with mage armor); **Fort** +15, **Ref** +12, **Will** +10 + * ``` + * + * ```md + * **Floor Hardness** 18, **Floor HP** 72 (BT 36); + * **Channel Hardness** 12, **Channel HP** 48 (BT24 ) to destroy a channel gate; + * **Immunities** critical hits; + * **Resistances** precision damage; + * **Weaknesses** bludgeoning damage + * ``` * * @param ac The armor class as a {@link QuteDataArmorClass} * @param savingThrows The saving throws, as {@link QuteDataDefenses.QuteSavingThrows} @@ -64,10 +68,11 @@ public String toString() { /** * Pathfinder 2e saving throws. Example default rendering: - *
- * Fort +10 (+12 vs. poison), Ref +5 (+7 vs. traps), Will +4 (+6 vs. mental); +1 status to + * + * ```md + * **Fort** +10 (+12 vs. poison), **Ref** +5 (+7 vs. traps), **Will** +4 (+6 vs. mental); +1 status to * all saves vs. magic - *
+ * ``` * * @param fort Fortitude saving throw bonus, as a {@link QuteDataGenericStat.QuteDataNamedBonus} * @param ref Reflex saving throw bonus, as a {@link QuteDataGenericStat.QuteDataNamedBonus} diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDuration.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDuration.java index 5cd8f18e1..ce195dd96 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDuration.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDuration.java @@ -5,9 +5,7 @@ * than an activity, or a {@link QuteDataActivity}. Use {@link QuteDataDuration#isActivity()} to check whether this * duration is an activity. * - *

* Using this directly will give the default representation for either object. - *

*/ public sealed interface QuteDataDuration permits QuteDataActivity, QuteDataTimedDuration { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataFrequency.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataFrequency.java index 91bcea4a1..c87438f63 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataFrequency.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataFrequency.java @@ -7,15 +7,16 @@ import java.util.List; /** - * A description of a frequency e.g. "once", which may include an interval that this is repeated for. Examples: - *
    - *
  • once per day
  • - *
  • once per hour
  • - *
  • 3 times per day
  • - *
  • {@code recurs=true}: once every day
  • - *
  • {@code overcharge=true}: once per day, plus overcharge
  • - *
  • {@code interval=2}: once per 2 days
  • - *
+ * A description of a frequency e.g. "once", which may include an interval that this is repeated for. + * + * Examples: + * + * - once per day + * - once per hour + * - 3 times per day + * - {@code recurs=true}: once every day + * - {@code overcharge=true}: once per day, plus overcharge + * - {@code interval=2}: once per 2 days * * @param value The number represented by the frequency, integer * @param unit The unit the frequency is in, string. Required. diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataGenericStat.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataGenericStat.java index 431ec8e4e..230c28a45 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataGenericStat.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataGenericStat.java @@ -32,11 +32,9 @@ default String formattedNotes() { /** * A basic {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat QuteDataGenericStat} which provides - * only a value and possibly a note. Default representation: + * only a value and possibly a note. * - *
- * 10 (some note) (some other note) - *
+ * Default representation: `10 (some note) (some other note)` */ class SimpleStat implements QuteDataGenericStat { private final Integer value; @@ -72,10 +70,10 @@ public List notes() { } /** - * A Pathfinder 2e named bonus, potentially with other conditional bonuses. Example default representation: - *
- * Stealth +36 (+42 in forests) (ignores tremorsense) - *
+ * A Pathfinder 2e named bonus, potentially with other conditional bonuses. + * + * Example default representation: + * `Stealth +36 (+42 in forests) (ignores tremorsense)` * * @param name The name of the skill * @param value The standard bonus associated with this skill diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataHpHardnessBt.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataHpHardnessBt.java index 14adbee5b..48cd92316 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataHpHardnessBt.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataHpHardnessBt.java @@ -14,20 +14,23 @@ /** * Hit Points, Hardness, and a broken threshold for hazards and shields. Used for creatures, hazards, and shields. * - *

* Hazard example with a broken threshold and note: - *

- *
Hardness 10, HP (BT) 30 (15) to destroy a channel gate
* - *

+ * ```md + * **Hardness** 10, **HP (BT)** 30 (15) to destroy a channel gate + * ``` + * * Hazard example with a name, broken threshold, and note: - *

- *
Floor Hardness 10, Floor HP 30 (BT 15) to destroy a channel gate
* - *

+ * ```md + * **Floor Hardness** 10, **Floor HP** 30 (BT 15) to destroy a channel gate + * ``` + * * Creature example with a name and ability: - *

- *
Head Hardness 10, Head HP 30 (hydra regeneration)
+ * + * ```md + * **Head Hardness** 10, **Head HP** 30 (hydra regeneration) + * ``` * * @param hp The HP as a {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataHpHardnessBt.HpStat HpStat} (optional) * @param hardness Hardness as a {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat.SimpleStat SimpleStat} @@ -64,9 +67,7 @@ public String toStringWithName(String name) { /** * HP value and associated notes. Referencing this directly provides a default representation, e.g. - *
- * 15 to destroy a head (head regrowth) - *
+ * `15 to destroy a head (head regrowth)` * * @param value The HP value itself * @param abilities Any abilities associated with the HP diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSpeed.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSpeed.java index f2fad4266..22244680f 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSpeed.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSpeed.java @@ -9,6 +9,10 @@ import java.util.Optional; /** + * Examples: + * + * - `10 feet, swim 20 feet (some note); some ability` + * - `10 feet, swim 20 feet, some ability` * * @param value The land speed in feet * @param otherSpeeds Other speeds, as a map of (name, speed in feet) @@ -36,16 +40,6 @@ public String formattedSpeeds() { formatMap(otherSpeeds, "%s %d feet"::formatted)); } - /** - * Examples: - *
- * 10 feet, swim 20 feet (some note); some ability - *
- *
- * 10 feet, swim 20 feet, some ability - *
- * - */ @Override public String toString() { return join(notes.isEmpty() ? ", " : " ", formattedSpeeds(), formattedNotes()); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTimedDuration.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTimedDuration.java index 3365d3c9e..e53ea972f 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTimedDuration.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTimedDuration.java @@ -5,14 +5,17 @@ import java.util.List; +import dev.ebullient.convert.io.JavadocVerbatim; + /** * A duration of time, represented by a numerical value and a unit. Sometimes this includes a custom display string, - * for durations which cannot be represented using the normal structure. Examples: - *
    - *
  • A duration of 3 minutes:
    3 minutes
  • - *
  • A duration of 1 turn:
    until the end of your next turn
  • - *
  • An unlimited duration:
    unlimited
  • - *
+ * for durations which cannot be represented using the normal structure. + * + * Examples: + * + * - A duration of 3 minutes: `3 minutes` + * - A duration of 1 turn: `until the end of your next turn` + * - An unlimited duration: `unlimited` * * @param value The quantity of time * @param unit The unit that the quantity is measured in, as a {@link QuteDataTimedDuration.DurationUnit} @@ -34,6 +37,8 @@ public boolean hasCustomDisplay() { return !notes.isEmpty(); } + /** Returns a comma delimited string containing all notes. */ + @JavadocVerbatim @Override public String formattedNotes() { return join(", ", notes); diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDeity.java index 44bb01ec8..17ecb857b 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDeity.java @@ -17,18 +17,15 @@ /** * Pf2eTools Deity attributes ({@code deity2md.txt}) - *

+ * * Deities are rendered both standalone and inline (as an admonition block). - * The default template can render both. It contains - * some special syntax to handle the inline case. - *

- *

+ * The default template can render both. + * It uses special syntax to handle the inline case. + * * Use `%%--` to mark the end of the preamble (frontmatter and * other leading content only appropriate to the standalone case). - *

- *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteDeity extends Pf2eQuteBase { @@ -73,11 +70,11 @@ public QuteDeity(Pf2eSources sources, List text, Tags tags, /** * Pf2eTools cleric divine attributes * - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.actionType}`. - *

+ * */ @TemplateData public static class QuteDeityCleric implements QuteUtil { @@ -121,11 +118,11 @@ public String toString() { /** * Pf2eTools avatar attributes * - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.actionType}`. - *

+ * */ @TemplateData public static class QuteDivineAvatar implements QuteUtil { @@ -141,19 +138,17 @@ public static class QuteDivineAvatar implements QuteUtil { /** * Example: * - *
- *

- * Cee-el-aye When casting the avatar spell, a worshipper of the Cee-el-aye typically begins reading + * ```md + * **Cee-el-aye** When casting the avatar spell, a worshipper of the Cee-el-aye typically begins reading * entirely too much JSON, and gains the following additional abilities. - *

- *

+ * * Speed 50 feet, burrow 70 feet, immune to petrified; * shield (15 Hardness, can't be damaged); - * Melee polytool (reach 15 feet), Damage 6d6+6 slashing; - * Ranged pull request (nonlethal, reach 9358 miles), Damage 3d6+3 mental plus commit + * **Melee** polytool (reach 15 feet), **Damage** 6d6+6 slashing; + * **Ranged** pull request (nonlethal, reach 9358 miles), **Damage** 3d6+3 mental plus commit * history; - * Commit History A creature who reviews the pull request must spend the next 1d4 hours reading code. - *

+ * **Commit History** A creature who reviews the pull request must spend the next 1d4 hours reading code. + * ``` */ @Override public String toString() { @@ -167,11 +162,11 @@ public String toString() { /** * Pf2eTools divine intercession attributes. * - *

+ * * This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.actionType}`. - *

+ * */ @TemplateData public static class QuteDivineIntercession { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java index 35d15054e..fc9f64fe0 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java @@ -9,18 +9,15 @@ /** * Pf2eTools Feat attributes ({@code feat2md.txt}) - *

+ * * Feats are rendered both standalone and inline (as an admonition block). - * The default template can render both. It contains - * some special syntax to handle the inline case. - *

- *

+ * The default template can render both. + * It uses special syntax to handle the inline case. + * * Use `%%--` to mark the end of the preamble (frontmatter and * other leading content only appropriate to the standalone case). - *

- *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteFeat extends Pf2eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java index d2ac50883..a869db30c 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java @@ -13,18 +13,15 @@ /** * Pf2eTools Hazard attributes ({@code hazard2md.txt}) - *

+ * * Hazards are rendered both standalone and inline (as an admonition block). - * The default template can render both. It contains - * some special syntax to handle the inline case. - *

- *

+ * The default template can render both. + * It uses special syntax to handle the inline case. + * * Use `%%--` to mark the end of the preamble (frontmatter and * other leading content only appropriate to the standalone case). - *

- *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteHazard extends Pf2eQuteBase { @@ -54,21 +51,22 @@ public class QuteHazard extends Pf2eQuteBase { * The hazard's actions, as a list of * {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction QuteAbilityOrAffliction}. * - *

* Using the elements directly will give a default rendering, but if you want more * control you can use {@code isAffliction} and {@code isAbility} to check whether it's an affliction or an * ability. Example: - *

* - *
+     *
+     * ```md
      * {#each resource.actions}
      * {#if it.isAffliction}
+     *
      * **Affliction** {it}
      * {#else if it.isAbility}
+     *
      * **Ability** {it}
      * {/if}
      * {/each}
-     * 
+ * ``` */ public final List actions; @@ -132,10 +130,8 @@ public String convertToEmbed(String content, String title, String admonition) { /** * Pf2eTools hazard attributes. * - *

* This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. - *

* * @param value The hazard's Stealth bonus * @param minProf The minimum Perception proficiency required to be able to roll against the hazard's Stealth diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java index be4bef495..071c97558 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java @@ -14,9 +14,7 @@ /** * Pf2eTools Attack attributes (inline/embedded, {@code inline-attack2md.txt}) * - *

* When used directly, renders according to {@code inline-attack2md.txt} - *

*/ @TemplateData public final class QuteInlineAttack implements QuteDataGenericStat, QuteUtil.Renderable { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java index cbc8be22d..e599d4fe6 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java @@ -13,9 +13,8 @@ /** * Pf2eTools Item attributes - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteItem extends Pf2eQuteBase { @@ -99,11 +98,9 @@ public QuteItem(Pf2eSources sources, List text, Tags tags, /** * Pf2eTools item activation attributes. * - *

* This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.activate}`. - *

*/ @TemplateData public static class QuteItemActivate implements QuteUtil { @@ -144,9 +141,10 @@ public String toString() { /** * Pf2eTools item shield attributes. When referenced directly, provides a default formatting, e.g. - *

- * AC Bonus +2; Speed Penalty —; Hardness 3; HP (BT) 12 (6) - *

+ * + * ```md + * **AC Bonus** +2; **Speed Penalty** —; **Hardness** 3; **HP (BT)** 12 (6) + * ``` * * @param ac AC bonus for the shield, as {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass QuteDataArmorClass} * (required) @@ -171,11 +169,9 @@ public String toString() { /** * Pf2eTools item armor attributes * - *

* This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.armor}`. - *

*/ @TemplateData public static class QuteItemArmorData implements QuteUtil { @@ -212,24 +208,25 @@ public String toString() { /** * Pf2eTools item weapon attributes * - *

* This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. - * To use it, reference it directly:
- * ```
- * {#for weapons in resource.weapons}
- * {weapons}
- * {/for}
- * ```
- * or, using `{#each}` instead:
- * ```
- * {#each resource.weapons}
- * {it}
- * {/each}
+ * + * To use it, reference it directly: + * + * ```md + * {#for weapons in resource.weapons} + * {weapons} + * {/for} + * ``` + * + * or, using `{#each}` instead: + * + * ```md + * {#each resource.weapons} + * {it} + * {/each} * ``` - *

*/ - @TemplateData public static class QuteItemWeaponData implements QuteUtil { /** Formatted string. Weapon type */ @@ -265,22 +262,24 @@ public String toString() { /** * Pf2eTools item variant attributes * - *

* This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. - * To use it, reference it directly:
- * ```
- * {#for variants in resource.variants}
- * {variants}
- * {/for}
- * ```
- * or, using `{#each}` instead:
- * ```
- * {#each resource.variants}
- * {it}
- * {/each}
+ * + * To use it, reference it directly: + * + * ```md + * {#for variants in resource.variants} + * {variants} + * {/for} + * ``` + * + * or, using `{#each}` instead: + * + * ```md + * {#each resource.variants} + * {it} + * {/each} * ``` - *

*/ @TemplateData public static class QuteItemVariant implements QuteUtil { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java index e550b561a..562aff6b8 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java @@ -13,9 +13,8 @@ /** * Pf2eTools Ritual attributes ({@code ritual2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteRitual extends Pf2eQuteBase { @@ -65,11 +64,9 @@ public QuteRitual(Pf2eSources sources, List text, Tags tags, /** * Pf2eTools ritual casting attributes * - *

* This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.casting}`. - *

*/ @TemplateData public static class QuteRitualCasting implements QuteUtil { @@ -103,11 +100,9 @@ public String toString() { /** * Pf2eTools ritual check attributes * - *

* This data object provides a default mechanism for creating * a marked up string based on the attributes that are present. * To use it, reference it directly: `{resource.checks}`. - *

*/ @TemplateData public static class QuteRitualChecks implements QuteUtil { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java index 69a73c457..c53d30dc1 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java @@ -7,6 +7,7 @@ import java.util.Collection; import java.util.List; +import dev.ebullient.convert.io.JavadocVerbatim; import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.Tags; @@ -15,9 +16,8 @@ /** * Pf2eTools Spell attributes ({@code spell2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteSpell extends Pf2eQuteBase { @@ -108,18 +108,23 @@ public boolean getHasSections() { /** * The components required for the spell, as a formatted string. Example: - *
- * somatic, verbal - *
+ * + * ```md + * [somatic](#), [verbal](#) + * ``` */ + @JavadocVerbatim public String formattedComponents() { return join(", ", components); } /** - * Details about the saving throw for a spell. Example default representations: - *
basic Reflex or Fortitude
- *
basic Reflex, Fortitude, or Willpower
+ * Details about the saving throw for a spell. + * + * Example default representations: + * + * - `basic Reflex or Fortitude` + * - `basic Reflex, Fortitude, or Willpower` * * @param saves The saving throws that can be used for this spell (list of strings) * @param basic True if this is a basic save (boolean) @@ -136,9 +141,12 @@ public String toString() { } /** - * Details about the duration of the spell. Example default representations: - *
1 minute
- *
sustained up to 1 minute
+ * Details about the duration of the spell. + * + * Example default representations: + * + * - `1 minute` + * - `sustained up to 1 minute` * * @param sustained Whether this is a sustained spell, boolean * @param dismissable Whether this spell can be dismissed, boolean. Not included in the default representation. @@ -164,10 +172,9 @@ public String toString() { /** * Pf2eTools spell target attributes. - *

+ * * This attribute will render itself as labeled elements * if you reference it directly: `{resource.targeting}`. - *

*/ @TemplateData public static class QuteSpellTarget implements QuteUtil { @@ -195,10 +202,9 @@ public String toString() { /** * Pf2eTools spell Amp attributes - *

+ * * This attribute will render itself as labeled elements * if you reference it directly: `{resource.amp}`. - *

*/ @TemplateData public static class QuteSpellAmp implements QuteUtil { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTrait.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTrait.java index 1481c38ba..d97d0cf66 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTrait.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTrait.java @@ -8,9 +8,8 @@ /** * Pf2eTools Trait attributes ({@code trait2md.txt}) - *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase} - *

*/ @TemplateData public class QuteTrait extends Pf2eQuteBase { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java index 4692ca6ac..7dd32c940 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java @@ -13,14 +13,12 @@ /** * Pf2eTools Trait index attributes ({@code indexTrait.md}) - *

+ * * This replaces the index usually generated for folders. * The default template for the trait consructs a list of links to * traits grouped by category. - *

- *

+ * * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote} - *

*/ @TemplateData public class QuteTraitIndex extends Pf2eQuteNote { diff --git a/src/main/resources/convertData.json b/src/main/resources/convertData.json index 4a7403d2e..eada461ba 100644 --- a/src/main/resources/convertData.json +++ b/src/main/resources/convertData.json @@ -1,7 +1,12 @@ { "config5e": { + "constants": { + "imgRoot": "https://raw.githubusercontent.com/5etools-mirror-3/5etools-img/main/", + "tools5eSource": "https://api.github.com/repos/5etools-mirror-3/5etools-src/releases/latest" + }, "templateKeys": [ "background", + "bastion", "class", "deck", "deity", @@ -24,10 +29,7 @@ { "type": "entries", "name": "Attunement", - "source": "DMG", - "page": 136, - "srd": true, - "basicRules": true, + "edition": "classic", "entries": [ "Some magic items require a creature to form a bond with them before their magical properties can be used. This bond is called attunement, and certain items have a prerequisite for it. If the prerequisite is a class, a creature must be a member of that class to attune to the item. (If the class is a spellcasting class, a monster qualifies if it has spell slots and uses that class's spell list.) If the prerequisite is to be a spellcaster, a creature qualifies if it can cast at least one spell using its traits or features, not using a magic item or the like.", "Without becoming attuned to an item that requires attunement, a creature gains only its nonmagical benefits, unless its description states otherwise. For example, a magic shield that requires attunement provides the benefits of a normal shield to a creature not attuned to it, but none of its magical properties.", @@ -51,235 +53,69 @@ ] }, { - "name": "General and Weapon Properties", - "srd": true, - "basicRules": true, + "type": "entries", + "name": "Attunement", + "edition": "one", + "source": "XPHB", + "page": 232, + "id": "717", "entries": [ + "Some magic items require a creature to form a bond\u2014called Attunement\u2014with them before the creature can use an item's magical properties. Without becoming attuned to an item that requires Attunement, you gain only its nonmagical benefits unless its description states otherwise. For example, a magic Shield that requires Attunement provides the benefits of a normal Shield if you aren't attuned to it, but none of its magical properties.", { "type": "entries", - "name": "Ammunition", - "itemType": "A", - "abbreviation": "A", - "srd": true, - "basicRules": true, - "source": "PHB", - "page": 146, - "entries": [ - "You can use a weapon that has the ammunition property to make a ranged attack only if you have ammunition to fire from the weapon. Each time you attack with the weapon, you expend one piece of ammunition. Drawing the ammunition from a quiver, case, or other container is part of the attack. Loading a one-handed weapon requires a free hand. At the end of the battle, you can recover half your expended ammunition by taking a minute to search the battlefield.", - "If you use a weapon that has the ammunition property to make a melee attack, you treat the weapon as an improvised weapon. A sling must be loaded to deal any damage when used in this way." - ] - }, - { - "type": "entries", - "name": "Ammunition (Firearm)", - "itemType": "AF", - "abbreviation": "AF", - "page": 267, - "source": "DMG", - "entries": [ - "You can use a weapon that has the ammunition property to make a ranged attack only if you have ammunition to fire from the weapon. Each time you attack with the weapon, you expend one piece of ammunition. Drawing the ammunition from a quiver, case, or other container is part of the attack. Loading a one-handed weapon requires a free hand. The ammunition of a firearm is destroyed upon use.", - "If you use a weapon that has the ammunition property to make a melee attack, you treat the weapon as an improvised weapon. A sling must be loaded to deal any damage when used in this way." - ] - }, - { - "type": "entries", - "name": "Burst Fire", - "abbreviation": "BF", - "page": 267, - "source": "DMG", - "entries": [ - "A weapon that has the burst fire property can make a single-target attack, or it can spray a 10-foot-cube area within normal range with shots. Each creature in the area must succeed on a DC 15 Dexterity saving throw or take the weapon's normal damage. This action uses ten pieces of ammunition." - ] - }, - { - "type": "entries", - "name": "Finesse", - "abbreviation": "F", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, - "entries": [ - "When making an attack with a finesse weapon, you use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls." - ] - }, - { - "type": "entries", - "name": "Heavy", - "abbreviation": "H", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, - "entries": [ - "Creatures that are Small or Tiny have disadvantage on attack rolls with heavy weapons. A heavy weapon's size and bulk make it too large for a Small or Tiny creature to use effectively." - ] - }, - { - "type": "entries", - "name": "Light", - "abbreviation": "L", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, - "entries": [ - "A light weapon is small and easy to handle, making it ideal for use when fighting with two weapons." - ] - }, - { - "type": "entries", - "name": "Loading", - "abbreviation": "LD", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, - "entries": [ - "Because of the time required to load this weapon, you can fire only one piece of ammunition from it when you use an action, bonus action, or reaction to fire it, regardless of the number of attacks you can normally make." - ] - }, - { - "type": "entries", - "name": "Martial", - "srd": true, - "basicRules": true, - "entries": [ - "Martial weapons, including swords, axes, and polearms, require more specialized training to use effectively. Most warriors use martial weapons because these weapons put their fighting style and training to best use." - ] - }, - { - "type": "entries", - "name": "Poison", - "source": "DMG", - "page": 257, - "entries": [ - "Given their insidious and deadly nature, poisons are illegal in most societies but are a favorite tool among assassins, drow, and other evil creatures.", - "Poisons come in the following four types.", - { - "type": "entries", - "name": "Contact", - "page": 257, - "entries": [ - "Contact poison can be smeared on an object and remains potent until it is touched or washed off. A creature that touches contact poison with exposed skin suffers its effects." - ], - "id": "2f9" - }, - { - "type": "entries", - "name": "Ingested", - "page": 257, - "entries": [ - "A creature must swallow an entire dose of ingested poison to suffer its effects. You might decide that a partial dose has a reduced effect, such as allowing advantage on the saving throw or dealing only half damage on a failed save. The dose can be delivered in food or a liquid." - ], - "id": "2fa" - }, - { - "type": "entries", - "name": "Inhaled", - "page": 257, - "entries": [ - "These poisons are powders or gases that take effect when inhaled. Blowing the powder or releasing the gas subjects creatures in a 5-foot cube to its effect. The resulting cloud dissipates immediately afterward. Holding one's breath is ineffective against inhaled poisons, as they affect nasal membranes, tear ducts, and other parts of the body." - ], - "id": "2fb" - }, - { - "type": "entries", - "name": "Injury", - "page": 257, - "entries": [ - "Injury poison can be applied to weapons, ammunition, trap components, and other objects that deal piercing or slashing damage and remains potent until delivered through a wound or washed off. A creature that takes piercing or slashing damage from an object coated with the poison is exposed to its effects." - ], - "id": "2fc" - } - ] - }, - { - "type": "entries", - "name": "Range", - "source": "PHB", - "page": 146, - "itemType": "R", - "srd": true, - "basicRules": true, - "entries": [ - "A weapon that can be used to make a ranged attack has a range in parentheses after the ammunition or thrown property. The range lists two numbers. The first is the weapon's normal range in feet, and the second indicates the weapon's long range. When attacking a target beyond normal range, you have disadvantage on the attack roll. You can't attack a target beyond the weapon's long range." - ] - }, - { - "type": "entries", - "name": "Reach", - "abbreviation": "R", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, + "name": "Attune during a Short Rest", + "page": 232, + "id": "718", "entries": [ - "This weapon adds 5 feet to your reach when you attack with it. This property also determines your reach for opportunity attacks with a reach weapon." + "Attuning to an item requires you to spend a {@variantrule Short Rest|XPHB} focused on only that item while being in physical contact with it (this can't be the same Short Rest used to learn the item's properties). This focus can take the form of weapon practice (for a Weapon), meditation (for a Wand), or some other appropriate activity. If the Short Rest is interrupted, the Attunement attempt fails. Otherwise, at the end of the Short Rest, you're attuned to the magic item and can access its full magical capabilities." ] }, { "type": "entries", - "name": "Reload", - "abbreviation": "RLD", - "source": "DMG", - "page": 267, + "name": "No More Than Three Items", + "page": 232, + "id": "719", "entries": [ - "A limited number of shots can be made with a weapon that has the reload property. A character must then reload it using an action or a bonus action (the character's choice)." + "You can be attuned to no more than three magic items at a time. Any attempt to attune to a fourth item fails; you must end your Attunement to an item first. Additionally, you can't attune to more than one copy of an item. For example, you can't attune to more than one {@item Ring of Protection} at a time." ] }, { "type": "entries", - "name": "Special", - "abbreviation": "S", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, + "name": "Ending Attunement", + "page": 232, + "id": "71a", "entries": [ - "A weapon with the special property has unusual rules governing its use, explained in the weapon's description (see \"Special Weapons\")." + "Your Attunement to an item ends if you no longer satisfy the prerequisites for Attunement, if the item has been more than 100 feet away for at least 24 hours, if you die, or if another creature attunes to the item. You can also voluntarily end Attunement by spending another {@variantrule Short Rest|XPHB} focused on the item unless the item is cursed." ] }, { "type": "entries", - "name": "Thrown", - "abbreviation": "T", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, - "entries": [ - "If a weapon has the thrown property, you can throw the weapon to make a ranged attack. If the weapon is a melee weapon, you use the same ability modifier for that attack roll and damage roll that you would use for a melee attack with the weapon. For example, if you throw a handaxe, you use your Strength, but if you throw a dagger, you can use either your Strength or your Dexterity, since the dagger has the finesse property." - ] - }, - { - "type": "entries", - "name": "Two-Handed", - "abbreviation": "2H", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, + "name": "Optional Attunement", "entries": [ - "This weapon requires two hands to use. This property is relevant only when you attack with the weapon, not when you simply hold it." + "Attunement may be required for this item." ] }, { "type": "entries", - "name": "Versatile", - "abbreviation": "V", - "source": "PHB", - "page": 147, - "srd": true, - "basicRules": true, + "name": "Requires Attunement", "entries": [ - "This weapon can be used with one or two hands. A damage value in parentheses appears with the property\u2014the damage when the weapon is used with two hands to make a melee attack." + "Attunement is required for this item." ] } ] }, + { + "name": "General and Weapon Properties", + "srd": true, + "basicRules": true, + "entries": [ + ] + }, { "name": "Improvised Weapons", + "edition": "classic", + "source": "PHB", + "page": 147, "srd": true, "basicRules": true, "entries": [ @@ -288,10 +124,22 @@ "An object that bears no resemblance to a weapon deals 1d4 damage (the DM assigns a damage type appropriate to the object). If a character uses a ranged weapon to make a melee attack, or throws a melee weapon that does not have the thrown property, it also deals 1d4 damage. An improvised thrown weapon has a normal range of 20 feet and a long range of 60 feet." ] }, + { + "name": "Improvised Weapons", + "edition": "one", + "source": "XPHB", + "page": 213, + "id": "645", + "entries": [ + "If you use an object\u2014such as a table leg, frying pan, or bottle\u2014as a makeshift weapon, see \"{@variantrule Improvised Weapons|XPHB}\" in the {@book rules glossary|XPHB|10|Improvised Weapons}. Also see those rules if you wield a weapon in an unusual way, such as using a Ranged weapon to make a melee attack." + ] + }, { "name": "Silvered Weapons", "srd": true, "basicRules": true, + "source": "PHB", + "page": 148, "entries": [ "Some monsters that have immunity or resistance to nonmagical weapons are susceptible to silver weapons, so cautious adventurers invest extra coin to plate their weapons with silver. You can silver a single weapon or ten pieces of ammunition for 100 gp. This cost represents not only the price of the silver, but the time and expertise needed to add silver to the weapon without making it less effective." ] @@ -300,6 +148,8 @@ "name": "Special Weapons", "srd": true, "basicRules": true, + "source": "PHB", + "page": 148, "entries": [ "Weapons with special rules are described here.", { @@ -320,18 +170,391 @@ ] }, { - "name": "Vestige", - "source": "TDCSR", - "page": 200, + "type": "entries", + "name": "Cursed Items", + "source": "DMG", + "page": 138, "entries": [ - "A Vestige of Divergence, see {@book Tal'Dorei Treasures|TDCSR|5|Vestiges of Divergence} for more information." + "Some magic items bear curses that bedevil their users, sometimes long after a user has stopped using an item. Most methods of identifying items, including the identify spell, fail to reveal the presence of a curse, although lore might hint at it.", + "Attunement to a cursed item can't be ended voluntarily unless the curse is broken first, such as with the {@spell remove curse} spell." + ] + }, + { + "type": "entries", + "name": "Poison", + "source": "DMG", + "page": 257, + "entries": [ + "Given their insidious and deadly nature, poisons are illegal in most societies but are a favorite tool among assassins, drow, and other evil creatures.", + "Poisons come in the following four types.", + { + "type": "entries", + "name": "Contact", + "page": 257, + "entries": [ + "Contact poison can be smeared on an object and remains potent until it is touched or washed off. A creature that touches contact poison with exposed skin suffers its effects." + ], + "id": "2f9" + }, + { + "type": "entries", + "name": "Ingested", + "page": 257, + "entries": [ + "A creature must swallow an entire dose of ingested poison to suffer its effects. You might decide that a partial dose has a reduced effect, such as allowing advantage on the saving throw or dealing only half damage on a failed save. The dose can be delivered in food or a liquid." + ], + "id": "2fa" + }, + { + "type": "entries", + "name": "Inhaled", + "page": 257, + "entries": [ + "These poisons are powders or gases that take effect when inhaled. Blowing the powder or releasing the gas subjects creatures in a 5-foot cube to its effect. The resulting cloud dissipates immediately afterward. Holding one's breath is ineffective against inhaled poisons, as they affect nasal membranes, tear ducts, and other parts of the body." + ], + "id": "2fb" + }, + { + "type": "entries", + "name": "Injury", + "page": 257, + "entries": [ + "Injury poison can be applied to weapons, ammunition, trap components, and other objects that deal piercing or slashing damage and remains potent until delivered through a wound or washed off. A creature that takes piercing or slashing damage from an object coated with the poison is exposed to its effects." + ], + "id": "2fc" + } ] } ] }, - "constants": { - "imgRoot": "https://raw.githubusercontent.com/5etools-mirror-2/5etools-img/main/" - }, + "basicRules": [ + "action|attack|phb", + "action|cast a spell|phb", + "action|dash|phb", + "action|disengage|phb", + "action|dodge|phb", + "action|escape a grapple|phb", + "action|grapple|phb", + "action|help|phb", + "action|hide|phb", + "action|opportunity attack|phb", + "action|ready|phb", + "action|search|phb", + "action|shove|phb", + "action|two-weapon fighting|phb", + "action|use an object|phb", + "background|acolyte|phb", + "background|criminal|phb", + "background|folk hero|phb", + "background|noble|phb", + "background|sage|phb", + "background|soldier|phb", + "classtype|cleric|phb", + "classtype|fighter|phb", + "classtype|rogue|phb", + "classtype|wizard|phb", + "condition|blinded|phb", + "condition|charmed|phb", + "condition|deafened|phb", + "condition|frightened|phb", + "condition|grappled|phb", + "condition|incapacitated|phb", + "condition|invisible|phb", + "condition|paralyzed|phb", + "condition|petrified|phb", + "condition|poisoned|phb", + "condition|prone|phb", + "condition|restrained|phb", + "condition|stunned|phb", + "condition|unconscious|phb", + "itemproperty|2h|phb", + "itemproperty|a|phb", + "itemproperty|f|phb", + "itemproperty|h|phb", + "itemproperty|ld|phb", + "itemproperty|l|phb", + "itemproperty|rld|dmg", + "itemproperty|r|phb", + "itemproperty|s|phb", + "itemproperty|t|phb", + "itemproperty|v|phb", + "itemtype|$c|phb", + "itemtype|at|phb", + "itemtype|a|phb", + "itemtype|fd|phb", + "itemtype|gs|phb", + "itemtype|g|phb", + "itemtype|ha|phb", + "itemtype|ins|phb", + "itemtype|la|phb", + "itemtype|ma|phb", + "itemtype|mnt|phb", + "itemtype|m|phb", + "itemtype|oth|phb", + "itemtype|p|phb", + "itemtype|r|phb", + "itemtype|scf|phb", + "itemtype|s|phb", + "itemtype|tah|phb", + "itemtype|tg|phb", + "itemtype|t|phb", + "itemtype|veh|phb", + "race|dwarf|phb", + "race|elf|phb", + "race|halfling|phb", + "race|human|phb", + "sense|blindsight|phb", + "sense|darkvision|phb", + "sense|truesight|phb", + "skill|acrobatics|phb", + "skill|animal handling|phb", + "skill|arcana|phb", + "skill|athletics|phb", + "skill|deception|phb", + "skill|history|phb", + "skill|insight|phb", + "skill|intimidation|phb", + "skill|investigation|phb", + "skill|medicine|phb", + "skill|nature|phb", + "skill|perception|phb", + "skill|performance|phb", + "skill|persuasion|phb", + "skill|religion|phb", + "skill|sleight of hand|phb", + "skill|stealth|phb", + "skill|survival|phb", + "subclass|champion|fighter|phb|phb", + "subclass|life domain|cleric|phb|phb", + "subclass|school of evocation|wizard|phb|phb", + "subclass|thief|rogue|phb|phb", + "subrace|dwarf (hill)|dwarf|phb|phb", + "subrace|dwarf (mountain)|dwarf|phb|phb", + "subrace|elf (high)|elf|phb|phb", + "subrace|elf (wood)|elf|phb|phb", + "subrace|halfling (lightfoot)|halfling|phb|phb", + "subrace|halfling (stout)|halfling|phb|phb", + "subrace|human (variant)|human|phb|phb" + ], + "freeRules2024": [ + "action|attack|xphb", + "action|dash|xphb", + "action|disengage|xphb", + "action|dodge|xphb", + "action|escape a grapple|xphb", + "action|help|xphb", + "action|hide|xphb", + "action|influence|xphb", + "action|magic|xphb", + "action|opportunity attack|xphb", + "action|ready|xphb", + "action|search|xphb", + "action|study|xphb", + "action|two-weapon fighting|xphb", + "action|utilize|xphb", + "background|acolyte|xphb", + "background|criminal|xphb", + "background|sage|xphb", + "background|soldier|xphb", + "classtype|barbarian|xphb", + "classtype|bard|xphb", + "classtype|cleric|xphb", + "classtype|druid|xphb", + "classtype|fighter|xphb", + "classtype|monk|xphb", + "classtype|paladin|xphb", + "classtype|ranger|xphb", + "classtype|rogue|xphb", + "classtype|sorcerer|xphb", + "classtype|warlock|xphb", + "classtype|wizard|xphb", + "condition|blinded|xphb", + "condition|charmed|xphb", + "condition|deafened|xphb", + "condition|exhaustion|xphb", + "condition|frightened|xphb", + "condition|grappled|xphb", + "condition|incapacitated|xphb", + "condition|invisible|xphb", + "condition|paralyzed|xphb", + "condition|petrified|xphb", + "condition|poisoned|xphb", + "condition|prone|xphb", + "condition|restrained|xphb", + "condition|stunned|xphb", + "condition|unconscious|xphb", + "hazard|burning|xphb", + "hazard|dehydration|xphb", + "hazard|falling|xphb", + "hazard|malnutrition|xphb", + "hazard|suffocation|xphb", + "feat|ability score improvement|xphb", + "feat|alert|xphb", + "feat|archery|xphb", + "feat|boon of combat prowess|xphb", + "feat|boon of dimensional travel|xphb", + "feat|boon of fate|xphb", + "feat|boon of irresistible offense|xphb", + "feat|boon of the night spirit|xphb", + "feat|boon of truesight|xphb", + "feat|defense|xphb", + "feat|great weapon fighting|xphb", + "feat|magic initiate|xphb", + "feat|savage attacker|xphb", + "feat|skilled|xphb", + "feat|two-weapon fighting|xphb", + "race|dwarf|xphb", + "race|elf|xphb", + "race|halfling|xphb", + "race|human|xphb", + "sense|blindsight|xphb", + "sense|darkvision|xphb", + "sense|tremorsense|xphb", + "sense|truesight|xphb", + "skill|acrobatics|xphb", + "skill|animal handling|xphb", + "skill|arcana|xphb", + "skill|athletics|xphb", + "skill|deception|xphb", + "skill|history|xphb", + "skill|insight|xphb", + "skill|intimidation|xphb", + "skill|investigation|xphb", + "skill|medicine|xphb", + "skill|nature|xphb", + "skill|perception|xphb", + "skill|performance|xphb", + "skill|persuasion|xphb", + "skill|religion|xphb", + "skill|sleight of hand|xphb", + "skill|stealth|xphb", + "skill|survival|xphb", + "subclass|champion|fighter|xphb|xphb", + "subclass|circle of the land|druid|xphb|xphb", + "subclass|college of lore|bard|xphb|xphb", + "subclass|draconic sorcery|sorcerer|xphb|xphb", + "subclass|evoker|wizard|xphb|xphb", + "subclass|fiend patron|warlock|xphb|xphb", + "subclass|hunter|ranger|xphb|xphb", + "subclass|life domain|cleric|xphb|xphb", + "subclass|oath of devotion|paladin|xphb|xphb", + "subclass|path of the berserker|barbarian|xphb|xphb", + "subclass|thief|rogue|xphb|xphb", + "subclass|warrior of the open hand|monk|xphb|xphb", + "variantrule|ability check|xphb", + "variantrule|ability score and modifier|xphb", + "variantrule|action|xphb", + "variantrule|advantage|xphb", + "variantrule|adventure|xphb", + "variantrule|alignment|xphb", + "variantrule|ally|xphb", + "variantrule|area of effect|xphb", + "variantrule|armor class|xphb", + "variantrule|armor training|xphb", + "variantrule|attack roll|xphb", + "variantrule|attitude|xphb", + "variantrule|attunement|xphb", + "variantrule|bloodied|xphb", + "variantrule|bonus action|xphb", + "variantrule|breaking objects|xphb", + "variantrule|bright light|xphb", + "variantrule|burrow speed|xphb", + "variantrule|campaign|xphb", + "variantrule|cantrip|xphb", + "variantrule|carrying capacity|xphb", + "variantrule|challenge rating|xphb", + "variantrule|character sheet|xphb", + "variantrule|climb speed|xphb", + "variantrule|climbing|xphb", + "variantrule|condition|xphb", + "variantrule|cone [area of effect]|xphb", + "variantrule|cover|xphb", + "variantrule|crawling|xphb", + "variantrule|creature type|xphb", + "variantrule|creature|xphb", + "variantrule|critical hit|xphb", + "variantrule|cube [area of effect]|xphb", + "variantrule|curses|xphb", + "variantrule|cylinder [area of effect]|xphb", + "variantrule|d20 test|xphb", + "variantrule|damage roll|xphb", + "variantrule|damage threshold|xphb", + "variantrule|damage types|xphb", + "variantrule|damage|xphb", + "variantrule|darkness|xphb", + "variantrule|dead|xphb", + "variantrule|death saving throw|xphb", + "variantrule|difficult terrain|xphb", + "variantrule|difficulty class|xphb", + "variantrule|dim light|xphb", + "variantrule|disadvantage|xphb", + "variantrule|encounter|xphb", + "variantrule|enemy|xphb", + "variantrule|experience points|xphb", + "variantrule|expertise|xphb", + "variantrule|fly speed|xphb", + "variantrule|flying|xphb", + "variantrule|friendly [attitude]|xphb", + "variantrule|grappling|xphb", + "variantrule|hazard|xphb", + "variantrule|healing|xphb", + "variantrule|heavily obscured|xphb", + "variantrule|heroic inspiration|xphb", + "variantrule|high jump|xphb", + "variantrule|hit point dice|xphb", + "variantrule|hit points|xphb", + "variantrule|hostile [attitude]|xphb", + "variantrule|hover|xphb", + "variantrule|illusions|xphb", + "variantrule|immunity|xphb", + "variantrule|improvised weapons|xphb", + "variantrule|indifferent [attitude]|xphb", + "variantrule|initiative|xphb", + "variantrule|jumping|xphb", + "variantrule|knocking out a creature|xphb", + "variantrule|lightly obscured|xphb", + "variantrule|long jump|xphb", + "variantrule|long rest|xphb", + "variantrule|magical effect|xphb", + "variantrule|monster|xphb", + "variantrule|nonplayer character|xphb", + "variantrule|object|xphb", + "variantrule|occupied space|xphb", + "variantrule|passive perception|xphb", + "variantrule|per day|xphb", + "variantrule|player character|xphb", + "variantrule|possession|xphb", + "variantrule|proficiency|xphb", + "variantrule|reaction|xphb", + "variantrule|resistance|xphb", + "variantrule|ritual|xphb", + "variantrule|round down|xphb", + "variantrule|save|xphb", + "variantrule|saving throw|xphb", + "variantrule|shape-shifting|xphb", + "variantrule|short rest|xphb", + "variantrule|simultaneous effects|xphb", + "variantrule|size|xphb", + "variantrule|skill|xphb", + "variantrule|speed|xphb", + "variantrule|spell|xphb", + "variantrule|spell attack|xphb", + "variantrule|spellcasting focus|xphb", + "variantrule|sphere [area of effect]|xphb", + "variantrule|stable|xphb", + "variantrule|stat block|xphb", + "variantrule|swim speed|xphb", + "variantrule|swimming|xphb", + "variantrule|target|xphb", + "variantrule|telepathy|xphb", + "variantrule|teleportation|xphb", + "variantrule|temporary hit points|xphb", + "variantrule|unarmed strike|xphb", + "variantrule|unoccupied space|xphb", + "variantrule|vulnerability|xphb", + "variantrule|weapon attack|xphb", + "variantrule|weapon|xphb" + ], "markerFiles": [ "bestiary/bestiary-mm.json", "cultsboons.json", @@ -342,6 +565,7 @@ "aliases": { "item|alchemist's tools|phb": "item|alchemist's supplies|phb", "item|alchemists' supplies|phb": "item|alchemist's supplies|phb", + "item|backpack|dmg": "item|backpack|phb", "item|breastplate|dmg": "item|breastplate|phb", "item|caltrops (20)|phb": "item|caltrops (bag of 20)|phb", "item|crysal ball|dmg": "item|crystal ball|dmg", @@ -351,6 +575,7 @@ "item|painter's tools|phb": "item|painter's supplies|phb", "item|pale tincture (ingested)|dmg": "item|pale tincture|dmg", "item|potion of speed|phb": "item|potion of speed|dmg", + "item|prosthetic limb|erlw": "item|prosthetic limb|tce", "item|rope of climbing|xge": "item|rope of climbing|dmg", "item|shield, +1|dmg": "item|+1 shield|dmg", "item|thieves tools|phb": "item|thieves' tools|phb", @@ -359,13 +584,23 @@ "spell|bane spell|phb": "spell|bane|phb", "spell|ceremony|phb": "spell|ceremony|xge", "spell|charm monster|phb": "spell|charm monster|xge", + "spell|commpelled duel|phb": "spell|compelled duel|phb", "spell|deception|phb": "skill|deception|phb", "spell|detect good and evil|phb": "spell|detect evil and good|phb", "spell|enlarge|phb": "spell|enlarge/reduce|phb", + "spell|frostbite|phb": "spell|frostbite|xge", "spell|history|phb": "skill|history|phb", "spell|infestation|phb": "spell|infestation|xge", + "spell|less restoration|phb": "spell|lesser restoration|phb", "spell|phatasmal force|phb": "spell|phantasmal force|phb", + "spell|magic aura|phb": "spell|arcanist's magic aura|phb", + "spell|mind sliver|phb": "spell|mind sliver|tce", + "spell|ray of enfeeblemen t|phb": "spell|ray of enfeeblement|phb", "spell|reduce|phb": "spell|enlarge/reduce|phb", + "spell|tenser 's floating disk|phb": "spell|tenser's floating disk|phb", + "spell|thunderclap|phb": "spell|thunderclap|xge", + "spell|toll the dead|phb": "spell|toll the dead|xge", + "spell|word of radiance|phb": "spell|word of radiance|xge", "trap|quicksand|dmg": "hazard|quicksand|dmg" }, "fixes": { @@ -420,6 +655,7 @@ "actions.json", "adventures.json", "backgrounds.json", + "bastions.json", "bestiary", "bestiary/legendarygroups.json", "bestiary/template.json", @@ -430,8 +666,16 @@ "deities.json", "feats.json", "fluff-backgrounds.json", + "fluff-bastions.json", + "fluff-conditionsdiseases.json", + "fluff-feats.json", "fluff-items.json", + "fluff-objects.json", + "fluff-optionalfeatures.json", "fluff-races.json", + "fluff-rewards.json", + "fluff-trapshazards.json", + "fluff-vehicles.json", "generated/gendata-tables.json", "items-base.json", "items.json", diff --git a/src/main/resources/sourceMap.json b/src/main/resources/sourceMap.json deleted file mode 100644 index fa36fadc9..000000000 --- a/src/main/resources/sourceMap.json +++ /dev/null @@ -1,490 +0,0 @@ -{ - "config5e": { - "abvToName": { - "AAG": "Astral Adventurer's Guide", - "AATM": "Adventure Atlas: The Mortuary", - "AI": "Acquisitions Incorporated", - "AitFR-AVT": "Adventures in the Forgotten Realms: A Verdant Tomb", - "AitFR-DN": "Adventures in the Forgotten Realms: Deepest Night", - "AitFR-FCD": "Adventures in the Forgotten Realms: From Cyan Depths", - "AitFR-ISF": "Adventures in the Forgotten Realms: In Scarlet Flames", - "AitFR-THP": "Adventures in the Forgotten Realms: The Hidden Page", - "AitFR": "Adventures in the Forgotten Realms", - "AL": "Adventurers' League", - "ALCoS": "Adventurers League: Curse of Strahd", - "ALEE": "Adventurers League: Elemental Evil", - "ALRoD": "Adventurers League: Rage of Demons", - "AWM": "Adventure with Muk", - "AZfyT": "A Zib for your Thoughts", - "BAM": "Boo's Astral Menagerie", - "BGDIA": "Baldur's Gate: Descent Into Avernus", - "BGG": "Bigby Presents: Glory of the Giants", - "BMT": "The Book of Many Things", - "CM": "Candlekeep Mysteries", - "CoA": "Chains of Asmodeus", - "CoS": "Curse of Strahd", - "CRCotN": "Critical Role: Call of the Netherdeep", - "DC": "Divine Contention", - "DIP": "Dragon of Icespire Peak", - "DitLCoT": "Descent into the Lost Caverns of Tsojcanth", - "DMG": "Dungeon Master's Guide", - "DMTCRG": "The Deck of Many Things: Card Reference Guide", - "DoD": "Domains of Delight", - "DoDk": "Dungeons of Drakkenheim", - "DoSI": "Dragons of Stormwreck Isle", - "DSotDQ": "Dragonlance: Shadow of the Dragon Queen", - "EEPC": "Elemental Evil Player's Companion", - "EET": "Elemental Evil: Trinkets", - "EFR": "Eberron: Forgotten Relics", - "DD": "Dangerous Designs", - "FS": "Frozen Sick", - "ToR": "Tide of Retribution", - "US": "Unwelcome Spirits", - "EGW_DD": "Dangerous Designs", - "EGW_FS": "Frozen Sick", - "EGW_ToR": "Tide of Retribution", - "EGW_US": "Unwelcome Spirits", - "EGW": "Explorer's Guide to Wildemount", - "ERLW": "Eberron: Rising from the Last War", - "ESK": "Essentials Kit", - "FTD": "Fizban's Treasury of Dragons", - "GGR": "Guildmasters' Guide to Ravnica", - "GHLoE": "Grim Hollow: Lairs of Etharis", - "GoS": "Ghosts of Saltmarsh", - "GotSF": "Giants of the Star Forge", - "HAT-LMI": "Honor Among Thieves: Legendary Magic Items", - "HAT-TG": "Honor Among Thieves: Thieves' Gallery", - "HF": "Heroes' Feast", - "HFFotM": "Heroes' Feast Flavors of the Multiverse", - "HFStCM": "Heroes' Feast: Saving the Children's Menu", - "HftT": "Hunt for the Thessalhydra", - "HoL": "The House of Lament", - "HotDQ": "Hoard of the Dragon Queen", - "HWCS": "Humblewood Campaign Setting", - "HWAitW": "Humblewood: Adventure in the Wood", - "IDRotF": "Icewind Dale: Rime of the Frostmaiden", - "IMR": "Infernal Machine Rebuild", - "JttRC": "Journeys through the Radiant Citadel", - "KftGV": "Keys from the Golden Vault", - "KKW": "Krenko's Way", - "LK": "Lightning Keep", - "LLK": "Lost Laboratory of Kwalish", - "LMoP": "Lost Mine of Phandelver", - "LoX": "Light of Xaryxis", - "LR": "Locathah Rising", - "LRDT": "Red Dragon's Tale: A LEGO Adventure", - "MaBJoV": "Minsc and Boo's Journal of Villainy", - "MCV1SC": "Monstrous Compendium Volume 1: Spelljammer Creatures", - "MCV2DC": "Monstrous Compendium Volume 2: Dragonlance Creatures", - "MCV3MC": "Monstrous Compendium Volume 3: Minecraft Creatures", - "MCV4EC": "Monstrous Compendium Volume 3: 4: Eldraine Creatures", - "MFF": "Mordenkainen's Fiendish Folio", - "MGELFT": "Muk's Guide To Everything He Learned From Tasha", - "MisMV1": "Misplaced Monsters: Volume 1", - "MM": "Monster Manual", - "MOT": "Mythic Odysseys of Theros", - "MPMM": "Mordenkainen Presents: Monsters of the Multiverse", - "MPP": "Morte's Planar Parade", - "MTF": "Mordenkainen's Tome of Foes", - "NRH-ASS": "NERDS Restoring Harmony: A Sticky Situation", - "NRH-AT": "NERDS Restoring Harmony: Adventure Together", - "NRH-AVitW": "NERDS Restoring Harmony: A Voice in the Wilderness", - "NRH-AWoL": "NERDS Restoring Harmony: A Web of Lies", - "NRH-CoI": "NERDS Restoring Harmony: Circus of Illusions", - "NRH-TCMC": "NERDS Restoring Harmony: The Candy Mountain Caper", - "NRH-TLT": "NERDS Restoring Harmony: The Lost Tomb", - "NRH": "NERDS Restoring Harmony", - "OGA": "One Grung Above", - "OotA": "Out of the Abyss", - "OoW": "The Orrery of the Wanderer", - "PaBTSO": "Phandelver and Below: The Shattered Obelisk", - "PHB": "Player's Handbook", - "PiP": "Peril in Pinegrove", - "PotA": "Princes of the Apocalypse", - "PSA": "Plane Shift: Amonkhet", - "PSD": "Plane Shift: Dominaria", - "PSI": "Plane Shift: Innistrad", - "PSK": "Plane Shift: Kaladesh", - "PSX": "Plane Shift: Ixalan", - "PSZ": "Plane Shift: Zendikar", - "QftIS": "Quests from the Infinite Staircase", - "RMBRE": "The Lost Dungeon of Rickedness: Big Rick Energy", - "RMR": "Dungeons & Dragons vs. Rick and Morty: Basic Rules", - "RoT": "The Rise of Tiamat", - "RoTOS": "The Rise of Tiamat Online Supplement", - "RtG": "Return to Glory", - "SAC": "Sage Advice Compendium", - "SADS": "Sapphire Anniversary Dice Set", - "SAiS": "Spelljammer: Adventures in Space", - "SatO": "Sigil and the Outlands", - "SCAG": "Sword Coast Adventurer's Guide", - "SCC-ARiR": "A Reckoning in Ruins", - "SCC-CK": "Campus Kerfuffle", - "SCC-HfMT": "Hunt for Mage Tower", - "SCC-TMM": "The Magister's Masquerade", - "SCC": "Strixhaven: A Curriculum of Chaos", - "SCREEN_DUNGEON_KIT": "Dungeon Master's Screen: Dungeon Kit", - "SCREEN_WILDERNESS_KIT": "Dungeon Master's Screen: Wilderness Kit", - "SRC_SCREEN_SPELLJAMMER": "ScreenSpelljammer", - "SCREEN": "Dungeon Master's Screen", - "SDW": "Sleeping Dragon's Wake", - "SjA": "Spelljammer Academy", - "SJA": "Spelljammer Academy", - "SKT": "Storm King's Thunder", - "SLW": "Storm Lord's Wrath", - "TCE": "Tasha's Cauldron of Everything", - "TD": "Tarot Deck", - "TDCSR": "Tal'Dorei Campaign Setting Reborn", - "TLK": "The Lost Kenku", - "ToA": "Tomb of Annihilation", - "ToB1-2023": "Tome of Beasts 1 (2023 Edition)", - "ToD": "Tyranny of Dragons", - "ToFW": "Turn of Fortune's Wheel", - "TTP": "The Tortle Package", - "TftYP": "Tales from the Yawning Portal", - "TftYP-AtG": "Tales from the Yawning Portal: Against the Giants", - "TftYP-DiT": "Tales from the Yawning Portal: Dead in Thay", - "TftYP-TFoF": "Tales from the Yawning Portal: The Forge of Fury", - "TftYP-THSoT": "Tales from the Yawning Portal: The Hidden Shrine of Tamoachan", - "TftYP-ToH": "Tales from the Yawning Portal: Tomb of Horrors", - "TftYP-TSC": "Tales from the Yawning Portal: The Sunless Citadel", - "TftYP-WPM": "Tales from the Yawning Portal: White Plume Mountain", - "UA20F": "Unearthed Arcana: 2020 Feats", - "UA20POR": "Unearthed Arcana: 2020 Psionic Options Revisited", - "UA20SC1": "Unearthed Arcana: 2020 Subclasses: Part 1", - "UA20SC2": "Unearthed Arcana: 2020 Subclasses: Part 2", - "UA20SC3": "Unearthed Arcana: 2020 Subclasses: Part 3", - "UA20SC4": "Unearthed Arcana: 2020 Subclasses: Part 4", - "UA20SC5": "Unearthed Arcana: 2020 Subclasses: Part 5", - "UA20SMT": "Unearthed Arcana: 2020 Spells and Magic Tattoos", - "UA20SCR": "Unearthed Arcana: 2020 Subclasses Revisited", - "UA21DO": "Unearthed Arcana: 2021 Draconic Options", - "UA21FF": "Unearthed Arcana: 2021 Folk of the Feywild", - "UA21GL": "Unearthed Arcana: 2021 Gothic Lineages", - "UA21MoS": "Unearthed Arcana: 2021 Mages of Strixhaven", - "UA21TotM": "Unearthed Arcana: 2021 Travelers of the Multiverse", - "UA22GO": "Unearthed Arcana: 2022 Giant Options", - "UA22HoK": "Unearthed Arcana: 2022 Heroes of Krynn", - "UA22HoKR": "Unearthed Arcana: 2022 Heroes of Krynn Revisited", - "UA22WotM": "Unearthed Arcana: 2022 Wonders of the Multivers", - "UA3PE": "Unearthed Arcana: Three-Pillar Experience", - "UAA": "Unearthed Arcana: Artificer", - "UAAR": "Unearthed Arcana: Artificer Revisited", - "UAATOSC": "Unearthed Arcana: A Trio of Subclasses", - "UABAM": "Unearthed Arcana: Barbarian and Monk", - "UABAP": "Unearthed Arcana: Bard and Paladin", - "UABBC": "Unearthed Arcana: Bard: Bard Colleges", - "UABPP": "Unearthed Arcana: Barbarian Primal Paths", - "UACAM": "Unearthed Arcana: Centaurs and Minotaurs", - "UACDD": "Unearthed Arcana: Cleric: Divine Domains", - "UACDW": "Unearthed Arcana: Cleric, Druid, and Wizard", - "UACFV": "Unearthed Arcana: Class Feature Variants", - "UAD": "Unearthed Arcana: Druid", - "UAEAG": "Unearthed Arcana: Eladrin and Gith", - "UAEBB": "Unearthed Arcana: Eberron", - "UAESR": "Unearthed Arcana: Elf Subraces", - "UAF": "Unearthed Arcana: Fighter", - "UAFFR": "Unearthed Arcana: Feats for Races", - "UAFFS": "Unearthed Arcana: Feats for Skills", - "UAFO": "Unearthed Arcana: Fiendish Options", - "UAFRR": "Unearthed Arcana: Fighter, Ranger, and Rogue", - "UAFRW": "Unearthed Arcana: Fighter, Rogue, and Wizard", - "UAFT": "Unearthed Arcana: Feats", - "UAGH": "Unearthed Arcana: Gothic Heroes", - "UAGHI": "Unearthed Arcana: Greyhawk Initiative", - "UAGSS": "Unearthed Arcana: Giant Soul Sorcerer", - "UAKOO": "Unearthed Arcana: Kits of Old", - "UALDR": "Unearthed Arcana: Light, Dark, Underdark!", - "UAM": "Unearthed Arcana: Monk", - "UAMAC": "Unearthed Arcana: Mass Combat", - "UAMC": "Unearthed Arcana: Modifying Classes", - "UAMDM": "Unearthed Arcana: Modern Magic", - "UAOD": "Unearthed Arcana: Order Domain", - "UAOSS": "Unearthed Arcana: Of Ships and the Sea", - "UAP": "Unearthed Arcana: Paladin", - "UAPCRM": "Unearthed Arcana: Prestige Classes and Rune Magic", - "UAR": "Unearthed Arcana: Ranger", - "UARAR": "Unearthed Arcana: Ranger and Rogue", - "UARCO": "Unearthed Arcana: Revised Class Options", - "UARoE": "Unearthed Arcana: Races of Eberron", - "UARoR": "Unearthed Arcana: Races of Ravnica", - "UARSC": "Unearthed Arcana: Revised Subclasses", - "UAS": "Unearthed Arcana: Sorcerer", - "UASAW": "Unearthed Arcana: Sorcerer and Warlock", - "UASIK": "Unearthed Arcana: Sidekicks", - "UASSP": "Unearthed Arcana: Starter Spells", - "UATF": "Unearthed Arcana: The Faithful", - "UATMC": "Unearthed Arcana: The Mystic Class", - "UATOBM": "Unearthed Arcana: That Old Black Magic", - "UATRR": "Unearthed Arcana: The Ranger, Revised", - "UATSC": "Unearthed Arcana: Three Subclasses", - "UAVR": "Unearthed Arcana: Variant Rules", - "UAWA": "Unearthed Arcana: Waterborne Adventures", - "UAWAW": "Unearthed Arcana: Warlock and Wizard", - "UAWGE": "Wayfinder's Guide to Eberron", - "UAWR": "Unearthed Arcana: Wizard Revisited", - "VD": "Vecna Dossier", - "VEoR": "Vecna: Eve of Ruin", - "VGM": "Volo's Guide to Monsters", - "VNotEE": "Vecna: Nest of the Eldritch Eye", - "VRGR": "Van Richten's Guide to Ravenloft", - "WBtW": "The Wild Beyond the Witchlight", - "WDH": "Waterdeep: Dragon Heist", - "WDMM": "Waterdeep: Dungeon of the Mad Mage", - "XGE": "Xanathar's Guide to Everything", - "XMtS": "X Marks the Spot" - }, - "longToAbv": { - "ALCurseOfStrahd": "ALCoS", - "ALElementalEvil": "ALEE", - "ALRageOfDemons": "ALRoD", - "HEROES_FEAST": "HF", - "PS-A": "PSA", - "PS-D": "PSD", - "PS-I": "PSI", - "PS-K": "PSK", - "PS-X": "PSX", - "PS-Z": "PSZ", - "SCC_ARiR": "SCC-ARir", - "SCC_CK": "SCC-CK", - "SCC_HfMT": "SCC-HfMT", - "SCC_TMM": "SCC-TMM", - "ScreenDungeonKit": "SCREEN_DUNGEON_KIT", - "ScreenWildernessKit": "SCREEN_WILDERNESS_KIT", - "ScreenSpelljammer": "SRC_SCREEN_SPELLJAMMER", - "Screen": "SCREEN", - "TYP": "TftYP", - "TYP_AtG": "TftYP-AtG", - "TYP_DiT": "TftYP-DiT", - "TYP_TFoF": "TftYP-TFoF", - "TYP_THSoT": "TftYP-THSoT", - "TYP_ToH": "TftYP-ToHs", - "TYP_TSC": "TftYP-TSC", - "TYP_WPM": "TftYP-WPM", - "UA2020F": "UA20F", - "UA2020Feats": "UA20F", - "UA2020POR": "UA20POR", - "UA2020PsionicOptionsRevisited": "UA20POR", - "UA2020SC1": "UA20S1", - "UA2020SC2": "UA20S2", - "UA2020SC3": "UA20S3", - "UA2020SC4": "UA20S4", - "UA2020SC5": "UA20S5", - "UA2020SCR": "UA20SCR", - "UA2020SMT": "UA20SMT", - "UA2020SpellsAndMagicTattoos": "UA20SMT", - "UA2020SubclassesPt1": "UA20S1", - "UA2020SubclassesPt2": "UA20S2", - "UA2020SubclassesPt3": "UA20S3", - "UA2020SubclassesPt4": "UA20S4", - "UA2020SubclassesPt5": "UA20S5", - "UA2020SubclassesRevisited": "UA20SCR", - "UA2021DO": "UA21DO", - "UA2021DraconicOptions": "UA21DO", - "UA2021FF": "UA21FF", - "UA2021FolkOfTheFeywild": "UA21FF", - "UA2021GL": "UA21GL", - "UA2021GothicLineages": "UA21GL", - "UA2021MoS": "UA21MoS", - "UA2021MagesOfStrixhaven": "UA21MoS", - "UA2021TotM": "UA21TotM", - "UA2021TravelersOfTheMultiverse": "UA21TotM", - "UA2022GO": "UA22GO", - "UA2022GiantOptions": "UA22GO", - "UA2022HoK": "UA22HoK", - "UA2022HeroesOfKrynn": "UA22HoK", - "UA2022HoKR": "UA22HoKR", - "UA2022HeroesOfKrynnRevisited": "UA22HoKR", - "UA2022WotM": "UA22WotM", - "UA2022WondersOfTheMultiverse": "UA22WotM", - "UAATrioOfSubclasses": "UAATOSC", - "UAArtificer": "UAA", - "UAArtificerRevisited": "UAAR", - "UABarbarianAndMonk": "UABAM", - "UABarbarianPrimalPaths": "UABPP", - "UABardAndPaladin": "UABAP", - "UABardBardColleges": "UABBC", - "UACentaursMinotaurs": "UACAM", - "UAClassFeatureVariants": "UACFV", - "UAClericDivineDomains": "UACDD", - "UAClericDruidWizard": "UACDW", - "UADruid": "UAD", - "UAEberron": "UAEBB", - "UAEladrinAndGith": "UAEAG", - "UAElfSubraces": "UAESR", - "UAFeats": "UAFT", - "UAFeatsForRaces": "UAFFR", - "UAFeatsForSkills": "UAFFS", - "UAFiendishOptions": "UAFO", - "UAFighter": "UAF", - "UAFighterRangerRogue": "UAFRR", - "UAFighterRogueWizard": "UAFRW", - "UAGiantSoulSorcerer": "UAGSS", - "UAGothicHeroes": "UAGH", - "UAGreyhawkInitiative": "UAGHI", - "UAKitsOfOld": "UAKOO", - "UALightDarkUnderdark": "UALDR", - "UAMassCombat": "UAMAC", - "UAModernMagic": "UAMDM", - "UAModifyingClasses": "UAMC", - "UAMonk": "UAM", - "UAOfShipsAndSea": "UAOSS", - "UAOrderDomain": "UAOD", - "UAPaladin": "UAP", - "UAPrestigeClassesRunMagic": "UAPCRM", - "UARacesOfEberron": "UARoE", - "UARacesOfRavnica": "UARoR", - "UARanger": "UAR", - "UARangerAndRogue": "UARAR", - "UARevisedClassOptions": "UARCO", - "UARevisedSubclasses": "UARSC", - "UASidekicks": "UASIK", - "UASorcerer": "UAS", - "UASorcererAndWarlock": "UASAW", - "UAStarterSpells": "UASSP", - "UAThatOldBlackMagic": "UATOBM", - "UATheFaithful": "UATF", - "UATheMysticClass": "UATMC", - "UATheRangerRevised": "UATRR", - "UAThreePillarExperience": "UA3PE", - "UAThreeSubclasses": "UATSC", - "UAVariantRules": "UAVR", - "UAWarlockAndWizard": "UAWAW", - "UAWaterborneAdventures": "UAWA", - "UAWayfindersGuideToEberron": "UAWGE", - "UAWizardRevisited": "UAWR" - } - }, - "configPf2e": { - "abvToName": { - "7DfS0": "Seven Dooms for Sandpoint Player's Guide", - "AAWS": "Azarketi Ancestry Web Supplement", - "AFoF": "A Fistful of Flowers", - "AFFM": "A Few Flowers More", - "APG": "Advanced Player's Guide", - "AV0": "Abomination Vaults Player's Guide", - "AV1": "Abomination Vaults #1: Ruins of Gauntlight", - "AV2": "Abomination Vaults #2: Hands of the Devil", - "AV3": "Abomination Vaults #3: Eyes of Empty Death", - "AVH": "Abomination Vaults Hardcover", - "AoA0": "Age of Ashes Player's Guide", - "AoA1": "Age of Ashes #1: Hellknight Hill", - "AoA2": "Age of Ashes #2: Cult of Cinders", - "AoA3": "Age of Ashes #3: Tomorrow Must Burn", - "AoA4": "Age of Ashes #4: Fires of the Haunted City", - "AoA5": "Age of Ashes #5: Against the Scarlet Triad", - "AoA6": "Age of Ashes #6: Broken Promises", - "AoE0": "Agents of Edgewatch Player's Guide", - "AoE1": "Agents of Edgewatch #1: Devil at the Dreaming Palace", - "AoE2": "Agents of Edgewatch #2: Sixty Feet Under", - "AoE3": "Agents of Edgewatch #3: All or Nothing", - "AoE4": "Agents of Edgewatch #4: Assault on Hunting Lodge Seven", - "AoE5": "Agents of Edgewatch #5: Belly of the Black Whale", - "AoE6": "Agents of Edgewatch #6: Ruins of the Radiant Siege", - "B1": "Bestiary", - "B2": "Bestiary 2", - "B3": "Bestiary 3", - "BB": "Beginner Box", - "BL0": "Blood Lords Player's Guide", - "BL1": "Blood Lords #1: Zombie Feast", - "BL2": "Blood Lords #2: Graveclaw", - "BL3": "Blood Lords #3: Field of Maidens", - "BL4": "Blood Lords #4: The Ghouls Hunger", - "BL5": "Blood Lords #5: A Taste of Ashes", - "BL6": "Blood Lords #6: Ghost King's Rage", - "BotD": "Book of the Dead", - "CC0": "Curtain Call Player's Guide", - "CFD": "Critical Fumble Deck", - "CHD": "Critical Hit Deck", - "CRB": "Core Rulebook", - "DA": "Dark Archive", - "DaLl": "Dinner at Lionlodge", - "EC0": "Extinction Curse Player's Guide", - "EC1": "Extinction Curse #1: The Show Must Go On", - "EC2": "Extinction Curse #2: Legacy of the Lost God", - "EC3": "Extinction Curse #3: Life's Long Shadows", - "EC4": "Extinction Curse #4: Siege of the Dinosaurs", - "EC5": "Extinction Curse #5: Lord of the Black Sands", - "EC6": "Extinction Curse #6: The Apocalypse Prophet", - "FRP0": "Fists of the Ruby Phoenix Player's Guide", - "FRP1": "Fists of the Ruby Phoenix #1: Despair on Danger Island", - "FRP2": "Fists of the Ruby Phoenix #2: Ready? Fight!", - "FRP3": "Fists of the Ruby Phoenix #3: King of the Mountain", - "FoP": "The Fall of Plaguestone", - "G&G": "Guns & Gears", - "GMG": "Gamemastery Guide", - "GW0": "Gatewalkers Player's Guide", - "GW1": "Gatewalkers #1: The Seventh Arch", - "GW2": "Gatewalkers #2: They Watched the Stars", - "GW3": "Gatewalkers #3: Dreamers of the Nameless Spires", - "HPD": "Hero Point Deck", - "LOACLO": "Lost Omens: Absalom, City of Lost Omens", - "LOAG": "Lost Omens: Ancestry Guide", - "LOCG": "Lost Omens: Character Guide", - "LOGM": "Lost Omens: Gods & Magic", - "LOGMWS": "Lost Omens: Gods & Magic Web Supplement", - "LOHh": "Lost Omens: Highhelm", - "LOIL": "Lost Omens: Impossible Lands", - "LOKL": "Lost Omens: Knights of Lastwall", - "LOL": "Lost Omens: Legends", - "LOME": "Lost Omens: The Mwangi Expanse", - "LOMM": "Lost Omens: Monsters of Myth", - "LOPSG": "Lost Omens: Pathfinder Society Guide", - "LOTG": "Lost Omens: Travel Guide", - "LOTGB": "Lost Omens: The Grand Bazaar", - "LOTXWG": "Lost Omens: Tian Xia World Guide", - "LOWG": "Lost Omens: World Guide", - "LTiBA": "Little Trouble in Big Absalom", - "Mal": "Malevolence", - "MotM": "Mark of the Mantis", - "NGD": "Night of the Gray Death", - "OoA0": "Outlaws of Alkenstar Player's Guide", - "OoA1": "Outlaws of Alkenstar #1: Punks in a Powder Keg", - "OoA2": "Outlaws of Alkenstar #2: Cradle of Quartz", - "OoA3": "Outlaws of Alkenstar #3: The Smoking Gun", - "PC1": "Player Core", - "PFUM": "PATHFINDER: FUMBUS!", - "POS1": "Pathfinder One-Shot: Sundered Waves", - "QFF0": "Quest for the Frozen Flame Player's Guide", - "QFF1": "Quest for the Frozen Flame #1: Broken Tusk Moon", - "QFF2": "Quest for the Frozen Flame #2: Lost Mammoth Valley", - "QFF3": "Quest for the Frozen Flame #3: Burning Tundra", - "RoE": "Rage of Elements", - "Rust": "Rusthenge", - "SKT0": "Sky King's Tomb Player's Guide", - "SaS": "Shadows at Sundown", - "SF0": "Stolen Fate Player's Guide", - "Sli": "The Slithering", - "SoG0": "Season of Ghosts Player's Guide", - "SoG1": "Season of Ghosts #1: The Summer That Never Was", - "SoG2": "Season of Ghosts #2: Let the Leaves Fall", - "SoG3": "Season of Ghosts #3: No Breath to Cry", - "SoG4": "Season of Ghosts #4: To Bloom Below the Web", - "SoM": "Secrets of Magic", - "SoT0": "Strength of Thousands Player's Guide", - "SoT1": "Strength of Thousands #1: Kindled Magic", - "SoT2": "Strength of Thousands #2: Spoken on the Song Wind", - "SoT3": "Strength of Thousands #3: Hurricane's Howl", - "SoT4": "Strength of Thousands #4: Secrets of the Temple-City", - "SoT5": "Strength of Thousands #5: Doorway to the Red Star", - "SoT6": "Strength of Thousands #6: Shadows of the Ancients", - "TaL": "Torment and Legacy", - "TEC": "The Enmity Cycle", - "TV": "Treasure Vault", - "TiO": "Troubles in Otari", - "ToK": "Threshold of Knowledge", - "WoW0": "Wardens of Wildwood Player's Guide", - "WoW1": "Wardens of Wildwood #1: Pactbreaker", - "WoW2": "Wardens of Wildwood #2: Severed at the Root", - "WoW3": "Wardens of Wildwood #3: Shepherd of Decay", - "WtD1": "Wake the Dead #1", - "WtD2": "Wake the Dead #2", - "WtD3": "Wake the Dead #3", - "WtD4": "Wake the Dead #4", - "WtD5": "Wake the Dead #5" - }, - "longToAbv": { - "GnG": "G&G" - } - } -} diff --git a/src/main/resources/sourceMap.yaml b/src/main/resources/sourceMap.yaml new file mode 100644 index 000000000..09a7c4b07 --- /dev/null +++ b/src/main/resources/sourceMap.yaml @@ -0,0 +1,910 @@ +--- +config5e: + reference: + AAG: + name: "Astral Adventurer's Guide" + date: "2022-08-16" + AATM: + name: "Adventure Atlas: The Mortuary" + date: "2023-10-17" + AI: + name: "Acquisitions Incorporated" + date: "2019-06-18" + AL: + name: "Adventurers' League" + ALCoS: + name: "Adventurers League: Curse of Strahd" + date: "2016-03-15" + ALEE: + name: "Adventurers League: Elemental Evil" + date: "2015-04-07" + ALRoD: + name: "Adventurers League: Rage of Demons" + date: "2015-09-15" + AWM: + name: "Adventure with Muk" + date: "2019-11-12" + AZfyT: + name: "A Zib for your Thoughts" + date: "2019-03-05" + AitFR: + name: "Adventures in the Forgotten Realms" + date: "2021-06-30" + AitFR-AVT: + name: "Adventures in the Forgotten Realms: A Verdant Tomb" + date: "2021-07-14" + AitFR-DN: + name: "Adventures in the Forgotten Realms: Deepest Night" + date: "2021-07-21" + AitFR-FCD: + name: "Adventures in the Forgotten Realms: From Cyan Depths" + date: "2021-07-28" + AitFR-ISF: + name: "Adventures in the Forgotten Realms: In Scarlet Flames" + date: "2021-06-30" + AitFR-THP: + name: "Adventures in the Forgotten Realms: The Hidden Page" + date: "2021-07-07" + BAM: + name: "Boo's Astral Menagerie" + date: "2022-08-16" + BGDIA: + name: "Baldur's Gate: Descent Into Avernus" + date: "2019-09-17" + BGG: + name: "Bigby Presents: Glory of the Giants" + date: "2023-08-15" + BMT: + name: "The Book of Many Things" + date: "2023-11-14" + CM: + name: "Candlekeep Mysteries" + date: "2021-03-16" + CRCotN: + name: "Critical Role: Call of the Netherdeep" + date: "2022-03-15" + CoA: + name: "Chains of Asmodeus" + date: "2023-10-30" + CoS: + name: "Curse of Strahd" + date: "2016-03-15" + DC: + name: "Divine Contention" + date: "2019-06-24" + DD: + name: "Dangerous Designs" + date: "2020-03-17" + DIP: + name: "Dragon of Icespire Peak" + date: "2019-06-24" + DMG: + name: "Dungeon Master's Guide" + date: "2014-12-09" + DMTCRG: + name: "The Deck of Many Things: Card Reference Guide" + date: "2023-11-14" + DSotDQ: + name: "Dragonlance: Shadow of the Dragon Queen" + date: "2022-11-22" + DitLCoT: + name: "Descent into the Lost Caverns of Tsojcanth" + date: "2024-03-26" + DoD: + name: "Domains of Delight" + date: "2021-09-21" + DoDk: + name: "Dungeons of Drakkenheim" + date: "2023-12-21" + DoSI: + name: "Dragons of Stormwreck Isle" + date: "2022-07-31" + EEPC: + name: "Elemental Evil Player's Companion" + date: "2015-03-10" + EET: + name: "Elemental Evil: Trinkets" + date: "2015-03-10" + EFR: + name: "Eberron: Forgotten Relics" + date: "2019-11-19" + EGW: + name: "Explorer's Guide to Wildemount" + date: "2020-03-17" + EGW_DD: + name: "Dangerous Designs" + date: "2020-03-17" + EGW_FS: + name: "Frozen Sick" + date: "2020-03-17" + EGW_ToR: + name: "Tide of Retribution" + date: "2020-03-17" + EGW_US: + name: "Unwelcome Spirits" + date: "2020-03-17" + ERLW: + name: "Eberron: Rising from the Last War" + date: "2019-11-19" + ESK: + name: "Essentials Kit" + date: "2019-06-24" + FS: + name: "Frozen Sick" + date: "2020-03-17" + FTD: + name: "Fizban's Treasury of Dragons" + date: "2021-11-26" + GGR: + name: "Guildmasters' Guide to Ravnica" + date: "2018-11-20" + GHLoE: + name: "Grim Hollow: Lairs of Etharis" + date: "2023-11-30" + GoS: + name: "Ghosts of Saltmarsh" + date: "2019-05-21" + GotSF: + name: "Giants of the Star Forge" + date: "2023-08-01" + HAT-LMI: + name: "Honor Among Thieves: Legendary Magic Items" + HAT-TG: + name: "Honor Among Thieves: Thieves' Gallery" + HF: + name: "Heroes' Feast" + date: "2020-10-27" + HFDoMM: + name: "Heroes' Feast: The Deck of Many Morsels" + date: "2024-10-01" + HFFotM: + name: "Heroes' Feast Flavors of the Multiverse" + date: "2023-11-07" + HFStCM: + name: "Heroes' Feast: Saving the Children's Menu" + date: "2023-11-21" + HWAitW: + name: "Humblewood: Adventure in the Wood" + date: "2019-06-17" + HWCS: + name: "Humblewood Campaign Setting" + date: "2019-06-17" + HftT: + name: "Hunt for the Thessalhydra" + date: "2019-05-01" + HoL: + name: "The House of Lament" + date: "2021-05-18" + HotDQ: + name: "Hoard of the Dragon Queen" + date: "2014-08-19" + IDRotF: + name: "Icewind Dale: Rime of the Frostmaiden" + date: "2020-09-15" + IMR: + name: "Infernal Machine Rebuild" + date: "2019-11-12" + JttRC: + name: "Journeys through the Radiant Citadel" + date: "2022-07-19" + KKW: + name: "Krenko's Way" + date: "2018-11-20" + KftGV: + name: "Keys from the Golden Vault" + date: "2023-02-21" + LK: + name: "Lightning Keep" + date: "2023-09-26" + LLK: + name: "Lost Laboratory of Kwalish" + date: "2018-11-10" + LMoP: + name: "Lost Mine of Phandelver" + date: "2014-07-15" + LR: + name: "Locathah Rising" + date: "2019-09-19" + LRDT: + name: "Red Dragon's Tale: A LEGO Adventure" + date: "2024-04-01" + LoX: + name: "Light of Xaryxis" + date: "2022-08-16" + MCV1SC: + name: "Monstrous Compendium Volume 1: Spelljammer Creatures" + date: "2022-04-21" + MCV2DC: + name: "Monstrous Compendium Volume 2: Dragonlance Creatures" + date: "2022-12-05" + MCV3MC: + name: "Monstrous Compendium Volume 3: Minecraft Creatures" + date: "2023-03-28" + MCV4EC: + name: "Monstrous Compendium Volume 3: 4: Eldraine Creatures" + date: "2023-09-21" + MFF: + name: "Mordenkainen's Fiendish Folio" + date: "2019-11-12" + MGELFT: + name: "Muk's Guide To Everything He Learned From Tasha" + date: "2020-12-01" + MM: + name: "Monster Manual" + date: "2014-09-30" + MOT: + name: "Mythic Odysseys of Theros" + date: "2020-06-02" + MPMM: + name: "Mordenkainen Presents: Monsters of the Multiverse" + date: "2022-01-25" + MPP: + name: "Morte's Planar Parade" + date: "2023-10-17" + MTF: + name: "Mordenkainen's Tome of Foes" + date: "2018-05-29" + MaBJoV: + name: "Minsc and Boo's Journal of Villainy" + date: "2021-10-05" + MisMV1: + name: "Misplaced Monsters: Volume 1" + date: "2023-05-03" + NRH: + name: "NERDS Restoring Harmony" + date: "2021-09-01" + NRH-ASS: + name: "NERDS Restoring Harmony: A Sticky Situation" + date: "2021-09-01" + NRH-AT: + name: "NERDS Restoring Harmony: Adventure Together" + date: "2021-09-01" + NRH-AVitW: + name: "NERDS Restoring Harmony: A Voice in the Wilderness" + date: "2021-09-01" + NRH-AWoL: + name: "NERDS Restoring Harmony: A Web of Lies" + date: "2021-09-01" + NRH-CoI: + name: "NERDS Restoring Harmony: Circus of Illusions" + date: "2021-09-01" + NRH-TCMC: + name: "NERDS Restoring Harmony: The Candy Mountain Caper" + date: "2021-09-01" + NRH-TLT: + name: "NERDS Restoring Harmony: The Lost Tomb" + date: "2021-09-01" + OGA: + name: "One Grung Above" + date: "2017-10-11" + OoW: + name: "The Orrery of the Wanderer" + date: "2019-06-18" + OotA: + name: "Out of the Abyss" + date: "2015-09-15" + PHB: + name: "Player's Handbook" + date: "2014-08-19" + PSA: + name: "Plane Shift: Amonkhet" + date: "2017-07-06" + PSD: + name: "Plane Shift: Dominaria" + date: "2018-07-31" + PSI: + name: "Plane Shift: Innistrad" + date: "2016-07-12" + PSK: + name: "Plane Shift: Kaladesh" + date: "2017-02-16" + PSX: + name: "Plane Shift: Ixalan" + date: "2018-01-09" + PSZ: + name: "Plane Shift: Zendikar" + date: "2016-04-27" + PaBTSO: + name: "Phandelver and Below: The Shattered Obelisk" + date: "2023-09-19" + PaF: + name: "Puncheons and Flagons" + date: "2024-08-27" + PiP: + name: "Peril in Pinegrove" + date: "2023-11-20" + PotA: + name: "Princes of the Apocalypse" + date: "2015-04-07" + QftIS: + name: "Quests from the Infinite Staircase" + date: "2024-07-16" + RMBRE: + name: "The Lost Dungeon of Rickedness: Big Rick Energy" + date: "2019-11-19" + RMR: + name: "Dungeons & Dragons vs. Rick and Morty: Basic Rules" + date: "2019-11-19" + RoT: + name: "The Rise of Tiamat" + date: "2014-11-04" + RoTOS: + name: "The Rise of Tiamat Online Supplement" + date: "2014-11-04" + RtG: + name: "Return to Glory" + date: "2021-05-21" + SAC: + name: "Sage Advice Compendium" + date: "2019-01-31" + SADS: + name: "Sapphire Anniversary Dice Set" + date: "2019-12-12" + SAiS: + name: "Spelljammer: Adventures in Space" + date: "2022-08-16" + SCAG: + name: "Sword Coast Adventurer's Guide" + date: "2015-11-03" + SCC: + name: "Strixhaven: A Curriculum of Chaos" + date: "2021-12-07" + SCC-ARiR: + name: "A Reckoning in Ruins" + date: "2021-12-07" + SCC-CK: + name: "Campus Kerfuffle" + date: "2021-12-07" + SCC-HfMT: + name: "Hunt for Mage Tower" + date: "2021-12-07" + SCC-TMM: + name: "The Magister's Masquerade" + date: "2021-12-07" + ScoEE: + name: "Scions of Elemental Evil" + date: "2024-10-24" + SCREEN: + name: "Dungeon Master's Screen" + date: "2015-01-20" + SCREEN_DUNGEON_KIT: + name: "Dungeon Master's Screen: Dungeon Kit" + date: "2020-09-21" + SCREEN_WILDERNESS_KIT: + name: "Dungeon Master's Screen: Wilderness Kit" + date: "2020-11-17" + SCREEN_SPELLJAMMER: + name: "Dungeon Master's Screen: Spelljammer" + date: "2022-08-16" + SDW: + name: "Sleeping Dragon's Wake" + date: "2019-06-24" + SKT: + name: "Storm King's Thunder" + date: "2016-09-06" + SLW: + name: "Storm Lord's Wrath" + date: "2019-06-24" + SatO: + name: "Sigil and the Outlands" + date: "2023-10-17" + SjA: + name: "Spelljammer Academy" + date: "2022-07-11" + TCE: + name: "Tasha's Cauldron of Everything" + date: "2020-11-17" + TD: + name: "Tarot Deck" + date: "2022-05-24" + TDCSR: + name: "Tal'Dorei Campaign Setting Reborn" + date: "2022-01-18" + TLK: + name: "The Lost Kenku" + date: "2017-11-28" + TTP: + name: "The Tortle Package" + date: "2017-09-19" + TftYP: + name: "Tales from the Yawning Portal" + TftYP-AtG: + name: "Tales from the Yawning Portal: Against the Giants" + date: "2017-04-04" + TftYP-DiT: + name: "Tales from the Yawning Portal: Dead in Thay" + date: "2017-04-04" + TftYP-TFoF: + name: "Tales from the Yawning Portal: The Forge of Fury" + date: "2017-04-04" + TftYP-THSoT: + name: "Tales from the Yawning Portal: The Hidden Shrine of Tamoachan" + date: "2017-04-04" + TftYP-TSC: + name: "Tales from the Yawning Portal: The Sunless Citadel" + date: "2017-04-04" + TftYP-ToH: + name: "Tales from the Yawning Portal: Tomb of Horrors" + date: "2017-04-04" + TftYP-WPM: + name: "Tales from the Yawning Portal: White Plume Mountain" + date: "2017-04-04" + ToA: + name: "Tomb of Annihilation" + date: "2017-09-19" + ToB1-2023: + name: "Tome of Beasts 1 (2023 Edition)" + date: "2023-05-31" + ToD: + name: "Tyranny of Dragons" + date: "2019-10-22" + ToFW: + name: "Turn of Fortune's Wheel" + date: "2023-10-17" + ToR: + name: "Tide of Retribution" + date: "2020-03-17" + UATMC: + name: "Unearthed Arcana: The Mystic Class" + date: "2017-03-13" + US: + name: "Unwelcome Spirits" + date: "2020-03-17" + UtHftLH: + name: "Uni and the Hunt for the Lost Horn" + date: "2024-09-24" + VD: + name: "Vecna Dossier" + date: "2022-06-09" + VEoR: + name: "Vecna: Eve of Ruin" + date: "2024-05-21" + VGM: + name: "Volo's Guide to Monsters" + date: "2016-11-15" + VNotEE: + name: "Vecna: Nest of the Eldritch Eye" + date: "2024-04-16" + VRGR: + name: "Van Richten's Guide to Ravenloft" + date: "2021-05-18" + WBtW: + name: "The Wild Beyond the Witchlight" + date: "2021-09-21" + WDH: + name: "Waterdeep: Dragon Heist" + date: "2018-09-18" + WDMM: + name: "Waterdeep: Dungeon of the Mad Mage" + date: "2018-11-20" + XDMG: + name: "Dungeon Master's Guide (2024)" + date: "2024-11-12" + XGE: + name: "Xanathar's Guide to Everything" + date: "2017-11-21" + XMM: + name: "Monster Manual (2024)" + date: "2025-02-18" + XMtS: + name: "X Marks the Spot" + date: "2017-12-11" + XPHB: + name: "Player's Handbook (2024)" + date: "2024-09-17" + XScreen: + name: "Dungeon Master's Screen (2024)" + date: "2024-11-12" + longToAbv: + ALCurseOfStrahd: "ALCoS" + ALElementalEvil: "ALEE" + ALRageOfDemons: "ALRoD" + HEROES_FEAST: "HF" + PS-A: "PSA" + PS-D: "PSD" + PS-I: "PSI" + PS-K: "PSK" + PS-X: "PSX" + PS-Z: "PSZ" + SCC_ARiR: "SCC-ARir" + SCC_CK: "SCC-CK" + SCC_HfMT: "SCC-HfMT" + SCC_TMM: "SCC-TMM" + Screen: "SCREEN" + ScreenDungeonKit: "SCREEN_DUNGEON_KIT" + ScreenSpelljammer: "SRC_SCREEN_SPELLJAMMER" + ScreenWildernessKit: "SCREEN_WILDERNESS_KIT" + TYP: "TftYP" + TYP_AtG: "TftYP-AtG" + TYP_DiT: "TftYP-DiT" + TYP_TFoF: "TftYP-TFoF" + TYP_THSoT: "TftYP-THSoT" + TYP_TSC: "TftYP-TSC" + TYP_ToH: "TftYP-ToHs" + TYP_WPM: "TftYP-WPM" + UATheMysticClass: "UATMC" +configPf2e: + reference: + "7DfS0": + name: "Seven Dooms for Sandpoint Player's Guide" + date: "2024-03-08" + AAWS: + name: "Azarketi Ancestry Web Supplement" + date: "2021-02-24" + AFFM: + name: "A Few Flowers More" + date: "2023-07-23" + AFoF: + name: "A Fistful of Flowers" + date: "2022-07-25" + APG: + name: "Advanced Player's Guide" + date: "2020-07-30" + AV0: + name: "Abomination Vaults Player's Guide" + date: "2021-01-15" + AV1: + name: "Abomination Vaults #1: Ruins of Gauntlight" + date: "2021-01-15" + AV2: + name: "Abomination Vaults #2: Hands of the Devil" + date: "2021-02-24" + AV3: + name: "Abomination Vaults #3: Eyes of Empty Death" + date: "2021-04-07" + AVH: + name: "Abomination Vaults Hardcover" + date: "2022-05-25" + AoA0: + name: "Age of Ashes Player's Guide" + date: "2019-08-01" + AoA1: + name: "Age of Ashes #1: Hellknight Hill" + date: "2019-08-01" + AoA2: + name: "Age of Ashes #2: Cult of Cinders" + date: "2019-09-01" + AoA3: + name: "Age of Ashes #3: Tomorrow Must Burn" + date: "2019-09-18" + AoA4: + name: "Age of Ashes #4: Fires of the Haunted City" + date: "2019-10-16" + AoA5: + name: "Age of Ashes #5: Against the Scarlet Triad" + date: "2019-11-13" + AoA6: + name: "Age of Ashes #6: Broken Promises" + date: "2019-12-12" + AoE0: + name: "Agents of Edgewatch Player's Guide" + date: "2020-07-08" + AoE1: + name: "Agents of Edgewatch #1: Devil at the Dreaming Palace" + date: "2020-07-30" + AoE2: + name: "Agents of Edgewatch #2: Sixty Feet Under" + date: "2020-08-26" + AoE3: + name: "Agents of Edgewatch #3: All or Nothing" + date: "2020-09-15" + AoE4: + name: "Agents of Edgewatch #4: Assault on Hunting Lodge Seven" + date: "2020-10-14" + AoE5: + name: "Agents of Edgewatch #5: Belly of the Black Whale" + date: "2020-11-15" + AoE6: + name: "Agents of Edgewatch #6: Ruins of the Radiant Siege" + date: "2020-12-15" + B1: + name: "Bestiary" + date: "2019-08-01" + B2: + name: "Bestiary 2" + date: "2020-05-27" + B3: + name: "Bestiary 3" + date: "2021-04-07" + BB: + name: "Beginner Box" + date: "2020-11-11" + BL0: + name: "Blood Lords Player's Guide" + date: "2022-06-29" + BL1: + name: "Blood Lords #1: Zombie Feast" + date: "2022-07-27" + BL2: + name: "Blood Lords #2: Graveclaw" + date: "2022-08-31" + BL3: + name: "Blood Lords #3: Field of Maidens" + date: "2022-09-21" + BL4: + name: "Blood Lords #4: The Ghouls Hunger" + date: "2022-10-19" + BL5: + name: "Blood Lords #5: A Taste of Ashes" + date: "2022-11-16" + BL6: + name: "Blood Lords #6: Ghost King's Rage" + date: "2022-12-14" + BotD: + name: "Book of the Dead" + date: "2022-04-27" + CC0: + name: "Curtain Call Player's Guide" + date: "2024-07-10" + CFD: + name: "Critical Fumble Deck" + date: "2019-10-16" + CHD: + name: "Critical Hit Deck" + date: "2019-10-16" + CRB: + name: "Core Rulebook" + date: "2019-08-01" + DA: + name: "Dark Archive" + date: "2022-07-27" + DaLl: + name: "Dinner at Lionlodge" + date: "2021-05-30" + EC0: + name: "Extinction Curse Player's Guide" + date: "2020-01-13" + EC1: + name: "Extinction Curse #1: The Show Must Go On" + date: "2020-01-30" + EC2: + name: "Extinction Curse #2: Legacy of the Lost God" + date: "2020-02-26" + EC3: + name: "Extinction Curse #3: Life's Long Shadows" + date: "2020-03-26" + EC4: + name: "Extinction Curse #4: Siege of the Dinosaurs" + date: "2020-04-29" + EC5: + name: "Extinction Curse #5: Lord of the Black Sands" + date: "2020-05-27" + EC6: + name: "Extinction Curse #6: The Apocalypse Prophet" + date: "2020-06-24" + FRP0: + name: "Fists of the Ruby Phoenix Player's Guide" + date: "2021-04-12" + FRP1: + name: "Fists of the Ruby Phoenix #1: Despair on Danger Island" + date: "2021-07-07" + FRP2: + name: "Fists of the Ruby Phoenix #2: Ready? Fight!" + date: "2021-07-07" + FRP3: + name: "Fists of the Ruby Phoenix #3: King of the Mountain" + date: "2021-07-07" + FoP: + name: "The Fall of Plaguestone" + date: "2019-08-01" + G&G: + name: "Guns & Gears" + date: "2021-10-13" + GMG: + name: "Gamemastery Guide" + date: "2020-02-26" + GW0: + name: "Gatewalkers Player's Guide" + date: "2023-01-10" + GW1: + name: "Gatewalkers #1: The Seventh Arch" + date: "2023-01-25" + GW2: + name: "Gatewalkers #2: They Watched the Stars" + date: "2023-02-22" + GW3: + name: "Gatewalkers #3: Dreamers of the Nameless Spires" + date: "2023-03-29" + HPD: + name: "Hero Point Deck" + date: "2021-11-10" + HStR: + name: "Head-Shot the Rot" + date: "2021-10-21" + LOACLO: + name: "Lost Omens: Absalom, City of Lost Omens" + date: "2021-12-22" + LOAG: + name: "Lost Omens: Ancestry Guide" + date: "2021-02-24" + LOCG: + name: "Lost Omens: Character Guide" + date: "2019-10-16" + LOGM: + name: "Lost Omens: Gods & Magic" + date: "2020-01-29" + LOGMWS: + name: "Lost Omens: Gods & Magic Web Supplement" + date: "2020-01-29" + LOHh: + name: "Lost Omens: Highhelm" + date: "2023-06-28" + LOIL: + name: "Lost Omens: Impossible Lands" + date: "2021-11-06" + LOKL: + name: "Lost Omens: Knights of Lastwall" + date: "2022-05-25" + LOL: + name: "Lost Omens: Legends" + date: "2020-07-30" + LOME: + name: "Lost Omens: The Mwangi Expanse" + date: "2021-07-07" + LOMM: + name: "Lost Omens: Monsters of Myth" + date: "2021-12-22" + LOPSG: + name: "Lost Omens: Pathfinder Society Guide" + date: "2020-10-14" + LOTG: + name: "Lost Omens: Travel Guide" + date: "2022-08-31" + LOTGB: + name: "Lost Omens: The Grand Bazaar" + date: "2021-10-13" + LOTXWG: + name: "Lost Omens: Tian Xia World Guide" + date: "2024-04-24" + LOWG: + name: "Lost Omens: World Guide" + date: "2019-08-31" + LTiBA: + name: "Little Trouble in Big Absalom" + date: "2020-07-25" + Mal: + name: "Malevolence" + date: "2021-07-07" + MotM: + name: "Mark of the Mantis" + date: "2022-02-23" + NGD: + name: "Night of the Gray Death" + date: "2021-10-13" + OoA0: + name: "Outlaws of Alkenstar Player's Guide" + date: "2022-03-28" + OoA1: + name: "Outlaws of Alkenstar #1: Punks in a Powder Keg" + date: "2022-04-27" + OoA2: + name: "Outlaws of Alkenstar #2: Cradle of Quartz" + date: "2022-05-25" + OoA3: + name: "Outlaws of Alkenstar #3: The Smoking Gun" + date: "2022-06-29" + PC1: + name: "Player Core" + date: "2023-11-15" + PFUM: + name: "PATHFINDER: FUMBUS!" + date: "2021-11-11" + POS1: + name: "Pathfinder One-Shot: Sundered Waves" + date: "2021-03-06" + QFF0: + name: "Quest for the Frozen Flame Player's Guide" + date: "2021-12-20" + QFF1: + name: "Quest for the Frozen Flame #1: Broken Tusk Moon" + date: "2021-01-26" + QFF2: + name: "Quest for the Frozen Flame #2: Lost Mammoth Valley" + date: "2021-02-23" + QFF3: + name: "Quest for the Frozen Flame #3: Burning Tundra" + date: "2021-03-30" + RoE: + name: "Rage of Elements" + date: "2023-08-02" + Rust: + name: "Rusthenge" + date: "2023-10-18" + SF0: + name: "Stolen Fate Player's Guide" + date: "2023-04-13" + SKT0: + name: "Sky King's Tomb Player's Guide" + date: "2023-07-13" + SaS: + name: "Shadows at Sundown" + date: "2022-05-25" + Sli: + name: "The Slithering" + date: "2020-07-30" + SoG0: + name: "Season of Ghosts Player's Guide" + date: "2023-10-02" + SoG1: + name: "Season of Ghosts #1: The Summer That Never Was" + date: "2023-10-18" + SoG2: + name: "Season of Ghosts #2: Let the Leaves Fall" + date: "2023-11-15" + SoG3: + name: "Season of Ghosts #3: No Breath to Cry" + date: "2023-12-23" + SoG4: + name: "Season of Ghosts #4: To Bloom Below the Web" + date: "2024-01-31" + SoM: + name: "Secrets of Magic" + date: "2021-09-01" + SoT0: + name: "Strength of Thousands Player's Guide" + date: "2021-07-26" + SoT1: + name: "Strength of Thousands #1: Kindled Magic" + date: "2021-08-05" + SoT2: + name: "Strength of Thousands #2: Spoken on the Song Wind" + date: "2021-09-01" + SoT3: + name: "Strength of Thousands #3: Hurricane's Howl" + date: "2021-10-13" + SoT4: + name: "Strength of Thousands #4: Secrets of the Temple-City" + date: "2021-10-13" + SoT5: + name: "Strength of Thousands #5: Doorway to the Red Star" + date: "2021-11-10" + SoT6: + name: "Strength of Thousands #6: Shadows of the Ancients" + date: "2021-07-26" + TEC: + name: "The Enmity Cycle" + date: "2023-05-24" + TV: + name: "Treasure Vault" + date: "2023-02-22" + TaL: + name: "Torment and Legacy" + date: "2019-09-11" + TiO: + name: "Troubles in Otari" + date: "2020-12-09" + ToK: + name: "Threshold of Knowledge" + date: "2021-11-19" + TotT0: + name: "Triumph of the Tusk Player's Guide" + date: "2024-10-09" + WoW0: + name: "Wardens of Wildwood Player's Guide" + date: "2024-04-23" + WoW1: + name: "Wardens of Wildwood #1: Pactbreaker" + date: "2024-04-23" + WoW2: + name: "Wardens of Wildwood #2: Severed at the Root" + date: "2024-05-22" + WoW3: + name: "Wardens of Wildwood #3: Shepherd of Decay" + date: "2024-06-26" + WtD1: + name: "Wake the Dead #1" + date: "2023-05-31" + WtD2: + name: "Wake the Dead #2" + date: "2023-07-26" + WtD3: + name: "Wake the Dead #3" + date: "2023-09-27" + WtD4: + name: "Wake the Dead #4" + date: "2023-11-28" + WtD5: + name: "Wake the Dead #5" + date: "2024-01-31" + longToAbv: + GnG: "G&G" diff --git a/src/main/resources/templates/tools5e/bastion2md.txt b/src/main/resources/templates/tools5e/bastion2md.txt new file mode 100644 index 000000000..3c7a3ccc7 --- /dev/null +++ b/src/main/resources/templates/tools5e/bastion2md.txt @@ -0,0 +1,28 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-bastion +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +*{#if resource.prerequisite}Level {resource.level} {/if}Bastion facility* + +{#if resource.prerequisite} +- **Prerequisites**: {resource.prerequisite} +{/if}{#if resource.space } +- **Space**: {resource.spaceDescription} +{/if}{#if resource.hirelings } +- **Hirelings**: {resource.hirelingDescription} +{/if}{#if resource.orders } +- **{resource.orders.pluralizeLabel("Order")}**: {resource.orders.join(", ")} +{/if}{#if resource.text } + +{resource.text} +{/if} + +*Source: {resource.source}* diff --git a/src/scss/dnd5e-compendium.scss b/src/scss/dnd5e-compendium.scss index 3154c6f54..732e5af1e 100644 --- a/src/scss/dnd5e-compendium.scss +++ b/src/scss/dnd5e-compendium.scss @@ -1,10 +1,10 @@ /*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-compendium.scss */ -@use 'dnd5e/admonitions'; -@use 'dnd5e/statblock'; -@use 'dnd5e/float-images'; -@use 'dnd5e/no-inline-title'; +@use 'dnd5e/admonitions' as *; +@use 'dnd5e/statblock' as *; +@use 'dnd5e/float-images' as *; +@use 'dnd5e/no-inline-title' as *; -// Change background in Initiative Tracker creature view +// Change background in Initiative Tracker creature view // trim margin on included statblock admonitions .creature-view-container.workspace-leaf-content { background-color: var(--background-primary-alt); diff --git a/src/test/java/dev/ebullient/convert/CustomTemplatesIT.java b/src/test/java/dev/ebullient/convert/CustomTemplatesIT.java index f7fe29c83..b12771b09 100644 --- a/src/test/java/dev/ebullient/convert/CustomTemplatesIT.java +++ b/src/test/java/dev/ebullient/convert/CustomTemplatesIT.java @@ -8,6 +8,6 @@ public class CustomTemplatesIT extends CustomTemplatesTest { @BeforeAll public static void setupDir() { - setupDir("Tools5eDataConvertIT"); + setupDir("templates-IT"); } } diff --git a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java index 36e68daa4..e7ad85b49 100644 --- a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java +++ b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java @@ -24,21 +24,21 @@ public class CustomTemplatesTest { @BeforeAll public static void setupDir() { - setupDir("Tools5eDataConvertTest"); + setupDir("templates"); } - @AfterAll - public static void cleanup() { - System.out.println("Done."); - } - - public static void setupDir(String root) { + public static void setupDir(String name) { tui = new Tui(); tui.init(null, false, false); - testOutput = TestUtils.OUTPUT_ROOT_5E.resolve(root).resolve("test-cli"); + testOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name); testOutput.toFile().mkdirs(); } + @AfterAll + public static void cleanup() { + System.out.println("Done."); + } + @Test @Launch({ "--help" }) void testCommandHelp(LaunchResult result) { @@ -65,7 +65,7 @@ void testCommandBadTemplates(QuarkusMainLauncher launcher) { LaunchResult result = launcher.launch("--index", "--background=garbage.txt", "-o", target.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); assertThat(result.exitCode()) @@ -82,7 +82,7 @@ void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) { LaunchResult result = launcher.launch("--index", "-o", target.toString(), TestUtils.PATH_5E_TOOLS_DATA.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.TEST_RESOURCES.resolve("sources-bad-template.json").toString()); assertThat(result.exitCode()) @@ -109,7 +109,7 @@ void testCommandTemplates_5e(QuarkusMainLauncher launcher) { "--spell", TestUtils.TEST_RESOURCES.resolve("other/spell.txt").toString(), "--subclass", TestUtils.TEST_RESOURCES.resolve("other/subclass.txt").toString(), "-o", target.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); assertThat(result.exitCode()) @@ -161,9 +161,9 @@ void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) { TestUtils.deleteDir(target); LaunchResult result = launcher.launch("--debug", "--index", - "-c", TestUtils.TEST_RESOURCES.resolve("sources-templates.json").toString(), + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-templates.json").toString(), "-o", target.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); assertThat(result.exitCode()) diff --git a/src/test/java/dev/ebullient/convert/Pf2eDataConvertIT.java b/src/test/java/dev/ebullient/convert/Pf2eDataConvertIT.java index e46cf9bfd..e44f0930b 100644 --- a/src/test/java/dev/ebullient/convert/Pf2eDataConvertIT.java +++ b/src/test/java/dev/ebullient/convert/Pf2eDataConvertIT.java @@ -8,6 +8,6 @@ public class Pf2eDataConvertIT extends Pf2eDataConvertTest { @BeforeAll public static void setupDir() { - setupDir("Pf2eDataConvertIT"); + setupDir("test-cli-IT"); } } diff --git a/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java b/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java index 2a76d7a7d..882db629f 100644 --- a/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java +++ b/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java @@ -2,13 +2,18 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import dev.ebullient.convert.io.Tui; @@ -18,12 +23,21 @@ @QuarkusMainTest public class Pf2eDataConvertTest { - static Path testOutput; + static Path testOutputRoot; static Tui tui; + Path testOutput; + @BeforeAll public static void setupDir() { - setupDir("Pf2eDataConvertTest"); + setupDir("test-cli"); + } + + public static void setupDir(String name) { + tui = new Tui(); + tui.init(null, false, false); + testOutputRoot = TestUtils.OUTPUT_ROOT_PF2.resolve(name); + testOutputRoot.toFile().mkdirs(); } @AfterAll @@ -31,29 +45,39 @@ public static void cleanup() { System.out.println("Done."); } - public static void setupDir(String root) { - tui = new Tui(); - tui.init(null, false, false); - testOutput = TestUtils.OUTPUT_ROOT_PF2.resolve(root).resolve("test-cli"); - testOutput.toFile().mkdirs(); + @BeforeEach + public void setup() { + testOutput = null; // test should set this to something readable } @AfterEach - public void clear() { + public void clear() throws IOException { + assertThat(testOutput).isNotNull(); // make sure test set this + + Path logFile = Path.of("ttrpg-convert.out.txt"); + if (Files.exists(logFile)) { + Path filePath = testOutput.resolve(logFile); + Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING); + + String content = Files.readString(filePath, Charset.forName("UTF-8")); + if (content.contains("Exception")) { + tui.errorf("Exception found in %s", filePath); + } + } TestUtils.cleanupReferences(); } @Test void testLiveData_Pf2eAllSources(QuarkusMainLauncher launcher) { + testOutput = testOutputRoot.resolve("all-index"); if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) { // All, I mean it. Really for real.. ALL. - final Path allIndex = testOutput.resolve("all-index"); - TestUtils.deleteDir(allIndex); + TestUtils.deleteDir(testOutput); - List args = new ArrayList<>(List.of("--index", "--debug", - "-s", "ALL", - "-o", allIndex.toString(), + List args = new ArrayList<>(List.of("--index", "--log", + "-o", testOutput.toString(), "-g", "pf2e", + TestUtils.TEST_RESOURCES.resolve("sources-from-all.json").toString(), TestUtils.PATH_PF2E_TOOLS_DATA.toString())); args.addAll(TestUtils.getFilesFrom(TestUtils.PATH_PF2E_TOOLS_DATA.resolve("adventure")) @@ -70,9 +94,9 @@ void testLiveData_Pf2eAllSources(QuarkusMainLauncher launcher) { Tui tui = new Tui(); tui.init(null, false, false); - TestUtils.assertDirectoryContents(allIndex, tui, (p, content) -> { + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { List errors = new ArrayList<>(); - content.forEach(l -> TestUtils.checkMarkdownLink(allIndex.toString(), p, l, errors)); + content.forEach(l -> TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors)); return errors; }); } diff --git a/src/test/java/dev/ebullient/convert/TestUtils.java b/src/test/java/dev/ebullient/convert/TestUtils.java index 72bd6fb90..2ad6edfba 100644 --- a/src/test/java/dev/ebullient/convert/TestUtils.java +++ b/src/test/java/dev/ebullient/convert/TestUtils.java @@ -21,19 +21,25 @@ import org.junit.jupiter.api.Assertions; +import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter; import io.quarkus.test.junit.main.LaunchResult; public class TestUtils { + public final static boolean USING_MAVEN = System.getProperty("maven.home") != null; + public final static Path PROJECT_PATH = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + public final static Path OUTPUT_ROOT_5E = PROJECT_PATH.resolve("target/test-5e"); + public final static Path OUTPUT_5E_DATA = OUTPUT_ROOT_5E.resolve(USING_MAVEN ? "data" : "data-ide"); + public final static Path OUTPUT_ROOT_PF2 = PROJECT_PATH.resolve("target/test-pf2"); public final static Path TEST_RESOURCES = PROJECT_PATH.resolve("src/test/resources/"); // for compile/test purposes. Must clone/sync separately. - public final static Path PATH_5E_TOOLS_DATA = PROJECT_PATH.resolve("sources/5etools-mirror-2.github.io/data"); + public final static Path PATH_5E_TOOLS_DATA = PROJECT_PATH.resolve("sources/5etools-src/data"); public final static Path PATH_5E_TOOLS_IMAGES = PROJECT_PATH.resolve("sources/5etools-img"); public final static Path PATH_5E_HOMEBREW = PROJECT_PATH.resolve("sources/5e-homebrew"); @@ -64,6 +70,7 @@ public class TestUtils { public static void cleanupReferences() { TestUtils.pathHeadings.clear(); TestUtils.pathBlockReferences.clear(); + TtrpgConfig.init(Tui.instance()); } public static void checkMarkdownLink(String baseDir, Path p, String line, List errors) { diff --git a/src/test/java/dev/ebullient/convert/Tools5eDataConvertIT.java b/src/test/java/dev/ebullient/convert/Tools5eDataConvertIT.java index e61606606..aaf9c2c19 100644 --- a/src/test/java/dev/ebullient/convert/Tools5eDataConvertIT.java +++ b/src/test/java/dev/ebullient/convert/Tools5eDataConvertIT.java @@ -8,6 +8,6 @@ public class Tools5eDataConvertIT extends Tools5eDataConvertTest { @BeforeAll public static void setupDir() { - setupDir("Tools5eDataConvertIT"); + setupDir("test-cli-IT"); } } diff --git a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java index 32f840107..d798597af 100644 --- a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java +++ b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java @@ -2,13 +2,18 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import dev.ebullient.convert.io.Tui; @@ -18,20 +23,29 @@ @QuarkusMainTest public class Tools5eDataConvertTest { - static Path testOutput; + static Path rootTestOutput; static Tui tui; + Path testOutput; + @BeforeAll public static void setupDir() { - setupDir("Tools5eDataConvertTest"); + setupDir("test-cli"); + } - tui.printlnf("5eTools sources (%s): %s", + public static void setupDir(String name) { + tui = new Tui(); + tui.init(null, false, false); + rootTestOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name); + rootTestOutput.toFile().mkdirs(); + + tui.infof("5eTools sources (%s): %s", TestUtils.PATH_5E_TOOLS_DATA.toFile().exists(), TestUtils.PATH_5E_TOOLS_DATA); - tui.printlnf("5eTools images (%s): %s", + tui.infof("5eTools images (%s): %s", TestUtils.PATH_5E_TOOLS_IMAGES.toFile().exists(), TestUtils.PATH_5E_TOOLS_IMAGES); - tui.printlnf("5eTools homebrew (%s): %s", + tui.infof("5eTools homebrew (%s): %s", TestUtils.PATH_5E_HOMEBREW.toFile().exists(), TestUtils.PATH_5E_HOMEBREW); } @@ -41,40 +55,81 @@ public static void cleanup() { System.out.println("Done."); } - public static void setupDir(String root) { - tui = new Tui(); - tui.init(null, false, false); - testOutput = TestUtils.OUTPUT_ROOT_5E.resolve(root).resolve("test-cli"); - testOutput.toFile().mkdirs(); + @BeforeEach + public void setup() { + testOutput = null; // test should set this to something readable } @AfterEach - public void clear() { + public void clear() throws IOException { + assertThat(testOutput).isNotNull(); // make sure test set this + + Path logFile = Path.of("ttrpg-convert.out.txt"); + if (Files.exists(logFile)) { + Path filePath = testOutput.resolve(logFile); + Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING); + + String content = Files.readString(filePath, Charset.forName("UTF-8")); + if (content.contains("Exception")) { + tui.errorf("Exception found in %s", filePath); + } + } TestUtils.cleanupReferences(); } @Test - void testLiveData_5e(QuarkusMainLauncher launcher) { + void testLiveData_defaultSrd(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("default-index"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { // SRD - final Path srd_index = testOutput.resolve("srd-index"); - TestUtils.deleteDir(srd_index); + TestUtils.deleteDir(testOutput); + + Tui.instance().infof("--- Default content ----- "); - LaunchResult result = launcher.launch("--index", "--debug", - "-o", srd_index.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.TEST_RESOURCES.resolve("dice-roller.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); + } + } - // Subset - final Path subset_index = testOutput.resolve("subset-index"); - TestUtils.deleteDir(subset_index); + @Test + void testLiveData_2014Srd(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("srd-2014-index"); + if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { + // SRD 2014 + Tui.instance().infof("--- 2014 SRD ----- "); + TestUtils.deleteDir(testOutput); + + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-2014-srd.yaml").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), + TestUtils.PATH_5E_TOOLS_DATA.toString()); + assertThat(result.exitCode()) + .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) + .isEqualTo(0); + } + } - result = launcher.launch("--index", "-s", "PHB,DMG,XGE,SCAG", - "-o", subset_index.toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); + @Test + void testLiveData_2024Srd(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("srd-2024-index"); + if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { + // SRD 2024 + Tui.instance().infof("--- 2024 SRD ----- "); + TestUtils.deleteDir(testOutput); + + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-2024-srd.yaml").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/sources-subset.json").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), + TestUtils.PATH_5E_TOOLS_DATA.toString()); assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); @@ -83,20 +138,20 @@ void testLiveData_5e(QuarkusMainLauncher launcher) { @Test void testLiveData_5eAllSources(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("all-index"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { // All, I mean it. Really for real.. ALL. - final Path allIndex = testOutput.resolve("all-index"); - TestUtils.deleteDir(allIndex); + TestUtils.deleteDir(testOutput); - List args = new ArrayList<>(List.of("--index", "--debug", - "-c", TestUtils.TEST_RESOURCES.resolve("sources-images.yaml").toString(), - "-o", allIndex.toString(), + List args = new ArrayList<>(List.of("--log", "--index", + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-images.yaml").toString(), + "-o", testOutput.toString(), TestUtils.PATH_5E_TOOLS_DATA.toString())); if (TestUtils.PATH_5E_TOOLS_IMAGES.toFile().exists()) { - args.add(TestUtils.TEST_RESOURCES.resolve("images-from-local.json").toString()); + args.add(TestUtils.TEST_RESOURCES.resolve("5e/images-from-local.json").toString()); } else { - args.add(TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString()); + args.add(TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString()); } args.addAll(TestUtils.getFilesFrom(TestUtils.PATH_5E_TOOLS_DATA.resolve("adventure"))); @@ -107,12 +162,10 @@ void testLiveData_5eAllSources(QuarkusMainLauncher launcher) { .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); - Tui tui = new Tui(); - tui.init(null, false, false); - TestUtils.assertDirectoryContents(allIndex, tui, (p, content) -> { + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { List errors = new ArrayList<>(); content.forEach(l -> { - TestUtils.checkMarkdownLink(allIndex.toString(), p, l, errors); + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); TestUtils.commonTests(p, l, errors); }); return errors; @@ -122,23 +175,24 @@ void testLiveData_5eAllSources(QuarkusMainLauncher launcher) { @Test void testLiveData_5eOneSource(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("erlw"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("erlw"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); // No basics - LaunchResult result = launcher.launch("-s", "ERLW", "--debug", - "-o", target.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-single.yaml").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); - TestUtils.assertDirectoryContents(target, tui, (p, content) -> { + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { List errors = new ArrayList<>(); content.forEach(l -> { - TestUtils.checkMarkdownLink(target.toString(), p, l, errors); + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); TestUtils.commonTests(p, l, errors); if (l.matches(".*-ua[^.]\\.md.*$")) { errors.add(String.format("Found UA resources in %s: %s", p.toString(), l)); @@ -151,19 +205,19 @@ void testLiveData_5eOneSource(QuarkusMainLauncher launcher) { @Test void testLiveData_5eHomebrew(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("homebrew"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists() && TestUtils.PATH_5E_HOMEBREW.toFile().exists()) { - Path target = testOutput.resolve("homebrew"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); - List args = new ArrayList<>(List.of("--debug", "--index", "--log", - "-c", TestUtils.TEST_RESOURCES.resolve("sources-homebrew.json").toString(), - "-o", target.toString(), + List args = new ArrayList<>(List.of("--index", "--log", + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-homebrew.json").toString(), + "-o", testOutput.toString(), TestUtils.PATH_5E_TOOLS_DATA.toString())); if (TestUtils.PATH_5E_TOOLS_IMAGES.toFile().exists()) { - args.add(TestUtils.TEST_RESOURCES.resolve("images-from-local.json").toString()); + args.add(TestUtils.TEST_RESOURCES.resolve("5e/images-from-local.json").toString()); } else { - args.add(TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString()); + args.add(TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString()); } LaunchResult result = launcher.launch(args.toArray(new String[0])); @@ -171,21 +225,21 @@ void testLiveData_5eHomebrew(QuarkusMainLauncher launcher) { .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); - assertThat(target.resolve("compendium/adventures/a-diamond-in-the-rough")).isDirectory(); - assertThat(target.resolve("compendium/adventures/book-of-lairs")).isDirectory(); - assertThat(target.resolve("compendium/adventures/call-from-the-deep")).isDirectory(); - assertThat(target.resolve("compendium/adventures/tavern-of-the-lost")).isDirectory(); - assertThat(target.resolve("compendium/books/arkadia")).isDirectory(); - assertThat(target.resolve("compendium/books/hamunds-herbalism-handbook")).isDirectory(); - assertThat(target.resolve("compendium/books/plane-shift-amonkhet")).isDirectory(); + assertThat(testOutput.resolve("compendium/adventures/a-diamond-in-the-rough")).isDirectory(); + assertThat(testOutput.resolve("compendium/adventures/book-of-lairs")).isDirectory(); + assertThat(testOutput.resolve("compendium/adventures/call-from-the-deep")).isDirectory(); + assertThat(testOutput.resolve("compendium/adventures/tavern-of-the-lost")).isDirectory(); + assertThat(testOutput.resolve("compendium/books/arkadia")).isDirectory(); + assertThat(testOutput.resolve("compendium/books/hamunds-herbalism-handbook")).isDirectory(); + assertThat(testOutput.resolve("compendium/books/plane-shift-amonkhet")).isDirectory(); - assertThat(target.resolve("compendium/backgrounds/cook-variant-dndwiki-bestbackgrounds.md")).isRegularFile(); - assertThat(target.resolve("compendium/classes/alchemist-dynamo-engineer-vss.md")).isRegularFile(); + assertThat(testOutput.resolve("compendium/backgrounds/cook-variant-dndwiki-bestbackgrounds.md")).isRegularFile(); + assertThat(testOutput.resolve("compendium/classes/alchemist-dynamo-engineer-vss.md")).isRegularFile(); - TestUtils.assertDirectoryContents(target, tui, (p, content) -> { + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { List errors = new ArrayList<>(); content.forEach(l -> { - TestUtils.checkMarkdownLink(target.toString(), p, l, errors); + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); TestUtils.commonTests(p, l, errors); }); return errors; @@ -195,14 +249,14 @@ void testLiveData_5eHomebrew(QuarkusMainLauncher launcher) { @Test void testLiveData_5eUA(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("ua"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists() && TestUtils.PATH_5E_HOMEBREW.toFile().exists()) { - Path target = testOutput.resolve("ua"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); - LaunchResult result = launcher.launch("--debug", "--index", - "-c", TestUtils.TEST_RESOURCES.resolve("sources-ua.json").toString(), - "-o", target.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + LaunchResult result = launcher.launch("--log", "--index", + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-ua.json").toString(), + "-o", testOutput.toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString(), TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana - Downtime.json").toString(), TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana - Encounter Building.json").toString(), @@ -226,10 +280,10 @@ void testLiveData_5eUA(QuarkusMainLauncher launcher) { assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); - TestUtils.assertDirectoryContents(target, tui, (p, content) -> { + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { List errors = new ArrayList<>(); content.forEach(l -> { - TestUtils.checkMarkdownLink(target.toString(), p, l, errors); + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); TestUtils.commonTests(p, l, errors); }); return errors; @@ -239,38 +293,38 @@ void testLiveData_5eUA(QuarkusMainLauncher launcher) { @Test void testCommand_5eBookAdventureInJson(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("json-book-adventure"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("json-book-adventure"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); - LaunchResult result = launcher.launch("--index", - "-o", target.toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString(), - TestUtils.TEST_RESOURCES.resolve("sources-book-adventure.json").toString()); + TestUtils.TEST_RESOURCES.resolve("5e/sources-book-adventure.json").toString()); assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); - List dirs = List.of(target.resolve("compend ium/adventures/the-wild-beyond-the-witchlight"), - target.resolve("compend ium/books/players-handbook")); + List dirs = List.of(testOutput.resolve("compend ium/adventures/the-wild-beyond-the-witchlight"), + testOutput.resolve("compend ium/books/players-handbook-2014")); dirs.forEach(d -> { assertThat(d).isDirectory(); }); - List files = List.of(target.resolve("compend ium/backgrounds/witchlight-hand-wbtw.md"), - target.resolve("compend ium/backgrounds/folk-hero.md")); + List files = List.of(testOutput.resolve("compend ium/backgrounds/witchlight-hand-wbtw.md"), + testOutput.resolve("compend ium/backgrounds/folk-hero.md")); files.forEach(f -> { assertThat(f).isRegularFile(); }); - TestUtils.assertDirectoryContents(target, tui, (p, content) -> { + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { List errors = new ArrayList<>(); content.forEach(l -> { - TestUtils.checkMarkdownLink(target.toString(), p, l, errors); + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); TestUtils.commonTests(p, l, errors); if (l.contains("/ru les/")) { errors.add("Found '/ru les/' " + p); // not escaped @@ -286,14 +340,14 @@ void testCommand_5eBookAdventureInJson(QuarkusMainLauncher launcher) { @Test void testCommand_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("yaml-adventure"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("yaml-adventure"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); - LaunchResult result = launcher.launch("--index", - "-o", target.toString(), - "-c", TestUtils.TEST_RESOURCES.resolve("sources-no-phb.yaml").toString(), - TestUtils.TEST_RESOURCES.resolve("images-remote.json").toString(), + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-no-phb.yaml").toString(), + TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); assertThat(result.exitCode()) @@ -301,9 +355,9 @@ void testCommand_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { .isEqualTo(0); List dirs = List.of( - target.resolve("compendium/adventures/lost-mine-of-phandelver"), - target.resolve("compendium/adventures/waterdeep-dragon-heist"), - target.resolve("compendium/books/volos-guide-to-monsters")); + testOutput.resolve("compendium/adventures/lost-mine-of-phandelver"), + testOutput.resolve("compendium/adventures/waterdeep-dragon-heist"), + testOutput.resolve("compendium/books/volos-guide-to-monsters")); dirs.forEach(d -> { assertThat(d).isDirectory(); diff --git a/src/test/java/dev/ebullient/convert/config/ConfiguratorTest.java b/src/test/java/dev/ebullient/convert/config/ConfiguratorTest.java index bfe97d3eb..53d2f5899 100644 --- a/src/test/java/dev/ebullient/convert/config/ConfiguratorTest.java +++ b/src/test/java/dev/ebullient/convert/config/ConfiguratorTest.java @@ -1,6 +1,6 @@ package dev.ebullient.convert.config; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.file.Path; @@ -10,8 +10,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.databind.node.ObjectNode; - import dev.ebullient.convert.TestUtils; import dev.ebullient.convert.config.CompendiumConfig.Configurator; import dev.ebullient.convert.io.Tui; @@ -44,38 +42,17 @@ public void testPath() throws Exception { }); } - @Test - public void testPathNested() throws Exception { - TtrpgConfig.init(tui, Datasource.tools5e); - Configurator test = new Configurator(tui); - - tui.readFile(TestUtils.TEST_RESOURCES.resolve("paths.json"), List.of(), (f, node) -> { - ObjectNode parent = Tui.MAPPER.createObjectNode(); - ObjectNode ttrpg = Tui.MAPPER.createObjectNode(); - parent.set("ttrpg", ttrpg); - ttrpg.set("5e", node); - - test.readConfigIfPresent(parent); - CompendiumConfig config = TtrpgConfig.getConfig(); - - assertThat(config).isNotNull(); - assertThat(config).isNotNull(); - assertThat(config.compendiumVaultRoot()).isEqualTo(""); - assertThat(config.compendiumFilePath()).isEqualTo(CompendiumConfig.CWD); - assertThat(config.rulesVaultRoot()).isEqualTo("rules/"); - assertThat(config.rulesFilePath()).isEqualTo(Path.of("rules/")); - }); - - } - @Test public void testSources() throws Exception { TtrpgConfig.init(tui, Datasource.tools5e); Configurator test = new Configurator(tui); - tui.readFile(TestUtils.TEST_RESOURCES.resolve("sources.json"), List.of(), (f, node) -> { + tui.readFile(TestUtils.TEST_RESOURCES.resolve("5e/sources.json"), List.of(), (f, node) -> { test.readConfigIfPresent(node); CompendiumConfig config = TtrpgConfig.getConfig(); + config.resolveAdventures(); + config.resolveBooks(); + config.resolveHomebrew(); assertThat(config).isNotNull(); assertThat(config.allSources()).isFalse(); @@ -110,12 +87,12 @@ public void testBooksAdventures() throws Exception { TtrpgConfig.init(tui, Datasource.tools5e); Configurator test = new Configurator(tui); - tui.readFile(TestUtils.TEST_RESOURCES.resolve("sources-book-adventure.json"), List.of(), (f, node) -> { + tui.readFile(TestUtils.TEST_RESOURCES.resolve("5e/sources-book-adventure.json"), List.of(), (f, node) -> { test.readConfigIfPresent(node); CompendiumConfig config = TtrpgConfig.getConfig(); - Collection books = config.getBooks(); - Collection adventures = config.getAdventures(); + Collection books = config.resolveBooks(); + Collection adventures = config.resolveAdventures(); assertThat(config).isNotNull(); assertThat(books).contains("book/book-phb.json"); @@ -148,7 +125,7 @@ public void testSourcesNoImages() throws Exception { TtrpgConfig.init(tui, Datasource.tools5e); Configurator test = new Configurator(tui); - tui.readFile(TestUtils.TEST_RESOURCES.resolve("images-remote.json"), List.of(), (f, node) -> { + tui.readFile(TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json"), List.of(), (f, node) -> { test.readConfigIfPresent(node); CompendiumConfig config = TtrpgConfig.getConfig(); diff --git a/src/test/java/dev/ebullient/convert/config/ConfiguratorUtil.java b/src/test/java/dev/ebullient/convert/config/ConfiguratorUtil.java index 532a41b1d..e6f17c526 100644 --- a/src/test/java/dev/ebullient/convert/config/ConfiguratorUtil.java +++ b/src/test/java/dev/ebullient/convert/config/ConfiguratorUtil.java @@ -15,13 +15,12 @@ public static CompendiumConfig testCustomTemplate(String key, Path p) { } public static CompendiumConfig createNewConfig(Tui tui) { - TtrpgConfig.init(tui, Datasource.tools5e); - return TtrpgConfig.getConfig(Datasource.tools5e); + return createNewConfig(tui, Datasource.tools5e); } public static CompendiumConfig createNewConfig(Tui tui, Datasource datasource) { TtrpgConfig.init(tui, datasource); - return TtrpgConfig.getConfig(datasource); + return TtrpgConfig.getConfig(); } public static CompendiumConfig copy(CompendiumConfig base, TemplatePaths newTemplates) { diff --git a/src/test/java/dev/ebullient/convert/config/ConfigurationExampleTest.java b/src/test/java/dev/ebullient/convert/config/ExportDocsTest.java similarity index 50% rename from src/test/java/dev/ebullient/convert/config/ConfigurationExampleTest.java rename to src/test/java/dev/ebullient/convert/config/ExportDocsTest.java index 15c16dfdf..664ccd9dd 100644 --- a/src/test/java/dev/ebullient/convert/config/ConfigurationExampleTest.java +++ b/src/test/java/dev/ebullient/convert/config/ExportDocsTest.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeAll; @@ -16,13 +17,16 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaVersion; +import dev.ebullient.convert.TestUtils; import dev.ebullient.convert.config.TtrpgConfig.ConfigKeys; +import dev.ebullient.convert.config.TtrpgConfig.SourceReference; import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.JsonNodeReader; import io.quarkus.arc.Arc; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest -public class ConfigurationExampleTest { +public class ExportDocsTest { protected static Tui tui; @BeforeAll @@ -35,36 +39,77 @@ public static void prepare() { public void exportSourceMap() throws Exception { Path in = Path.of("src/test/resources/sourcemap.txt"); Path out = Path.of("docs/sourceMap.md"); + Path sourceTypes = Path.of("src/test/resources/5e-sourceTypes.json"); - JsonNode node = Tui.MAPPER.readTree(TtrpgConfig.class.getResourceAsStream("/sourceMap.json")); + final SourceTypes types; + + if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { + JsonNode adventures = Tui.MAPPER.readTree(TestUtils.PATH_5E_TOOLS_DATA.resolve("adventures.json").toFile()); + var adventureIds = AdventureList.adventure.streamFrom(adventures) + .map(x -> x.get("id").asText()) + .toList(); + + JsonNode books = Tui.MAPPER.readTree(TestUtils.PATH_5E_TOOLS_DATA.resolve("books.json").toFile()); + var bookIds = AdventureList.book.streamFrom(books) + .map(x -> x.get("id").asText()) + .toList(); + + types = new SourceTypes(adventureIds, bookIds); + + // Update list + tui.writeJsonFile(sourceTypes, types); + } else { + types = Tui.MAPPER.readValue(sourceTypes.toFile(), SourceTypes.class); + } + + JsonNode node = Tui.readTreeFromResource("/sourceMap.yaml"); StringBuilder tools5e = new StringBuilder(); - writeToBuilder(ConfigKeys.config5e.get(node), tools5e); + writeToBuilder(ConfigKeys.config5e.getFrom(node), tools5e, types, "5eTools"); StringBuilder toolsPf2e = new StringBuilder(); - writeToBuilder(ConfigKeys.configPf2e.get(node), toolsPf2e); + writeToBuilder(ConfigKeys.configPf2e.getFrom(node), toolsPf2e, null, "Pf2eTools"); String result = Files.readString(in) .replace("", tools5e.toString()) .replace("", toolsPf2e.toString()); Files.writeString(out, result, StandardOpenOption.CREATE); - } - void writeToBuilder(JsonNode configMap, StringBuilder builder) { - if (ConfigKeys.abvToName.existsIn(configMap)) { - builder.append("### Abbreviations to long name\n\n"); - builder.append("| Abbreviation | Long name |\n"); - builder.append("|--------------|-----------|\n"); - - ConfigKeys.abvToName.getAsMap(configMap).entrySet() + void writeToBuilder(JsonNode configMap, StringBuilder builder, SourceTypes sourceTypes, String section) { + if (ConfigKeys.reference.existsIn(configMap)) { + if (sourceTypes == null) { + builder.append("### " + section + " Abbreviations to long name\n\n"); + builder.append("| Abbreviation | Long name |\n"); + builder.append("|--------------|-----------|\n"); + } else { + builder.append("### " + section + " Abbreviations to long name\n\n"); + builder.append("| Abbreviation | Long name | Type |\n"); + builder.append("|--------------|-----------|-------|\n"); + } + + ConfigKeys.reference.getAs(configMap, TtrpgConfig.MAP_REFERENCE).entrySet() .stream().sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) - .forEach(e -> builder.append("| ").append(e.getKey()).append(" | ").append(e.getValue()).append(" |\n")); + .forEach(e -> { + SourceReference ref = e.getValue(); + builder.append("| ").append(e.getKey()) + .append(" | ").append(ref.name); + if (sourceTypes != null) { + String type = "reference"; + if (sourceTypes.adventure.contains(e.getKey())) { + type = "adventure"; + } else if (sourceTypes.book.contains(e.getKey())) { + type = "book"; + } + builder.append(" | ").append(type); + } + builder.append(" |\n"); + }); } if (ConfigKeys.longToAbv.existsIn(configMap)) { builder.append("\n"); - builder.append("### Alternate abbreviation mapping\n\n"); + builder.append("### " + section + " Alternate abbreviation mapping\n\n"); builder.append( "You may see these abbreviations referenced in source material, this is how they map to sources listed above.\n\n"); builder.append("| Abbreviation | Alias |\n"); @@ -72,21 +117,24 @@ void writeToBuilder(JsonNode configMap, StringBuilder builder) { ConfigKeys.longToAbv.getAsMap(configMap).entrySet() .stream().sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) - .forEach(e -> builder.append("| ").append(e.getKey()).append(" | ").append(e.getValue()).append(" |\n")); + .forEach(e -> builder.append("| ").append(e.getKey()).append(" | ").append(e.getValue()) + .append(" |\n")); } - } @Test public void exportExample() throws Exception { - CompendiumConfig.InputConfig tools5Config = new CompendiumConfig.InputConfig(); + UserConfig tools5Config = new UserConfig(); + + tools5Config.sources.toolsRoot = "local/5etools/data"; + tools5Config.sources.reference.add("DMG"); + tools5Config.sources.book.add("PHB"); + tools5Config.sources.adventure.add("LMoP"); + tools5Config.sources.homebrew.add("homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json"); - tools5Config.from.add("PHB"); - tools5Config.fullSource.book.add("PHB"); - tools5Config.fullSource.adventure.add("LMoP"); - tools5Config.fullSource.homebrew.add("homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json"); tools5Config.paths.compendium = "/compendium/"; tools5Config.paths.rules = "/compendium/rules/"; + tools5Config.excludePattern.add("race|.*|dmg"); tools5Config.exclude.addAll(List.of( "monster|expert|dc", @@ -94,10 +142,13 @@ public void exportExample() throws Exception { "monster|expert|slw")); tools5Config.include.add("race|changeling|mpmm"); tools5Config.includeGroup.add("familiars"); + tools5Config.template.put("background", "examples/templates/tools5e/images-background2md.txt"); + tools5Config.images.copyExternal = Boolean.TRUE; tools5Config.images.copyInternal = Boolean.TRUE; tools5Config.images.internalRoot = "local/path/for/remote/images"; + tools5Config.useDiceRoller = true; tools5Config.yamlStatblocks = true; tools5Config.tagPrefix = "ttrpg-cli"; @@ -105,11 +156,11 @@ public void exportExample() throws Exception { tui.writeJsonFile(Path.of("examples/config/config.5e.json"), tools5Config); tui.writeYamlFile(Path.of("examples/config/config.5e.yaml"), tools5Config); - CompendiumConfig.InputConfig pf2eConfig = new CompendiumConfig.InputConfig(); - pf2eConfig.from.add("CRB"); - pf2eConfig.from.add("GMG"); - pf2eConfig.fullSource.book.add("crb"); - pf2eConfig.fullSource.book.add("gmg"); + UserConfig pf2eConfig = new UserConfig(); + pf2eConfig.sources.reference.add("CRB"); + pf2eConfig.sources.reference.add("GMG"); + pf2eConfig.sources.book.add("crb"); + pf2eConfig.sources.book.add("gmg"); pf2eConfig.paths.compendium = "compendium/"; pf2eConfig.paths.rules = "compendium/rules/"; @@ -128,12 +179,33 @@ public void exportExample() throws Exception { @Test public void exportSchema() throws IOException { - SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, + OptionPreset.PLAIN_JSON) + .withObjectMapper(Tui.MAPPER) .build(); SchemaGenerator generator = new SchemaGenerator(config); - JsonNode jsonSchema = generator.generateSchema(CompendiumConfig.InputConfig.class); + JsonNode jsonSchema = generator.generateSchema(UserConfig.class); Files.writeString(Path.of("examples/config/config.schema.json"), jsonSchema.toPrettyString()); } + + static class SourceTypes { + List adventure = new ArrayList<>(); + List book = new ArrayList<>(); + + SourceTypes() { + } + + SourceTypes(List adventure, List book) { + this.adventure.addAll(adventure); + this.book.addAll(book); + } + } + + enum AdventureList implements JsonNodeReader { + adventure, + book, + id + } } diff --git a/src/test/java/dev/ebullient/convert/qute/ImageRefTest.java b/src/test/java/dev/ebullient/convert/qute/ImageRefTest.java index 479d2665c..96891e4ca 100644 --- a/src/test/java/dev/ebullient/convert/qute/ImageRefTest.java +++ b/src/test/java/dev/ebullient/convert/qute/ImageRefTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import dev.ebullient.convert.config.Datasource; import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.dnd5e.Tools5eIndex; @@ -25,6 +26,7 @@ public class ImageRefTest { public static void prepare() { tui = Arc.container().instance(Tui.class).get(); tui.init(null, true, false); + TtrpgConfig.init(tui, Datasource.tools5e); index = new Tools5eIndex(TtrpgConfig.getConfig()); } @@ -80,11 +82,11 @@ public void testEncodedRemoteUrl() throws Exception { // use Remote URL ref = new ImageRef.Builder() - .setUrl("https://raw.githubusercontent.com/5etools-mirror-2/5etools-img/main/bestiary/tokens/MM/Giant Owl.webp#token") + .setUrl("https://raw.githubusercontent.com/5etools-mirror-3/5etools-img/main/bestiary/tokens/MM/Giant Owl.webp#token") .build(); assertThat(ref.url()) .isEqualTo( - "https://raw.githubusercontent.com/5etools-mirror-2/5etools-img/main/bestiary/tokens/MM/Giant%20Owl.webp#token"); + "https://raw.githubusercontent.com/5etools-mirror-3/5etools-img/main/bestiary/tokens/MM/Giant%20Owl.webp#token"); } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java index 35f9a7777..7bb44e97e 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java @@ -1,18 +1,15 @@ package dev.ebullient.convert.tools.dnd5e; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; - import dev.ebullient.convert.TestUtils; import dev.ebullient.convert.config.CompendiumConfig; import dev.ebullient.convert.config.CompendiumConfig.Configurator; @@ -32,29 +29,31 @@ public class CommonDataTests { protected final Templates templates; protected final Path toolsData; - protected final boolean dataPresent; - protected final boolean imgPresent; + public final boolean dataPresent; - protected Tools5eIndex index; - protected TestInput variant; + public final Tools5eIndex index; + public final TestInput variant; enum TestInput { all, - subset, - none; + allNewest, + none, + noneEdition, + srd2014, + srd2024, + subset2014, + subset2024, + ; } public CommonDataTests(TestInput variant, Path toolsData) throws Exception { this.toolsData = toolsData; dataPresent = toolsData.toFile().exists(); - imgPresent = toolsData.toString().contains("mirror-2") - ? TestUtils.PATH_5E_TOOLS_IMAGES.toFile().exists() - : false; this.variant = variant; tui = Arc.container().instance(Tui.class).get(); - tui.init(null, true, false); + tui.init(null, !TestUtils.USING_MAVEN, true, true); templates = Arc.container().instance(Templates.class).get(); tui.setTemplates(templates); @@ -63,54 +62,122 @@ public CommonDataTests(TestInput variant, Path toolsData) throws Exception { TtrpgConfig.setToolsPath(TestUtils.PATH_5E_TOOLS_DATA); configurator = new Configurator(tui); - if (imgPresent) { - configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("images-from-local.json")); - } else { - configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("images-remote.json")); - } + configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json")); index = new Tools5eIndex(TtrpgConfig.getConfig()); if (dataPresent) { templates.setCustomTemplates(TtrpgConfig.getConfig()); + var additional = new ArrayList<>(List.of("adventures.json", "books.json")); switch (variant) { - case none: - // do nothing. SRD! - break; - case subset: - // use default: compendium/ and rules/ - configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("sources.json")); - break; - case all: + case none -> { + // do nothing. SRD content.. newest of all editions (so 2024) + } + case noneEdition -> { + // no content specified (just SRD) + // Do not follow reprints across editions + var o = Tui.MAPPER.createObjectNode() + .put("reprintBehavior", "edition"); + configurator.readConfigIfPresent(o); + } + case srd2014 -> { + // only 2014 + var o = Tui.MAPPER.createObjectNode() + .set("sources", Tui.MAPPER.createObjectNode() + .set("reference", Tui.MAPPER.createArrayNode() + .add("srd").add("basicrules"))); + configurator.readConfigIfPresent(o); + } + case srd2024 -> { + // only 2024 + var o = Tui.MAPPER.createObjectNode() + .set("sources", Tui.MAPPER.createObjectNode() + .set("reference", Tui.MAPPER.createArrayNode() + .add("srd52").add("freerules2024"))); + configurator.readConfigIfPresent(o); + } + case subset2014 -> { + var o = Tui.MAPPER.createObjectNode() + .set("sources", Tui.MAPPER.createObjectNode() + .set("reference", Tui.MAPPER.createArrayNode() + .add("mm").add("tce").add("xge"))); + configurator.readConfigIfPresent(o); + + additional.addAll(List.of( + "adventure/adventure-lmop.json", + "book/book-dmg.json", + "book/book-mm.json", + "book/book-phb.json")); + } + case subset2024 -> { + var o = Tui.MAPPER.createObjectNode() + .set("sources", Tui.MAPPER.createObjectNode() + .set("reference", Tui.MAPPER.createArrayNode() + .add("mpmm"))); + configurator.readConfigIfPresent(o); + + additional.addAll(List.of( + "adventure/adventure-dsotdq.json", + "book/book-tdcsr.json", + "book/book-xphb.json", + "book/book-xdmg.json")); + } + case allNewest -> { + // default behavior: newest only configurator.addSources(List.of("*")); - // use default: / and rules/ + additional.addAll(List.of( + "adventure/adventure-wdh.json", + "adventure/adventure-pota.json", + "book/book-vgm.json", + "book/book-phb.json", "book/book-xphb.json", + "book/book-dmg.json", "book/book-xdmg.json")); + } + case all -> { configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("paths.json")); - break; - } + // add book/adventure (beyond reference material) + configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("5e/sources.json")); + configurator.addSources(List.of("*")); - var additional = new ArrayList<>(List.of("adventures.json", "books.json")); - if (variant != TestInput.none) { - additional.addAll(List.of("adventure/adventure-wdh.json", "adventure/adventure-pota.json", "book/book-vgm.json", - "book/book-phb.json")); + additional.addAll(List.of( + "adventure/adventure-wdh.json", "adventure/adventure-pota.json", + "book/book-vgm.json", + "book/book-phb.json", "book/book-xphb.json", + "book/book-dmg.json", "book/book-xdmg.json")); + + // Literally all. Ignore reprints + var o = Tui.MAPPER.createObjectNode() + .put("reprintBehavior", "all"); + configurator.readConfigIfPresent(o); + } } for (String x : additional) { tui.readFile(toolsData.resolve(x), TtrpgConfig.getFixes(x), index::importTree); } + tui.readToolsDir(toolsData, index::importTree); index.prepare(); } } - public void cleanup() { - tui.close(); + public void afterEach() throws Exception { configurator.setUseDiceRoller(DiceRoller.disabled); templates.setCustomTemplates(TtrpgConfig.getConfig()); + TestUtils.cleanupReferences(); } - public void done() { + public void afterAll(Path outputPath) throws IOException { index.cleanup(); + + assertThat(Tools5eIndex.getInstance()).isNull(); + tui.close(); + Path logFile = Path.of("ttrpg-convert.out.txt"); + if (Files.exists(logFile)) { + Path newFile = outputPath.resolve(logFile); + Files.move(logFile, newFile, StandardCopyOption.REPLACE_EXISTING); + } + System.out.println("Done."); } public void testKeyIndex(Path outputPath) throws Exception { @@ -123,16 +190,20 @@ public void testKeyIndex(Path outputPath) throws Exception { index.writeFilteredIndex(p1Source); assertThat(p1Full).exists(); - JsonNode fullIndex = Tui.MAPPER.readTree(p1Full.toFile()); - ArrayNode fullIndexKeys = fullIndex.withArray("keys"); - assertThat(fullIndexKeys).isNotNull(); - assertThat(fullIndexKeys).isNotEmpty(); - assertThat(p1Source).exists(); - JsonNode filteredIndex = Tui.MAPPER.readTree(p1Source.toFile()); - ArrayNode filteredIndexKeys = filteredIndex.withArray("keys"); - assertThat(filteredIndexKeys).isNotNull(); - assertThat(filteredIndexKeys).isNotEmpty(); + } + } + + public void testAdventures(Path outputPath) { + tui.setOutputPath(outputPath); + if (dataPresent) { + Path testDir = deleteDir(Tools5eIndexType.adventureData, outputPath, index.compendiumFilePath()); + + MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); + index.markdownConverter(writer) + .writeFiles(Tools5eIndexType.adventureData); + + TestUtils.assertDirectoryContents(testDir, tui); } } @@ -143,14 +214,25 @@ public void testBackgroundList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.background) - .writeNotesAndTables() - .writeImages(); + .writeFiles(Tools5eIndexType.background); TestUtils.assertDirectoryContents(backgroundDir, tui); } } + public void testBookList(Path outputPath) { + tui.setOutputPath(outputPath); + if (dataPresent) { + Path testDir = deleteDir(Tools5eIndexType.bookData, outputPath, index.compendiumFilePath()); + + MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); + index.markdownConverter(writer) + .writeFiles(Tools5eIndexType.bookData); + + TestUtils.assertDirectoryContents(testDir, tui); + } + } + public void testClassList(Path outputPath) { tui.setOutputPath(outputPath); @@ -159,8 +241,7 @@ public void testClassList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.classtype) - .writeImages(); + .writeFiles(Tools5eIndexType.classtype); TestUtils.assertDirectoryContents(classDir, tui, (p, content) -> { List e = new ArrayList<>(); @@ -192,19 +273,7 @@ public void testDeckList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.deck) - .writeImages(); - - TestUtils.assertDirectoryContents(outDir, tui); - if (imgPresent) { - Path imageDir = outDir.resolve("img"); - assertThat(imageDir).isDirectory(); - } - - List srd = List.of(outDir.resolve("deck-of-illusions.md"), outDir.resolve("deck-of-many-things.md")); - List some = List.of(outDir.resolve("roleplaying-cards-wbtw.md")); - List all = List.of(outDir.resolve("elder-runes-deck-wdmm.md")); - testVariants(srd, some, all); + .writeFiles(Tools5eIndexType.deck); TestUtils.assertDirectoryContents(outDir, tui); } @@ -218,22 +287,21 @@ public void testDeityList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.deity) - .writeImages(); - - List srd = List.of(outDir.resolve("celtic-lugh.md"), outDir.resolve("forgotten-realms-oghma.md")); - List some = List.of(outDir.resolve("dragonlance-majere.md")); - List all = List.of(outDir.resolve("exandria-lolth.md")); - testVariants(srd, some, all); - - if (imgPresent) { - Path imageDir = outDir.resolve("img"); - if (variant == TestInput.none) { - assertThat(imageDir).doesNotExist(); - } else { - assertThat(imageDir).isDirectory(); - } - } + .writeFiles(Tools5eIndexType.deity); + + TestUtils.assertDirectoryContents(outDir, tui); + } + } + + public void testFacilityList(Path outputPath) { + tui.setOutputPath(outputPath); + if (dataPresent) { + Path outDir = deleteDir(Tools5eIndexType.facility, outputPath, index.compendiumFilePath()); + + MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); + index.markdownConverter(writer) + .writeFiles(Tools5eIndexType.facility); + TestUtils.assertDirectoryContents(outDir, tui); } } @@ -245,8 +313,7 @@ public void testFeatList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.feat) - .writeImages(); + .writeFiles(Tools5eIndexType.feat); TestUtils.assertDirectoryContents(featDir, tui); } @@ -260,8 +327,7 @@ public void testItemList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(List.of(Tools5eIndexType.item, Tools5eIndexType.itemGroup)) - .writeImages(); + .writeFiles(List.of(Tools5eIndexType.item, Tools5eIndexType.itemGroup)); TestUtils.assertDirectoryContents(itemDir, tui); } @@ -276,20 +342,7 @@ public void testMonsterList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(List.of(Tools5eIndexType.monster, Tools5eIndexType.legendaryGroup)) - .writeImages(); - - if (imgPresent) { - Path tokenDir = bestiaryDir.resolve("undead/token"); - assertThat(tokenDir.toFile()).exists(); - } - - Path lgDir = bestiaryDir.resolve("legendary-group"); - if (variant == TestInput.none) { - assertThat(lgDir).doesNotExist(); - } else { - assertThat(lgDir).exists(); - } + .writeFiles(List.of(Tools5eIndexType.monster, Tools5eIndexType.legendaryGroup)); try (Stream paths = Files.list(bestiaryDir)) { paths.forEach(p -> { @@ -442,19 +495,7 @@ public void testObjectList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(List.of( - Tools5eIndexType.object)) - .writeImages(); - - if (imgPresent) { - Path imageDir = outDir.resolve("token"); - assertThat(imageDir).exists(); - } - - List srd = List.of(outDir.resolve("generic-object.md")); - List some = List.of(outDir.resolve("ballista.md")); - List all = List.of(outDir.resolve("boilerdrak-dsotdq.md")); - testVariants(srd, some, all); + .writeFiles(List.of(Tools5eIndexType.object)); TestUtils.assertDirectoryContents(outDir, tui); } @@ -470,8 +511,7 @@ public void testOptionalFeatureList(Path outputPath) { index.markdownConverter(writer) .writeFiles(List.of( Tools5eIndexType.optionalFeatureTypes, - Tools5eIndexType.optionalfeature)) - .writeImages(); + Tools5eIndexType.optfeature)); TestUtils.assertDirectoryContents(ofDir, tui); } @@ -485,8 +525,7 @@ public void testPsionicList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.psionic) - .writeImages(); + .writeFiles(Tools5eIndexType.psionic); TestUtils.assertDirectoryContents(outDir, tui); } @@ -500,8 +539,7 @@ public void testRaceList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.race) - .writeImages(); + .writeFiles(Tools5eIndexType.race); TestUtils.assertDirectoryContents(raceDir, tui); } @@ -515,12 +553,9 @@ public void testRewardList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.reward) - .writeImages(); + .writeFiles(Tools5eIndexType.reward); - if (variant == TestInput.none) { - assertThat(rewardDir).doesNotExist(); - } else { + if (rewardDir.toFile().exists()) { TestUtils.assertDirectoryContents(rewardDir, tui); } } @@ -536,9 +571,9 @@ public void testRules(Path outputPath) { TestUtils.deleteDir(index.rulesFilePath()); MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); - index.markdownConverter(writer) - .writeNotesAndTables() - .writeImages(); + index.markdownConverter(writer).writeFiles(Stream.of(Tools5eIndexType.values()) + .filter(x -> x.isOutputType() && !x.useCompendiumBase()) + .toList()); TestUtils.assertDirectoryContents(outputPath.resolve(index.rulesFilePath()), tui); } @@ -552,8 +587,7 @@ public void testSpellList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.spell) - .writeImages(); + .writeFiles(Tools5eIndexType.spell); TestUtils.assertDirectoryContents(spellDir, tui); } @@ -567,8 +601,7 @@ public void testTrapsHazardsList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(List.of(Tools5eIndexType.trap, Tools5eIndexType.hazard)) - .writeImages(); + .writeFiles(List.of(Tools5eIndexType.trap, Tools5eIndexType.hazard)); TestUtils.assertDirectoryContents(trapsDir, tui); } @@ -582,12 +615,9 @@ public void testVehicleList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(List.of(Tools5eIndexType.vehicle)) - .writeImages(); + .writeFiles(List.of(Tools5eIndexType.vehicle)); - if (variant == TestInput.none) { - assertThat(outDir).doesNotExist(); - } else { + if (outDir.toFile().exists()) { TestUtils.assertDirectoryContents(outDir, tui); } } @@ -599,27 +629,31 @@ public Path compendiumFilePath() { Path deleteDir(Tools5eIndexType type, Path outputPath, Path vaultPath) { final String relative = type.getRelativePath(); - final Path typeDir = outputPath.resolve(vaultPath).resolve(relative); + final Path typeDir = outputPath.resolve(vaultPath).resolve(relative).normalize(); TestUtils.deleteDir(typeDir); return typeDir; } - void testVariants(List srd, List some, List all) { - if (variant == TestInput.none) { - assertAll( - () -> srd.forEach(path -> assertThat(path).exists()), - () -> some.forEach(path -> assertThat(path).doesNotExist()), - () -> all.forEach(path -> assertThat(path).doesNotExist())); - } else if (variant == TestInput.subset) { - assertAll( - () -> srd.forEach(path -> assertThat(path).exists()), - () -> some.forEach(path -> assertThat(path).exists()), - () -> all.forEach(path -> assertThat(path).doesNotExist())); - } else { - assertAll( - () -> srd.forEach(path -> assertThat(path).exists()), - () -> some.forEach(path -> assertThat(path).exists()), - () -> all.forEach(path -> assertThat(path).exists())); + public void assert_Present(String key) { + assertOrigin(key); + assertThat(index.getNode(key)) + .describedAs(variant.name() + " should contain " + key) + .isNotNull(); + } + + public void assert_MISSING(String key) { + assertOrigin(key); + assertThat(index.getNode(key)) + .describedAs(variant.name() + " should not contain " + key) + .isNull(); + } + + public void assertOrigin(String key) { + if (key.contains("level spell")) { + key = key.replaceAll(" \\(.*?level spell\\)", "").trim(); } + assertThat(index.getOrigin(key)) + .describedAs("Origin should contain " + key) + .isNotNull(); } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java new file mode 100644 index 000000000..183ec50b4 --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java @@ -0,0 +1,224 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterAllNewestTest { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.allNewest; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + // All sources, but reprints will be followed. + // PHB elements should be missing/replaced by XPHB equivalents (e.g.) + if (commonTests.dataPresent) { + commonTests.assert_MISSING("action|attack|phb"); + commonTests.assert_Present("action|attack|xphb"); + commonTests.assert_MISSING("action|cast a spell|phb"); + commonTests.assert_MISSING("action|disengage|phb"); + commonTests.assert_Present("action|disengage|xphb"); + commonTests.assert_MISSING("background|sage|phb"); + commonTests.assert_Present("background|sage|xphb"); + commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|bard|phb"); + commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_Present("deity|auril|faerûnian|scag"); + commonTests.assert_Present("deity|auril|forgotten realms|phb"); + commonTests.assert_Present("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); + commonTests.assert_Present("deity|the mockery|eberron|erlw"); + commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_Present("deity|the traveler|eberron|erlw"); + commonTests.assert_MISSING("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_Present("deity|the traveler|exandria|tdcsr"); + commonTests.assert_MISSING("disease|cackle fever|dmg"); + commonTests.assert_Present("disease|cackle fever|xdmg"); + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_Present("feat|dueling|xphb"); + commonTests.assert_MISSING("feat|grappler|phb"); + commonTests.assert_Present("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_Present("feat|moderately armored|xphb"); + commonTests.assert_Present("hazard|quicksand pit|xdmg"); + commonTests.assert_MISSING("hazard|quicksand|dmg"); + commonTests.assert_MISSING("hazard|razorvine|dmg"); + commonTests.assert_Present("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); + commonTests.assert_Present("itemgroup|arcane focus|xphb"); + commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); + commonTests.assert_Present("itemgroup|carpet of flying|xdmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_Present("itemgroup|ioun stone|xdmg"); + commonTests.assert_MISSING("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_Present("itemgroup|musical instrument|xphb"); + commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); + commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); + commonTests.assert_Present("itemproperty|2h|xphb"); + commonTests.assert_MISSING("itemproperty|bf|dmg"); + commonTests.assert_Present("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemtype|$c|phb"); + commonTests.assert_Present("itemtype|$c|xphb"); + commonTests.assert_MISSING("itemtype|$g|dmg"); + commonTests.assert_Present("itemtype|$g|xdmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_Present("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_Present("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_MISSING("item|acid (vial)|phb"); + commonTests.assert_Present("item|acid absorbing tattoo|tce"); + commonTests.assert_Present("item|acid|xphb"); + commonTests.assert_Present("item|alchemist's doom|scc"); + commonTests.assert_MISSING("item|alchemist's fire (flask)|phb"); + commonTests.assert_Present("item|alchemist's fire|xphb"); + commonTests.assert_MISSING("item|alchemist's supplies|phb"); + commonTests.assert_Present("item|alchemist's supplies|xphb"); + commonTests.assert_MISSING("item|amulet of health|dmg"); + commonTests.assert_Present("item|amulet of health|xdmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|dmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_MISSING("item|armor of invulnerability|dmg"); + commonTests.assert_Present("item|armor of invulnerability|xdmg"); + commonTests.assert_MISSING("item|automatic pistol|dmg"); + commonTests.assert_MISSING("item|automatic rifle|dmg"); + commonTests.assert_Present("item|automatic rifle|xdmg"); + commonTests.assert_MISSING("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_Present("item|ball bearings|xphb"); + commonTests.assert_Present("item|ball bearing|phb"); + commonTests.assert_MISSING("item|chain (10 feet)|phb"); + commonTests.assert_MISSING("item|chain mail|phb"); + commonTests.assert_Present("item|chain mail|xphb"); + commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_Present("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_Present("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_Present("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_MISSING("monster|animated object (huge)|phb"); + commonTests.assert_MISSING("monster|ape|mm"); + commonTests.assert_Present("monster|ape|xphb"); + commonTests.assert_MISSING("monster|ash zombie|lmop"); + commonTests.assert_Present("monster|ash zombie|pabtso"); + commonTests.assert_MISSING("monster|awakened shrub|mm"); + commonTests.assert_Present("monster|awakened shrub|xmm"); + commonTests.assert_MISSING("monster|beast of the land|tce"); + commonTests.assert_Present("monster|beast of the land|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|cat|mm"); + commonTests.assert_Present("monster|cat|xphb"); + commonTests.assert_Present("monster|derro savant|mpmm"); + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_Present("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); + commonTests.assert_Present("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); + commonTests.assert_Present("optfeature|ambush|xphb"); + commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); + commonTests.assert_Present("optfeature|investment of the chain master|xphb"); + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_Present("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_MISSING("race|tiefling|phb"); + commonTests.assert_Present("race|tiefling|xphb"); + commonTests.assert_Present("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_Present("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_Present("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); + commonTests.assert_Present("reward|blessing of wound closure|xdmg"); + commonTests.assert_MISSING("reward|boon of combat prowess|dmg"); + commonTests.assert_MISSING("reward|boon of dimensional travel|dmg"); + commonTests.assert_MISSING("reward|boon of fate|dmg"); + commonTests.assert_MISSING("reward|boon of fortitude|dmg"); + commonTests.assert_Present("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); + commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); + commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); + commonTests.assert_Present("spell|acid splash|xphb"); + commonTests.assert_Present("spell|aganazzar's scorcher|xge"); + commonTests.assert_MISSING("spell|blade barrier|phb"); + commonTests.assert_Present("spell|blade barrier|xphb"); + commonTests.assert_MISSING("spell|feeblemind|phb"); + commonTests.assert_Present("spell|illusory dragon|xge"); + commonTests.assert_MISSING("spell|illusory script|phb"); + commonTests.assert_Present("spell|illusory script|xphb"); + commonTests.assert_Present("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); + commonTests.assert_Present("status|surprised|xphb"); + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_MISSING("trap|collapsing roof|dmg"); + commonTests.assert_Present("trap|collapsing roof|xdmg"); + commonTests.assert_MISSING("trap|falling net|dmg"); + commonTests.assert_Present("trap|falling net|xdmg"); + commonTests.assert_MISSING("trap|pits|dmg"); + commonTests.assert_MISSING("trap|poison darts|dmg"); + commonTests.assert_Present("trap|poison needle trap|xge"); + commonTests.assert_MISSING("trap|poison needle|dmg"); + commonTests.assert_Present("trap|poisoned darts|xdmg"); + commonTests.assert_MISSING("trap|rolling sphere|dmg"); + commonTests.assert_Present("trap|rolling stone|xdmg"); + commonTests.assert_Present("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_Present("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_Present("vehicle|apparatus of kwalish|xdmg"); + } + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java new file mode 100644 index 000000000..9fc2e22e7 --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java @@ -0,0 +1,488 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterFields; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterAllTest { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.all; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + // All sources, but reprints will be followed. + // PHB elements should be missing/replaced by XPHB equivalents (e.g.) + if (commonTests.dataPresent) { + commonTests.assert_Present("action|attack|phb"); + commonTests.assert_Present("action|attack|xphb"); + commonTests.assert_Present("action|cast a spell|phb"); + commonTests.assert_Present("action|disengage|phb"); + commonTests.assert_Present("action|disengage|xphb"); + commonTests.assert_Present("background|sage|phb"); + commonTests.assert_Present("background|sage|xphb"); + commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|bard|phb"); + commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_Present("condition|blinded|phb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_Present("deity|auril|faerûnian|scag"); + commonTests.assert_Present("deity|auril|forgotten realms|phb"); + commonTests.assert_Present("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_Present("deity|chemosh|dragonlance|phb"); + commonTests.assert_Present("deity|the mockery|eberron|erlw"); + commonTests.assert_Present("deity|the mockery|eberron|phb"); + commonTests.assert_Present("deity|the traveler|eberron|erlw"); + commonTests.assert_Present("deity|the traveler|eberron|phb"); + commonTests.assert_Present("deity|the traveler|exandria|egw"); + commonTests.assert_Present("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); + commonTests.assert_Present("disease|cackle fever|xdmg"); + commonTests.assert_Present("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_Present("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_Present("feat|grappler|xphb"); + commonTests.assert_Present("feat|mobile|phb"); + commonTests.assert_Present("feat|moderately armored|phb"); + commonTests.assert_Present("feat|moderately armored|xphb"); + commonTests.assert_Present("hazard|quicksand pit|xdmg"); + commonTests.assert_Present("hazard|quicksand|dmg"); + commonTests.assert_Present("hazard|razorvine|dmg"); + commonTests.assert_Present("hazard|razorvine|xdmg"); + commonTests.assert_Present("itemgroup|arcane focus|phb"); + commonTests.assert_Present("itemgroup|arcane focus|xphb"); + commonTests.assert_Present("itemgroup|carpet of flying|dmg"); + commonTests.assert_Present("itemgroup|carpet of flying|xdmg"); + commonTests.assert_Present("itemgroup|ioun stone|dmg"); + commonTests.assert_Present("itemgroup|ioun stone|llk"); + commonTests.assert_Present("itemgroup|ioun stone|xdmg"); + commonTests.assert_Present("itemgroup|musical instrument|phb"); + commonTests.assert_Present("itemgroup|musical instrument|scag"); + commonTests.assert_Present("itemgroup|musical instrument|xphb"); + commonTests.assert_Present("itemgroup|spell scroll|dmg"); + commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_Present("itemproperty|2h|phb"); + commonTests.assert_Present("itemproperty|2h|xphb"); + commonTests.assert_Present("itemproperty|bf|dmg"); + commonTests.assert_Present("itemproperty|bf|xdmg"); + commonTests.assert_Present("itemtype|$c|phb"); + commonTests.assert_Present("itemtype|$c|xphb"); + commonTests.assert_Present("itemtype|$g|dmg"); + commonTests.assert_Present("itemtype|$g|xdmg"); + commonTests.assert_Present("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_Present("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_Present("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_Present("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_Present("item|acid (vial)|phb"); + commonTests.assert_Present("item|acid absorbing tattoo|tce"); + commonTests.assert_Present("item|acid|xphb"); + commonTests.assert_Present("item|alchemist's doom|scc"); + commonTests.assert_Present("item|alchemist's fire (flask)|phb"); + commonTests.assert_Present("item|alchemist's fire|xphb"); + commonTests.assert_Present("item|alchemist's supplies|phb"); + commonTests.assert_Present("item|alchemist's supplies|xphb"); + commonTests.assert_Present("item|amulet of health|dmg"); + commonTests.assert_Present("item|amulet of health|xdmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|dmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_Present("item|armor of invulnerability|dmg"); + commonTests.assert_Present("item|armor of invulnerability|xdmg"); + commonTests.assert_Present("item|automatic pistol|dmg"); + commonTests.assert_Present("item|automatic rifle|dmg"); + commonTests.assert_Present("item|automatic rifle|xdmg"); + commonTests.assert_Present("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_Present("item|ball bearings|xphb"); + commonTests.assert_Present("item|ball bearing|phb"); + commonTests.assert_Present("item|chain (10 feet)|phb"); + commonTests.assert_Present("item|chain mail|phb"); + commonTests.assert_Present("item|chain mail|xphb"); + commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_Present("monster|abjurer wizard|mpmm"); + commonTests.assert_Present("monster|abjurer|vgm"); + commonTests.assert_Present("monster|alkilith|mpmm"); + commonTests.assert_Present("monster|alkilith|mtf"); + commonTests.assert_Present("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_Present("monster|animated object (huge)|phb"); + commonTests.assert_Present("monster|ape|mm"); + commonTests.assert_Present("monster|ape|xphb"); + commonTests.assert_Present("monster|ash zombie|lmop"); + commonTests.assert_Present("monster|ash zombie|pabtso"); + commonTests.assert_Present("monster|awakened shrub|mm"); + commonTests.assert_Present("monster|awakened shrub|xmm"); + commonTests.assert_Present("monster|beast of the land|tce"); + commonTests.assert_Present("monster|beast of the land|xphb"); + commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_Present("monster|cat|mm"); + commonTests.assert_Present("monster|cat|xphb"); + commonTests.assert_Present("monster|derro savant|mpmm"); + commonTests.assert_Present("monster|derro savant|mtf"); + commonTests.assert_Present("monster|derro savant|oota"); + commonTests.assert_Present("monster|sibriex|mpmm"); + commonTests.assert_Present("monster|sibriex|mtf"); + commonTests.assert_Present("object|trebuchet|dmg"); + commonTests.assert_Present("object|trebuchet|xdmg"); + commonTests.assert_Present("optfeature|ambush|tce"); + commonTests.assert_Present("optfeature|ambush|xphb"); + commonTests.assert_Present("optfeature|investment of the chain master|tce"); + commonTests.assert_Present("optfeature|investment of the chain master|xphb"); + commonTests.assert_Present("race|bugbear|erlw"); + commonTests.assert_Present("race|bugbear|mpmm"); + commonTests.assert_Present("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); + commonTests.assert_Present("race|tiefling|xphb"); + commonTests.assert_Present("race|warforged|erlw"); + commonTests.assert_Present("race|yuan-ti pureblood|vgm"); + commonTests.assert_Present("race|yuan-ti|mpmm"); + commonTests.assert_Present("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_Present("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_Present("reward|blessing of wound closure|dmg"); + commonTests.assert_Present("reward|blessing of wound closure|xdmg"); + commonTests.assert_Present("reward|boon of combat prowess|dmg"); + commonTests.assert_Present("reward|boon of dimensional travel|dmg"); + commonTests.assert_Present("reward|boon of fate|dmg"); + commonTests.assert_Present("reward|boon of fortitude|dmg"); + commonTests.assert_Present("reward|boon of high magic|dmg"); + commonTests.assert_Present("sense|blindsight|phb"); + commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_Present("skill|athletics|phb"); + commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_Present("spell|acid splash|phb"); + commonTests.assert_Present("spell|acid splash|xphb"); + commonTests.assert_Present("spell|aganazzar's scorcher|xge"); + commonTests.assert_Present("spell|blade barrier|phb"); + commonTests.assert_Present("spell|blade barrier|xphb"); + commonTests.assert_Present("spell|feeblemind|phb"); + commonTests.assert_Present("spell|illusory dragon|xge"); + commonTests.assert_Present("spell|illusory script|phb"); + commonTests.assert_Present("spell|illusory script|xphb"); + commonTests.assert_Present("spell|wrath of nature|xge"); + commonTests.assert_Present("status|surprised|phb"); + commonTests.assert_Present("status|surprised|xphb"); + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_Present("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_Present("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); + commonTests.assert_Present("trap|collapsing roof|xdmg"); + commonTests.assert_Present("trap|falling net|dmg"); + commonTests.assert_Present("trap|falling net|xdmg"); + commonTests.assert_Present("trap|pits|dmg"); + commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_Present("trap|poison needle trap|xge"); + commonTests.assert_Present("trap|poison needle|dmg"); + commonTests.assert_Present("trap|poisoned darts|xdmg"); + commonTests.assert_Present("trap|rolling sphere|dmg"); + commonTests.assert_Present("trap|rolling stone|xdmg"); + commonTests.assert_Present("variantrule|facing|dmg"); + commonTests.assert_Present("variantrule|falling|xge"); + commonTests.assert_Present("variantrule|familiars|mm"); + commonTests.assert_Present("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_Present("vehicle|apparatus of kwalish|xdmg"); + } + } + + @Test + public void testAdventures() { + commonTests.testAdventures(outputPath); + } + + @Test + public void testBackgroundList() { + commonTests.testBackgroundList(outputPath); + } + + @Test + public void testBooks() { + commonTests.testBookList(outputPath); + } + + @Test + public void testClassList() { + commonTests.testClassList(outputPath); + } + + @Test + public void testDeckList() { + commonTests.testDeckList(outputPath); + } + + @Test + public void testDeityList() { + commonTests.testDeityList(outputPath); + } + + @Test + public void testFacilityList() { + commonTests.testFacilityList(outputPath); + } + + @Test + public void testFeatList() { + commonTests.testFeatList(outputPath); + } + + @Test + public void testItemList() { + commonTests.testItemList(outputPath); + } + + @Test + public void testMagicVariants() { + if (!TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { + return; + } + + // "requires":[{"type":"HA"},{"type":"MA"}], "excludes": {"name": "Hide Armor" } + JsonNode adamantineArmor = commonTests.index.getOrigin("magicvariant|adamantine armor|dmg"); + + // "requires":[{"type":"M"}],"excludes":{"property":"2H"} + JsonNode armBlade = commonTests.index.getOrigin("magicvariant|armblade|erlw"); + + // "requires":[{"type":"R"},{"type":"T"}], + JsonNode arrowSlaying = commonTests.index.getOrigin("magicvariant|arrow of slaying (*)|dmg"); + + // "requires":[{"sword":true}] + JsonNode luckBlade = commonTests.index.getOrigin("magicvariant|luck blade|dmg"); + + // "requires":[{"type":"SCF","scfType":"arcane"}], + // "excludes":{"name":["Staff","Rod","Wand"]} + JsonNode orbOfShielding = commonTests.index.getOrigin("magicvariant|orb of shielding (irian quartz)|erlw"); + + // "requires":[{"type":"R"},{"property":"T"}], + // "excludes":{"net":true} + JsonNode oceanicWeapon = commonTests.index.getOrigin("magicvariant|oceanic weapon|tdcsr"); + + JsonNode x; + + x = commonTests.index.getOrigin("item|arrow|phb"); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, arrowSlaying)) + .describedAs("arrowSlaying: Arrow has one required property") + .isTrue(); + + x = commonTests.index.getOrigin("item|crystal|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade)) + .describedAs("armBlade: Crystal is not a two-handed weapon (2H)") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, orbOfShielding)) + .describedAs("orbOfShielding: Crystal does not have excluded name") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, oceanicWeapon)) + .describedAs("oceanicWeapon: Crystal does not have excluded property (net)") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, armBlade)) + .describedAs("armBlade: Crystal is not a melee type (M)") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, arrowSlaying)) + .describedAs("arrowSlaying: Crystal does not have either required property") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade)) + .describedAs("luckBlade: Crystal is not a sword") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding)) + .describedAs("orbOfShielding: Crystal has required property (SCF)") + .isTrue(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, oceanicWeapon)) + .describedAs("oceanicWeapon: Crystal is not the right type (R) and does not have the right property (T)") + .isFalse(); + + x = commonTests.index.getOrigin("item|dagger|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade)) + .describedAs("armBlade: Dagger is not a two-handed weapon (2H)") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, armBlade)) + .describedAs("armBlade: Dagger is a melee type (M)") + .isTrue(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade)) + .describedAs("luckBlade: Dagger is not a sword") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding)) + .describedAs("orbOfShielding: Dagger does not have the required property (SCF / arcane)") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, oceanicWeapon)) + .describedAs("oceanicWeapon: Dagger has one of two required properties") + .isTrue(); + + x = commonTests.index.getOrigin("item|greatsword|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade)) + .describedAs("armBlade: Greatsword is a two-handed weapon (2H)") + .isTrue(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade)) + .describedAs("luckBlade: Greatsword is a sword") + .isTrue(); + + x = commonTests.index.getOrigin("item|net|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, oceanicWeapon)) + .describedAs("oceanicWeapon: Net property is excluded") + .isTrue(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, oceanicWeapon)) + .describedAs("oceanicWeapon: Net has the right type (R) and the right property (T)") + .isTrue(); + + x = commonTests.index.getOrigin("item|scimitar|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade)) + .describedAs("armBlade: Scimitar is not a two-handed weapon (2H)") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, armBlade)) + .describedAs("armBlade: Scimitar is a melee type (M)") + .isTrue(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade)) + .describedAs("luckBlade: Scimitar is a sword") + .isTrue(); + + x = commonTests.index.getOrigin("item|wand|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, orbOfShielding)) + .describedAs("orbOfShielding: Wand is an excluded name") + .isTrue(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding)) + .describedAs("orbOfShielding: Wand has the required property (SCF / arcane)") + .isTrue(); + + x = commonTests.index.getOrigin("item|wooden staff|phb"); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding)) + .describedAs("orbOfShielding: Wooden staff (SCF / druid) does not have all required properties (SCF / arcane)") + .isFalse(); + + x = commonTests.index.getOrigin("item|chain mail|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, adamantineArmor)) + .describedAs("adamantineArmor: Chain Mail is not excluded") + .isFalse(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, adamantineArmor)) + .describedAs("adamantineArmor: Chain Mail is HA") + .isTrue(); + + x = commonTests.index.getOrigin("item|hide armor|phb"); + assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, adamantineArmor)) + .describedAs("adamantineArmor: Hide Armor is excluded") + .isTrue(); + assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, adamantineArmor)) + .describedAs("adamantineArmor: Hide Armor is MA") + .isTrue(); + } + + @Test + public void testMonsterList() { + commonTests.testMonsterList(outputPath); + + if (!TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { + return; + } + + JsonNode x; + + x = commonTests.index.getOrigin("monster|reduced-threat aboleth|tftyp"); + JsonNode hp = MonsterFields.hp.getFrom(x); + assertThat(hp).isNotNull(); + assertThat(MonsterFields.average.getFrom(hp).toString()) + .describedAs("Reduced Threat monsters should have a template with stat modifications applied") + .isEqualTo("67.0"); + assertThat(MonsterFields.trait.getFrom(x).toPrettyString()) + .describedAs("Reduced Threat monsters should have a template with stat modifications applied") + .contains("Reduced Threat"); + } + + @Test + public void testMonsterAlternateScores() { + commonTests.testMonsterAlternateScores(outputPath); + } + + @Test + public void testMonsterYamlHeader() { + commonTests.testMonsterYamlHeader(outputPath); + } + + @Test + public void testMonsterYamlBody() { + commonTests.testMonsterYamlBody(outputPath); + } + + @Test + public void testObjectList() { + commonTests.testObjectList(outputPath); + } + + @Test + public void testOptionalFeatureList() { + commonTests.testOptionalFeatureList(outputPath); + } + + @Test + public void testPsionicList() { + commonTests.testPsionicList(outputPath); + } + + @Test + public void testRaceList() { + commonTests.testRaceList(outputPath); + } + + @Test + public void testRewardList() { + commonTests.testRewardList(outputPath); + } + + @Test + public void testRules() { + commonTests.testRules(outputPath); + } + + @Test + public void testSpellList() { + commonTests.testSpellList(outputPath); + } + + @Test + public void testTrapsHazardsList() { + commonTests.testTrapsHazardsList(outputPath); + } + + @Test + public void testVehicleList() { + commonTests.testVehicleList(outputPath); + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java new file mode 100644 index 000000000..9eb3f471d --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java @@ -0,0 +1,224 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterNoneEditionTest { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.noneEdition; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + if (commonTests.dataPresent) { + commonTests.assert_MISSING("action|attack|phb"); + commonTests.assert_Present("action|attack|xphb"); + commonTests.assert_MISSING("action|cast a spell|phb"); + commonTests.assert_MISSING("action|disengage|phb"); + commonTests.assert_Present("action|disengage|xphb"); + commonTests.assert_Present("background|sage|phb"); + commonTests.assert_Present("background|sage|xphb"); + commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|bard|phb"); + commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); + commonTests.assert_Present("deity|auril|forgotten realms|phb"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); + commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); + commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); + commonTests.assert_MISSING("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); // part of basic rules + commonTests.assert_MISSING("disease|cackle fever|xdmg"); + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); + commonTests.assert_MISSING("hazard|quicksand|dmg"); + commonTests.assert_MISSING("hazard|razorvine|dmg"); + commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); + commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); + commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); + commonTests.assert_MISSING("itemgroup|carpet of flying|xdmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_MISSING("itemgroup|ioun stone|xdmg"); + commonTests.assert_MISSING("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_Present("itemgroup|musical instrument|xphb"); + commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); + commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); + commonTests.assert_Present("itemproperty|2h|xphb"); + commonTests.assert_MISSING("itemproperty|bf|dmg"); + commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemtype|$c|phb"); + commonTests.assert_Present("itemtype|$c|xphb"); + commonTests.assert_MISSING("itemtype|$g|dmg"); + commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_MISSING("item|acid (vial)|phb"); + commonTests.assert_MISSING("item|acid absorbing tattoo|tce"); + commonTests.assert_Present("item|acid|xphb"); + commonTests.assert_MISSING("item|alchemist's doom|scc"); + commonTests.assert_MISSING("item|alchemist's fire (flask)|phb"); + commonTests.assert_Present("item|alchemist's fire|xphb"); + commonTests.assert_MISSING("item|alchemist's supplies|phb"); + commonTests.assert_Present("item|alchemist's supplies|xphb"); + commonTests.assert_MISSING("item|amulet of health|dmg"); + commonTests.assert_Present("item|amulet of health|xdmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|dmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_Present("item|armor of invulnerability|dmg"); + commonTests.assert_MISSING("item|armor of invulnerability|xdmg"); + commonTests.assert_MISSING("item|automatic pistol|dmg"); + commonTests.assert_MISSING("item|automatic rifle|dmg"); + commonTests.assert_MISSING("item|automatic rifle|xdmg"); + commonTests.assert_MISSING("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_Present("item|ball bearings|xphb"); + commonTests.assert_Present("item|ball bearing|phb"); + commonTests.assert_MISSING("item|chain (10 feet)|phb"); + commonTests.assert_Present("item|chain mail|phb"); + commonTests.assert_Present("item|chain mail|xphb"); + commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_MISSING("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_MISSING("monster|animated object (huge)|phb"); + commonTests.assert_Present("monster|ape|mm"); + commonTests.assert_MISSING("monster|ape|xphb"); + commonTests.assert_MISSING("monster|ash zombie|lmop"); + commonTests.assert_MISSING("monster|ash zombie|pabtso"); + commonTests.assert_Present("monster|awakened shrub|mm"); + commonTests.assert_MISSING("monster|awakened shrub|xmm"); + commonTests.assert_MISSING("monster|beast of the land|tce"); + commonTests.assert_MISSING("monster|beast of the land|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_Present("monster|cat|mm"); + commonTests.assert_MISSING("monster|cat|xphb"); + commonTests.assert_MISSING("monster|derro savant|mpmm"); + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_MISSING("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); + commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); + commonTests.assert_MISSING("optfeature|ambush|xphb"); + commonTests.assert_Present("optfeature|dueling|phb"); + commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); + commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); // in srd + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|xdmg"); + commonTests.assert_MISSING("reward|boon of combat prowess|dmg"); + commonTests.assert_MISSING("reward|boon of dimensional travel|dmg"); + commonTests.assert_MISSING("reward|boon of fate|dmg"); + commonTests.assert_MISSING("reward|boon of fortitude|dmg"); + commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); + commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); + commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); + commonTests.assert_Present("spell|acid splash|xphb"); + commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); + commonTests.assert_MISSING("spell|blade barrier|phb"); + commonTests.assert_Present("spell|blade barrier|xphb"); + commonTests.assert_MISSING("spell|feeblemind|phb"); + commonTests.assert_MISSING("spell|illusory dragon|xge"); + commonTests.assert_MISSING("spell|illusory script|phb"); + commonTests.assert_Present("spell|illusory script|xphb"); + commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); + commonTests.assert_Present("status|surprised|xphb"); + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); // srd + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); + commonTests.assert_MISSING("trap|collapsing roof|xdmg"); + commonTests.assert_Present("trap|falling net|dmg"); + commonTests.assert_MISSING("trap|falling net|xdmg"); + commonTests.assert_Present("trap|pits|dmg"); + commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poison needle trap|xge"); + commonTests.assert_Present("trap|poison needle|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); + commonTests.assert_Present("trap|rolling sphere|dmg"); + commonTests.assert_MISSING("trap|rolling stone|xdmg"); + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + } + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java new file mode 100644 index 000000000..084ae67f7 --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java @@ -0,0 +1,224 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterNoneTest { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.none; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + if (commonTests.dataPresent) { + commonTests.assert_MISSING("action|attack|phb"); + commonTests.assert_Present("action|attack|xphb"); + commonTests.assert_MISSING("action|cast a spell|phb"); + commonTests.assert_MISSING("action|disengage|phb"); + commonTests.assert_Present("action|disengage|xphb"); + commonTests.assert_MISSING("background|sage|phb"); + commonTests.assert_Present("background|sage|xphb"); + commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|bard|phb"); + commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); + commonTests.assert_Present("deity|auril|forgotten realms|phb"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); + commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); + commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); + commonTests.assert_MISSING("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); // part of basic rules + commonTests.assert_MISSING("disease|cackle fever|xdmg"); + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); + commonTests.assert_MISSING("hazard|quicksand|dmg"); + commonTests.assert_MISSING("hazard|razorvine|dmg"); + commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); + commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); + commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); + commonTests.assert_MISSING("itemgroup|carpet of flying|xdmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_MISSING("itemgroup|ioun stone|xdmg"); + commonTests.assert_MISSING("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_Present("itemgroup|musical instrument|xphb"); + commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); + commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); + commonTests.assert_Present("itemproperty|2h|xphb"); + commonTests.assert_MISSING("itemproperty|bf|dmg"); + commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemtype|$c|phb"); + commonTests.assert_Present("itemtype|$c|xphb"); + commonTests.assert_MISSING("itemtype|$g|dmg"); + commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_MISSING("item|acid (vial)|phb"); + commonTests.assert_MISSING("item|acid absorbing tattoo|tce"); + commonTests.assert_Present("item|acid|xphb"); + commonTests.assert_MISSING("item|alchemist's doom|scc"); + commonTests.assert_MISSING("item|alchemist's fire (flask)|phb"); + commonTests.assert_Present("item|alchemist's fire|xphb"); + commonTests.assert_MISSING("item|alchemist's supplies|phb"); + commonTests.assert_Present("item|alchemist's supplies|xphb"); + commonTests.assert_MISSING("item|amulet of health|dmg"); + commonTests.assert_Present("item|amulet of health|xdmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|dmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_Present("item|armor of invulnerability|dmg"); + commonTests.assert_MISSING("item|armor of invulnerability|xdmg"); + commonTests.assert_MISSING("item|automatic pistol|dmg"); + commonTests.assert_MISSING("item|automatic rifle|dmg"); + commonTests.assert_MISSING("item|automatic rifle|xdmg"); + commonTests.assert_MISSING("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_Present("item|ball bearings|xphb"); + commonTests.assert_Present("item|ball bearing|phb"); + commonTests.assert_MISSING("item|chain (10 feet)|phb"); + commonTests.assert_MISSING("item|chain mail|phb"); + commonTests.assert_Present("item|chain mail|xphb"); + commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_MISSING("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_MISSING("monster|animated object (huge)|phb"); + commonTests.assert_Present("monster|ape|mm"); + commonTests.assert_MISSING("monster|ape|xphb"); + commonTests.assert_MISSING("monster|ash zombie|lmop"); + commonTests.assert_MISSING("monster|ash zombie|pabtso"); + commonTests.assert_Present("monster|awakened shrub|mm"); + commonTests.assert_MISSING("monster|awakened shrub|xmm"); + commonTests.assert_MISSING("monster|beast of the land|tce"); + commonTests.assert_MISSING("monster|beast of the land|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_Present("monster|cat|mm"); + commonTests.assert_MISSING("monster|cat|xphb"); + commonTests.assert_MISSING("monster|derro savant|mpmm"); + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_MISSING("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); + commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); + commonTests.assert_MISSING("optfeature|ambush|xphb"); + commonTests.assert_Present("optfeature|dueling|phb"); + commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); + commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); // in srd + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|xdmg"); + commonTests.assert_MISSING("reward|boon of combat prowess|dmg"); + commonTests.assert_MISSING("reward|boon of dimensional travel|dmg"); + commonTests.assert_MISSING("reward|boon of fate|dmg"); + commonTests.assert_MISSING("reward|boon of fortitude|dmg"); + commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); + commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); + commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); + commonTests.assert_Present("spell|acid splash|xphb"); + commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); + commonTests.assert_MISSING("spell|blade barrier|phb"); + commonTests.assert_Present("spell|blade barrier|xphb"); + commonTests.assert_MISSING("spell|feeblemind|phb"); + commonTests.assert_MISSING("spell|illusory dragon|xge"); + commonTests.assert_MISSING("spell|illusory script|phb"); + commonTests.assert_Present("spell|illusory script|xphb"); + commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); + commonTests.assert_Present("status|surprised|xphb"); + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); // srd + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); + commonTests.assert_MISSING("trap|collapsing roof|xdmg"); + commonTests.assert_Present("trap|falling net|dmg"); + commonTests.assert_MISSING("trap|falling net|xdmg"); + commonTests.assert_Present("trap|pits|dmg"); + commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poison needle trap|xge"); + commonTests.assert_Present("trap|poison needle|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); + commonTests.assert_Present("trap|rolling sphere|dmg"); + commonTests.assert_MISSING("trap|rolling stone|xdmg"); + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + } + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java new file mode 100644 index 000000000..906b096ac --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java @@ -0,0 +1,222 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterSrd2014Test { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.srd2014; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + if (commonTests.dataPresent) { + commonTests.assert_Present("action|attack|phb"); + commonTests.assert_MISSING("action|attack|xphb"); + commonTests.assert_Present("action|cast a spell|phb"); + commonTests.assert_Present("action|disengage|phb"); + commonTests.assert_MISSING("action|disengage|xphb"); + commonTests.assert_Present("background|sage|phb"); + commonTests.assert_MISSING("background|sage|xphb"); + commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|bard|phb"); + commonTests.assert_MISSING("classtype|bard|xphb"); + commonTests.assert_Present("condition|blinded|phb"); + commonTests.assert_MISSING("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); + commonTests.assert_Present("deity|auril|forgotten realms|phb"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); + commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); + commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); + commonTests.assert_MISSING("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); + commonTests.assert_MISSING("disease|cackle fever|xdmg"); + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_MISSING("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); + commonTests.assert_MISSING("hazard|quicksand|dmg"); + commonTests.assert_MISSING("hazard|razorvine|dmg"); + commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); + commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); + commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); + commonTests.assert_MISSING("itemgroup|carpet of flying|xdmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_MISSING("itemgroup|ioun stone|xdmg"); + commonTests.assert_Present("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_MISSING("itemgroup|musical instrument|xphb"); + commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); + commonTests.assert_MISSING("itemgroup|spell scroll|xdmg"); + commonTests.assert_Present("itemproperty|2h|phb"); + commonTests.assert_MISSING("itemproperty|2h|xphb"); + commonTests.assert_MISSING("itemproperty|bf|dmg"); + commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_Present("itemtype|$c|phb"); + commonTests.assert_MISSING("itemtype|$c|xphb"); + commonTests.assert_MISSING("itemtype|$g|dmg"); + commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_Present("item|acid (vial)|phb"); + commonTests.assert_MISSING("item|acid absorbing tattoo|tce"); + commonTests.assert_MISSING("item|acid|xphb"); + commonTests.assert_MISSING("item|alchemist's doom|scc"); + commonTests.assert_Present("item|alchemist's fire (flask)|phb"); + commonTests.assert_MISSING("item|alchemist's fire|xphb"); + commonTests.assert_Present("item|alchemist's supplies|phb"); + commonTests.assert_MISSING("item|alchemist's supplies|xphb"); + commonTests.assert_Present("item|amulet of health|dmg"); + commonTests.assert_MISSING("item|amulet of health|xdmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|dmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_Present("item|armor of invulnerability|dmg"); + commonTests.assert_MISSING("item|armor of invulnerability|xdmg"); + commonTests.assert_MISSING("item|automatic pistol|dmg"); + commonTests.assert_MISSING("item|automatic rifle|dmg"); + commonTests.assert_MISSING("item|automatic rifle|xdmg"); + commonTests.assert_Present("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_MISSING("item|ball bearings|xphb"); + commonTests.assert_Present("item|ball bearing|phb"); + commonTests.assert_Present("item|chain (10 feet)|phb"); + commonTests.assert_Present("item|chain mail|phb"); + commonTests.assert_MISSING("item|chain mail|xphb"); + commonTests.assert_MISSING("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_MISSING("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_MISSING("monster|animated object (huge)|phb"); + commonTests.assert_Present("monster|ape|mm"); + commonTests.assert_MISSING("monster|ape|xphb"); + commonTests.assert_MISSING("monster|ash zombie|lmop"); + commonTests.assert_MISSING("monster|ash zombie|pabtso"); + commonTests.assert_Present("monster|awakened shrub|mm"); + commonTests.assert_MISSING("monster|awakened shrub|xmm"); + commonTests.assert_MISSING("monster|beast of the land|tce"); + commonTests.assert_MISSING("monster|beast of the land|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_Present("monster|cat|mm"); + commonTests.assert_MISSING("monster|cat|xphb"); + commonTests.assert_MISSING("monster|derro savant|mpmm"); + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_MISSING("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); + commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); + commonTests.assert_MISSING("optfeature|ambush|xphb"); + commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); + commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_MISSING("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|xdmg"); + commonTests.assert_MISSING("reward|boon of combat prowess|dmg"); + commonTests.assert_MISSING("reward|boon of dimensional travel|dmg"); + commonTests.assert_MISSING("reward|boon of fate|dmg"); + commonTests.assert_MISSING("reward|boon of fortitude|dmg"); + commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_Present("sense|blindsight|phb"); + commonTests.assert_MISSING("sense|blindsight|xphb"); + commonTests.assert_Present("skill|athletics|phb"); + commonTests.assert_MISSING("skill|athletics|xphb"); + commonTests.assert_Present("spell|acid splash|phb"); + commonTests.assert_MISSING("spell|acid splash|xphb"); + commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); + commonTests.assert_Present("spell|blade barrier|phb"); + commonTests.assert_MISSING("spell|blade barrier|xphb"); + commonTests.assert_Present("spell|feeblemind|phb"); + commonTests.assert_MISSING("spell|illusory dragon|xge"); + commonTests.assert_Present("spell|illusory script|phb"); + commonTests.assert_MISSING("spell|illusory script|xphb"); + commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_Present("status|surprised|phb"); + commonTests.assert_MISSING("status|surprised|xphb"); + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); + commonTests.assert_MISSING("trap|collapsing roof|xdmg"); + commonTests.assert_Present("trap|falling net|dmg"); + commonTests.assert_MISSING("trap|falling net|xdmg"); + commonTests.assert_Present("trap|pits|dmg"); + commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poison needle trap|xge"); + commonTests.assert_Present("trap|poison needle|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); + commonTests.assert_Present("trap|rolling sphere|dmg"); + commonTests.assert_MISSING("trap|rolling stone|xdmg"); + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + } + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java new file mode 100644 index 000000000..a78306dc6 --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java @@ -0,0 +1,222 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterSrd2024Test { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.srd2024; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + if (commonTests.dataPresent) { + commonTests.assert_MISSING("action|attack|phb"); + commonTests.assert_Present("action|attack|xphb"); + commonTests.assert_MISSING("action|cast a spell|phb"); + commonTests.assert_MISSING("action|disengage|phb"); + commonTests.assert_Present("action|disengage|xphb"); + commonTests.assert_MISSING("background|sage|phb"); + commonTests.assert_Present("background|sage|xphb"); + commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|bard|phb"); + commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); + commonTests.assert_MISSING("deity|auril|forgotten realms|phb"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); + commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); + commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); + commonTests.assert_MISSING("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_MISSING("disease|cackle fever|dmg"); + commonTests.assert_MISSING("disease|cackle fever|xdmg"); + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_MISSING("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); + commonTests.assert_MISSING("hazard|quicksand|dmg"); + commonTests.assert_MISSING("hazard|razorvine|dmg"); + commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); + commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); + commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); + commonTests.assert_MISSING("itemgroup|carpet of flying|xdmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_MISSING("itemgroup|ioun stone|xdmg"); + commonTests.assert_MISSING("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_Present("itemgroup|musical instrument|xphb"); + commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); + commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); + commonTests.assert_Present("itemproperty|2h|xphb"); + commonTests.assert_MISSING("itemproperty|bf|dmg"); + commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemtype|$c|phb"); + commonTests.assert_Present("itemtype|$c|xphb"); + commonTests.assert_MISSING("itemtype|$g|dmg"); + commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_MISSING("item|acid (vial)|phb"); + commonTests.assert_MISSING("item|acid absorbing tattoo|tce"); + commonTests.assert_Present("item|acid|xphb"); + commonTests.assert_MISSING("item|alchemist's doom|scc"); + commonTests.assert_MISSING("item|alchemist's fire (flask)|phb"); + commonTests.assert_Present("item|alchemist's fire|xphb"); + commonTests.assert_MISSING("item|alchemist's supplies|phb"); + commonTests.assert_Present("item|alchemist's supplies|xphb"); + commonTests.assert_MISSING("item|amulet of health|dmg"); + commonTests.assert_Present("item|amulet of health|xdmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|dmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_MISSING("item|armor of invulnerability|dmg"); + commonTests.assert_MISSING("item|armor of invulnerability|xdmg"); + commonTests.assert_MISSING("item|automatic pistol|dmg"); + commonTests.assert_MISSING("item|automatic rifle|dmg"); + commonTests.assert_MISSING("item|automatic rifle|xdmg"); + commonTests.assert_MISSING("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_Present("item|ball bearings|xphb"); + commonTests.assert_MISSING("item|ball bearing|phb"); + commonTests.assert_MISSING("item|chain (10 feet)|phb"); + commonTests.assert_MISSING("item|chain mail|phb"); + commonTests.assert_Present("item|chain mail|xphb"); + commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_MISSING("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_MISSING("monster|animated object (huge)|phb"); + commonTests.assert_MISSING("monster|ape|mm"); + commonTests.assert_MISSING("monster|ape|xphb"); + commonTests.assert_MISSING("monster|ash zombie|lmop"); + commonTests.assert_MISSING("monster|ash zombie|pabtso"); + commonTests.assert_MISSING("monster|awakened shrub|mm"); + commonTests.assert_MISSING("monster|awakened shrub|xmm"); + commonTests.assert_MISSING("monster|beast of the land|tce"); + commonTests.assert_MISSING("monster|beast of the land|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|cat|mm"); + commonTests.assert_MISSING("monster|cat|xphb"); + commonTests.assert_MISSING("monster|derro savant|mpmm"); + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_MISSING("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); + commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); + commonTests.assert_MISSING("optfeature|ambush|xphb"); + commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); + commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_MISSING("race|tiefling|phb"); + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|xdmg"); + commonTests.assert_MISSING("reward|boon of combat prowess|dmg"); + commonTests.assert_MISSING("reward|boon of dimensional travel|dmg"); + commonTests.assert_MISSING("reward|boon of fate|dmg"); + commonTests.assert_MISSING("reward|boon of fortitude|dmg"); + commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); + commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); + commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); + commonTests.assert_Present("spell|acid splash|xphb"); + commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); + commonTests.assert_MISSING("spell|blade barrier|phb"); + commonTests.assert_Present("spell|blade barrier|xphb"); + commonTests.assert_MISSING("spell|feeblemind|phb"); + commonTests.assert_MISSING("spell|illusory dragon|xge"); + commonTests.assert_MISSING("spell|illusory script|phb"); + commonTests.assert_Present("spell|illusory script|xphb"); + commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); + commonTests.assert_Present("status|surprised|xphb"); + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_MISSING("trap|collapsing roof|dmg"); + commonTests.assert_MISSING("trap|collapsing roof|xdmg"); + commonTests.assert_MISSING("trap|falling net|dmg"); + commonTests.assert_MISSING("trap|falling net|xdmg"); + commonTests.assert_MISSING("trap|pits|dmg"); + commonTests.assert_MISSING("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poison needle trap|xge"); + commonTests.assert_MISSING("trap|poison needle|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); + commonTests.assert_MISSING("trap|rolling sphere|dmg"); + commonTests.assert_MISSING("trap|rolling stone|xdmg"); + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + } + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java new file mode 100644 index 000000000..cac2f056b --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java @@ -0,0 +1,222 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterSubset2014Test { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.subset2014; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + if (commonTests.dataPresent) { + commonTests.assert_Present("action|attack|phb"); + commonTests.assert_MISSING("action|attack|xphb"); + commonTests.assert_Present("action|cast a spell|phb"); + commonTests.assert_Present("action|disengage|phb"); + commonTests.assert_MISSING("action|disengage|xphb"); + commonTests.assert_Present("background|sage|phb"); + commonTests.assert_MISSING("background|sage|xphb"); + commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|bard|phb"); + commonTests.assert_MISSING("classtype|bard|xphb"); + commonTests.assert_Present("condition|blinded|phb"); + commonTests.assert_MISSING("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); + commonTests.assert_Present("deity|auril|forgotten realms|phb"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_Present("deity|chemosh|dragonlance|phb"); + commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); + commonTests.assert_Present("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); + commonTests.assert_Present("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); + commonTests.assert_MISSING("disease|cackle fever|xdmg"); + commonTests.assert_Present("feat|alert|phb"); + commonTests.assert_MISSING("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_Present("feat|mobile|phb"); + commonTests.assert_Present("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); + commonTests.assert_Present("hazard|quicksand|dmg"); + commonTests.assert_Present("hazard|razorvine|dmg"); + commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_Present("itemgroup|arcane focus|phb"); + commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); + commonTests.assert_Present("itemgroup|carpet of flying|dmg"); + commonTests.assert_MISSING("itemgroup|carpet of flying|xdmg"); + commonTests.assert_Present("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_MISSING("itemgroup|ioun stone|xdmg"); + commonTests.assert_Present("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_MISSING("itemgroup|musical instrument|xphb"); + commonTests.assert_Present("itemgroup|spell scroll|dmg"); + commonTests.assert_MISSING("itemgroup|spell scroll|xdmg"); + commonTests.assert_Present("itemproperty|2h|phb"); + commonTests.assert_MISSING("itemproperty|2h|xphb"); + commonTests.assert_Present("itemproperty|bf|dmg"); + commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_Present("itemtype|$c|phb"); + commonTests.assert_MISSING("itemtype|$c|xphb"); + commonTests.assert_Present("itemtype|$g|dmg"); + commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_Present("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_Present("item|acid (vial)|phb"); + commonTests.assert_Present("item|acid absorbing tattoo|tce"); + commonTests.assert_MISSING("item|acid|xphb"); + commonTests.assert_MISSING("item|alchemist's doom|scc"); + commonTests.assert_Present("item|alchemist's fire (flask)|phb"); + commonTests.assert_MISSING("item|alchemist's fire|xphb"); + commonTests.assert_Present("item|alchemist's supplies|phb"); + commonTests.assert_MISSING("item|alchemist's supplies|xphb"); + commonTests.assert_Present("item|amulet of health|dmg"); + commonTests.assert_MISSING("item|amulet of health|xdmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|dmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_Present("item|armor of invulnerability|dmg"); + commonTests.assert_MISSING("item|armor of invulnerability|xdmg"); + commonTests.assert_Present("item|automatic pistol|dmg"); + commonTests.assert_Present("item|automatic rifle|dmg"); + commonTests.assert_MISSING("item|automatic rifle|xdmg"); + commonTests.assert_Present("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_MISSING("item|ball bearings|xphb"); + commonTests.assert_Present("item|ball bearing|phb"); + commonTests.assert_Present("item|chain (10 feet)|phb"); + commonTests.assert_Present("item|chain mail|phb"); + commonTests.assert_MISSING("item|chain mail|xphb"); + commonTests.assert_MISSING("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_MISSING("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_Present("monster|animated object (huge)|phb"); + commonTests.assert_Present("monster|ape|mm"); + commonTests.assert_MISSING("monster|ape|xphb"); + commonTests.assert_Present("monster|ash zombie|lmop"); + commonTests.assert_MISSING("monster|ash zombie|pabtso"); + commonTests.assert_Present("monster|awakened shrub|mm"); + commonTests.assert_MISSING("monster|awakened shrub|xmm"); + commonTests.assert_Present("monster|beast of the land|tce"); + commonTests.assert_MISSING("monster|beast of the land|xphb"); + commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_Present("monster|cat|mm"); + commonTests.assert_MISSING("monster|cat|xphb"); + commonTests.assert_MISSING("monster|derro savant|mpmm"); + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_MISSING("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_Present("object|trebuchet|dmg"); + commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_Present("optfeature|ambush|tce"); + commonTests.assert_MISSING("optfeature|ambush|xphb"); + commonTests.assert_Present("optfeature|investment of the chain master|tce"); + commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_MISSING("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_Present("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_Present("reward|blessing of wound closure|dmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|xdmg"); + commonTests.assert_Present("reward|boon of combat prowess|dmg"); + commonTests.assert_Present("reward|boon of dimensional travel|dmg"); + commonTests.assert_Present("reward|boon of fate|dmg"); + commonTests.assert_Present("reward|boon of fortitude|dmg"); + commonTests.assert_Present("reward|boon of high magic|dmg"); + commonTests.assert_Present("sense|blindsight|phb"); + commonTests.assert_MISSING("sense|blindsight|xphb"); + commonTests.assert_Present("skill|athletics|phb"); + commonTests.assert_MISSING("skill|athletics|xphb"); + commonTests.assert_Present("spell|acid splash|phb"); + commonTests.assert_MISSING("spell|acid splash|xphb"); + commonTests.assert_Present("spell|aganazzar's scorcher|xge"); + commonTests.assert_Present("spell|blade barrier|phb"); + commonTests.assert_MISSING("spell|blade barrier|xphb"); + commonTests.assert_Present("spell|feeblemind|phb"); + commonTests.assert_Present("spell|illusory dragon|xge"); + commonTests.assert_Present("spell|illusory script|phb"); + commonTests.assert_MISSING("spell|illusory script|xphb"); + commonTests.assert_Present("spell|wrath of nature|xge"); + commonTests.assert_Present("status|surprised|phb"); + commonTests.assert_MISSING("status|surprised|xphb"); + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); + commonTests.assert_MISSING("trap|collapsing roof|xdmg"); + commonTests.assert_Present("trap|falling net|dmg"); + commonTests.assert_MISSING("trap|falling net|xdmg"); + commonTests.assert_Present("trap|pits|dmg"); + commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_Present("trap|poison needle trap|xge"); + commonTests.assert_Present("trap|poison needle|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); + commonTests.assert_Present("trap|rolling sphere|dmg"); + commonTests.assert_MISSING("trap|rolling stone|xdmg"); + commonTests.assert_Present("variantrule|facing|dmg"); + commonTests.assert_Present("variantrule|falling|xge"); + commonTests.assert_Present("variantrule|familiars|mm"); + commonTests.assert_Present("variantrule|simultaneous effects|xge"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + } + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java new file mode 100644 index 000000000..1464bd1bf --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java @@ -0,0 +1,221 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class FilterSubset2024Test { + + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.subset2024; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + // This uses test/resources/sources.json to constrain sources + commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + if (commonTests.dataPresent) { + commonTests.assert_MISSING("action|attack|phb"); + commonTests.assert_Present("action|attack|xphb"); + commonTests.assert_MISSING("action|cast a spell|phb"); + commonTests.assert_MISSING("action|disengage|phb"); + commonTests.assert_Present("action|disengage|xphb"); + commonTests.assert_MISSING("background|sage|phb"); + commonTests.assert_Present("background|sage|xphb"); + commonTests.assert_MISSING("classtype|bard|phb"); + commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); + commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); + commonTests.assert_MISSING("deity|auril|forgotten realms|phb"); + commonTests.assert_Present("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); + commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); + commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); + commonTests.assert_MISSING("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_Present("deity|the traveler|exandria|tdcsr"); + commonTests.assert_MISSING("disease|cackle fever|dmg"); + commonTests.assert_Present("disease|cackle fever|xdmg"); + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_Present("feat|dueling|xphb"); + commonTests.assert_MISSING("feat|grappler|phb"); + commonTests.assert_Present("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_Present("feat|moderately armored|xphb"); + commonTests.assert_Present("hazard|quicksand pit|xdmg"); + commonTests.assert_MISSING("hazard|quicksand|dmg"); + commonTests.assert_MISSING("hazard|razorvine|dmg"); + commonTests.assert_Present("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); + commonTests.assert_Present("itemgroup|arcane focus|xphb"); + commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); + commonTests.assert_Present("itemgroup|carpet of flying|xdmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_Present("itemgroup|ioun stone|xdmg"); + commonTests.assert_MISSING("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_Present("itemgroup|musical instrument|xphb"); + commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); + commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); + commonTests.assert_Present("itemproperty|2h|xphb"); + commonTests.assert_MISSING("itemproperty|bf|dmg"); + commonTests.assert_Present("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemtype|$c|phb"); + commonTests.assert_Present("itemtype|$c|xphb"); + commonTests.assert_MISSING("itemtype|$g|dmg"); + commonTests.assert_Present("itemtype|$g|xdmg"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_Present("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_Present("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_MISSING("item|acid (vial)|phb"); + commonTests.assert_MISSING("item|acid absorbing tattoo|tce"); + commonTests.assert_Present("item|acid|xphb"); + commonTests.assert_MISSING("item|alchemist's doom|scc"); + commonTests.assert_MISSING("item|alchemist's fire (flask)|phb"); + commonTests.assert_Present("item|alchemist's fire|xphb"); + commonTests.assert_MISSING("item|alchemist's supplies|phb"); + commonTests.assert_Present("item|alchemist's supplies|xphb"); + commonTests.assert_MISSING("item|amulet of health|dmg"); + commonTests.assert_Present("item|amulet of health|xdmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|dmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_MISSING("item|armor of invulnerability|dmg"); + commonTests.assert_Present("item|armor of invulnerability|xdmg"); + commonTests.assert_MISSING("item|automatic pistol|dmg"); + commonTests.assert_MISSING("item|automatic rifle|dmg"); + commonTests.assert_Present("item|automatic rifle|xdmg"); + commonTests.assert_MISSING("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_Present("item|ball bearings|xphb"); + commonTests.assert_MISSING("item|ball bearing|phb"); + commonTests.assert_MISSING("item|chain (10 feet)|phb"); + commonTests.assert_MISSING("item|chain mail|phb"); + commonTests.assert_Present("item|chain mail|xphb"); + commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_Present("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_Present("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_Present("monster|animated object (5th-level spell)|xphb"); + commonTests.assert_MISSING("monster|animated object (huge)|phb"); + commonTests.assert_MISSING("monster|ape|mm"); + commonTests.assert_Present("monster|ape|xphb"); + commonTests.assert_MISSING("monster|ash zombie|lmop"); + commonTests.assert_MISSING("monster|ash zombie|pabtso"); + commonTests.assert_MISSING("monster|awakened shrub|mm"); + commonTests.assert_MISSING("monster|awakened shrub|xmm"); + commonTests.assert_MISSING("monster|beast of the land|tce"); + commonTests.assert_Present("monster|beast of the land|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); + commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|cat|mm"); + commonTests.assert_Present("monster|cat|xphb"); + commonTests.assert_Present("monster|derro savant|mpmm"); + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_Present("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); + commonTests.assert_Present("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); + commonTests.assert_Present("optfeature|ambush|xphb"); + commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); + commonTests.assert_Present("optfeature|investment of the chain master|xphb"); + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_Present("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_MISSING("race|tiefling|phb"); + commonTests.assert_Present("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_Present("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_Present("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); + commonTests.assert_Present("reward|blessing of wound closure|xdmg"); + commonTests.assert_MISSING("reward|boon of combat prowess|dmg"); + commonTests.assert_MISSING("reward|boon of dimensional travel|dmg"); + commonTests.assert_MISSING("reward|boon of fate|dmg"); + commonTests.assert_MISSING("reward|boon of fortitude|dmg"); + commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); + commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); + commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); + commonTests.assert_Present("spell|acid splash|xphb"); + commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); + commonTests.assert_MISSING("spell|blade barrier|phb"); + commonTests.assert_Present("spell|blade barrier|xphb"); + commonTests.assert_MISSING("spell|feeblemind|phb"); + commonTests.assert_MISSING("spell|illusory dragon|xge"); + commonTests.assert_MISSING("spell|illusory script|phb"); + commonTests.assert_Present("spell|illusory script|xphb"); + commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); + commonTests.assert_Present("status|surprised|xphb"); + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_MISSING("trap|collapsing roof|dmg"); + commonTests.assert_Present("trap|collapsing roof|xdmg"); + commonTests.assert_MISSING("trap|falling net|dmg"); + commonTests.assert_Present("trap|falling net|xdmg"); + commonTests.assert_MISSING("trap|pits|dmg"); + commonTests.assert_MISSING("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poison needle trap|xge"); + commonTests.assert_MISSING("trap|poison needle|dmg"); + commonTests.assert_Present("trap|poisoned darts|xdmg"); + commonTests.assert_MISSING("trap|rolling sphere|dmg"); + commonTests.assert_Present("trap|rolling stone|xdmg"); + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_Present("vehicle|apparatus of kwalish|xdmg"); + } + } +} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataNoneTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataNoneTest.java deleted file mode 100644 index 092d52dd3..000000000 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataNoneTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.ebullient.convert.tools.dnd5e; - -import java.nio.file.Path; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import dev.ebullient.convert.TestUtils; -import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; -import io.quarkus.test.junit.QuarkusTest; - -@QuarkusTest -public class JsonDataNoneTest { - - static CommonDataTests commonTests; - static final Path outputPath = TestUtils.OUTPUT_ROOT_5E.resolve("none"); - - @BeforeAll - public static void setupDir() throws Exception { - outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(TestInput.none, TestUtils.PATH_5E_TOOLS_DATA); - } - - @AfterAll - public static void done() { - System.out.println("Done."); - commonTests.done(); - } - - @AfterEach - public void cleanup() { - commonTests.cleanup(); - } - - @Test - public void testKeyIndex() throws Exception { - commonTests.testKeyIndex(outputPath); - } - - @Test - public void testBackgroundList() { - commonTests.testBackgroundList(outputPath); - } - - @Test - public void testClassList() { - commonTests.testClassList(outputPath); - } - - @Test - public void testDeckList() { - commonTests.testDeckList(outputPath); - } - - @Test - public void testDeityList() { - commonTests.testDeityList(outputPath); - } - - @Test - public void testFeatList() { - commonTests.testFeatList(outputPath); - } - - @Test - public void testItemList() { - commonTests.testItemList(outputPath); - } - - @Test - public void testObjectList() { - commonTests.testObjectList(outputPath); - } - - @Test - public void testMonsterList() { - commonTests.testMonsterList(outputPath); - } - - @Test - public void testOptionalFeatureList() { - commonTests.testOptionalFeatureList(outputPath); - } - - @Test - public void testRewardList() { - commonTests.testRewardList(outputPath); - } - - @Test - public void testRaceList() { - commonTests.testRaceList(outputPath); - } - - @Test - public void testRules() { - commonTests.testRules(outputPath); - } - - @Test - public void testSpellList() { - commonTests.testSpellList(outputPath); - } - - @Test - public void testTrapsHazardsList() { - commonTests.testTrapsHazardsList(outputPath); - } - - @Test - public void testVehicleList() { - commonTests.testVehicleList(outputPath); - } -} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataSubsetTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataSubsetTest.java deleted file mode 100644 index 9012b2946..000000000 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataSubsetTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package dev.ebullient.convert.tools.dnd5e; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.nio.file.Path; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import dev.ebullient.convert.TestUtils; -import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; -import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; -import io.quarkus.test.junit.QuarkusTest; - -@QuarkusTest -public class JsonDataSubsetTest { - - static CommonDataTests commonTests; - static final Path outputPath = TestUtils.OUTPUT_ROOT_5E.resolve("subset"); - - @BeforeAll - public static void setupDir() throws Exception { - outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(TestInput.subset, TestUtils.PATH_5E_TOOLS_DATA); - } - - @AfterAll - public static void done() { - System.out.println("Done."); - } - - @AfterEach - public void cleanup() { - commonTests.cleanup(); - } - - @Test - public void testKeyIndex() throws Exception { - commonTests.testKeyIndex(outputPath); - } - - @Test - public void testBackgroundList() { - commonTests.testBackgroundList(outputPath); - } - - @Test - public void testClassList() { - commonTests.testClassList(outputPath); - } - - @Test - public void testDeckList() { - commonTests.testDeckList(outputPath); - } - - @Test - public void testDeityList() { - commonTests.testDeityList(outputPath); - } - - @Test - public void testFeatList() { - commonTests.testFeatList(outputPath); - } - - @Test - public void testItemList() { - commonTests.testItemList(outputPath); - } - - @Test - public void testMonsterList() { - commonTests.testMonsterList(outputPath); - - if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - // Tree blight is from Curse of Strahd, but is also present in - // The Wild Beyond the Witchlight --> an "otherSource". - // The tree blight should be included when WBtW is included - Path treeBlight = outputPath - .resolve(commonTests.compendiumFilePath()) - .resolve(Tools5eQuteBase.monsterPath(false, "plant")) - .resolve("tree-blight-cos.md"); - assertThat(treeBlight).exists(); - } - } - - @Test - public void testObjectList() { - commonTests.testObjectList(outputPath); - } - - @Test - public void testOptionalFeatureList() { - commonTests.testOptionalFeatureList(outputPath); - } - - @Test - public void testRaceList() { - commonTests.testRaceList(outputPath); - - // Changeling from mpmm is a reprint.. - if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - final String raceRelative = Tools5eIndexType.race.getRelativePath(); - - // Single included race: changeling from mpmm - Path changeling = outputPath - .resolve(commonTests.compendiumFilePath()) - .resolve(raceRelative) - .resolve("changeling-mpmm.md"); - assertThat(changeling).exists(); - } - } - - @Test - public void testRewardList() { - commonTests.testRewardList(outputPath); - } - - @Test - public void testRules() { - commonTests.testRules(outputPath); - } - - @Test - public void testSpellList() { - commonTests.testSpellList(outputPath); - } - - @Test - public void testTrapsHazardsList() { - commonTests.testTrapsHazardsList(outputPath); - } - - @Test - public void testVehicleList() { - commonTests.testVehicleList(outputPath); - } -} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataTest.java deleted file mode 100644 index cb0b366da..000000000 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/JsonDataTest.java +++ /dev/null @@ -1,289 +0,0 @@ -package dev.ebullient.convert.tools.dnd5e; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.nio.file.Path; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.JsonNode; - -import dev.ebullient.convert.TestUtils; -import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; -import dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterFields; -import io.quarkus.test.junit.QuarkusTest; - -@QuarkusTest -public class JsonDataTest { - - static CommonDataTests commonTests; - static final Path outputPath = TestUtils.OUTPUT_ROOT_5E.resolve("all"); - - @BeforeAll - public static void setupDir() throws Exception { - outputPath.toFile().mkdirs(); - commonTests = new CommonDataTests(TestInput.all, TestUtils.PATH_5E_TOOLS_DATA); - } - - @AfterAll - public static void done() { - System.out.println("Done."); - } - - @AfterEach - public void cleanup() { - commonTests.cleanup(); - } - - @Test - public void testKeyIndex() throws Exception { - commonTests.testKeyIndex(outputPath); - } - - @Test - public void testBackgroundList() { - commonTests.testBackgroundList(outputPath); - } - - @Test - public void testClassList() { - commonTests.testClassList(outputPath); - } - - @Test - public void testDeckList() { - commonTests.testDeckList(outputPath); - } - - @Test - public void testDeityList() { - commonTests.testDeityList(outputPath); - } - - @Test - public void testFeatList() { - commonTests.testFeatList(outputPath); - } - - @Test - public void testItemList() { - commonTests.testItemList(outputPath); - } - - @Test - public void testMagicVariants() { - if (!TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - return; - } - - // "requires":[{"type":"HA"},{"type":"MA"}], "excludes": {"name": "Hide Armor" } - JsonNode adamantineArmor = commonTests.index.getOrigin("magicvariant|adamantine armor|dmg"); - - // "requires":[{"type":"M"}],"excludes":{"property":"2H"} - JsonNode armBlade = commonTests.index.getOrigin("magicvariant|armblade|erlw"); - - // "requires":[{"type":"R"},{"type":"T"}], - JsonNode arrowSlaying = commonTests.index.getOrigin("magicvariant|arrow of slaying (*)|dmg"); - - // "requires":[{"sword":true}] - JsonNode luckBlade = commonTests.index.getOrigin("magicvariant|luck blade|dmg"); - - // "requires":[{"type":"SCF","scfType":"arcane"}], - // "excludes":{"name":["Staff","Rod","Wand"]} - JsonNode orbOfShielding = commonTests.index.getOrigin("magicvariant|orb of shielding (irian quartz)|erlw"); - - // "requires":[{"type":"R"},{"property":"T"}], - // "excludes":{"net":true} - JsonNode oceanicWeapon = commonTests.index.getOrigin("magicvariant|oceanic weapon|tdcsr"); - - JsonNode x; - - x = commonTests.index.getOrigin("item|arrow|phb"); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(arrowSlaying, x)) - .describedAs("arrowSlaying: Arrow has one required property") - .isTrue(); - - x = commonTests.index.getOrigin("item|crystal|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(armBlade, x)) - .describedAs("armBlade: Crystal is not a two-handed weapon (2H)") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(orbOfShielding, x)) - .describedAs("orbOfShielding: Crystal does not have excluded name") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(oceanicWeapon, x)) - .describedAs("oceanicWeapon: Crystal does not have excluded property (net)") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(armBlade, x)) - .describedAs("armBlade: Crystal is not a melee type (M)") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(arrowSlaying, x)) - .describedAs("arrowSlaying: Crystal does not have either required property") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(luckBlade, x)) - .describedAs("luckBlade: Crystal is not a sword") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(orbOfShielding, x)) - .describedAs("orbOfShielding: Crystal has required property (SCF)") - .isTrue(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(oceanicWeapon, x)) - .describedAs("oceanicWeapon: Crystal is not the right type (R) and does not have the right property (T)") - .isFalse(); - - x = commonTests.index.getOrigin("item|dagger|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(armBlade, x)) - .describedAs("armBlade: Dagger is not a two-handed weapon (2H)") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(armBlade, x)) - .describedAs("armBlade: Dagger is a melee type (M)") - .isTrue(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(luckBlade, x)) - .describedAs("luckBlade: Dagger is not a sword") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(orbOfShielding, x)) - .describedAs("orbOfShielding: Dagger does not have the required property (SCF / arcane)") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(oceanicWeapon, x)) - .describedAs("oceanicWeapon: Dagger has one of two required properties") - .isTrue(); - - x = commonTests.index.getOrigin("item|greatsword|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(armBlade, x)) - .describedAs("armBlade: Greatsword is a two-handed weapon (2H)") - .isTrue(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(luckBlade, x)) - .describedAs("luckBlade: Greatsword is a sword") - .isTrue(); - - x = commonTests.index.getOrigin("item|net|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(oceanicWeapon, x)) - .describedAs("oceanicWeapon: Net property is excluded") - .isTrue(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(oceanicWeapon, x)) - .describedAs("oceanicWeapon: Net has the right type (R) and the right property (T)") - .isTrue(); - - x = commonTests.index.getOrigin("item|scimitar|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(armBlade, x)) - .describedAs("armBlade: Scimitar is not a two-handed weapon (2H)") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(armBlade, x)) - .describedAs("armBlade: Scimitar is a melee type (M)") - .isTrue(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(luckBlade, x)) - .describedAs("luckBlade: Scimitar is a sword") - .isTrue(); - - x = commonTests.index.getOrigin("item|wand|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(orbOfShielding, x)) - .describedAs("orbOfShielding: Wand is an excluded name") - .isTrue(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(orbOfShielding, x)) - .describedAs("orbOfShielding: Wand has the required property (SCF / arcane)") - .isTrue(); - - x = commonTests.index.getOrigin("item|wooden staff|phb"); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(orbOfShielding, x)) - .describedAs("orbOfShielding: Wooden staff (SCF / druid) does not have all required properties (SCF / arcane)") - .isFalse(); - - x = commonTests.index.getOrigin("item|chain mail|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(adamantineArmor, x)) - .describedAs("adamantineArmor: Chain Mail is not excluded") - .isFalse(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(adamantineArmor, x)) - .describedAs("adamantineArmor: Chain Mail is HA") - .isTrue(); - - x = commonTests.index.getOrigin("item|hide armor|phb"); - assertThat(MagicVariant.INSTANCE.hasExcludedProperty(adamantineArmor, x)) - .describedAs("adamantineArmor: Hide Armor is excluded") - .isTrue(); - assertThat(MagicVariant.INSTANCE.hasRequiredProperty(adamantineArmor, x)) - .describedAs("adamantineArmor: Hide Armor is MA") - .isTrue(); - } - - @Test - public void testMonsterList() { - commonTests.testMonsterList(outputPath); - - if (!TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - return; - } - - JsonNode x; - - x = commonTests.index.getOrigin("monster|reduced-threat aboleth|tftyp"); - JsonNode hp = MonsterFields.hp.getFrom(x); - assertThat(hp).isNotNull(); - assertThat(MonsterFields.average.getFrom(hp).toString()) - .describedAs("Reduced Threat monsters should have a template with stat modifications applied") - .isEqualTo("67.0"); - assertThat(MonsterFields.trait.getFrom(x).toPrettyString()) - .describedAs("Reduced Threat monsters should have a template with stat modifications applied") - .contains("Reduced Threat"); - } - - @Test - public void testMonsterAlternateScores() { - commonTests.testMonsterAlternateScores(outputPath); - } - - @Test - public void testMonsterYamlHeader() { - commonTests.testMonsterYamlHeader(outputPath); - } - - @Test - public void testMonsterYamlBody() { - commonTests.testMonsterYamlBody(outputPath); - } - - @Test - public void testObjectList() { - commonTests.testObjectList(outputPath); - } - - @Test - public void testOptionalFeatureList() { - commonTests.testOptionalFeatureList(outputPath); - } - - @Test - public void testPsionicList() { - commonTests.testPsionicList(outputPath); - } - - @Test - public void testRaceList() { - commonTests.testRaceList(outputPath); - } - - @Test - public void testRewardList() { - commonTests.testRewardList(outputPath); - } - - @Test - public void testRules() { - commonTests.testRules(outputPath); - } - - @Test - public void testSpellList() { - commonTests.testSpellList(outputPath); - } - - @Test - public void testTrapsHazardsList() { - commonTests.testTrapsHazardsList(outputPath); - } - - @Test - public void testVehicleList() { - commonTests.testVehicleList(outputPath); - } -} diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/RegexTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/TextReplacementTest.java similarity index 75% rename from src/test/java/dev/ebullient/convert/tools/dnd5e/RegexTest.java rename to src/test/java/dev/ebullient/convert/tools/dnd5e/TextReplacementTest.java index df16267db..0ece049e9 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/RegexTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/TextReplacementTest.java @@ -14,7 +14,7 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonSourceCopier; -public class RegexTest implements JsonSource { +public class TextReplacementTest implements JsonSource { Tui tui = new Tui(); CompendiumConfig config = ConfiguratorUtil.createNewConfig(tui); @@ -113,33 +113,35 @@ public void testDiceFormla() { List example = List.of( "Spells cast from the spell gem have a save DC of 15 and an attack bonus of {@hit 9}.", "It has a Strength of 26 ({@d20 8}) and a Dexterity of 10 ({@d20 0})", - "{@dice 1d2-2+2d3+5} for regular dice rolls, {@dice 1d6;2d6} for multiple options;", + "{@dice 1d2-2+2d3+5} for regular dice rolls", + "{@dice 1d6;2d6} for multiple options;", "{@dice 1d6 + #$prompt_number:min=1,title=Enter a Number!,default=123$#} for input prompts)", "with extended {@dice 1d20+2|display text} and {@dice 1d20+2|display text|rolled by name}", "a special 'hit' version which assumes a d20 is to be rolled {@hit +7}", "There's also {@damage 1d12+3} and {@d20 -4}", - "{@scaledamage 2d6;3d6|2-9|1d6} {@scaledice 2d6|1,3,5,7,9|1d6|psi|extra amount}", + "scaledamage: {@scaledamage 2d6;3d6|2-9|1d6}, scaledice: {@scaledice 2d6|1,3,5,7,9|1d6|psi|extra amount}", "{@ability str 20}, {@savingThrow str 5}, and {@skillCheck animal_handling 5}", "with an Intelligence of {@d20 3|16}, a Wisdom of {@d20 0|10}, and a Charisma of {@d20 4|18}", "{@hit 3 plus PB} to hit; {@h}7 ({@damage 1d6 + 4}) piercing damage plus 7 ({@damage 2d6}) poison damage.", "{@d20 2|10|Perception} {@d20 -2|8|Perception}", - "{@hit +3|+3 to hit}", + "hit display text: {@hit +3|+3 to hit}", "{@atk mw} {@hit 9} to hit, reach 5 ft., one target. {@h}9 ({@damage 1d8 + 5}) piercing damage plus 7 ({@damage 2d6}) necrotic damage."); List disabled = List.of( "Spells cast from the spell gem have a save DC of 15 and an attack bonus of `+9`.", "It has a Strength of 26 (`+8`) and a Dexterity of 10 (`+0`)", - "`1d2-2+2d3+5` for regular dice rolls, `1d6` for multiple options;", - "`1d6 + [Number]` for input prompts)", - "with extended display text and display text", + "`1d2-2+2d3+5` for regular dice rolls", + "`1d6` or `2d6` for multiple options;", + "`1d6 + [Number]` for input prompts)", + "with extended display text (`+2`) and display text (`+2`)", "a special 'hit' version which assumes a d20 is to be rolled `+7`", "There's also `1d12+3` and `-4`", - "`1d6` `1d6`", + "scaledamage: `1d6`, scaledice: extra amount (`1d6`)", "Strength (5), Strength (+5), and [Animal Handling](rules/skills.md#Animal%20Handling) (+5)", "with an Intelligence of `+3` (`16`), a Wisdom of `+0` (`10`), and a Charisma of `+4` (`18`)", - "`3 plus PB` to hit; *Hit:* 7 (`1d6 + 4`) piercing damage plus 7 (`2d6`) poison damage.", + "`+3 plus PB` to hit; *Hit:* 7 (`1d6 + 4`) piercing damage plus 7 (`2d6`) poison damage.", "Perception (`+2`) Perception (`-2`)", - "+3 to hit", + "hit display text: +3 to hit", "*Melee Weapon Attack:* `+9` to hit, reach 5 ft., one target. *Hit:* 9 (`1d8 + 5`) piercing damage plus 7 (`2d6`) necrotic damage."); configurator.setUseDiceRoller(DiceRoller.disabled); @@ -149,20 +151,21 @@ public void testDiceFormla() { } List enabled = List.of( - "Spells cast from the spell gem have a save DC of 15 and an attack bonus of `dice: d20+9` (`+9`).", - "It has a Strength of `dice:+8|text(26)` (`+8`) and a Dexterity of `dice:+0|text(10)` (`+0`)", - "`dice: 1d2-2+2d3+5|avg|noform` (`1d2-2+2d3+5`) for regular dice rolls, `dice: 1d6|avg|noform` (`1d6`) for multiple options;", - "`1d6 + [Number]` for input prompts)", - "with extended display text and display text", - "a special 'hit' version which assumes a d20 is to be rolled `dice: d20+7` (`+7`)", - "There's also `dice: 1d12+3|avg|noform` (`1d12+3`) and `dice: d20-4` (`-4`)", - "`1d6` `1d6`", + "Spells cast from the spell gem have a save DC of 15 and an attack bonus of `dice:1d20+9|noform|text(+9)`.", + "It has a Strength of `dice:1d20+8|noform|text(26)` (`+8`) and a Dexterity of `dice:1d20+0|noform|text(10)` (`+0`)", + "`dice:1d2-2+2d3+5|noform|avg` (`1d2-2+2d3+5`) for regular dice rolls", + "`dice:1d6|noform|avg|text(1d6)` or `dice:2d6|noform|avg|text(2d6)` for multiple options;", + "`1d6 + [Number]` for input prompts)", + "with extended `dice:1d20+2|noform|avg|text(display text)` (`+2`) and `dice:1d20+2|noform|avg|text(display text)` (`+2`)", + "a special 'hit' version which assumes a d20 is to be rolled `dice:1d20+7|noform|text(+7)`", + "There's also `dice:1d12+3|noform|avg` (`1d12+3`) and `dice:1d20-4|noform|text(-4)`", + "scaledamage: `dice:1d6|noform|avg|text(1d6)`, scaledice: `dice:1d6|noform|avg|text(extra amount)` (`1d6`)", "Strength (5), Strength (`dice: d20+5|text(+5)`), and [Animal Handling](rules/skills.md#Animal%20Handling) (`dice: d20+5|text(+5)`)", - "with an Intelligence of `+3` (`16`), a Wisdom of `+0` (`10`), and a Charisma of `+4` (`18`)", - "`3 plus PB` to hit; *Hit:* `dice:1d6 + 4|text(7)` (`1d6 + 4`) piercing damage plus `dice:2d6|text(7)` (`2d6`) poison damage.", - "Perception (`dice: d20+2|text(+2)`) Perception (`dice: d20-2|text(-2)`)", - "+3 to hit", - "*Melee Weapon Attack:* `dice: d20+9` (`+9`) to hit, reach 5 ft., one target. *Hit:* `dice:1d8 + 5|text(9)` (`1d8 + 5`) piercing damage plus `dice:2d6|text(7)` (`2d6`) necrotic damage."); + "with an Intelligence of `dice:1d20+3|noform|text(+3)` (`16`), a Wisdom of `dice:1d20+0|noform|text(+0)` (`10`), and a Charisma of `dice:1d20+4|noform|text(+4)` (`18`)", + "`+3 plus PB` to hit; *Hit:* `dice:1d6+4|noform|avg|text(7)` (`1d6 + 4`) piercing damage plus `dice:2d6|noform|avg|text(7)` (`2d6`) poison damage.", + "Perception (`dice:1d20+2|noform|text(+2)`) Perception (`dice:1d20-2|noform|text(-2)`)", + "hit display text: +3 to hit", + "*Melee Weapon Attack:* `dice:1d20+9|noform|text(+9)` to hit, reach 5 ft., one target. *Hit:* `dice:1d8+5|noform|avg|text(9)` (`1d8 + 5`) piercing damage plus `dice:2d6|noform|avg|text(7)` (`2d6`) necrotic damage."); configurator.setUseDiceRoller(DiceRoller.enabled); for (int i = 0; i < example.size(); i++) { @@ -180,17 +183,18 @@ public void testDiceFormla() { List traits = List.of( "Spells cast from the spell gem have a save DC of 15 and an attack bonus of +9.", "It has a Strength of 26 (+8) and a Dexterity of 10 (+0)", - "1d2-2+2d3+5 for regular dice rolls, 1d6 for multiple options;", - "1d6 + [Number] for input prompts)", - "with extended display text and display text", + "1d2-2+2d3+5 for regular dice rolls", + "1d6 or 2d6 for multiple options;", + "1d6 + [Number] for input prompts)", + "with extended display text (+2) and display text (+2)", "a special 'hit' version which assumes a d20 is to be rolled +7", "There's also 1d12+3 and -4", - "`1d6` `1d6`", + "scaledamage: 1d6, scaledice: extra amount (1d6)", "Strength (5), Strength (+5), and [Animal Handling](rules/skills.md#Animal%20Handling) (+5)", "with an Intelligence of +3 (16), a Wisdom of +0 (10), and a Charisma of +4 (18)", - "3 plus PB to hit; *Hit:* 7 (1d6 + 4) piercing damage plus 7 (2d6) poison damage.", + "+3 plus PB to hit; *Hit:* 7 (1d6 + 4) piercing damage plus 7 (2d6) poison damage.", "Perception (+2) Perception (-2)", - "+3 to hit", + "hit display text: +3 to hit", "*Melee Weapon Attack:* +9 to hit, reach 5 ft., one target. *Hit:* 9 (1d8 + 5) piercing damage plus 7 (2d6) necrotic damage."); // Now we'll indicate that we're within a trait (for a statblock) @@ -210,9 +214,9 @@ public void testDiceFormla() { @Test void testSimplify() { - String example = " 7 (`dice: 2d6|avg|noform` (`2d6`))"; + String example = " 7 (`dice:2d6|avg|noform` (`2d6`))"; String result = this.simplifyFormattedDiceText(example); - assertThat(result).isEqualTo(" `dice:2d6|text(7)` (`2d6`)"); + assertThat(result).isEqualTo(" `dice:2d6|avg|noform|text(7)` (`2d6`)"); } @Override diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java b/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java index 3220d7833..b4bfd2b16 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java @@ -2,14 +2,16 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; - -import org.junit.jupiter.api.AfterEach; +import java.util.stream.Stream; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -41,7 +43,7 @@ enum TestInput { public CommonDataTests(TestInput variant) throws Exception { tui = Arc.container().instance(Tui.class).get(); - tui.init(null, true, false); + tui.init(null, !TestUtils.USING_MAVEN, true, true); templates = Arc.container().instance(Templates.class).get(); tui.setTemplates(templates); @@ -81,14 +83,22 @@ public CommonDataTests(TestInput variant) throws Exception { } } - @AfterEach - public void cleanup() { - tui.close(); - tui.setOutputPath(outputPath); + public void cleanup() throws Exception { configurator.setUseDiceRoller(DiceRoller.disabled); templates.setCustomTemplates(TtrpgConfig.getConfig()); } + public void done() throws IOException { + tui.close(); + Path logFile = Path.of("ttrpg-convert.out.txt"); + if (Files.exists(logFile)) { + Path newFile = outputPath.resolve(logFile); + ; + Files.move(logFile, newFile, StandardCopyOption.REPLACE_EXISTING); + } + System.out.println("Done."); + } + public void testDataIndex_pf2e() throws Exception { if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) { Path full = outputPath.resolve("allIndex.json"); @@ -125,7 +135,9 @@ public void testNotes_p2fe() throws Exception { if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeNotesAndTables() + .writeFiles(Stream.of(Pf2eIndexType.values()) + .filter(x -> x.isOutputType() && x.useQuteNote()) + .toList()) .writeImages(); TestUtils.assertDirectoryContents(rulesDir, tui); diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java index 18be21084..e402bbcfb 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java @@ -1,8 +1,10 @@ package dev.ebullient.convert.tools.pf2e; +import java.io.IOException; import java.util.List; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -21,8 +23,13 @@ public static void setupDir() throws Exception { } @AfterAll - public static void done() { - System.out.println("Done."); + public static void done() throws IOException { + commonTests.done(); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.cleanup(); } @Test diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java index 4e1138174..0efe5821e 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java @@ -1,8 +1,10 @@ package dev.ebullient.convert.tools.pf2e; +import java.io.IOException; import java.util.List; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -21,8 +23,13 @@ public static void setupDir() throws Exception { } @AfterAll - public static void done() { - System.out.println("Done."); + public static void done() throws IOException { + commonTests.done(); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.cleanup(); } @Test diff --git a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java index 16fa09391..261a11de5 100644 --- a/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java +++ b/src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java @@ -1,8 +1,10 @@ package dev.ebullient.convert.tools.pf2e; +import java.io.IOException; import java.util.List; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -21,8 +23,13 @@ public static void setupDir() throws Exception { } @AfterAll - public static void done() { - System.out.println("Done."); + public static void done() throws IOException { + commonTests.done(); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.cleanup(); } @Test diff --git a/src/test/resources/5e-sourceTypes.json b/src/test/resources/5e-sourceTypes.json new file mode 100644 index 000000000..bc6dc3e05 --- /dev/null +++ b/src/test/resources/5e-sourceTypes.json @@ -0,0 +1,145 @@ +{ + "adventure" : [ + "LMoP", + "HotDQ", + "RoT", + "PotA", + "OotA", + "CoS", + "SKT", + "TftYP-TSC", + "TftYP-TFoF", + "TftYP-THSoT", + "TftYP-WPM", + "TftYP-DiT", + "TftYP-AtG", + "TftYP-ToH", + "ToA", + "TTP", + "TLK", + "XMtS", + "WDH", + "LLK", + "WDMM", + "KKW", + "AZfyT", + "GoS", + "HftT", + "HWAitW", + "OoW", + "DIP", + "SLW", + "SDW", + "DC", + "BGDIA", + "LR", + "IMR", + "EFR", + "RMBRE", + "ToR", + "DD", + "FS", + "US", + "MOT-NSS", + "IDRotF", + "CM", + "HoL", + "RtG", + "AitFR-ISF", + "AitFR-THP", + "AitFR-AVT", + "AitFR-DN", + "AitFR-FCD", + "NRH-TCMC", + "NRH-AVitW", + "NRH-ASS", + "NRH-CoI", + "NRH-TLT", + "NRH-AWoL", + "NRH-AT", + "WBtW", + "SCC-CK", + "SCC-HfMT", + "SCC-TMM", + "SCC-ARiR", + "CRCotN", + "JttRC", + "DoSI", + "SjA", + "LoX", + "DSotDQ", + "KftGV", + "GotSF", + "PaBTSO", + "LK", + "ToFW", + "CoA", + "PiP", + "HFStCM", + "GHLoE", + "DoDk", + "DitLCoT", + "LRDT", + "VNotEE", + "VEoR", + "QftIS", + "UtHftLH", + "ScoEE" + ], + "book" : [ + "PHB", + "MM", + "DMG", + "Screen", + "SCAG", + "PS-Z", + "PS-I", + "AL", + "VGM", + "PS-K", + "PS-A", + "OGA", + "XGE", + "PS-X", + "MTF", + "PS-D", + "GGR", + "SAC", + "HWCS", + "AI", + "RMR", + "ERLW", + "EGW", + "MOT", + "ScreenDungeonKit", + "HF", + "ScreenWildernessKit", + "TCE", + "VRGR", + "DoD", + "MaBJoV", + "FTD", + "SCC", + "TDCSR", + "MPMM", + "TD", + "AAG", + "BAM", + "ScreenSpelljammer", + "HAT-TG", + "ToB1-2023", + "BGG", + "MCV4EC", + "MPP", + "SatO", + "AATM", + "HFFotM", + "BMT", + "DMTCRG", + "PaF", + "XPHB", + "XDMG", + "XScreen", + "XMM" + ] +} \ No newline at end of file diff --git a/src/test/resources/ermis-bg.json b/src/test/resources/5e/ermis-bg.json similarity index 100% rename from src/test/resources/ermis-bg.json rename to src/test/resources/5e/ermis-bg.json diff --git a/src/test/resources/images-from-local.json b/src/test/resources/5e/images-from-local.json similarity index 100% rename from src/test/resources/images-from-local.json rename to src/test/resources/5e/images-from-local.json diff --git a/src/test/resources/images-remote.json b/src/test/resources/5e/images-remote.json similarity index 100% rename from src/test/resources/images-remote.json rename to src/test/resources/5e/images-remote.json diff --git a/src/test/resources/psion.json b/src/test/resources/5e/psion.json similarity index 100% rename from src/test/resources/psion.json rename to src/test/resources/5e/psion.json diff --git a/src/test/resources/5e/sources-2014-srd.yaml b/src/test/resources/5e/sources-2014-srd.yaml new file mode 100644 index 000000000..fe8c325ac --- /dev/null +++ b/src/test/resources/5e/sources-2014-srd.yaml @@ -0,0 +1,4 @@ +sources: + reference: + - "srd" + - "basicrules" diff --git a/src/test/resources/5e/sources-2024-srd.yaml b/src/test/resources/5e/sources-2024-srd.yaml new file mode 100644 index 000000000..aa8585f0a --- /dev/null +++ b/src/test/resources/5e/sources-2024-srd.yaml @@ -0,0 +1,4 @@ +sources: + reference: + - "srd52" + - "freerules2024" diff --git a/src/test/resources/sources-book-adventure.json b/src/test/resources/5e/sources-book-adventure.json similarity index 100% rename from src/test/resources/sources-book-adventure.json rename to src/test/resources/5e/sources-book-adventure.json diff --git a/src/test/resources/sources-homebrew.json b/src/test/resources/5e/sources-homebrew.json similarity index 94% rename from src/test/resources/sources-homebrew.json rename to src/test/resources/5e/sources-homebrew.json index 613239df8..c51002bca 100644 --- a/src/test/resources/sources-homebrew.json +++ b/src/test/resources/5e/sources-homebrew.json @@ -7,8 +7,8 @@ "LMOP" ], "homebrew": [ - "src/test/resources/psion.json", - "src/test/resources/ermis-bg.json", + "src/test/resources/5e/psion.json", + "src/test/resources/5e/ermis-bg.json", "sources/5e-homebrew/adventure/Anthony Joyce; The Blood Hunter Adventure.json", "sources/5e-homebrew/adventure/JVC Parry; Call from the Deep.json", "sources/5e-homebrew/adventure/Kobold Press; Book of Lairs.json", @@ -38,7 +38,8 @@ "sources/5e-homebrew/creature/MCDM Productions; Flee, Mortals!.json", "sources/5e-homebrew/creature/Nerzugal Role-Playing; Nerzugal's Extended Bestiary.json", "sources/5e-homebrew/deity/Frog God Games; The Lost Lands.json", - "sources/5e-homebrew/collection/Loot Tavern; Heliana's Guide To Monster Hunting.json" + "sources/5e-homebrew/collection/Loot Tavern; Heliana's Guide To Monster Hunting.json", + "sources/5e-homebrew/race/Middle Finger of Vecna; Archon.json" ] }, "from": [ diff --git a/src/test/resources/5e/sources-images.yaml b/src/test/resources/5e/sources-images.yaml new file mode 100644 index 000000000..3dc9c7001 --- /dev/null +++ b/src/test/resources/5e/sources-images.yaml @@ -0,0 +1,43 @@ +--- +sources: + reference: + - ALL +include: + - classtype|artificer|tce + - classtype|barbarian|phb + - classtype|barbarian|xphb + - classtype|bard|phb + - classtype|bard|xphb + - classtype|cleric|phb + - classtype|cleric|xphb + - classtype|druid|phb + - classtype|druid|xphb + - classtype|expert sidekick|tce + - classtype|fighter|phb + - classtype|fighter|xphb + - classtype|monk|phb + - classtype|monk|xphb + - classtype|mystic|uathemysticclass + - classtype|paladin|phb + - classtype|paladin|xphb + - classtype|ranger|phb + - classtype|ranger|xphb + - classtype|rogue|phb + - classtype|rogue|xphb + - classtype|sorcerer|phb + - classtype|sorcerer|xphb + - classtype|spellcaster sidekick|tce + - classtype|warlock|phb + - classtype|warlock|xphb + - classtype|warrior sidekick|tce + - classtype|wizard|phb + - classtype|wizard|xphb +template: + background: "examples/templates/tools5e/images-background2md.txt" + item: "examples/templates/tools5e/images-item2md.txt" + monster: "examples/templates/tools5e/images-monster2md.txt" + object: "examples/templates/tools5e/images-object2md.txt" + race: "examples/templates/tools5e/images-race2md.txt" + spell: "examples/templates/tools5e/images-spell2md.txt" + vehicle: "examples/templates/tools5e/images-vehicle2md.txt" +useDiceRoller: true diff --git a/src/test/resources/sources-no-phb.yaml b/src/test/resources/5e/sources-no-phb.yaml similarity index 100% rename from src/test/resources/sources-no-phb.yaml rename to src/test/resources/5e/sources-no-phb.yaml diff --git a/src/test/resources/5e/sources-single.yaml b/src/test/resources/5e/sources-single.yaml new file mode 100644 index 000000000..36d4a9ef7 --- /dev/null +++ b/src/test/resources/5e/sources-single.yaml @@ -0,0 +1,3 @@ +sources: + book: + - ERLW diff --git a/src/test/resources/5e/sources-subset.json b/src/test/resources/5e/sources-subset.json new file mode 100644 index 000000000..ec4e7d9b0 --- /dev/null +++ b/src/test/resources/5e/sources-subset.json @@ -0,0 +1,5 @@ +{ + "from": [ + "PHB","DMG","XGE","SCAG" + ] +} diff --git a/src/test/resources/sources-templates.json b/src/test/resources/5e/sources-templates.json similarity index 100% rename from src/test/resources/sources-templates.json rename to src/test/resources/5e/sources-templates.json diff --git a/src/test/resources/sources-ua.json b/src/test/resources/5e/sources-ua.json similarity index 100% rename from src/test/resources/sources-ua.json rename to src/test/resources/5e/sources-ua.json diff --git a/src/test/resources/5e/sources.json b/src/test/resources/5e/sources.json new file mode 100644 index 000000000..a0057755a --- /dev/null +++ b/src/test/resources/5e/sources.json @@ -0,0 +1,24 @@ +{ + "sources": { + "book": [ + "PHB", + "DMG", + "XGE", + "TCE" + ], + "adventure": [ + "WbtW" + ] + }, + "include": [ + "race|changeling|mpmm" + ], + "exclude": [ + "monster\\|expert\\|dc", + "monster\\|expert\\|sdw", + "monster\\|expert\\|slw" + ], + "excludePattern": [ + "race\\|.*\\|dmg" + ] +} diff --git a/src/test/resources/sourcemap.txt b/src/test/resources/sourcemap.txt index 3b388d29d..ba4bdebd5 100644 --- a/src/test/resources/sourcemap.txt +++ b/src/test/resources/sourcemap.txt @@ -9,6 +9,9 @@ _Support content creators. Only use or include sources that you own._ ## Source name mapping for 5etools +- **2014** (sources/reference): "srd", "basicrules" +- **2024** (sources/reference): "srd52", "freerules2024" + ## Source name mapping for Pf2eTools diff --git a/src/test/resources/sources-images.yaml b/src/test/resources/sources-images.yaml deleted file mode 100644 index 6eec54780..000000000 --- a/src/test/resources/sources-images.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -from: -- "ALL" -template: - background: "examples/templates/tools5e/images-background2md.txt" - item: "examples/templates/tools5e/images-item2md.txt" - monster: "examples/templates/tools5e/images-monster2md.txt" - object: "examples/templates/tools5e/images-object2md.txt" - race: "examples/templates/tools5e/images-race2md.txt" - spell: "examples/templates/tools5e/images-spell2md.txt" - vehicle: "examples/templates/tools5e/images-vehicle2md.txt" -useDiceRoller: true diff --git a/src/test/resources/sources.json b/src/test/resources/sources.json deleted file mode 100644 index 5c41033df..000000000 --- a/src/test/resources/sources.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "from": [ - "PHB", - "DMG", - "XGE", - "TCE", - "WbtW" - ], - "include": [ - "race|changeling|mpmm" - ], - "exclude": [ - "monster|expert|dc", - "monster|expert|sdw", - "monster|expert|slw" - ], - "excludePattern": [ - "race|.*|dmg" - ] -} From 682ec19d93d83caa22a6d61cabfd3b6bbb6e6bb1 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Fri, 6 Dec 2024 10:49:32 -0500 Subject: [PATCH 057/119] =?UTF-8?q?=F0=9F=93=9D=20Add=20reprint=20behavior?= =?UTF-8?q?=20to=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/configuration.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index fa72d6a95..0f8e4ef1b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,13 +14,13 @@ This guide introduces you to configuring data transformations using the Command - [Source identifiers](#source-identifiers) - [Specify content with the `sources` key](#specify-content-with-the-sources-key) - [Homebrew](#homebrew) - - [Additional notes about homebrew](#additional-notes-about-homebrew) - [Reporting content errors to 5eTools](#reporting-content-errors-to-5etools) - [Specify target paths( `paths` key)](#specify-target-paths-paths-key) - [Refine content choices](#refine-content-choices) - [Excluding content matching an `excludePattern`](#excluding-content-matching-an-excludepattern) - [Excluding specific content with `exclude`](#excluding-specific-content-with-exclude) - [Including specific content with `include`](#including-specific-content-with-include) +- [Reprint behavior](#reprint-behavior) - [Use the dice roller plugin](#use-the-dice-roller-plugin) - [Render with Fantasy Statblocks](#render-with-fantasy-statblocks) - [Tag prefix](#tag-prefix) @@ -30,7 +30,7 @@ This guide introduces you to configuring data transformations using the Command - [Copying internal images](#copying-internal-images) - [Copying external images](#copying-external-images) - [Fallback paths](#fallback-paths) -- [🚚 Migrating `from`, `full-source`, and `convert`](#migrating-from-full-source-and-convert) +- [Migrating `from`, `full-source`, and `convert`](#migrating-from-full-source-and-convert) ## Overview @@ -116,6 +116,7 @@ Here's a more comprehensive `config.json` file. "include": [ "race|changeling|mpmm" ], + "reprintBehavior": "newest", "useDiceRoller": true, "tagPrefix": "ttrpg-cli", "template": { @@ -131,9 +132,10 @@ Additional capabilities: 2. **Define Vault Paths:** The [`path`](#specify-target-paths-paths-key) sets the vault path destination for `rules` content. 3. **Targeted exclusion:** [`excludePattern`](#excluding-content-matching-an-excludepattern) and [`exclude`](#excluding-specific-content-with-exclude) leaves out specific content. 4. **Targeted inclusion:** The [`include`](#including-specific-content-with-include) specifies content that is *always included*. -5. **Use the dice roller plugin:** The [`useDiceRoller`](#use-the-dice-roller-plugin) key enables the dice roller plugin. -6. **Tag prefix:** The [`tagPrefix`](#tag-prefix) key sets the prefix for tags generated by the CLI. -7. **Templates:** The [`template`](#templates) key specifies the templates to use for different types of content. +5. **[Reprint behavior](#reprint-behavior):** Only the latest/newest version of a resource should be emitted (this is the default). +6. **Use the dice roller plugin:** The [`useDiceRoller`](#use-the-dice-roller-plugin) key enables the dice roller plugin. +7. **Tag prefix:** The [`tagPrefix`](#tag-prefix) key sets the prefix for tags generated by the CLI. +8. **Templates:** The [`template`](#templates) key specifies the templates to use for different types of content. > [!WARNING] > **Windows Users**: Replace any `\` in your paths with '/' in your JSON and YAML files. @@ -152,6 +154,8 @@ If you're expecting to see content from a book or adventure and it's not showing ## Specify content with the `sources` key +> 🔥 Version 3.x or SNAPSHOT ONLY. If you're using a 2.x version of the CLI, use [the legacy version](#migrating-from-full-source-and-convert) + The CLI can emit content from a source in two ways: - "full text": notes for all content and reference data from your sources. @@ -343,6 +347,29 @@ Specify the data keys you want to include. This approach is ideal for content acquired in parts, like individual items from D&D Beyond. +## Reprint behavior + +> 🔥 Version 3.x or SNAPSHOT ONLY. + +Content is often reprinted or updated in later sources or editions. This setting lets you control how reprinted or revised content is handled when generating notes. + +``` json + "reprint": "newest" +``` + +This setting has 3 possible values: + +- **`newest`** (default): Only includes notes for the most recent version of reprinted content. +- **`edition`**: Focuses on consolidating content for compatible editions (especially for 5e rules). +Example: Combines 2014 and 2024 item updates, while preserving edition-specific class and subclass definitions. +- **`all`**: Includes notes for all reprinted versions from enabled sources + +Troubleshooting: + +- If the behavior isn’t what you expect, run with the --log option and check the log file. +The log will show whether a specific key was kept or dropped and explain why. +- To ensure a specific resource is included, add its key to the [`include` filter](#including-specific-content-with-include) instead of relying on reprint behavior. + ## Use the dice roller plugin The CLI can generate notes that include inline dice rolls. To enable this feature, set the `useDiceRoller` attribute to `true`. From 9a71b84dccd7634d6bb4d4f10111c36939b33c44 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Fri, 6 Dec 2024 21:08:58 -0500 Subject: [PATCH 058/119] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d93db43c..ab357f08d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ I use [Obsidian](https://obsidian.md) to keep track of my campaign notes. This p > > - 🚜 [**Review the changelog**](CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥). > - 🔮 Check out [**Conventions**](#conventions) and [**Recommendations**](#recommendations-for-using-the-cli). -> + + > [!WARNING] > The 5eTools data repositories have been taken down. This tool will still work to create Obsidian notes for data in this JSON format (homebrew, for example). From 528b97b6f828d65879e12452486ca9a5f2c90991 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Fri, 6 Dec 2024 21:11:12 -0500 Subject: [PATCH 059/119] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index ab357f08d..27858b315 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,7 @@ I use [Obsidian](https://obsidian.md) to keep track of my campaign notes. This p > > - 🚜 [**Review the changelog**](CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥). > - 🔮 Check out [**Conventions**](#conventions) and [**Recommendations**](#recommendations-for-using-the-cli). - - -> [!WARNING] -> The 5eTools data repositories have been taken down. This tool will still work to create Obsidian notes for data in this JSON format (homebrew, for example). +> - 🔥 Support for the 5e 2024 ruleset is [in progress](https://github.com/ebullient/ttrpg-convert-cli/discussions/586). ## Using the Command Line From 391ca79e5e1e3b0f73aee7b847ef32e23649b970 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:48:04 +0000 Subject: [PATCH 060/119] Bump github/codeql-action from 3.27.5 to 3.27.6 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.5 to 3.27.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f09c1c0a94de965c15400f5634aa42fac8fb8f88...aa578102511db1f4524ed59b8cc2bae4f6e88195) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9806746b4..ac5bcc0bf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 47889ba86..3f1db08d8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: sarif_file: results.sarif From f639fe72c4d78c8f840bec68f0ca1607c422dcca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:48:09 +0000 Subject: [PATCH 061/119] Bump actions/cache from 4.1.2 to 4.2.0 Bumps [actions/cache](https://github.com/actions/cache) from 4.1.2 to 4.2.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/6849a6489940f00c2f30c0fb92c6274307ccb58a...1bd1e32a3bdc45362d1e726936510720a7c30a57) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 6 +++--- .github/workflows/pull-request.yml | 8 ++++---- .github/workflows/tools-data.yml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index a7509a2fa..615d991e9 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -39,7 +39,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: sources/Pf2eTools key: ${{ steps.test-data-key.outputs.cache_key }} @@ -75,7 +75,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: sources/Pf2eTools @@ -109,7 +109,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e947045ec..eb59b5a16 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: tools5e-cache with: path: sources @@ -41,7 +41,7 @@ jobs: Data-5etools- Data-5etools - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: pf2e-cache with: path: sources/Pf2eTools @@ -77,7 +77,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: tools5e-cache with: path: sources @@ -87,7 +87,7 @@ jobs: Data-5etools enableCrossOsArchive: true - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: sources/Pf2eTools diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 7880e18c3..ce159acd5 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -37,7 +37,7 @@ jobs: - name: Check Cache Data id: test-data-check - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: sources key: ${{ steps.test-data-key.outputs.cache_key }} @@ -84,7 +84,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: sources @@ -121,7 +121,7 @@ jobs: with: fetch-depth: 1 - - uses: actions/cache/restore@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: sources From 0c5a2b7390caf507d6999059e8f57f6539026eb9 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 9 Dec 2024 17:34:25 -0500 Subject: [PATCH 062/119] =?UTF-8?q?=F0=9F=90=9B=20include=20sourceMap=20in?= =?UTF-8?q?=20native=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3178b003..61f9f01f4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,6 +7,6 @@ quarkus.test.continuous-testing=enabled # at runtime to conserve some memory quarkus.arc.detect-unused-false-positives=false -quarkus.native.resources.includes=*.json,*.svg,*.properties,*.txt +quarkus.native.resources.includes=*.json,*.yaml,*.svg,*.properties,*.txt quarkus.native.additional-build-args=--enable-url-protocols=https,-H:Log=registerResource:3 From a931ecd13841f3aafef6efd4dfe7eba937fe9305 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 9 Dec 2024 20:25:39 -0500 Subject: [PATCH 063/119] =?UTF-8?q?=F0=9F=90=9B=20register=20config=20for?= =?UTF-8?q?=20reflection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/dev/ebullient/convert/config/CompendiumConfig.java | 2 ++ .../dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java b/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java index f709974a8..b923ed150 100644 --- a/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java +++ b/src/main/java/dev/ebullient/convert/config/CompendiumConfig.java @@ -25,7 +25,9 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.ParseState; +import io.quarkus.runtime.annotations.RegisterForReflection; +@RegisterForReflection public class CompendiumConfig { public enum DiceRoller { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java index 654c19d34..193f8384b 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java @@ -17,7 +17,9 @@ import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewMetaTypes; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; +import io.quarkus.runtime.annotations.RegisterForReflection; +@RegisterForReflection public class OptionalFeatureIndex implements JsonSource { private final Map optFeatureIndex = new HashMap<>(); private final Tools5eIndex index; From f55a4cae096bf8169f5fbd1e005a93d8c17a2961 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 9 Dec 2024 20:51:19 -0500 Subject: [PATCH 064/119] =?UTF-8?q?=F0=9F=91=B7=20re-enable=20tools=20buil?= =?UTF-8?q?ds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tools-data.yml | 40 +++++++++++--------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index ce159acd5..914b9ca0a 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -8,7 +8,6 @@ on: env: JAVA_VERSION: 17 - NATIVE_VERSION: 22.3.2 GRAALVM_DIST: graalvm-community JAVA_DISTRO: temurin FAIL_ISSUE: 140 @@ -29,7 +28,8 @@ jobs: - name: Tools release cache key id: test-data-key run: | - LATEST_VERSION="v1.209.3" + LATEST_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/5etools-mirror-3/5etools-src/releases/latest | jq -r .tag_name) + echo $LATEST_VERSION echo "🔹 Use $LATEST_VERSION" echo "tools_version=${LATEST_VERSION}" >> $GITHUB_OUTPUT @@ -53,23 +53,17 @@ jobs: run: | mkdir -p sources - # echo "🔹 Download $LATEST_VERSION" - # ARTIFACT_URL="https://github.com/5etools-mirror-2/5etools-mirror-2.github.io/archive/refs/tags/$LATEST_VERSION.tar.gz" - # VER=$(echo $LATEST_VERSION | cut -c 2-) - # ROOT="5etools-mirror-2.github.io-$VER" + echo "🔹 Download $LATEST_VERSION" - # curl -LsS -o 5etools.tar.gz $ARTIFACT_URL - # tar xzf 5etools.tar.gz ${ROOT}/data - # mv ${ROOT} sources/5etools-mirror-2.github.io + gh repo clone 5etools-mirror-3/5etools-src sources/5etools-src -- --depth=1 -c advice.detachedHead=false -b $LATEST_VERSION + gh repo clone 5etools-mirror-3/5etools-img sources/5etools-img -- --depth=1 -c advice.detachedHead=false -b $LATEST_VERSION + gh repo clone TheGiddyLimit/unearthed-arcana sources/5e-unearthed-arcana -- --depth=1 + gh repo clone TheGiddyLimit/homebrew sources/5e-homebrew -- --depth=1 - # gh repo clone 5etools-mirror-2/5etools-img sources/5etools-img -- --depth=1 - # gh repo clone TheGiddyLimit/unearthed-arcana sources/5e-unearthed-arcana -- --depth=1 - # gh repo clone TheGiddyLimit/homebrew sources/5e-homebrew -- --depth=1 - - # # Remove image contents. We just need the files to exist (linking) - # find sources -type f -type f \ - # \( -iname \*.jpg -o -iname \*.png -o -iname \*.webp \) \ - # | while read FILE; do echo > "$FILE"; done + # Remove image contents. We just need the files to exist (linking) + find sources -type f -type f \ + \( -iname \*.jpg -o -iname \*.png -o -iname \*.webp \) \ + | while read FILE; do echo > "$FILE"; done ls -al sources @@ -134,24 +128,18 @@ jobs: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.JAVA_VERSION }} github-token: ${{ secrets.GITHUB_TOKEN }} - version: ${{ env.NATIVE_VERSION }} cache: 'maven' - - name: Build and run - id: mvn-build - run: | - ./mvnw -B -ntp -DskipFormat -DargLine="-Xmx6g" verify - - if: runner.os == 'Windows' name: clean before native build shell: cmd run: | - ./mvnw -B -ntp -DskipFormat clean + ./mvnw -B -ntp -DskipTests -DskipFormat clean - - name: Build and run in native mode + - name: Build, run, and test in native mode id: mvn-native-build run: | - ./mvnw -B -ntp -Dnative -DskipTests -DskipFormat verify + ./mvnw -B -ntp -Dnative -DskipFormat verify report-native-build: From 7650c2785f05051fa64d95ec1f49d664ce4c2805 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 9 Dec 2024 21:39:00 -0500 Subject: [PATCH 065/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20UTF=5F8=20Codeset?= =?UTF-8?q?=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/dev/ebullient/convert/io/Tui.java | 11 +++++++--- .../dev/ebullient/convert/qute/ImageRef.java | 21 +++++-------------- .../convert/Pf2eDataConvertTest.java | 4 ++-- .../convert/Tools5eDataConvertTest.java | 5 +++-- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/io/Tui.java b/src/main/java/dev/ebullient/convert/io/Tui.java index f41e75b0a..09bcf75dc 100644 --- a/src/main/java/dev/ebullient/convert/io/Tui.java +++ b/src/main/java/dev/ebullient/convert/io/Tui.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.io; import java.io.BufferedInputStream; +import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -11,11 +12,12 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -83,7 +85,7 @@ public static Tui instance() { }; public final static PrintWriter streamToWriter(PrintStream stream) { - return new PrintWriter(stream, true, Charset.forName("UTF-8")); + return new PrintWriter(stream, true, StandardCharsets.UTF_8); } public final static ObjectMapper MAPPER = initMapper(JsonMapper.builder() @@ -229,7 +231,10 @@ public void init(CommandSpec spec, boolean debug, boolean verbose, boolean log) if (log) { Path p = Path.of("ttrpg-convert.out.txt"); try { - this.log = new PrintWriter(Files.newOutputStream(p)); + BufferedWriter writer = Files.newBufferedWriter(p, StandardCharsets.UTF_8, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + this.log = new PrintWriter(writer, true); + VersionProvider vp = new VersionProvider(); List.of(vp.getVersion()).forEach(this.log::println); } catch (IOException e) { diff --git a/src/main/java/dev/ebullient/convert/qute/ImageRef.java b/src/main/java/dev/ebullient/convert/qute/ImageRef.java index 400b5be9d..0bb443b84 100644 --- a/src/main/java/dev/ebullient/convert/qute/ImageRef.java +++ b/src/main/java/dev/ebullient/convert/qute/ImageRef.java @@ -1,7 +1,6 @@ package dev.ebullient.convert.qute; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -215,12 +214,7 @@ public ImageRef build() { sourceUrl = imageRoot.getFallbackPath(sourceUrl) .replace('\\', '/'); - try { - // Remove escaped characters here (inconsistent escaping in the source URL) - sourceUrl = java.net.URLDecoder.decode(sourceUrl, StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - Tui.instance().errorf("Error decoding image URL %s: %s", sourceUrl, e.getMessage()); - } + sourceUrl = java.net.URLDecoder.decode(sourceUrl, StandardCharsets.UTF_8); boolean copyToVault = false; @@ -277,7 +271,7 @@ public static String escapeUrlImagePath(String url) { StringBuilder encodedPath = new StringBuilder(); for (char ch : path.toCharArray()) { if (allowedCharacters.indexOf(ch) == -1) { - byte[] bytes = String.valueOf(ch).getBytes("UTF-8"); + byte[] bytes = String.valueOf(ch).getBytes(StandardCharsets.UTF_8); for (byte b : bytes) { encodedPath.append(String.format("%%%02X", b)); } @@ -294,14 +288,9 @@ public static String escapeUrlImagePath(String url) { } public static final String fixUrl(String sourceUrl) { - try { - // Remove escaped characters here (inconsistent escaping in the source URL) - sourceUrl = java.net.URLDecoder.decode(sourceUrl, StandardCharsets.UTF_8.name()); - return escapeUrlImagePath(sourceUrl); - } catch (UnsupportedEncodingException e) { - Tui.instance().errorf("Error fixing URL %s: %s", sourceUrl, e.getMessage()); - return sourceUrl; - } + // Remove escaped characters here (inconsistent escaping in the source URL) + sourceUrl = java.net.URLDecoder.decode(sourceUrl, StandardCharsets.UTF_8); + return escapeUrlImagePath(sourceUrl); } } } diff --git a/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java b/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java index 882db629f..049f8dbbc 100644 --- a/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java +++ b/src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -59,7 +59,7 @@ public void clear() throws IOException { Path filePath = testOutput.resolve(logFile); Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING); - String content = Files.readString(filePath, Charset.forName("UTF-8")); + String content = Files.readString(filePath, StandardCharsets.UTF_8); if (content.contains("Exception")) { tui.errorf("Exception found in %s", filePath); } diff --git a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java index d798597af..26b5ca0ad 100644 --- a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java +++ b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -66,10 +66,11 @@ public void clear() throws IOException { Path logFile = Path.of("ttrpg-convert.out.txt"); if (Files.exists(logFile)) { + String content = Files.readString(logFile, StandardCharsets.UTF_8); + Path filePath = testOutput.resolve(logFile); Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING); - String content = Files.readString(filePath, Charset.forName("UTF-8")); if (content.contains("Exception")) { tui.errorf("Exception found in %s", filePath); } From a2aac62987b8073601955ab4a24d775507efe7fd Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 11 Dec 2024 19:18:40 -0500 Subject: [PATCH 066/119] Update and rename bug_report.md to bug_report.yaml change bug template --- .github/ISSUE_TEMPLATE/bug_report.md | 30 ----------- .github/ISSUE_TEMPLATE/bug_report.yaml | 70 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 30 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 8383a3cda..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Bug report -about: 'Something doesn''t work or look right... ' -title: "\U0001F41B " -labels: bug -assignees: '' - ---- - - -A clear and concise summary of what the bug is. Include the name/source of the badly rendered note, if applicable. - - -A clear and concise description of what you expected to happen. A text snippet is helpful. - - -A clear and concise description of what actually happened. A text snippet is helpful. - -## Configuration - - -- **OS**: Windows, Linux (distro), MacOS, MacOS M1 -- **CLI version**: output using the `--version` command -- **CLI type**: jar (java or jbang) or native command - -**Sources**: -- enabled sources -- custom templates (attach if possible) - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..6f26bce46 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,70 @@ +name: Bug Report +description: File a bug report +title: "🐛 " +labels: ["type: bug"] +body: + - type: markdown + attributes: + value: | + ## Before you start + + - [Check for updates](https://github.com/ebullient/ttrpg-convert-cli/releases) and make sure you're running the latest version. + - Look at existing bug reports to see if your issue has already been reported. + - Is this actually a bug? + - [Get Help](https://github.com/ebullient/ttrpg-convert-cli/tree/main#where-to-find-help) if you aren't confident you're doing things right. + - If this is something that you wish the tool could do, start a discussion or create a feature request instead. + + > [!TIP] + > + > - 🚜 [**Review the changelog**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥). + > - 🔮 Check out [**Conventions**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/README.md#conventions) and [**Recommendations**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/README.md#recommendations-for-using-the-cli). + > - 🔥 Support for the 5e 2024 ruleset is [in progress](https://github.com/ebullient/ttrpg-convert-cli/discussions/586). + > Messages you may see related to XPHB, XMM, XDMG, or HP formulas are all related to this change + + - type: textarea + id: the-problem + attributes: + label: Describe the bug + description: | + A clear and concise summary of what the bug is. + + Include an example and/or specific details about the resource in question. + validations: + required: true + + - type: markdown + attributes: + value: | + Please provide a log file. + + Run the command that exibits this bug as you usually would, but add the `--log` option. + [Attach the `ttrpg-convert.out.txt` file](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files) that is created as a result. + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen instead? + validations: + required: true + + - type: input + id: cli-version + attributes: + label: TTRPG CLI Version + description: Which version are you using? (paste the output of [`--version`](https://github.com/ebullient/ttrpg-convert-cli/tree/7650c2785f05051fa64d95ec1f49d664ce4c2805#convert-5etools-json-data)) + placeholder: 2.3.18 + validations: + required: true + + - type: checkboxes + id: operating-systems + attributes: + label: Which Operating Systems have you experienced this on? + description: You may select more than one. + options: + - label: Android + - label: iPhone/iPad + - label: Linux + - label: macOS + - label: Windows From 9f32e40a4d6c4e4e02966a6072098984ac2ce24b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:28:54 +0000 Subject: [PATCH 067/119] Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.11.1 to 3.11.2 Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.11.1 to 3.11.2. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.11.1...maven-javadoc-plugin-3.11.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a3eefe489..12eb67ffd 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 17 2.24.1 1.12.0 - 3.11.1 + 3.11.2 UTF-8 UTF-8 3.5.2 From 2c4289265315b933b08fdbef10e415b7346c4977 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 00:30:24 +0000 Subject: [PATCH 068/119] Bump quarkus.platform.version from 3.16.2 to 3.17.4 Bumps `quarkus.platform.version` from 3.16.2 to 3.17.4. Updates `io.quarkus.platform:quarkus-bom` from 3.16.2 to 3.17.4 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.16.2...3.17.4) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.16.2 to 3.17.4 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.16.2...3.17.4) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 12eb67ffd..6129c2a5c 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.16.2 + 3.17.4 3.26.3 3.4.0 From 18f256c341a41bbf6dd56e7b74226ac3d3e8cde9 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 11 Dec 2024 21:10:52 -0500 Subject: [PATCH 069/119] bump minimum native version --- .github/workflows/tools-data.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 914b9ca0a..550cfe9cb 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -8,8 +8,9 @@ on: env: JAVA_VERSION: 17 - GRAALVM_DIST: graalvm-community JAVA_DISTRO: temurin + NATIVE_JAVA_VERSION: 21 + GRAALVM_DIST: graalvm-community FAIL_ISSUE: 140 permissions: read-all @@ -126,7 +127,7 @@ jobs: - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 with: distribution: ${{ env.GRAALVM_DIST }} - java-version: ${{ env.JAVA_VERSION }} + java-version: ${{ env.NATIVE_JAVA_VERSION }} github-token: ${{ secrets.GITHUB_TOKEN }} cache: 'maven' From cbc66445792e1b64accc21c344e8c7d2aee7f5ad Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 12 Dec 2024 17:05:35 -0500 Subject: [PATCH 070/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20missing=20source?= =?UTF-8?q?=20text;=20magic=20variant=20fluff=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/ebullient/convert/tools/ToolsIndex.java | 3 ++- .../convert/tools/dnd5e/Json2QuteCommon.java | 7 ++++++- .../convert/tools/dnd5e/MagicVariant.java | 8 ++++++++ .../convert/tools/dnd5e/Tools5eIndex.java | 4 ++++ .../convert/tools/dnd5e/Tools5eSources.java | 16 ++++++++++++++-- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java index b5e3af88e..4f02f1278 100644 --- a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java @@ -19,7 +19,8 @@ enum TtrpgValue implements JsonNodeReader { indexKey, indexInputType, indexBaseItem, - isHomebrew; + isHomebrew, + indexFluffKey, } static ToolsIndex createIndex() { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java index f8107c95e..3e0bcf316 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java @@ -28,6 +28,7 @@ import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; import dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterFields; import dev.ebullient.convert.tools.dnd5e.qute.AbilityScores; import dev.ebullient.convert.tools.dnd5e.qute.AcHp; @@ -101,7 +102,11 @@ public String getFluffDescription(Tools5eIndexType fluffType, String heading, Li public List getFluff(Tools5eIndexType fluffType, String heading, List images) { List text = new ArrayList<>(); JsonNode fluffNode = null; - if (Tools5eFields.fluff.existsIn(rootNode)) { + if (TtrpgValue.indexFluffKey.existsIn(rootNode)) { + // Specific variant + String fluffKey = TtrpgValue.indexFluffKey.getTextOrEmpty(rootNode); + fluffNode = index.getOrigin(fluffKey); + } else if (Tools5eFields.fluff.existsIn(rootNode)) { fluffNode = Tools5eFields.fluff.getFrom(rootNode); JsonNode monsterFluff = Tools5eFields._monsterFluff.getFrom(fluffNode); if (monsterFluff != null) { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java index 922f2ea73..8d73d176d 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java @@ -140,6 +140,11 @@ private List findVariants(Tools5eIndex index, Tools5eIndexType type, index.addAlias(key, gvKey); } + String fluffKey = ItemField.hasFluff.booleanOrDefault(genericVariant, false) + || ItemField.hasFluffImages.booleanOrDefault(genericVariant, false) + ? Tools5eIndexType.itemFluff.createKey(genericVariant) + : null; + for (JsonNode baseItem : baseItems) { if (ItemField.packContents.existsIn(baseItem) || !editionMatch(baseItem, genericVariant) @@ -152,6 +157,9 @@ private List findVariants(Tools5eIndex index, Tools5eIndexType type, String newKey = Tools5eIndexType.item.createKey(specficVariant); TtrpgValue.indexInputType.setIn(specficVariant, Tools5eIndexType.item.name()); TtrpgValue.indexKey.setIn(specficVariant, newKey); + if (fluffKey != null) { + TtrpgValue.indexFluffKey.setIn(specficVariant, fluffKey); + } Tools5eSources.constructSources(newKey, specficVariant); if (spawnNewItems) { variants.add(new Tuple(newKey, specficVariant)); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 4c86aa003..cd6128757 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -890,6 +890,10 @@ public JsonNode getOrigin(String finalKey) { } } } + if (result == null) { + tui().debugf(Msg.UNRESOLVED, "No element found for %s", + finalKey); + } return result; } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java index e2d9dca84..c9c861cb4 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java @@ -16,6 +16,7 @@ import dev.ebullient.convert.config.CompendiumConfig; import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.FontRef; +import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.QuteBase; @@ -289,11 +290,15 @@ public String getSourceText(boolean useSrd) { } return String.join(" and ", bits); } - return sourceText; + return super.getSourceText(); } public JsonNode findNode() { - return Tools5eIndex.getInstance().getOrigin(this.key); + JsonNode result = Tools5eIndex.getInstance().getNode(key); + if (result == null) { + result = Tools5eIndex.getInstance().getOrigin(this.key); + } + return result; } protected String findName(IndexType type, JsonNode jsonElement) { @@ -311,6 +316,13 @@ protected String findSourceText(IndexType type, JsonNode jsonElement) { if (type == Tools5eIndexType.syntheticGroup) { return this.key.replaceAll(".*\\|([^|]+)\\|", "$1"); } + if (type == Tools5eIndexType.reference) { + return ""; + } + if (jsonElement == null) { + Tui.instance().logf(Msg.UNRESOLVED, "Resource %s has no jsonElement", this.key); + return ""; + } String srcText = super.findSourceText(type, jsonElement); JsonNode basicRules = jsonElement.get("basicRules"); From 9dbe2339c91b6146251ad16388166b0effb3753a Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 12 Dec 2024 19:01:43 -0500 Subject: [PATCH 071/119] =?UTF-8?q?=F0=9F=90=9B=20resolve=20magic/monster?= =?UTF-8?q?=20variants=20before=20reprints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../convert/tools/dnd5e/Tools5eIndex.java | 43 ++++++++++--------- src/main/resources/convertData.json | 1 + 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index cd6128757..41c8dbf5c 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -301,6 +301,14 @@ public void prepare() { tui().progressf("Resolving copies and link sources"); + // Find remaining/included base items + List baseItems = nodeIndex.values().stream() + .filter(n -> TtrpgValue.indexBaseItem.booleanOrDefault(n, false)) + .filter(n -> !ItemField.packContents.existsIn(n)) + .toList(); + + Map variants = new HashMap<>(); + // For each node: handle copies, link sources for (Entry entry : nodeIndex.entrySet()) { String key = entry.getKey(); @@ -344,8 +352,20 @@ public void prepare() { default -> { } } + + // Reprints do follow specialized variants, so we need to find the variants + // now (and will filter them out based on rules later...) + if (type.hasVariants()) { + List variantList = findVariants(key, jsonSource, baseItems); + for (Tuple variant : variantList) { + variants.put(variant.key, variant.node); + } + } } // end for each entry + nodeIndex.putAll(variants); + variants.clear(); + filteredIndex = new HashMap<>(nodeIndex.size()); tui().progressf("Applying source filters"); @@ -362,7 +382,8 @@ public void prepare() { for (var e : nodeIndex.entrySet()) { String key = e.getKey(); Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); - Tools5eSources sources = Tools5eSources.findSources(key); + // construct source if missing (which it may be for a variant) + Tools5eSources sources = Tools5eSources.constructSources(key, e.getValue()); Msg msgType = sources.filterRuleApplied() ? Msg.TARGET : Msg.FILTER; if (type.isFluffType()) { @@ -387,26 +408,6 @@ public void prepare() { tui().progressf("Removing dependent and dangling resources"); filteredIndex.keySet().removeIf(k -> otherwiseExcluded(k)); - // After we've removed reprints and otherwise excluded items, - // let's generate variants for monsters and magic items - - tui().progressf("Populating variants"); - - // Find remaining/included base items - List baseItems = filteredIndex.values().stream() - .filter(n -> TtrpgValue.indexBaseItem.booleanOrDefault(n, false)) - .filter(n -> !ItemField.packContents.existsIn(n)) - .toList(); - - // Find variant nodes (magic items, monsters) - List variantNodes = filteredIndex.entrySet().stream() - .filter(e -> Tools5eIndexType.getTypeFromKey(e.getKey()).hasVariants()) - .flatMap(e -> findVariants(e.getKey(), e.getValue(), baseItems).stream()) - .toList(); - - // Add the variants back into the index, which may replace the original - variantNodes.forEach(t -> filteredIndex.put(t.key, t.node)); - // Deities have their own glorious reprint mess, which we only need to deal with // when we aren't hoarding all the things. if (config.reprintBehavior() != ReprintBehavior.all) { diff --git a/src/main/resources/convertData.json b/src/main/resources/convertData.json index eada461ba..fb68a2841 100644 --- a/src/main/resources/convertData.json +++ b/src/main/resources/convertData.json @@ -580,6 +580,7 @@ "item|shield, +1|dmg": "item|+1 shield|dmg", "item|thieves tools|phb": "item|thieves' tools|phb", "item|wands of magic missiles|dmg": "item|wand of magic missiles|dmg", + "itemgroup|spell scroll|xphb": "itemgroup|spell scroll|xdmg", "spell|acid arrow|phb": "spell|melf's acid arrow|phb", "spell|bane spell|phb": "spell|bane|phb", "spell|ceremony|phb": "spell|ceremony|xge", From 6cf0059784bbb4ba25ebe1155d9b70fc2579973c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:33:44 +0000 Subject: [PATCH 072/119] Bump github/codeql-action from 3.27.6 to 3.27.9 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.6 to 3.27.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/aa578102511db1f4524ed59b8cc2bae4f6e88195...df409f7d9260372bd5f19e5b04e83cb3c43714ae) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ac5bcc0bf..b091ca51f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 + uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 + uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3f1db08d8..4901e7ffc 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 + uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 with: sarif_file: results.sarif From 5d92921e38bb147d14c3e4e5bc6b60ddac65893d Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Tue, 17 Dec 2024 15:17:41 -0500 Subject: [PATCH 073/119] =?UTF-8?q?=F0=9F=90=9B=20fix=20monster=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use versions of monsters from source data - remove notes per conjured spell level - look for the most specific version key for determining reprints --- .../convert/tools/JsonNodeReader.java | 21 +- .../convert/tools/JsonSourceCopier.java | 29 +- .../ebullient/convert/tools/ToolsIndex.java | 8 +- .../convert/tools/dnd5e/Json2QuteMonster.java | 340 ++++++------------ .../convert/tools/dnd5e/MagicVariant.java | 17 +- .../convert/tools/dnd5e/Tools5eIndex.java | 24 +- .../tools/dnd5e/Tools5eJsonSourceCopier.java | 31 +- .../tools/dnd5e/FilterAllNewestTest.java | 7 +- .../convert/tools/dnd5e/FilterAllTest.java | 5 +- .../tools/dnd5e/FilterNoneEditionTest.java | 5 +- .../convert/tools/dnd5e/FilterNoneTest.java | 5 +- .../tools/dnd5e/FilterSrd2014Test.java | 5 +- .../tools/dnd5e/FilterSrd2024Test.java | 5 +- .../tools/dnd5e/FilterSubset2014Test.java | 5 +- .../tools/dnd5e/FilterSubset2024Test.java | 5 +- 15 files changed, 217 insertions(+), 295 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java index 09264d58a..f258dfdf0 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java @@ -453,12 +453,16 @@ default ArrayNode ensureArrayIn(JsonNode target) { return target.withArray(this.nodeName()); } + default JsonNode copyFrom(JsonNode source) { + return getFrom(source).deepCopy(); + } + /** Destructive! */ - default void removeFrom(JsonNode target) { + default JsonNode removeFrom(JsonNode target) { if (target == null) { - return; + return null; } - ((ObjectNode) target).remove(this.nodeName()); + return ((ObjectNode) target).remove(this.nodeName()); } /** Destructive! */ @@ -497,4 +501,15 @@ default void link(JsonNode source, JsonNode target) { } ((ObjectNode) target).set(this.nodeName(), getFrom(source)); } + + /** Destructive! */ + default void moveFrom(JsonNode source, JsonNode target) { + if (source == null || target == null) { + return; + } + JsonNode value = removeFrom(source); + if (value != null) { + ((ObjectNode) target).set(this.nodeName(), value); + } + } } diff --git a/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java b/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java index 6af175aee..3edbfaf01 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java @@ -85,17 +85,17 @@ public JsonNode handleCopy(T type, JsonNode copyTo) { * @param _copy Node containing metadata about the copy */ protected void copyValues(T type, JsonNode copyFrom, ObjectNode copyTo, JsonNode _copy) { - JsonNode _preserve = MetaFields._preserve.getFromOrEmptyObjectNode(_copy); + JsonNode _preserve = MetaFields._preserve.getFrom(_copy); // null is ok // Copy required values from... for (Entry from : iterableFields(copyFrom)) { String k = from.getKey(); JsonNode copyToField = copyTo.get(k); if (copyToField != null && copyToField.isNull()) { - // copyToField exists as `null`. Remove the field. + // copyToField is present / exists as an intentional `null`. Remove the field. copyTo.remove(k); continue; } - if (copyToField == null) { + if (copyToField == null) { // undefined // not already present in copyTo -- should we copyFrom? // Do merge rules indicate the value should be preserved if (mergePreserveKey(type, k)) { @@ -166,7 +166,7 @@ protected void doModProp( case setProps -> doSetProps(originKey, modInfo, prop, target); case prefixSuffixStringProp -> doPrefixSuffixStringProp(originKey, modInfo, prop, target); // Arrays - case prependArr, appendArr, replaceArr, replaceOrAppendArr, appendIfNotExistsArr, insertArr, removeArr -> + case prependArr, appendArr, renameArr, replaceArr, replaceOrAppendArr, appendIfNotExistsArr, insertArr, removeArr -> doModArray(originKey, mode, modInfo, prop, target); // MATH case scalarAddProp -> doScalarAddProp(originKey, modInfo, prop, target); @@ -444,7 +444,7 @@ private void doModArray(String originKey, ModFieldMode mode, JsonNode modInfo, S String propPath = nodePath(prop); switch (mode) { - case insertArr, removeArr, replaceArr -> { + case insertArr, removeArr, renameArr, replaceArr -> { if (target.at(propPath).isMissingNode()) { tui().errorf("Error (%s): Unable to %s; %s is not present: %s", originKey, mode, prop, target); return; @@ -464,6 +464,7 @@ private void doModArray(String originKey, ModFieldMode mode, JsonNode modInfo, S MetaFields.index.intFrom(modInfo).filter(n -> n >= 0).orElse(targetArray.size()), items); case removeArr -> removeFromArray(originKey, modInfo, prop, targetArray); + case renameArr -> renameInArray(originKey, modInfo, targetArray); case replaceArr -> replaceArray(originKey, modInfo, targetArray, items); case replaceOrAppendArr -> { boolean didReplace = !targetArray.isEmpty() && replaceArray(originKey, modInfo, targetArray, items); @@ -557,6 +558,21 @@ public void removeFromArr(ArrayNode tgtArray, JsonNode items) { } } + protected void renameInArray(String originKey, JsonNode modInfo, ArrayNode tgtArray) { + JsonNode renames = ensureArray(MetaFields.renames.getFrom(modInfo)); + if (renames == null || !renames.isArray()) { + return; + } + + for (JsonNode renameNode : iterableElements(renames)) { + int index = findIndexByName(originKey, tgtArray, MetaFields.rename.getTextOrEmpty(renameNode)); + if (index >= 0) { + JsonNode element = tgtArray.get(index); + SourceField.name.setIn(element, MetaFields.with.getFrom(renameNode)); + } + } + } + protected boolean replaceArray(String originKey, JsonNode modInfo, ArrayNode tgtArray, JsonNode items) { if (items == null || !items.isArray()) { return false; @@ -657,6 +673,8 @@ public enum MetaFields implements JsonNodeReader { props, range, regex, + rename, + renames, replace, root, scalar, @@ -696,6 +714,7 @@ public enum ModFieldMode implements JsonNodeReader.FieldValue { prependArr, appendArr, + renameArr, replaceArr, replaceOrAppendArr, appendIfNotExistsArr, diff --git a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java index 4f02f1278..6e284877f 100644 --- a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java @@ -16,11 +16,13 @@ public interface ToolsIndex { // Special one-offs for accounting/tracking enum TtrpgValue implements JsonNodeReader { - indexKey, - indexInputType, indexBaseItem, - isHomebrew, indexFluffKey, + indexInputType, + indexKey, + indexParentKey, + indexVersionKeys, + isHomebrew, } static ToolsIndex createIndex() { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java index a05ef5bbe..1203dfc0d 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java @@ -3,33 +3,30 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.JsonSourceCopier.MetaFields; import dev.ebullient.convert.tools.Tags; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.Tuple; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; import dev.ebullient.convert.tools.dnd5e.qute.AcHp; import dev.ebullient.convert.tools.dnd5e.qute.QuteMonster; import dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SavesAndSkills; import dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Spellcasting; import dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Spells; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; -import io.quarkus.runtime.annotations.RegisterForReflection; public class Json2QuteMonster extends Json2QuteCommon { public static boolean isNpc(JsonNode source) { @@ -382,269 +379,146 @@ public String getImagePath() { return Tools5eQuteBase.monsterPath(isNpc, creatureType); } - public static List findMonsterVariants( + public static List findMonsterVariants( Tools5eIndex index, Tools5eIndexType type, String key, JsonNode jsonSource) { - if (MonsterFields.summonedBySpellLevel.existsIn(jsonSource)) { - return findConjuredMonsterVariants(index, type, key, jsonSource); - } else if (MonsterFields.type.existsIn(jsonSource) - && MonsterFields.type.existsIn(MonsterFields.type.getFrom(jsonSource)) - && MonsterFields.type.getFrom(MonsterFields.type.getFrom(jsonSource)).has("choose")) { - - List variants = new ArrayList<>(); - - // Produce a variant for each type - // "type": { - // "type": { - // "choose": [ - // "celestial", - // "fiend" - // ] - // } - // }, - - JsonNode choose = MonsterFields.choose.getFrom( - MonsterFields.type.getFrom( - MonsterFields.type.getFrom(jsonSource))); - - String name = SourceField.name.getTextOrEmpty(jsonSource); - for (JsonNode typeChoice : index.iterableElements(choose)) { - String variantName = String.format("%s (%s)", name, typeChoice.asText()); - ObjectNode adjustedSource = (ObjectNode) index.copyNode(jsonSource); - - adjustedSource.set("original", adjustedSource.get("name")); - adjustedSource.set("originalType", adjustedSource.get("type")); - adjustedSource.put("name", variantName); - adjustedSource.set("type", typeChoice); - - String newKey = type.createKey(adjustedSource); - variants.add(new Tuple(newKey, adjustedSource)); - } - return variants; - } if (key.contains("splugoth the returned") || key.contains("prophetess dran")) { MonsterFields.isNpc.setIn(jsonSource, true); // Fix. } - return List.of(new Tuple(key, jsonSource)); - } - public static List findConjuredMonsterVariants(Tools5eIndex index, Tools5eIndexType type, - String key, JsonNode jsonSource) { - final Pattern variantPattern = Pattern.compile("(\\d+) \\((.*?)\\)"); - int startLevel = MonsterFields.summonedBySpellLevel.intOrDefault(jsonSource, 0); - - String name = SourceField.name.getTextOrEmpty(jsonSource); - String hpString = MonsterFields.special.getTextOrEmpty(MonsterFields.hp.getFrom(jsonSource)); - - JsonNode acNode = MonsterFields.ac.getFirstFromArray(jsonSource); - String acString = MonsterFields.special.existsIn(acNode) - ? MonsterFields.special.getTextOrEmpty(acNode) - : acNode.asText(); - - List variants = new ArrayList<>(); - for (int i = startLevel; i < 10; i++) { - if (hpString.matches(" or \\d+") || - hpString.matches("(,\\s)?\\d+\\s\\([a-zA-Z ]+\\)")) { - String[] parts = {}; - String[] variantGroups = {}; - // "50 (Demon only) or 40 (Devil only) or 60 (Yugoloth only) + 15 for each spell - // level above 6th" - // "30 (Ghostly and Putrid only) or 20 (Skeletal only) + 10 for each spell level - // above 3rd" - if (hpString.matches(" or \\d+")) { - parts = hpString.split(" \\+ "); - variantGroups = parts[0].split(" or "); - } else if (hpString.matches("(,\\s)?\\d+\\s\\([a-zA-Z0-9_ ]+\\)")) { - // 10 (Medium or smaller), 20 (Large), 40 (Huge) - variantGroups = hpString.split(",\\s"); - } - for (String group : variantGroups) { - Matcher m = variantPattern.matcher(group); - if (m.find()) { - String amount = m.group(1); - String variant = m.group(2); - String hpText = amount; - if (parts.length > 1) { - hpText.concat(" + " + parts[1]); - } + for (JsonNode variant : MonsterFields.variant.iterateArrayFrom(jsonSource)) { + // There is a code path that is only followed if a variant also has _version + // but it doesn't seem like there are any examples of this in the data. + if (MonsterFields._versions.existsIn(variant)) { + Tui.instance().warnf(Msg.SOMEDAY, "\"Variant for %s has versions: %s", key, variant); + } + } - if (variant.contains(" and ")) { - for (String v : variant.split(" and ")) { - String variantName = String.format("%s (%s, %s-Level Spell)", - name, v.replace(" only", ""), JsonSource.levelToString(i)); - createVariant(index, variants, jsonSource, type, variantName, i, - hpText, acString); - } - } else { - String variantName = String.format("%s (%s, %s-Level Spell)", - name, variant.replace(" only", ""), JsonSource.levelToString(i)); - createVariant(index, variants, jsonSource, type, variantName, i, - hpText, acString); - } + boolean summonedCreature = MonsterFields.summonedBySpellLevel.existsIn(jsonSource); + boolean hasVersions = MonsterFields._versions.existsIn(jsonSource); + + if (summonedCreature || hasVersions) { + List versions = new ArrayList<>(); + List versionKeys = new ArrayList<>(); + + // Expand versions first + if (hasVersions) { + for (JsonNode vNode : MonsterFields._versions.iterateArrayFrom(jsonSource)) { + if (MonsterFields._abstract.existsIn(vNode) && MonsterFields._implementations.existsIn(vNode)) { + versions.addAll(getVersionsTemplate(vNode)); } else { - index.tui().warnf(Msg.UNKNOWN, "Unknown HP variant from %s: %s", key, hpString); + versions.add(getVersionsBasic(vNode)); } } - } else { - String variantName = String.format("%s (%s-level Spell)", name, JsonSource.levelToString(i)); - createVariant(index, variants, jsonSource, type, variantName, i, hpString, acString); + + // With each version... + for (JsonNode vNode : versions) { + // DataUtil.generic._getVersion(...) + String vKey = hydrateVersion(key, jsonSource, (ObjectNode) vNode, index); + versionKeys.add(vKey); + } + TtrpgValue.indexVersionKeys.setIn(jsonSource, Tui.MAPPER.valueToTree(versionKeys)); } + + // Add original after processing versions + versions.add(0, jsonSource); + + return versions; } - return variants; + + return List.of(jsonSource); } - static void createVariant(Tools5eIndex index, List variants, - JsonNode jsonSource, Tools5eIndexType type, - String variantName, int level, String hpString, String acString) { + public static String hydrateVersion(String parentKey, JsonNode parentSource, ObjectNode version, Tools5eIndex index) { + // DataUtil.generic._hydrateVersion({key}, {source}, {version}) + TtrpgValue.indexParentKey.setIn(version, parentKey); - ConjuredMonster fixed = new ConjuredMonster(level, variantName, hpString, acString, jsonSource); + Tools5eIndexType type = Tools5eIndexType.monster; + String versionKey = type.createKey(version); - ObjectNode adjustedSource = (ObjectNode) index.copyNode(jsonSource); - MonsterFields.original.setIn(adjustedSource, SourceField.name.getFrom(adjustedSource)); - SourceField.name.setIn(adjustedSource, fixed.getName()); - MonsterFields.ac.setIn(adjustedSource, fixed.getAc()); - MonsterFields.hp.setIn(adjustedSource, fixed.getHp()); + ObjectNode parentCopy = (ObjectNode) parentSource.deepCopy(); + MonsterFields._versions.removeFrom(parentCopy); + Tools5eFields.hasToken.removeFrom(parentCopy); + Tools5eFields.hasFluff.removeFrom(parentCopy); + Tools5eFields.hasFluffImages.removeFrom(parentCopy); - String newKey = type.createKey(adjustedSource); - variants.add(new Tuple(newKey, adjustedSource)); - } + filterSources(Tools5eFields.additionalSources, parentCopy, SourceField.source.getTextOrNull(version)); + filterSources(Tools5eFields.otherSources, parentCopy, SourceField.source.getTextOrNull(version)); - public static class ConjuredMonster { - final String name; - final MonsterAC monsterAc; - final MonsterHp monsterHp; + index.copier.mergeNodes(type, parentKey, parentCopy, version); - public ConjuredMonster(int level, String name, String hpString, String acString, JsonNode jsonSource) { - this.name = name; - this.monsterAc = new MonsterAC(level, acString); - this.monsterHp = new MonsterHp(level, hpString, jsonSource); - } + Tools5eSources.constructSources(versionKey, version); + return versionKey; + } - public JsonNode getName() { - return new TextNode(name); + private static void filterSources(JsonNodeReader field, ObjectNode parentCopy, String vesionSource) { + if (vesionSource == null) { + return; } - - public JsonNode getAc() { - MonsterAC[] result = new MonsterAC[] { monsterAc }; - return Tui.MAPPER.valueToTree(result); + JsonNode sources = field.ensureArrayIn(parentCopy); + Iterator it = sources.elements(); + while (it.hasNext()) { + JsonNode source = it.next(); + if (vesionSource.equals(source.asText())) { + it.remove(); + } } - - public JsonNode getHp() { - return Tui.MAPPER.valueToTree(monsterHp); + if (sources.isEmpty()) { + field.removeFrom(parentCopy); } } - @RegisterForReflection - @JsonInclude(JsonInclude.Include.NON_EMPTY) - public static class MonsterAC { - public final int ac; - public final String[] from; - public final String original; - - // "ac": [ - // { - // "ac": 19, - // "from": [ - // "natural armor" - // ] - // } - // ], - public MonsterAC(int level, String acString) { - this.original = acString; - if (acString.contains(" + ")) { - String[] parts = acString.split(" \\+ "); - int value = Integer.parseInt(parts[0]); - String armor = null; - // "11 + the level of the spell (natural armor)" - // "13 + PB (natural armor)" - if (parts[1].contains("the level of the spell")) { - value += level; - armor = parts[1] - .replace("the level of the spell", "") - .replace("(", "") - .replace(")", "") - .trim(); - } else if (parts[1].contains("PB")) { - armor = parts[1] - .replace("PB", "") - .replace("(", "") - .replace(")", "") - .trim() - + " + caster proficiency bonus"; - } - this.ac = value; - this.from = armor == null ? null : new String[] { armor }; - } else if (acString.matches("\\d+")) { - this.ac = Integer.parseInt(acString); - this.from = new String[] {}; - } else { - throw new IllegalArgumentException("Unknown AC pattern: " + acString); - } - } + public static JsonNode getVersionsBasic(JsonNode version) { + mutExpandCopy(version); + return version; } - @RegisterForReflection - @JsonInclude(JsonInclude.Include.NON_NULL) - public static class MonsterHp { - static final Pattern hpPattern = Pattern - .compile("(\\d+) \\+ (\\d+) for each spell level above (\\d) ?\\(?(.*?)?\\)?"); - - public final String special; - public final String original; - - // Want to go from: - // "40 + 10 for each spell level above 4th" - // "50 + 10 for each spell level above 5th (the dragon has a number of Hit Dice - // [d10s] equal to the level of the spell)" - // TO: - // "hp": { - // "average": 195, - // "formula": "17d12 + 85" - // }, - // OR - // "hp": { - // "special": "195", - // }, - public MonsterHp(int level, String hpString, JsonNode jsonSource) { - this.original = hpString; - - Integer value = null; - if (hpString.contains("Constitution modifier")) { - // "equal the undead's Constitution modifier + your spellcasting ability - // modifier + ten times the spell's level" - int con = jsonSource.get("con").asInt(); - value = con + (10 * level); - } else if (hpString.contains("half the hit point maximum of its summoner")) { - // nothing we can do - } else if (hpString.contains(" + ")) { - Matcher m = hpPattern.matcher(hpString); - if (m.find()) { - value = Integer.parseInt(m.group(1)); - int scale = level - Integer.parseInt(m.group(3)); - value += Integer.parseInt(m.group(2)) * scale; - } else { - // TODO: 20 (Air only) or 30 (Land and Water only) + 5 for each spell level above 2 - // nothing we can do right now - } - } else if (hpString.matches("^\\d+$")) { - value = Integer.parseInt(hpString); - } else if (hpString.matches("(\\d+\\s\\([a-zA-Z ]+\\),?\\s?)+")) { - // 10 (Medium or smaller), 20 (Large), 40 (Huge) - // nothing we can do right now - } else { - throw new IllegalArgumentException("Unknown HP pattern: " + hpString); - } + public static List getVersionsTemplate(JsonNode version) { + // DataUtil.generic._getVersions_template({ver}) + return MonsterFields._implementations.streamFrom(version) + .map(impl -> { + JsonNode cpyTemplate = MonsterFields._abstract.copyFrom(version); + mutExpandCopy(cpyTemplate); + + ObjectNode cpyImpl = impl.deepCopy(); + JsonNode _variables = MonsterFields._variables.removeFrom(cpyImpl); + if (_variables != null) { + Tui.instance().warnf(Msg.SOMEDAY, "Replace variables in templates. Templates: %s; Variables: %s", + cpyImpl, _variables); + } + ((ObjectNode) cpyTemplate).setAll(cpyImpl); + return cpyTemplate; + }) + .toList(); + } - this.special = value == null ? "" : value + ""; + public static void mutExpandCopy(JsonNode node) { + JsonNode _copy = Tui.MAPPER.createObjectNode(); + + // Move fields from the original node to the copy node + MetaFields._mod.moveFrom(node, _copy); + + // Make sure a preserve element exists (which it will not if the original node is empty) + MetaFields._preserve.moveFrom(node, _copy); + if (!MetaFields._preserve.existsIn(_copy)) { + MetaFields._preserve.setIn(_copy, Tui.MAPPER.createObjectNode().put("*", true)); } + + // Copy the copy node back to the original node + MetaFields._copy.setIn(node, _copy); } enum MonsterFields implements JsonNodeReader { + _abstract, + _implementations, + _variables, + _versions, ac, alignment, alignmentPrefix, average, + choose, cr, creatureType, // object -- alternate to monster type daily, @@ -668,7 +542,7 @@ enum MonsterFields implements JsonNodeReader { summonedBySpellLevel, trait, type, + variant, will, - choose, } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java index 8d73d176d..da343a363 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java @@ -24,7 +24,6 @@ import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField; -import dev.ebullient.convert.tools.dnd5e.Tools5eIndex.Tuple; import dev.ebullient.convert.tools.dnd5e.Tools5eSources.SourceAttributes; public class MagicVariant implements JsonSource { @@ -39,7 +38,7 @@ public class MagicVariant implements JsonSource { static final MagicVariant INSTANCE = new MagicVariant(); - public static List findSpecificVariants(Tools5eIndex index, Tools5eIndexType type, + public static List findSpecificVariants(Tools5eIndex index, Tools5eIndexType type, String key, JsonNode genericVariant, Tools5eJsonSourceCopier copier, List baseItems) { return INSTANCE.findVariants(index, type, key, genericVariant, copier, baseItems); @@ -114,10 +113,10 @@ private void populateVariant(final JsonNode variant) { } /** Update / replace item with variants (where appropriate) */ - private List findVariants(Tools5eIndex index, Tools5eIndexType type, + private List findVariants(Tools5eIndex index, Tools5eIndexType type, String key, JsonNode genericVariant, Tools5eJsonSourceCopier copier, List baseItems) { - List variants = new ArrayList<>(); + List variants = new ArrayList<>(); // baseItems.forEach((curBaseItem) => { // .... // genericVariants.forEach((curGenericVariant) => { @@ -131,13 +130,17 @@ private List findVariants(Tools5eIndex index, Tools5eIndexType type, // We're looping the other way (variant is the outer loop / is passed in) boolean spawnNewItems = key.contains(" (*)"); + ArrayNode specificVariantListNode = null; String gvKey = Tools5eIndexType.item.createKey(genericVariant); if (!spawnNewItems) { // Add generic variant to the list of variants as a regular item // Variations will be added to this item. TtrpgValue.indexInputType.setIn(genericVariant, Tools5eIndexType.item.name()); - variants.add(new Tuple(gvKey, genericVariant)); + TtrpgValue.indexKey.setIn(genericVariant, gvKey); + variants.add(genericVariant); index.addAlias(key, gvKey); + specificVariantListNode = ItemField._variants.ensureArrayIn(genericVariant); + ItemField._variants.setIn(genericVariant, specificVariantListNode); } String fluffKey = ItemField.hasFluff.booleanOrDefault(genericVariant, false) @@ -162,7 +165,7 @@ private List findVariants(Tools5eIndex index, Tools5eIndexType type, } Tools5eSources.constructSources(newKey, specficVariant); if (spawnNewItems) { - variants.add(new Tuple(newKey, specficVariant)); + variants.add(specficVariant); if (key.replace(" (*)", "").replace("magicvariant", "item").equals(newKey)) { index.addAlias(key, newKey); } @@ -172,7 +175,7 @@ private List findVariants(Tools5eIndex index, Tools5eIndexType type, } else { // add variant to list of variants for this generic variant // magic variant remains in index as a magic variant - ItemField._variants.ensureArrayIn(genericVariant).add(specficVariant); + specificVariantListNode.add(specficVariant); index.addAlias(newKey, gvKey); } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 41c8dbf5c..0a3335c93 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -307,7 +307,7 @@ public void prepare() { .filter(n -> !ItemField.packContents.existsIn(n)) .toList(); - Map variants = new HashMap<>(); + List variants = new ArrayList<>(); // For each node: handle copies, link sources for (Entry entry : nodeIndex.entrySet()) { @@ -356,14 +356,13 @@ public void prepare() { // Reprints do follow specialized variants, so we need to find the variants // now (and will filter them out based on rules later...) if (type.hasVariants()) { - List variantList = findVariants(key, jsonSource, baseItems); - for (Tuple variant : variantList) { - variants.put(variant.key, variant.node); - } + variants.addAll(findVariants(key, jsonSource, baseItems)); } } // end for each entry - nodeIndex.putAll(variants); + for (JsonNode variant : variants) { + nodeIndex.put(TtrpgValue.indexKey.getTextOrThrow(variant), variant); + } variants.clear(); filteredIndex = new HashMap<>(nodeIndex.size()); @@ -474,14 +473,14 @@ private void defineSubraces() { subraceIndex.clear(); } - List findVariants(String key, JsonNode jsonSource, List baseItems) { + List findVariants(String key, JsonNode jsonSource, List baseItems) { Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); if (type == Tools5eIndexType.magicvariant) { return MagicVariant.findSpecificVariants(this, type, key, jsonSource, copier, baseItems); } else if (type == Tools5eIndexType.monster) { return Json2QuteMonster.findMonsterVariants(this, type, key, jsonSource); } - return List.of(new Tuple(key, jsonSource)); + return List.of(jsonSource); } /** @@ -552,6 +551,15 @@ private boolean isReprinted(String finalKey, JsonNode jsonSource) { continue; } } + String lookupKey = finalKey.replace(sources.primarySource().toLowerCase(), ""); + String versionKey = TtrpgValue.indexVersionKeys.streamFrom(reprint) + .map(x -> x.asText()) + .filter(x -> x.startsWith(lookupKey)) + .findFirst().orElse(null); + if (versionKey != null) { + reprintKey = versionKey; // more specific version/variant for redirect + } + // Otherwise, we have a "newer" reprint that should be used instead tui().logf(Msg.REPRINT, "(drop | reprinted) %s ==> %s", finalKey, reprintKey); // 1) create an alias mapping the old key to the reprinted key diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java index 91df54991..49beb32f6 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java @@ -29,21 +29,24 @@ public class Tools5eJsonSourceCopier extends JsonSourceCopier "name", "colStyles", "style", "shortName", "subclassShortName", "id", "path"); private static final List _MERGE_REQUIRES_PRESERVE_BASE = List.of( - "_versions", + "page", + "otherSources", + "srd", + "srd52", "basicRules", + "freeRules2024", + "reprintedAs", "hasFluff", "hasFluffImages", "hasToken", - "indexInputType", // mine: do I have the usage right? - "indexKey", // mine: do I have the usage right? - "otherSources", - "page", - "reprintedAs", - "srd"); + "_versions"); private static final Map> _MERGE_REQUIRES_PRESERVE = Map.of( + // Monster fields that must be preserved Tools5eIndexType.monster, List.of("legendaryGroup", "environment", "soundClip", "altArt", "variant", "dragonCastingColor", "familiar"), + // Item fields that must be preserved Tools5eIndexType.item, List.of("lootTables", "tier"), + // Item Group fields that must be preserved Tools5eIndexType.itemGroup, List.of("lootTables", "tier")); private static final List COPY_ENTRY_PROPS = List.of( "action", "bonus", "reaction", "trait", "legendary", "mythic", "variant", "spellcasting", @@ -90,12 +93,14 @@ public JsonNode mergeSubrace(JsonNode subraceNode, JsonNode raceNode) { ObjectNode copyFrom = (ObjectNode) copyNode(subraceNode); ObjectNode subraceOut = (ObjectNode) copyNode(raceNode); - List.of("name", "source", "srd", "basicRules") + List.of("name", "source", "srd", "srd52", "basicRules", "freeRules2024") .forEach(p -> subraceOut.set("_base" + toTitleCase(p), subraceOut.get(p))); - List.of("subraces", "srd", "basicRules", "_versions", "hasFluff", "hasFluffImages", "_rawName") + List.of("subraces", "srd", "srd52", "basicRules", "freeRules2024", + "_versions", "hasFluff", "hasFluffImages", + "reprintedAs", "_rawName") .forEach(subraceOut::remove); - copyFrom.remove("__prop"); // cleanup: we copy remainder later + copyFrom.remove("__prop"); // cleanup JsonNode overwrite = MetaFields.overwrite.getFrom(copyFrom); @@ -194,7 +199,7 @@ public JsonNode mergeSubrace(JsonNode subraceNode, JsonNode raceNode) { // utils.js: static getCopy (impl, copyFrom, copyTo, templateData,...) { @Override - protected JsonNode mergeNodes(Tools5eIndexType type, String originKey, JsonNode copyFrom, ObjectNode target) { + public JsonNode mergeNodes(Tools5eIndexType type, String originKey, JsonNode copyFrom, ObjectNode target) { JsonNode _copy = MetaFields._copy.getFromOrEmptyObjectNode(target); normalizeMods(_copy); @@ -320,6 +325,10 @@ protected JsonNode resolveDynamicVariable( protected void doModProp( String originKey, JsonNode modInfo, JsonNode copyFrom, String prop, ObjectNode target, ModFieldMode mode) { + if (mode == null) { + tui().errorf("Error (%s): Missing mode for modProp (add value to ModFieldMode): %s", originKey, modInfo); + return; + } switch (mode) { // Bestiary case addAllSaves, addAllSkills, addSaves -> mode.notSupported(tui(), originKey, modInfo); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java index 183ec50b4..9cdbdc7eb 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java @@ -131,7 +131,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_Present("monster|alkilith|mpmm"); commonTests.assert_MISSING("monster|alkilith|mtf"); - commonTests.assert_Present("monster|animated object (5th-level spell)|xphb"); commonTests.assert_MISSING("monster|animated object (huge)|phb"); commonTests.assert_MISSING("monster|ape|mm"); commonTests.assert_Present("monster|ape|xphb"); @@ -141,8 +140,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("monster|awakened shrub|xmm"); commonTests.assert_MISSING("monster|beast of the land|tce"); commonTests.assert_Present("monster|beast of the land|xphb"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (air)|tce"); + commonTests.assert_Present("monster|bestial spirit (air)|xphb"); commonTests.assert_MISSING("monster|cat|mm"); commonTests.assert_Present("monster|cat|xphb"); commonTests.assert_Present("monster|derro savant|mpmm"); @@ -198,7 +197,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); commonTests.assert_MISSING("subrace|human|human|phb|phb"); commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling (zariel)|tiefling|phb|mtf"); commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); commonTests.assert_MISSING("trap|collapsing roof|dmg"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java index 9fc2e22e7..0c4e9cc12 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java @@ -136,7 +136,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("monster|abjurer|vgm"); commonTests.assert_Present("monster|alkilith|mpmm"); commonTests.assert_Present("monster|alkilith|mtf"); - commonTests.assert_Present("monster|animated object (5th-level spell)|xphb"); commonTests.assert_Present("monster|animated object (huge)|phb"); commonTests.assert_Present("monster|ape|mm"); commonTests.assert_Present("monster|ape|xphb"); @@ -146,8 +145,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("monster|awakened shrub|xmm"); commonTests.assert_Present("monster|beast of the land|tce"); commonTests.assert_Present("monster|beast of the land|xphb"); - commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_Present("monster|bestial spirit (air)|tce"); + commonTests.assert_Present("monster|bestial spirit (air)|xphb"); commonTests.assert_Present("monster|cat|mm"); commonTests.assert_Present("monster|cat|xphb"); commonTests.assert_Present("monster|derro savant|mpmm"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java index 9eb3f471d..e9e7b3761 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java @@ -130,7 +130,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); commonTests.assert_MISSING("monster|alkilith|mtf"); - commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); commonTests.assert_MISSING("monster|animated object (huge)|phb"); commonTests.assert_Present("monster|ape|mm"); commonTests.assert_MISSING("monster|ape|xphb"); @@ -140,8 +139,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|awakened shrub|xmm"); commonTests.assert_MISSING("monster|beast of the land|tce"); commonTests.assert_MISSING("monster|beast of the land|xphb"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (air)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (air)|xphb"); commonTests.assert_Present("monster|cat|mm"); commonTests.assert_MISSING("monster|cat|xphb"); commonTests.assert_MISSING("monster|derro savant|mpmm"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java index 084ae67f7..c6db87eca 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java @@ -130,7 +130,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); commonTests.assert_MISSING("monster|alkilith|mtf"); - commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); commonTests.assert_MISSING("monster|animated object (huge)|phb"); commonTests.assert_Present("monster|ape|mm"); commonTests.assert_MISSING("monster|ape|xphb"); @@ -140,8 +139,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|awakened shrub|xmm"); commonTests.assert_MISSING("monster|beast of the land|tce"); commonTests.assert_MISSING("monster|beast of the land|xphb"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (air)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (air)|xphb"); commonTests.assert_Present("monster|cat|mm"); commonTests.assert_MISSING("monster|cat|xphb"); commonTests.assert_MISSING("monster|derro savant|mpmm"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java index 906b096ac..1a75c0e43 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java @@ -129,7 +129,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); commonTests.assert_MISSING("monster|alkilith|mtf"); - commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); commonTests.assert_MISSING("monster|animated object (huge)|phb"); commonTests.assert_Present("monster|ape|mm"); commonTests.assert_MISSING("monster|ape|xphb"); @@ -139,8 +138,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|awakened shrub|xmm"); commonTests.assert_MISSING("monster|beast of the land|tce"); commonTests.assert_MISSING("monster|beast of the land|xphb"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (air)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (air)|xphb"); commonTests.assert_Present("monster|cat|mm"); commonTests.assert_MISSING("monster|cat|xphb"); commonTests.assert_MISSING("monster|derro savant|mpmm"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java index a78306dc6..44706f0c4 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java @@ -129,7 +129,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); commonTests.assert_MISSING("monster|alkilith|mtf"); - commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); commonTests.assert_MISSING("monster|animated object (huge)|phb"); commonTests.assert_MISSING("monster|ape|mm"); commonTests.assert_MISSING("monster|ape|xphb"); @@ -139,8 +138,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|awakened shrub|xmm"); commonTests.assert_MISSING("monster|beast of the land|tce"); commonTests.assert_MISSING("monster|beast of the land|xphb"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (air)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (air)|xphb"); commonTests.assert_MISSING("monster|cat|mm"); commonTests.assert_MISSING("monster|cat|xphb"); commonTests.assert_MISSING("monster|derro savant|mpmm"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java index cac2f056b..68fd4f0c0 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java @@ -129,7 +129,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); commonTests.assert_MISSING("monster|alkilith|mtf"); - commonTests.assert_MISSING("monster|animated object (5th-level spell)|xphb"); commonTests.assert_Present("monster|animated object (huge)|phb"); commonTests.assert_Present("monster|ape|mm"); commonTests.assert_MISSING("monster|ape|xphb"); @@ -139,8 +138,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|awakened shrub|xmm"); commonTests.assert_Present("monster|beast of the land|tce"); commonTests.assert_MISSING("monster|beast of the land|xphb"); - commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_Present("monster|bestial spirit (air)|tce"); + commonTests.assert_MISSING("monster|bestial spirit (air)|xphb"); commonTests.assert_Present("monster|cat|mm"); commonTests.assert_MISSING("monster|cat|xphb"); commonTests.assert_MISSING("monster|derro savant|mpmm"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java index 1464bd1bf..937b215e4 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java @@ -128,7 +128,6 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_Present("monster|alkilith|mpmm"); commonTests.assert_MISSING("monster|alkilith|mtf"); - commonTests.assert_Present("monster|animated object (5th-level spell)|xphb"); commonTests.assert_MISSING("monster|animated object (huge)|phb"); commonTests.assert_MISSING("monster|ape|mm"); commonTests.assert_Present("monster|ape|xphb"); @@ -138,8 +137,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|awakened shrub|xmm"); commonTests.assert_MISSING("monster|beast of the land|tce"); commonTests.assert_Present("monster|beast of the land|xphb"); - commonTests.assert_MISSING("monster|bestial spirit (2nd-level spell)|tce"); - commonTests.assert_Present("monster|bestial spirit (2nd-level spell)|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (air)|tce"); + commonTests.assert_Present("monster|bestial spirit (air)|xphb"); commonTests.assert_MISSING("monster|cat|mm"); commonTests.assert_Present("monster|cat|xphb"); commonTests.assert_Present("monster|derro savant|mpmm"); From 32875f93de6e9908610fa5b644012f4e7b0893c4 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Tue, 17 Dec 2024 21:35:13 -0500 Subject: [PATCH 074/119] =?UTF-8?q?=F0=9F=90=9B=20Correct=20creation=20of?= =?UTF-8?q?=20source=20text=20(and=20..=20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/ebullient/convert/tools/dnd5e/Tools5eSources.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java index c9c861cb4..6f9619796 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java @@ -360,7 +360,11 @@ protected String findSourceText(IndexType type, JsonNode jsonElement) { if (srdText.isBlank() && basicRulesText.isBlank()) { return sourceText; } - String srdBasic = "Available in " + srdText + (srdText.isEmpty() ? "" : " and ") + basicRulesText; + String srdBasic = "Available in " + srdText; + if (!srdText.isEmpty() && !basicRulesText.isEmpty()) { + srdBasic += " and "; + } + srdBasic += basicRulesText; return sourceText.isEmpty() ? srdBasic : sourceText + ". " + srdBasic; From 731792cee5323b7ce56c996d3b62c377b7fc0e53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 09:50:08 +0000 Subject: [PATCH 075/119] Bump actions/upload-artifact from 4.4.3 to 4.5.0 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.3 to 4.5.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882...6f51ac03b9356f520e9adb1b1b7802705f340c2b) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8209a0a72..8822f2021 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,14 +85,14 @@ jobs: zip -r target/${ARTIFACT}-${NEXT}-examples.zip docs examples default - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 id: upload-jar with: name: artifacts-runner path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 id: upload-zip with: name: artifacts-examples diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4901e7ffc..e910e5a48 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,7 +60,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: SARIF file path: results.sarif From 69f4f1534f4c688a58f717666581a8b7e0741c03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 09:49:58 +0000 Subject: [PATCH 076/119] Bump actions/setup-java from 4.5.0 to 4.6.0 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.5.0 to 4.6.0. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/8df1039502a15bceb9433410b1a100fbe190c53b...7a6d8a8234af8eb26422e24e3006232cccaa061b) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b091ca51f..c01860ca7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 615d991e9..d0e255f0a 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -83,7 +83,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index eb59b5a16..597d67bbd 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -51,7 +51,7 @@ jobs: Data-Pf2eTools - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8822f2021..ab8b1c374 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 550cfe9cb..ade21785f 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -87,7 +87,7 @@ jobs: fail-on-cache-miss: true - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRO }} From d36c9d0632dcb1da594413f1e1ffdcbd2d58dce7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 09:50:05 +0000 Subject: [PATCH 077/119] Bump github/codeql-action from 3.27.9 to 3.28.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.9 to 3.28.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/df409f7d9260372bd5f19e5b04e83cb3c43714ae...48ab28a6f5dbc2a99bf1e0131198dd8f1df78169) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c01860ca7..f0e5890ee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 + uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 + uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e910e5a48..f8fd10028 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 + uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 with: sarif_file: results.sarif From c560543c8544e1bc75d1b2e5d02015fa8906d760 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 09:06:44 +0000 Subject: [PATCH 078/119] Bump quarkus.platform.version from 3.17.4 to 3.17.5 Bumps `quarkus.platform.version` from 3.17.4 to 3.17.5. Updates `io.quarkus.platform:quarkus-bom` from 3.17.4 to 3.17.5 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.4...3.17.5) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.17.4 to 3.17.5 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.4...3.17.5) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6129c2a5c..e3bf630c3 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.17.4 + 3.17.5 3.26.3 3.4.0 From 53a5e6eb9d74b67b87d878bc6d5b80e304c6010c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 02:00:45 +0000 Subject: [PATCH 079/119] Bump org.assertj:assertj-core from 3.26.3 to 3.27.1 Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.26.3 to 3.27.1. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.26.3...assertj-build-3.27.1) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e3bf630c3..bddd21ec7 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ io.quarkus.platform 3.17.5 - 3.26.3 + 3.27.1 3.4.0 3.0.7 76.1 From aa9a6966386e301bded844072f2d297fb72f714f Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 2 Jan 2025 21:57:23 -0500 Subject: [PATCH 080/119] =?UTF-8?q?=F0=9F=91=B7=20graalvm=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request.yml | 18 +++++++++--------- .github/workflows/tools-data.yml | 3 +-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 597d67bbd..a444e10f4 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -8,20 +8,19 @@ on: - "src/**" - "ide-config" -permissions: - contents: read - actions: read - env: JAVA_VERSION: 17 - NATIVE_VERSION: 22.3.2 - GRAALVM_DIST: graalvm-community JAVA_DISTRO: temurin + NATIVE_JAVA_VERSION: 23 + GRAALVM_DIST: graalvm-community GH_BOT_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com" GH_BOT_NAME: "GitHub Action" -jobs: +permissions: + contents: read + actions: read +jobs: metadata: uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@main @@ -76,6 +75,8 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: tools5e-cache @@ -100,9 +101,8 @@ jobs: - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 with: distribution: ${{ env.GRAALVM_DIST }} - java-version: ${{ env.JAVA_VERSION }} + java-version: ${{ env.NATIVE_JAVA_VERSION }} github-token: ${{ secrets.GITHUB_TOKEN }} - version: ${{ env.NATIVE_VERSION }} cache: 'maven' - name: Build and run in native mode diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index ade21785f..b2b8e059c 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -9,7 +9,7 @@ on: env: JAVA_VERSION: 17 JAVA_DISTRO: temurin - NATIVE_JAVA_VERSION: 21 + NATIVE_JAVA_VERSION: 23 GRAALVM_DIST: graalvm-community FAIL_ISSUE: 140 @@ -111,7 +111,6 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 From 7f8f6068e58698b8f18ac16e36a6b66cc860db8f Mon Sep 17 00:00:00 2001 From: Enno Gottschalk Date: Fri, 3 Jan 2025 21:18:16 +0100 Subject: [PATCH 081/119] Fixed a wrong tag in configuration.md Only "reprintBehavior" is accepted as a tag. "reprint" leads to an error --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0f8e4ef1b..25b94048c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -354,7 +354,7 @@ This approach is ideal for content acquired in parts, like individual items from Content is often reprinted or updated in later sources or editions. This setting lets you control how reprinted or revised content is handled when generating notes. ``` json - "reprint": "newest" + "reprintBehavior": "newest" ``` This setting has 3 possible values: From c85710d3a4e55802abee1ed17274721c2bff4a82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 09:04:24 +0000 Subject: [PATCH 082/119] Bump org.assertj:assertj-core from 3.27.1 to 3.27.2 Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.1 to 3.27.2. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.1...assertj-build-3.27.2) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bddd21ec7..446532432 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ io.quarkus.platform 3.17.5 - 3.27.1 + 3.27.2 3.4.0 3.0.7 76.1 From 16af8831fd94cf5bb2adb6d043affc7f7b28a516 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 6 Jan 2025 18:35:39 -0500 Subject: [PATCH 083/119] =?UTF-8?q?=F0=9F=92=B8=20remove=20repo-specific?= =?UTF-8?q?=20funding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index bf5529ec7..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -buy_me_a_coffee: ebullient From 76cfc3bf8a3fed5c3571156482e9a030ca04c7af Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 13 Jan 2025 10:51:01 -0500 Subject: [PATCH 084/119] =?UTF-8?q?=E2=9C=A8=20{@hom}=20--=20Hit=20or=20Mi?= =?UTF-8?q?ss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index 0347bd08b..4a3707207 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -339,7 +339,8 @@ default String _replaceTokenText(String input, boolean nested) { .replaceAll("\\{@language ([^|}]+)\\|?[^}]*}", "$1") .replaceAll("\\{@book ([^}|]+)\\|?[^}]*}", "\"$1\"") .replaceAll("\\{@h}", "*Hit:* ") // render.js Renderer.tag - .replaceAll("\\{@m}", "*Miss:* ") // render.js Renderer.tag + .replaceAll("\\{@m}", "*Miss:* ") + .replaceAll("\\{@hom}", "*Hit or Miss:* ")// render.js Renderer.tag .replaceAll("\\{@actSaveFail}", "*Failure:*") // render.js Renderer.tag .replaceAll("\\{@actSaveSuccess}", "*Success:*") // render.js Renderer.tag .replaceAll("\\{@actSaveSuccessOrFail}", "*Failure or Success:*") // render.js Renderer.tag From 651b33cb7be272ce075f032162988d6b5d713ffe Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 13 Jan 2025 11:00:49 -0500 Subject: [PATCH 085/119] =?UTF-8?q?=F0=9F=90=9B=20dc=20tag=20in=20table=20?= =?UTF-8?q?caption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java index a6c57b7c9..b588f5756 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java @@ -745,7 +745,7 @@ default void appendTable(List text, JsonNode tableNode) { } if (!caption.isBlank()) { table.add(0, ""); - table.add(0, "**" + caption + "**"); + table.add(0, "**" + replaceText(caption) + "**"); } } finally { parseState().pop(pushTable); From 0c3854db538800bc78972577c6f5155753ca92c7 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 13 Jan 2025 11:17:01 -0500 Subject: [PATCH 086/119] =?UTF-8?q?=F0=9F=91=B7=20fix=20pf2e=20ci;=20fix?= =?UTF-8?q?=20issue=20write?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pf2e-tools-data.yml | 66 +++++++++++++-------------- .github/workflows/tools-data.yml | 3 ++ 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index d0e255f0a..9451a92ea 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -8,14 +8,12 @@ on: env: JAVA_VERSION: 17 - NATIVE_VERSION: 22.3.2 - GRAALVM_DIST: graalvm-community JAVA_DISTRO: temurin + NATIVE_JAVA_VERSION: 23 + GRAALVM_DIST: graalvm-community FAIL_ISSUE: 141 -permissions: - contents: read - actions: read +permissions: read-all jobs: cache-setup: @@ -25,12 +23,13 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 - name: Pf2e Tools release cache key id: test-data-key run: | - LATEST_RELEASE=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/Pf2eToolsOrg/Pf2eTools/releases/latest) - LATEST_VERSION=$(echo $LATEST_RELEASE | grep tag_name | sed -e 's/.*"tag_name": "\([^"]*\)".*/\1/') + LATEST_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/Pf2eToolsOrg/Pf2eTools/releases/latest | jq -r .tag_name) echo $LATEST_VERSION echo "🔹 Use $LATEST_VERSION" @@ -41,7 +40,7 @@ jobs: id: test-data-check uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: - path: sources/Pf2eTools + path: sources key: ${{ steps.test-data-key.outputs.cache_key }} lookup-only: true enableCrossOsArchive: true @@ -51,20 +50,20 @@ jobs: if: steps.test-data-check.outputs.cache-hit != 'true' env: LATEST_VERSION: ${{ steps.test-data-key.outputs.tools_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + mkdir -p sources + echo "🔹 Download $LATEST_VERSION" - ARTIFACT_URL="https://github.com/Pf2eToolsOrg/Pf2eTools/archive/refs/tags/$LATEST_VERSION.tar.gz" - VER=$(echo $LATEST_VERSION | cut -c 2-) - ROOT="Pf2eTools-$VER" - curl -LsS -o Pf2eTools.tar.gz $ARTIFACT_URL - tar xzf Pf2eTools.tar.gz ${ROOT}/data ${ROOT}/img + gh repo clone Pf2eToolsOrg/Pf2eTools sources/Pf2eTools -- --depth=1 -c advice.detachedHead=false -b $LATEST_VERSION + # Remove image contents. We just need the files to exist (linking) - find ${ROOT}/img -type f | while read FILE; do echo > "$FILE"; done + find sources -type f -type f \ + \( -iname \*.jpg -o -iname \*.png -o -iname \*.webp \) \ + | while read FILE; do echo > "$FILE"; done - mkdir -p sources - rm -rf sources/Pf2eTools - mv ${ROOT} sources/Pf2eTools + ls -al sources test-with-data: @@ -74,11 +73,13 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: - path: sources/Pf2eTools + path: sources key: ${{ needs.cache-setup.outputs.cache_key }} fail-on-cache-miss: true @@ -92,6 +93,7 @@ jobs: - name: Build with Maven id: mvn-build run: | + ls -al sources ./mvnw -B -ntp -DskipFormat verify native-test-with-data: @@ -106,13 +108,14 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: - path: sources/Pf2eTools + path: sources key: ${{ needs.cache-setup.outputs.cache_key }} fail-on-cache-miss: true enableCrossOsArchive: true @@ -120,30 +123,20 @@ jobs: - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 with: distribution: ${{ env.GRAALVM_DIST }} - java-version: ${{ env.JAVA_VERSION }} + java-version: ${{ env.NATIVE_JAVA_VERSION }} github-token: ${{ secrets.GITHUB_TOKEN }} - version: ${{ env.NATIVE_VERSION }} cache: 'maven' - - name: Build and run - id: mvn-build - env: - MAVEN_OPTS: "-Xmx1g" - run: | - ./mvnw -B -ntp -DskipFormat verify - - if: runner.os == 'Windows' name: clean before native build shell: cmd run: | - ./mvnw -B -ntp -DskipFormat clean + ./mvnw -B -ntp -DskipTests -DskipFormat clean - - name: Build and run in native mode + - name: Build, run, and test in native mode id: mvn-native-build - env: - MAVEN_OPTS: "-Xmx1g" run: | - ./mvnw -B -ntp -Dnative -DskipTests -DskipFormat verify + ./mvnw -B -ntp -Dnative -DskipFormat verify report-native-build: @@ -151,9 +144,14 @@ jobs: runs-on: ubuntu-latest if: ${{ failure() }} needs: [test-with-data, native-test-with-data] + permissions: + contents: read + issues: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 - id: gh-issue env: diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index b2b8e059c..2ea1d1248 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -147,6 +147,9 @@ jobs: runs-on: ubuntu-latest if: ${{ failure() }} needs: [test-with-data, native-test-with-data] + permissions: + contents: read + issues: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From 24e6e40aa21813996d50d1e02fd034c2239d2073 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:26:30 +0000 Subject: [PATCH 087/119] Bump actions/upload-artifact from 4.5.0 to 4.6.0 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.5.0 to 4.6.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/6f51ac03b9356f520e9adb1b1b7802705f340c2b...65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab8b1c374..1ca3d6055 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,14 +85,14 @@ jobs: zip -r target/${ARTIFACT}-${NEXT}-examples.zip docs examples default - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 id: upload-jar with: name: artifacts-runner path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 id: upload-zip with: name: artifacts-examples diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f8fd10028..524a5fd68 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,7 +60,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: SARIF file path: results.sarif From 939c2090459c67ff77e4f93bea7d8203b5140047 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:47:56 +0000 Subject: [PATCH 088/119] Bump quarkus.platform.version from 3.17.5 to 3.17.6 Bumps `quarkus.platform.version` from 3.17.5 to 3.17.6. Updates `io.quarkus.platform:quarkus-bom` from 3.17.5 to 3.17.6 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.5...3.17.6) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.17.5 to 3.17.6 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.5...3.17.6) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 446532432..e6cf68124 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.17.5 + 3.17.6 3.27.2 3.4.0 From 4f55c7aee8bd31cc86e82733995b80c68905e9a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:26:27 +0000 Subject: [PATCH 089/119] Bump github/codeql-action from 3.28.0 to 3.28.1 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.0 to 3.28.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/48ab28a6f5dbc2a99bf1e0131198dd8f1df78169...b6a472f63d85b9c78a3ac5e89422239fc15e9b3c) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f0e5890ee..ab7c11a01 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 524a5fd68..3105a43ca 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 with: sarif_file: results.sarif From dd143fbb3e39aa02390179768c8fc6efc957d642 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 13 Jan 2025 15:07:29 -0500 Subject: [PATCH 090/119] =?UTF-8?q?=F0=9F=90=9B=E2=9C=A8=E2=99=BB=EF=B8=8F?= =?UTF-8?q?=20Classes/subclasses;=20images=20in=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ♻️ Refactor class and subclass General fixes: - 🐛 include parent source for divided adventures - 🐛 fix item property rendering/filtering - 🔇 shorter logged stack traces Templates and images: - ✨ New template methods for fluff images - ⚡️ templates using new methods for image display - ✅ add additional example templates --- docs/templates/dnd5e/QuteBackground.md | 27 +- docs/templates/dnd5e/QuteBastion/README.md | 27 +- docs/templates/dnd5e/QuteClass/HitPointDie.md | 39 + .../dnd5e/QuteClass/Multiclassing.md | 58 + docs/templates/dnd5e/QuteClass/README.md | 104 ++ .../dnd5e/QuteClass/StartingEquipment.md | 56 + docs/templates/dnd5e/QuteDeck/README.md | 29 +- docs/templates/dnd5e/QuteDeity.md | 29 +- docs/templates/dnd5e/QuteFeat.md | 29 +- docs/templates/dnd5e/QuteHazard.md | 29 +- docs/templates/dnd5e/QuteItem/README.md | 27 +- docs/templates/dnd5e/QuteMonster/README.md | 27 +- docs/templates/dnd5e/QuteObject.md | 27 +- docs/templates/dnd5e/QutePsionic.md | 29 +- docs/templates/dnd5e/QuteRace.md | 27 +- docs/templates/dnd5e/QuteReward.md | 29 +- docs/templates/dnd5e/QuteSpell.md | 27 +- docs/templates/dnd5e/QuteSubclass.md | 29 +- docs/templates/dnd5e/QuteVehicle/README.md | 27 +- docs/templates/dnd5e/README.md | 2 +- docs/templates/dnd5e/Tools5eQuteBase.md | 29 +- .../tools5e/images-background2md.txt | 9 +- .../templates/tools5e/images-class2md.txt | 47 + examples/templates/tools5e/images-item2md.txt | 12 +- .../templates/tools5e/images-monster2md.txt | 16 +- .../templates/tools5e/images-object2md.txt | 22 +- examples/templates/tools5e/images-race2md.txt | 9 +- .../templates/tools5e/images-spell2md.txt | 11 +- .../templates/tools5e/images-subclass2md.txt | 31 + .../templates/tools5e/images-vehicle2md.txt | 18 +- .../tools5e/mixed/mixed-background2md.txt | 26 + .../tools5e/mixed/mixed-class2md.txt | 44 + .../tools5e/mixed/mixed-deity2md.txt | 36 + .../templates/tools5e/mixed/mixed-feat2md.txt | 27 + .../tools5e/mixed/mixed-hazard2md.txt | 21 + .../templates/tools5e/mixed/mixed-item2md.txt | 51 + .../tools5e/mixed/mixed-monster2md.txt | 113 ++ .../tools5e/mixed/mixed-object2md.txt | 65 + .../templates/tools5e/mixed/mixed-race2md.txt | 46 + .../tools5e/mixed/mixed-reward2md.txt | 24 + .../tools5e/mixed/mixed-spell2md.txt | 42 + .../tools5e/mixed/mixed-subclass2md.txt | 24 + .../tools5e/mixed/mixed-vehicle2md.txt | 114 ++ .../dev/ebullient/convert/StringUtil.java | 14 + .../ebullient/convert/config/TtrpgConfig.java | 10 + .../ebullient/convert/io/MarkdownWriter.java | 3 +- .../java/dev/ebullient/convert/io/Tui.java | 36 +- .../dev/ebullient/convert/qute/QuteUtil.java | 6 + .../convert/tools/JsonNodeReader.java | 23 +- .../convert/tools/JsonTextConverter.java | 12 +- .../convert/tools/dnd5e/ItemMastery.java | 19 +- .../convert/tools/dnd5e/ItemProperty.java | 23 +- .../convert/tools/dnd5e/ItemType.java | 23 +- .../tools/dnd5e/Json2QuteBackground.java | 2 +- .../convert/tools/dnd5e/Json2QuteBastion.java | 2 +- .../convert/tools/dnd5e/Json2QuteClass.java | 1337 ++++++++++------- .../convert/tools/dnd5e/Json2QuteCommon.java | 24 +- .../convert/tools/dnd5e/Json2QuteCompose.java | 21 +- .../convert/tools/dnd5e/Json2QuteFeat.java | 11 +- .../convert/tools/dnd5e/Json2QuteHazard.java | 11 +- .../convert/tools/dnd5e/Json2QuteItem.java | 4 +- .../convert/tools/dnd5e/Json2QuteObject.java | 2 +- .../tools/dnd5e/Json2QuteOptionalFeature.java | 12 +- .../convert/tools/dnd5e/Json2QuteReward.java | 9 +- .../convert/tools/dnd5e/Json2QuteSpell.java | 2 +- .../convert/tools/dnd5e/JsonSource.java | 7 +- .../tools/dnd5e/JsonTextReplacement.java | 164 +- .../tools/dnd5e/OptionalFeatureIndex.java | 5 +- .../convert/tools/dnd5e/SkillOrAbility.java | 18 +- .../tools/dnd5e/Tools5eHomebrewIndex.java | 9 +- .../convert/tools/dnd5e/Tools5eIndex.java | 197 +-- .../convert/tools/dnd5e/Tools5eIndexType.java | 8 +- .../convert/tools/dnd5e/Tools5eSources.java | 20 +- .../tools/dnd5e/qute/QuteBackground.java | 7 +- .../convert/tools/dnd5e/qute/QuteBastion.java | 6 +- .../convert/tools/dnd5e/qute/QuteClass.java | 366 ++++- .../convert/tools/dnd5e/qute/QuteDeck.java | 2 +- .../convert/tools/dnd5e/qute/QuteDeity.java | 2 +- .../convert/tools/dnd5e/qute/QuteFeat.java | 7 +- .../convert/tools/dnd5e/qute/QuteHazard.java | 7 +- .../convert/tools/dnd5e/qute/QuteItem.java | 13 +- .../convert/tools/dnd5e/qute/QuteMonster.java | 7 +- .../convert/tools/dnd5e/qute/QuteObject.java | 7 +- .../convert/tools/dnd5e/qute/QutePsionic.java | 3 +- .../convert/tools/dnd5e/qute/QuteRace.java | 5 +- .../convert/tools/dnd5e/qute/QuteReward.java | 7 +- .../convert/tools/dnd5e/qute/QuteSpell.java | 7 +- .../tools/dnd5e/qute/QuteSubclass.java | 9 +- .../convert/tools/dnd5e/qute/QuteVehicle.java | 8 +- .../tools/dnd5e/qute/Tools5eQuteBase.java | 68 +- .../convert/tools/pf2e/Json2QuteBook.java | 1 + .../convert/tools/pf2e/Json2QuteCompose.java | 2 + .../tools/pf2e/JsonTextReplacement.java | 1 + .../tools/pf2e/qute/QuteTraitIndex.java | 5 +- src/main/resources/convertData.json | 149 +- .../convert/CustomTemplatesTest.java | 108 +- .../convert/Tools5eDataConvertTest.java | 23 +- .../tools/dnd5e/FilterAllNewestTest.java | 7 +- .../convert/tools/dnd5e/FilterAllTest.java | 1 + .../tools/dnd5e/FilterNoneEditionTest.java | 1 + .../convert/tools/dnd5e/FilterNoneTest.java | 1 + .../tools/dnd5e/FilterSrd2014Test.java | 1 + .../tools/dnd5e/FilterSrd2024Test.java | 1 + .../tools/dnd5e/FilterSubset2014Test.java | 1 + src/test/resources/5e/sample.yaml | 93 ++ .../resources/5e/sources-book-adventure.json | 3 +- src/test/resources/5e/sources-images.yaml | 2 + 107 files changed, 3469 insertions(+), 1011 deletions(-) create mode 100644 docs/templates/dnd5e/QuteClass/HitPointDie.md create mode 100644 docs/templates/dnd5e/QuteClass/Multiclassing.md create mode 100644 docs/templates/dnd5e/QuteClass/README.md create mode 100644 docs/templates/dnd5e/QuteClass/StartingEquipment.md create mode 100644 examples/templates/tools5e/images-class2md.txt create mode 100644 examples/templates/tools5e/images-subclass2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-background2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-class2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-deity2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-feat2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-hazard2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-item2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-monster2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-object2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-race2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-reward2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-spell2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-subclass2md.txt create mode 100644 examples/templates/tools5e/mixed/mixed-vehicle2md.txt create mode 100644 src/test/resources/5e/sample.yaml diff --git a/docs/templates/dnd5e/QuteBackground.md b/docs/templates/dnd5e/QuteBackground.md index 6f4862c74..489d88f8e 100644 --- a/docs/templates/dnd5e/QuteBackground.md +++ b/docs/templates/dnd5e/QuteBackground.md @@ -6,12 +6,20 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### fluffImages -List of images for this background (as [ImageRef](../ImageRef.md)) +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -33,6 +41,21 @@ Formatted text listing other prerequisite conditions (optional) List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteBastion/README.md b/docs/templates/dnd5e/QuteBastion/README.md index c3dfa3b16..3d1c45910 100644 --- a/docs/templates/dnd5e/QuteBastion/README.md +++ b/docs/templates/dnd5e/QuteBastion/README.md @@ -6,12 +6,20 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath) ### fluffImages -List of images for this bastion (as [ImageRef](../../ImageRef.md), optional) +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -50,6 +58,21 @@ Formatted text listing other prerequisite conditions (optional) List of content superceded by this note (as [Reprinted](../../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteClass/HitPointDie.md b/docs/templates/dnd5e/QuteClass/HitPointDie.md new file mode 100644 index 000000000..10f641757 --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/HitPointDie.md @@ -0,0 +1,39 @@ +# HitPointDie + +Describes the hit point die used by the class. + +If referenced as a unit (ignoring inner attributes), it will render +formatted strings based on the class version (2024 or not). + +## Attributes + +[average](#average), [classic](#classic), [face](#face), [isClassic](#isclassic), [isSidekick](#issidekick), [name](#name), [number](#number), [sidekick](#sidekick) + + +### average + +The average value of a hit dice roll + +### classic + + +### face + +Die to roll (8, 10); This will be 0 for sidekicks + +### isClassic + +True if this is a 2014 class + +### isSidekick + +Explicit test for sidekick (alternate to 0 face) + +### name + + +### number + +How many dice to roll (pretty much always 1) + +### sidekick diff --git a/docs/templates/dnd5e/QuteClass/Multiclassing.md b/docs/templates/dnd5e/QuteClass/Multiclassing.md new file mode 100644 index 000000000..4637f39d4 --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/Multiclassing.md @@ -0,0 +1,58 @@ +# Multiclassing + +Describes the multiclassing information for the class. + +If referenced as a unit (ignoring inner attributes), it will render +formatted text describing multiclassing requirements and proficiencies. + +## Attributes + +[armor](#armor), [classic](#classic), [isClassic](#isclassic), [primaryAbility](#primaryability), [requirements](#requirements), [requirementsSpecial](#requirementsspecial), [skills](#skills), [text](#text), [tools](#tools), [weapons](#weapons) + + +### armor + +Armor proficiencies gained as formatted string +(optional) + +### classic + + +### isClassic + +True if this class is from the 2014 edition + +### primaryAbility + +Primary ability for multiclassing as formatted +string (optional) + +### requirements + +Prerequisites for multiclassing as formatted +string (optional) + +### requirementsSpecial + +Special prerequisites for multiclassing as +formatted string (optional) + +### skills + +Skill proficiencies gained as formatted string +(optional) + +### text + +Formatted text describing this multiclass +(optional) + +### tools + +Tool proficiencies gained as formatted string +(optional) + +### weapons + +Weapon proficiencies gained as formatted string +(optional) diff --git a/docs/templates/dnd5e/QuteClass/README.md b/docs/templates/dnd5e/QuteClass/README.md new file mode 100644 index 000000000..13086717b --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/README.md @@ -0,0 +1,104 @@ +# QuteClass + +5eTools class attributes (`class2md.txt`) + +Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). + +## Attributes + +[classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hitPointDie](#hitpointdie), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [primaryAbility](#primaryability), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath) + + +### classProgression + +Formatted callout containing class and feature progressions. + +### fluffImages + +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + +### hasSections + +True if the content (text) contains sections + +### hitDice + +Hit dice for this class as a single digit: 8 + +### hitPointDie + +Hit point die for this class as +[HitPointDie](HitPointDie.md) + +### hitRollAverage + +Average Hit dice roll as a single digit + +### labeledSource + +Formatted string describing the content's source(s): `_Source: _` + +### multiclassing + +Multiclassing requirements and proficiencies for this class as +[Multiclassing](Multiclassing.md) + +### name + +Note name + +### primaryAbility + +Formatted string describing the primary abilities for this class + +### reprintOf + +List of content superceded by this note (as [Reprinted](../../Reprinted.md)) + +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + +### source + +String describing the content's source(s) + +### sourceAndPage + +Book sources as list of [SourceAndPage](../../SourceAndPage.md) + +### startingEquipment + +Formatted text describing starting equipment as +[StartingEquipment](StartingEquipment.md) + +### tags + +Collected tags for inclusion in frontmatter + +### text + +Formatted text. For most templates, this is the bulk of the content. + +### vaultPath + +Path to this note in the vault diff --git a/docs/templates/dnd5e/QuteClass/StartingEquipment.md b/docs/templates/dnd5e/QuteClass/StartingEquipment.md new file mode 100644 index 000000000..2e47375f4 --- /dev/null +++ b/docs/templates/dnd5e/QuteClass/StartingEquipment.md @@ -0,0 +1,56 @@ +# StartingEquipment + +Describes the starting equipment for the class. + +If referenced as a unit (ignoring inner attributes), it will render +structured text describing starting proficiencies and equipment *2014* vs +*2024*. + +## Attributes + +[armor](#armor), [armorString](#armorstring), [classic](#classic), [equipment](#equipment), [isClassic](#isclassic), [joinOrDefault](#joinordefault), [proficiencies](#proficiencies), [savingThrows](#savingthrows), [skills](#skills), [tools](#tools), [weapons](#weapons) + + +### armor + +List of armor as formatted strings (links) + +### armorString + +Create a structured string describing armor training. +Slighly different formatting and joining for 2014 vs 2024 materials. + +### classic + + +### equipment + +List of equipment as formatted strings (links) + +### isClassic + +True if this class is from the 2014 edition + +### joinOrDefault + +Given a list of strings, return a formatted string with a conjunction. + +### proficiencies + +Formatted string of class proficiencies + +### savingThrows + +List of saving throws + +### skills + +List of skills as formatted strings (links) + +### tools + +List of tools as formatted strings (links) + +### weapons + +List of weapons as formatted strings (links) diff --git a/docs/templates/dnd5e/QuteDeck/README.md b/docs/templates/dnd5e/QuteDeck/README.md index 337c335bd..d9603bdce 100644 --- a/docs/templates/dnd5e/QuteDeck/README.md +++ b/docs/templates/dnd5e/QuteDeck/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[cardBack](#cardback), [cards](#cards), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[cardBack](#cardback), [cards](#cards), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### cardBack @@ -17,6 +17,18 @@ Image from the back of the card as [ImageRef](../../ImageRef.md) (optional) List of cards in the deck +### fluffImages + +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteDeity.md b/docs/templates/dnd5e/QuteDeity.md index 6386943c1..cc4e89dfb 100644 --- a/docs/templates/dnd5e/QuteDeity.md +++ b/docs/templates/dnd5e/QuteDeity.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[alignment](#alignment), [altNames](#altnames), [category](#category), [domains](#domains), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) +[alignment](#alignment), [altNames](#altnames), [category](#category), [domains](#domains), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) ### alignment @@ -25,6 +25,18 @@ Category of this deity: Lesser Idols, Prime Deities Category of this deity: Nature, Tempest +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -53,6 +65,21 @@ Province of this deity: Discovery, Luck, Storms, Travel, ... List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteFeat.md b/docs/templates/dnd5e/QuteFeat.md index 07c4bf4a2..bbbbcd922 100644 --- a/docs/templates/dnd5e/QuteFeat.md +++ b/docs/templates/dnd5e/QuteFeat.md @@ -6,9 +6,21 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Formatted text listing other prerequisite conditions (optional) List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteHazard.md b/docs/templates/dnd5e/QuteHazard.md index 7637565dc..0edfaf173 100644 --- a/docs/templates/dnd5e/QuteHazard.md +++ b/docs/templates/dnd5e/QuteHazard.md @@ -6,9 +6,21 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -29,6 +41,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteItem/README.md b/docs/templates/dnd5e/QuteItem/README.md index 081d054b1..6727caa13 100644 --- a/docs/templates/dnd5e/QuteItem/README.md +++ b/docs/templates/dnd5e/QuteItem/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [source](#source), [sourceAndPage](#sourceandpage), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) +[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) ### armorClass @@ -35,7 +35,15 @@ Formatted string of item details. Will include some combination of tier, rarity, ### fluffImages -List of images for this item as [ImageRef](../../ImageRef.md) +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -73,6 +81,21 @@ List of content superceded by this note (as [Reprinted](../../Reprinted.md)) Detailed information about this item as [Variant](Variant.md) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteMonster/README.md b/docs/templates/dnd5e/QuteMonster/README.md index 8b1bff4b2..00d815372 100644 --- a/docs/templates/dnd5e/QuteMonster/README.md +++ b/docs/templates/dnd5e/QuteMonster/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml @@ -66,12 +66,20 @@ Formatted text describing the creature's environment. Usually a single word. ### fluffImages -List of [ImageRef](../../ImageRef.md) related to the creature +List of images as [ImageRef](../../ImageRef.md) (optional) ### fullType Creature type (lowercase) and subtype if present: `{resource.type} ({resource.subtype})` +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -163,6 +171,21 @@ Creature ability scores as [AbilityScores](../AbilityScores.md) Comma-separated string of creature senses (if present). +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### size Creature size (capitalized) diff --git a/docs/templates/dnd5e/QuteObject.md b/docs/templates/dnd5e/QuteObject.md index 8fae1d303..fcc39cba8 100644 --- a/docs/templates/dnd5e/QuteObject.md +++ b/docs/templates/dnd5e/QuteObject.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml @@ -50,7 +50,15 @@ Creature type (lowercase); optional ### fluffImages -List of [ImageRef](../ImageRef.md) related to the creature +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -108,6 +116,21 @@ Object ability scores as [AbilityScores](AbilityScores.md)) Comma-separated string of object senses (if present). +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### size Object size (capitalized) diff --git a/docs/templates/dnd5e/QutePsionic.md b/docs/templates/dnd5e/QutePsionic.md index 258e320e7..438cbf891 100644 --- a/docs/templates/dnd5e/QutePsionic.md +++ b/docs/templates/dnd5e/QutePsionic.md @@ -6,13 +6,25 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[focus](#focus), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [focus](#focus), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + ### focus Psionic focus (string) +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteRace.md b/docs/templates/dnd5e/QuteRace.md index 150c9b433..5ff714a2e 100644 --- a/docs/templates/dnd5e/QuteRace.md +++ b/docs/templates/dnd5e/QuteRace.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [description](#description), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) +[ability](#ability), [description](#description), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) ### ability @@ -19,7 +19,15 @@ Formatted text describing the race. Optional. Same as {resource.text} ### fluffImages -List of images for this race (as [ImageRef](../ImageRef.md)) +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -37,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### size Size: Small or Medium diff --git a/docs/templates/dnd5e/QuteReward.md b/docs/templates/dnd5e/QuteReward.md index bbe12babe..b6af1f0d2 100644 --- a/docs/templates/dnd5e/QuteReward.md +++ b/docs/templates/dnd5e/QuteReward.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [detail](#detail), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[ability](#ability), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### ability @@ -17,6 +17,18 @@ Description of special ability granted by this reward, if defined separately. Th Reward detail string (similar to item detail). May include the reward type and rarity if either are defined. +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -33,6 +45,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### signatureSpells Formatted text describing sigature spells. Not commonly used. diff --git a/docs/templates/dnd5e/QuteSpell.md b/docs/templates/dnd5e/QuteSpell.md index 418bb5fe7..4851be787 100644 --- a/docs/templates/dnd5e/QuteSpell.md +++ b/docs/templates/dnd5e/QuteSpell.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) +[classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) ### classList @@ -27,7 +27,15 @@ Formatted: spell range ### fluffImages -List of images for this spell (as [ImageRef](../ImageRef.md)) +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -61,6 +69,21 @@ true for ritual spells Spell school +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteSubclass.md b/docs/templates/dnd5e/QuteSubclass.md index 8d70d7f29..63bdd02e5 100644 --- a/docs/templates/dnd5e/QuteSubclass.md +++ b/docs/templates/dnd5e/QuteSubclass.md @@ -6,13 +6,25 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classProgression](#classprogression), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### classProgression A pre-foramatted markdown callout describing subclass spell or feature progression +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -41,6 +53,21 @@ Source of the parent class (abbreviation) List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/docs/templates/dnd5e/QuteVehicle/README.md b/docs/templates/dnd5e/QuteVehicle/README.md index 142e5a78e..0a8548a7c 100644 --- a/docs/templates/dnd5e/QuteVehicle/README.md +++ b/docs/templates/dnd5e/QuteVehicle/README.md @@ -10,7 +10,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[action](#action), [fluffImages](#fluffimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) +[action](#action), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) ### action @@ -19,7 +19,15 @@ List of vehicle actions as a collection of [NamedText](../../NamedText.md) ### fluffImages -List of [ImageRef](../../ImageRef.md) related to the creature +List of images as [ImageRef](../../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present ### hasSections @@ -75,6 +83,21 @@ Ship capacity and pace attributes as [ShipCrewCargoPace](ShipCrewCargoPace.md). Ship sections and traits as [ShipAcHp](ShipAcHp.md) (hull, sails, oars, .. ) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### sizeDimension Ship size and dimensions. Used by Ship, Infernal War Machine diff --git a/docs/templates/dnd5e/README.md b/docs/templates/dnd5e/README.md index 35e4c2545..51f427436 100644 --- a/docs/templates/dnd5e/README.md +++ b/docs/templates/dnd5e/README.md @@ -15,7 +15,7 @@ This data object provides a default mechanism for creating a marked up string based on the attributes that are present. - [QuteBackground](QuteBackground.md): 5eTools background attributes (`background2md.txt`). - [QuteBastion](QuteBastion/README.md): 5eTools background attributes (`bastion2md.txt`). -- [QuteClass](QuteClass.md): 5eTools class attributes (`class2md.txt`) +- [QuteClass](QuteClass/README.md): 5eTools class attributes (`class2md.txt`) Extension of [Tools5eQuteBase](Tools5eQuteBase.md). - [QuteDeck](QuteDeck/README.md): 5eTools deck attributes (`deck2md.txt`) diff --git a/docs/templates/dnd5e/Tools5eQuteBase.md b/docs/templates/dnd5e/Tools5eQuteBase.md index ea39f7144..99bc792bd 100644 --- a/docs/templates/dnd5e/Tools5eQuteBase.md +++ b/docs/templates/dnd5e/Tools5eQuteBase.md @@ -8,9 +8,21 @@ for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### fluffImages + +List of images as [ImageRef](../ImageRef.md) (optional) + +### hasImages + +Return true if any images are present + +### hasMoreImages + +Return true if more than one image is present + ### hasSections True if the content (text) contains sections @@ -27,6 +39,21 @@ Note name List of content superceded by this note (as [Reprinted](../Reprinted.md)) +### showAllImages + +Return embedded wikilinks for all images +If there is more than one, they will be displayed in a gallery. + +### showMoreImages + +Return embedded wikilinks for all but the first image +If there is more than one, they will be displayed in a gallery. + +### showPortraitImage + +Return an embedded wikilink to the first image +Will have the "right" anchor tag. + ### source String describing the content's source(s) diff --git a/examples/templates/tools5e/images-background2md.txt b/examples/templates/tools5e/images-background2md.txt index e67ed4b4f..edcd1404e 100644 --- a/examples/templates/tools5e/images-background2md.txt +++ b/examples/templates/tools5e/images-background2md.txt @@ -11,17 +11,14 @@ aliases: ["{resource.name}"] --- # {resource.name} *Source: {resource.source}* -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} {#if resource.prerequisite} ***Prerequisites*** {resource.prerequisite} {/if} {resource.text} -{#if resource.fluffImages.size > 1 } +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each} +{resource.showMoreImages} {/if} diff --git a/examples/templates/tools5e/images-class2md.txt b/examples/templates/tools5e/images-class2md.txt new file mode 100644 index 000000000..9772a3894 --- /dev/null +++ b/examples/templates/tools5e/images-class2md.txt @@ -0,0 +1,47 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +*Source: {resource.source}* + +{#if resource.classProgression } +{resource.classProgression} + +{/if} +{#if resource.hasImages }{resource.showPortraitImage} + +{/if} +## Hit Points + +{#if resource.hitDice } +- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level +- **Hit Points at First Level:** {resource.hitDice} + CON +- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON (minimum of 1) +{#else} +- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.) +- **Hit Points at First Level:** *x* + CON +- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1) +{/if} + +## Starting {resource.name} + +{resource.startingEquipment} + +{#if resource.multiclassing } +## Multiclassing {resource.name} + +{resource.multiclassing} +{/if}{#if resource.hasMoreImages } + +{resource.showMoreImages} + +{/if} +{resource.text} diff --git a/examples/templates/tools5e/images-item2md.txt b/examples/templates/tools5e/images-item2md.txt index 18721da8a..9b0552f45 100644 --- a/examples/templates/tools5e/images-item2md.txt +++ b/examples/templates/tools5e/images-item2md.txt @@ -15,9 +15,7 @@ aliases: --- # {resource.name} {#if resource.detail }*{resource.detail}* -{/if}{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{/if}{#if resource.hasImages }{resource.showPortraitImage}{/if} {#if resource.prerequisite} - **Prerequisites**: {resource.prerequisite} @@ -44,12 +42,10 @@ aliases: {/if}{#if resource.text } {resource.text} -{/if} -{#if resource.fluffImages.size > 1 } +{/if}{#if resource.hasMoreImages } + +{resource.showMoreImages} -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} - -{/if}{/each} {/if}{#if resource.variants } **Variants**: diff --git a/examples/templates/tools5e/images-monster2md.txt b/examples/templates/tools5e/images-monster2md.txt index 7c0cc5048..5fe5d7be4 100644 --- a/examples/templates/tools5e/images-monster2md.txt +++ b/examples/templates/tools5e/images-monster2md.txt @@ -13,18 +13,14 @@ aliases: ["{resource.name}"] *Source: {resource.source}* {#if resource.description } -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -![{first.title}]({first.vaultPath}#right) -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} {resource.description} +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0} -{it.getEmbeddedLink} -{/if}{/each} -{#else} -{#each resource.fluffImages} -{it.getEmbeddedLink} -{/each} +{resource.showMoreImages} +{/if} +{#else if resource.hasImages} +{resource.showAllImages} {/if}{#if resource.hasSections } ## Statblock diff --git a/examples/templates/tools5e/images-object2md.txt b/examples/templates/tools5e/images-object2md.txt index f8d09dd96..5776ccbce 100644 --- a/examples/templates/tools5e/images-object2md.txt +++ b/examples/templates/tools5e/images-object2md.txt @@ -9,20 +9,20 @@ tags: aliases: ["{resource.name}"] --- # {resource.name} -*Source: {resource.source}* +*Source: {resource.source}* {#if resource.text } -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage} + +{/if} {resource.text} -{#each resource.fluffImages}{#if it_index != 0} -{it.getEmbeddedLink()} -{/if}{/each} -{#else} -{#each resource.fluffImages} -{it.getEmbeddedLink()} -{/each} +{#if resource.hasMoreImages } + +{resource.showMoreImages} +{/if} +{#else if resource.hasImages } +{resource.showAllImages} + {/if}{#if resource.hasSections } ## Statblock diff --git a/examples/templates/tools5e/images-race2md.txt b/examples/templates/tools5e/images-race2md.txt index 090910d98..08b76940a 100644 --- a/examples/templates/tools5e/images-race2md.txt +++ b/examples/templates/tools5e/images-race2md.txt @@ -11,9 +11,7 @@ aliases: ["{resource.name}"] --- # {resource.name} *Source: {resource.source}* -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} - **Ability Scores**: {resource.ability} {#if resource.type} @@ -35,8 +33,7 @@ aliases: ["{resource.name}"] {resource.description} {/if} -{#if resource.fluffImages.size > 1 } +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each} +{resource.showMoreImages} {/if} diff --git a/examples/templates/tools5e/images-spell2md.txt b/examples/templates/tools5e/images-spell2md.txt index 5f53b942f..f1f6131a0 100644 --- a/examples/templates/tools5e/images-spell2md.txt +++ b/examples/templates/tools5e/images-spell2md.txt @@ -18,9 +18,7 @@ aliases: ["{resource.name}"] # {resource.name} %%-- Embedded content starts on the next line. --%% *{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}* -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} - **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if} - **Range:** {resource.range} @@ -32,11 +30,8 @@ aliases: ["{resource.name}"] {#if resource.hasSections } ## Summary -{/if} -{#if resource.fluffImages.size > 1 } - -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each} +{/if}{#if resource.hasMoreImages } +{resource.showMoreImages} {/if}{#if resource.classes } **Classes**: {resource.classes} diff --git a/examples/templates/tools5e/images-subclass2md.txt b/examples/templates/tools5e/images-subclass2md.txt new file mode 100644 index 000000000..c47b319b5 --- /dev/null +++ b/examples/templates/tools5e/images-subclass2md.txt @@ -0,0 +1,31 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +*{resource.parentClassLink}{#if resource.subclassTitle}: {resource.subclassTitle}{/if}* +*Source: {resource.source}* + +{#if resource.classProgression } +{resource.classProgression} + +{/if} +{#if resource.text } +{#if resource.hasImages }{resource.showPortraitImage} + +{/if} +{resource.text} +{#if resource.hasMoreImages } + +{resource.showMoreImages} +{/if} +{#else if resource.hasImages} +{resource.showAllImages} +{/if} diff --git a/examples/templates/tools5e/images-vehicle2md.txt b/examples/templates/tools5e/images-vehicle2md.txt index e82a39971..ed733a721 100644 --- a/examples/templates/tools5e/images-vehicle2md.txt +++ b/examples/templates/tools5e/images-vehicle2md.txt @@ -12,20 +12,20 @@ aliases: ["{resource.name}"] %%-- Embedded content starts on the next line. --%% *Source: {resource.source}* {#if resource.text && !resource.isObject }{! ----- Fluff for non-OBJECT vehicles ----- !} -{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} -{first.getEmbeddedLink("right")} -{/let}{/if} +{#if resource.hasImages }{resource.showPortraitImage}{/if} {resource.text} -{#if resource.fluffImages.size > 1 } +{#if resource.hasMoreImages } -{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} -{/if}{/each}{/if} +{resource.showMoreImages} + +{/if} {#else}{! ----- Fluff images for OBJECT vehicles (or no text) ----- !} +{#if resource.hasImages } -{#each resource.fluffImages} -{it.getEmbeddedLink} -{/each} +{resource.showAllImages} + +{/if} {/if}{#if !resource.isObject && resource.hasSections } ## Statblock diff --git a/examples/templates/tools5e/mixed/mixed-background2md.txt b/examples/templates/tools5e/mixed/mixed-background2md.txt new file mode 100644 index 000000000..90cc64d4e --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-background2md.txt @@ -0,0 +1,26 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-background +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +{resource.text} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-class2md.txt b/examples/templates/tools5e/mixed/mixed-class2md.txt new file mode 100644 index 000000000..98a32812e --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-class2md.txt @@ -0,0 +1,44 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.classProgression } +{resource.classProgression} + +{/if} +## Hit Points + +{#if resource.hitDice } +- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level +- **Hit Points at First Level:** {resource.hitDice} + CON +- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON (minimum of 1) +{#else} +- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.) +- **Hit Points at First Level:** *x* + CON +- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1) +{/if} + +## Starting a {resource.name} + +{resource.startingEquipment} + +{#if resource.multiclassing } +## Multiclassing {resource.name} + +{resource.multiclassing} +{/if} + +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-deity2md.txt b/examples/templates/tools5e/mixed/mixed-deity2md.txt new file mode 100644 index 000000000..e05f7ef7a --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-deity2md.txt @@ -0,0 +1,36 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-deity +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"{#each resource.altNames}, "{it}"{/each}] +--- +# {resource.name} +{#if resource.image} +{resource.image.getEmbeddedLink("symbol")}{/if} + +{#if resource.altNames } +- **Alternate Names**: {#each resource.altNames}{it}{#if it_hasNext}, {/if}{/each} +{/if}{#if resource.alignment } +- **Alignment**: {resource.alignment} +{/if}{#if resource.category } +- **Category**: {resource.category} +{/if}{#if resource.domains } +- **Domains**: {resource.domains} +{/if}{#if resource.pantheon } +- **Pantheon**: {resource.pantheon} +{/if}{#if resource.province } +- **Province**: {resource.province} +{/if}{#if resource.symbol } +- **Symbol**: {resource.symbol} +{/if} + +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-feat2md.txt b/examples/templates/tools5e/mixed/mixed-feat2md.txt new file mode 100644 index 000000000..e48fdf637 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-feat2md.txt @@ -0,0 +1,27 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-feat +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.level || resource.prerequisite} +{#if resource.prerequisite} +***Prerequisites*** {resource.prerequisite} +{/if} +{#if resource.level} +***Level*** {resource.level} +{/if} + +{/if} +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-hazard2md.txt b/examples/templates/tools5e/mixed/mixed-hazard2md.txt new file mode 100644 index 000000000..5a7c50174 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-hazard2md.txt @@ -0,0 +1,21 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-hazard +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.hazardType }*{resource.hazardType}* +{/if}{#if resource.text } + +{resource.text} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-item2md.txt b/examples/templates/tools5e/mixed/mixed-item2md.txt new file mode 100644 index 000000000..d33bd191c --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-item2md.txt @@ -0,0 +1,51 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-item +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.detail }*{resource.detail}* +{/if}{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +{#if resource.armorClass } +- **Armor Class**: {resource.armorClass} +{#else if resource.damage }{#if resource.damage2h } +- **Damage**: + - One-handed: {resource.damage} + - Two-handed: {resource.damage2h} +{#else} +- **Damage**: {resource.damage} +{/if}{#if resource.range } +- **Range**: {resource.range} +{/if}{/if}{#if resource.properties } +- **Properties**: {resource.properties} +{/if}{#if resource.strengthRequirement } +- **Strength**: Requires {resource.strengthRequirement} STR. +{/if}{#if resource.stealthPenalty } +- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks. +{/if} +- **Cost**: {#if resource.cost }{resource.cost}{#else}⏤{/if} +- **Weight**: {#if resource.weight }{resource.weight} lbs.{#else}⏤{/if} +{#if resource.text } + +{resource.text} +{/if} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} + +{/if}{/each} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} + diff --git a/examples/templates/tools5e/mixed/mixed-monster2md.txt b/examples/templates/tools5e/mixed/mixed-monster2md.txt new file mode 100644 index 000000000..25058a62e --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-monster2md.txt @@ -0,0 +1,113 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-monster +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +statblock: true +statblock-link: "#^statblock" +{resource.5eInitiativeYaml} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.description } +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +![{first.title}]({first.vaultPath}#right) +{/let}{/if} +{resource.description} + +{#each resource.fluffImages}{#if it_index != 0} +{it.getEmbeddedLink} +{/if}{/each} +{#else} +{#each resource.fluffImages} +{it.getEmbeddedLink} +{/each} +{/if}{#if resource.hasSections } + +## Statblock +{/if} + +```ad-statblock +title: {resource.name}{#if resource.token} +![{resource.token.title}]({resource.token.vaultPath}#token){/if} +*{resource.size} {resource.fullType}, {resource.alignment}* + +- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if} +- **Hit Points** {resource.hp} {#if resource.hitDice }({resource.hitDice}){/if} {#if resource.hpText }({resource.hpText}){/if} +- **Speed** {resource.speed} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +- **Proficiency Bonus** {resource.pb} +- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if} +- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if} +- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive} +{#if resource.vulnerable } +- **Damage Vulnerabilities** {resource.vulnerable} +{/if}{#if resource.resist} +- **Damage Resistances** {resource.resist} +{/if}{#if resource.immune} +- **Damage Immunities** {resource.immune} +{/if}{#if resource.conditionImmune} +- **Condition Immunities** {resource.conditionImmune} +{/if} +- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if} +- **Challenge** {resource.cr} +{#if resource.trait} + +## Traits +{#for trait in resource.trait} + +{#if trait.name }***{trait.name}.*** {/if}{trait.desc} +{/for}{/if}{#if resource.spellcasting}{#for spellcasting in resource.spellcasting} + +***{spellcasting.name}.*** {spellcasting.desc} +{/for}{/if}{#if resource.action} + +## Actions +{#for action in resource.action} + +{#if action.name }***{action.name}.*** {/if}{action.desc} +{/for}{/if}{#if resource.bonusAction} + +## Bonus Actions +{#for bonusAction in resource.bonusAction} + +{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc} +{/for}{/if}{#if resource.reaction} + +## Reactions +{#for reaction in resource.reaction} + +{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc} +{/for}{/if}{#if resource.legendary} + +## Legendary Actions +{#for legendary in resource.legendary} + +{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc} +{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup} + +## {group.name} + +{group.desc} +{/for}{/if} +``` +^statblock +{#if resource.environment } + +## Environment + +{resource.environment} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-object2md.txt b/examples/templates/tools5e/mixed/mixed-object2md.txt new file mode 100644 index 000000000..9dfaed906 --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-object2md.txt @@ -0,0 +1,65 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-object +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for}{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.text } +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} +{resource.text} +{#each resource.fluffImages}{#if it_index != 0} +{it.getEmbeddedLink()} +{/if}{/each} +{#else} +{#each resource.fluffImages} +{it.getEmbeddedLink()} +{/each} +{/if}{#if resource.hasSections } +## Statblock + +{/if} +```ad-statblock +title: {resource.name}{#if resource.token} +![{resource.token.title}]({resource.token.vaultPath}#token){/if} +*{resource.size} {resource.objectType}{#if resource.creatureType } ({resource.creatureType}){/if}* + +- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if} +- **Hit Points** {resource.hp or ' '} {#if resource.hpText }({resource.hpText}){/if} +- **Speed** {resource.speed} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{#if resource.senses } +- **Senses** {resource.senses} +{/if}{#if resource.vulnerable } +- **Damage Vulnerabilities** {resource.vulnerable} +{/if}{#if resource.resist} +- **Damage Resistances** {resource.resist} +{/if}{#if resource.immune} +- **Damage Immunities** {resource.immune} +{/if}{#if resource.conditionImmune} +- **Condition Immunities** {resource.conditionImmune} +{/if} +{#if resource.action} + +## Actions +{#for action in resource.action} + +{#if action.name }***{action.name}.*** {/if}{action.desc} +{/for}{/if} +``` +^statblock{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-race2md.txt b/examples/templates/tools5e/mixed/mixed-race2md.txt new file mode 100644 index 000000000..467fb1cfa --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-race2md.txt @@ -0,0 +1,46 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-race +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} + +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +- **Ability Scores**: {resource.ability} +{#if resource.type} +- **Type**: {resource.type} +{/if} +- **Size**: {resource.size} +- **Speed**: {resource.speed} +{#if resource.spellcasting} +- **Spellcasting**: {resource.spellcasting} +{/if} + +## Traits + +{resource.traits} +{#if resource.description} + +## Description + +{resource.description} + +{/if} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-reward2md.txt b/examples/templates/tools5e/mixed/mixed-reward2md.txt new file mode 100644 index 000000000..043f973dd --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-reward2md.txt @@ -0,0 +1,24 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-reward +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +{#if resource.detail }*{resource.detail}* +{/if}{#if resource.signatureSpells } + +- **Signature Spells**: {resource.signatureSpells} +{/if}{#if resource.text } + +{resource.text} +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-spell2md.txt b/examples/templates/tools5e/mixed/mixed-spell2md.txt new file mode 100644 index 000000000..504ffb77b --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-spell2md.txt @@ -0,0 +1,42 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-spell +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +%%-- Embedded content starts on the next line. --%% +*{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}* +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +- **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if} +- **Range:** {resource.range} +- **Components:** {resource.components} +- **Duration:** {resource.duration} + +{resource.text} + +{#if resource.hasSections } +## Summary + +{/if} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each} + +{/if}{#if resource.classes } +**Classes**: {resource.classes} + +{/if}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-subclass2md.txt b/examples/templates/tools5e/mixed/mixed-subclass2md.txt new file mode 100644 index 000000000..0b92cca9b --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-subclass2md.txt @@ -0,0 +1,24 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-class +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for} +{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +*{resource.parentClassLink}{#if resource.subclassTitle}: {resource.subclassTitle}{/if}* + +{#if resource.classProgression } +{resource.classProgression} + +{/if} + +{resource.text}{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/examples/templates/tools5e/mixed/mixed-vehicle2md.txt b/examples/templates/tools5e/mixed/mixed-vehicle2md.txt new file mode 100644 index 000000000..c86287a6a --- /dev/null +++ b/examples/templates/tools5e/mixed/mixed-vehicle2md.txt @@ -0,0 +1,114 @@ +--- +obsidianUIMode: preview +cssclasses: json5e-vehicle +{#if resource.tags } +tags: +{#for tag in resource.tags} +- {tag} +{/for}{/if} +aliases: ["{resource.name}"] +--- +# {resource.name} +%%-- Embedded content starts on the next line. --%% +*Source: {resource.source}* +{#if resource.text && !resource.isObject }{! ----- Fluff for non-OBJECT vehicles ----- !} +{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)} +{first.getEmbeddedLink("right")} +{/let}{/if} + +{resource.text} +{#if resource.fluffImages.size > 1 } + +{#each resource.fluffImages}{#if it_index != 0}{it.getEmbeddedLink} +{/if}{/each}{/if} +{#else}{! ----- Fluff images for OBJECT vehicles (or no text) ----- !} + +{#each resource.fluffImages} +{it.getEmbeddedLink} +{/each} + +{/if}{#if !resource.isObject && resource.hasSections } +## Statblock + +{/if} +```ad-statblock +title: {resource.name}{#if resource.token} +![{resource.token.title}]({resource.token.vaultPath}#token){/if} +*{resource.sizeDimension}; {resource.terrain}* +{#if resource.shipCrewCargoPace} + +{resource.shipCrewCargoPace} +{/if}{#if resource.isObject }{! ----- BEGIN OBJECT (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.text } + +{resource.text} + +{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isCreature }{! ----- BEGIN CREATURE (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isWarMachine }{! ----- BEGIN INFERNAL WAR MACHINE (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isSpelljammer }{! ----- BEGIN SPELLJAMMER (type) ----- !} +{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{#else if resource.isShip }{! ----- BEGIN SHIP (type) ----- !} +{#if resource.scores} + +|STR|DEX|CON|INT|WIS|CHA| +|:---:|:---:|:---:|:---:|:---:|:---:| +|{resource.scores}| + +{/if}{#if resource.immuneResist } +{resource.immuneResist} +{/if}{#if resource.action} + +## Actions +{#each resource.action} + +{it} +{/each}{/if}{#if resource.shipSections}{#each resource.shipSections} + +{it} +{/each}{/if} +{/if}{! END SHIP (type) !} +``` +^statblock{#if resource.source } + +## Sources + +*{resource.source}*{/if} diff --git a/src/main/java/dev/ebullient/convert/StringUtil.java b/src/main/java/dev/ebullient/convert/StringUtil.java index 89238dbce..028861265 100644 --- a/src/main/java/dev/ebullient/convert/StringUtil.java +++ b/src/main/java/dev/ebullient/convert/StringUtil.java @@ -373,6 +373,20 @@ public static String toOrdinal(String level) { } } + public static String toAnchorTag(String x) { + return x.replace(" ", "%20") + .replace(":", "") + .replace(".", "") + .replace('‑', '-'); + } + + // markdown link to href + public static String markdownLinkToHtml(String x) { + return x.replaceAll("\\[([^\\]]+)\\]\\(([^\\s)]+)(?:\\s\"[^\"]*\")?\\)", + "$1"); + + } + /** * A {@link java.util.stream.Collector} which converts the elements to strings, and joins the non-empty, non-null * strings into a single string. Allows providing an optional final delimiter that will be inserted before the diff --git a/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java b/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java index 179ac1ae3..8590dad67 100644 --- a/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java +++ b/src/main/java/dev/ebullient/convert/config/TtrpgConfig.java @@ -12,6 +12,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Consumer; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -117,6 +118,15 @@ public static void includeAdditionalSource(String src) { } } + public static void addReferenceEntries(Consumer callback) { + if (datasource == Datasource.tools5e) { + JsonNode srdEntries = TtrpgConfig.activeGlobalConfig("srdEntries"); + for (JsonNode property : ConfigKeys.properties.iterateArrayFrom(srdEntries)) { + callback.accept(property); + } + } + } + public static class ImageRoot { final String internalImageRoot; final boolean copyInternal; diff --git a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java index 7d7bc9133..6cc264313 100644 --- a/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java +++ b/src/main/java/dev/ebullient/convert/io/MarkdownWriter.java @@ -71,7 +71,8 @@ public void writeFiles(Path basePath, List elements) { for (Map.Entry> pathEntry : pathMap.entrySet()) { if (pathEntry.getValue().size() > 1) { - tui.warnf("Conflict: several entries would write to the same file:\n %s", + tui.warnf("Conflict: several entries would write to the same file: (%s)\n %s", + pathEntry.getKey().fileName, pathEntry.getValue().stream().map(x -> String.format("[%s]: %s", x.getName(), x.sources().getKey())) diff --git a/src/main/java/dev/ebullient/convert/io/Tui.java b/src/main/java/dev/ebullient/convert/io/Tui.java index 09bcf75dc..37d5af99d 100644 --- a/src/main/java/dev/ebullient/convert/io/Tui.java +++ b/src/main/java/dev/ebullient/convert/io/Tui.java @@ -173,13 +173,6 @@ public static String slugify(String s) { return slugifier().slugify(s); } - public static String toAnchorTag(String x) { - return x.replace(" ", "%20") - .replace(":", "") - .replace(".", "") - .replace('‑', '-'); - } - static final boolean picocliDebugEnabled = "DEBUG".equalsIgnoreCase(System.getProperty("picocli.trace")); Ansi ansi; @@ -324,6 +317,12 @@ private void verboseMsg(Msg msgWrap, String output, Object... params) { } } + public void log(Throwable t, boolean keepException) { + if (log != null) { + log.println(captureStackTrace(t, keepException)); + } + } + public void logf(String output, Object... params) { logf(Msg.NOOP, output, params); } @@ -380,7 +379,7 @@ private void error(Throwable ex, String errorMsg) { .replace("java.nio.file.NoSuchFileException: ", "File not found: ")); errLine(message, colors.errorText(message)); if (ex != null && log != null) { - ex.printStackTrace(log); + log.println(captureStackTrace(ex, true)); } } @@ -505,7 +504,6 @@ private void copyRemoteImage(ImageRef image, Path targetPath) { public boolean readFile(Path p, List fixes, BiConsumer callback) { inputRoot.add(p.getParent().toAbsolutePath()); try { - progressf("Reading %s", p); File f = p.toFile(); String contents = Files.readString(p); for (Fix fix : fixes) { @@ -644,4 +642,24 @@ public static JsonNode readTreeFromResource(String resource) { public static String jsonStringify(Object o) { return Tui.MAPPER.valueToTree(o).toPrettyString(); } + + public static String captureStackTrace(Throwable t, boolean keepException) { + var stackTrace = t.getStackTrace(); + if (stackTrace == null || stackTrace.length == 0) { + return keepException ? t.toString() : ""; + } + StringBuilder sb = new StringBuilder(); + if (keepException) { + sb.append(Msg.DEBUG.wrap(t.toString())).append("\n"); + } else { + sb.append(Msg.DEBUG.wrap(t.getMessage())).append("\n"); + } + for (StackTraceElement e : stackTrace) { + if (e.getClassName().startsWith("picocli")) { + break; + } + sb.append("\tat ").append(e.toString()).append("\n"); + } + return sb.toString(); + } } diff --git a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java index 6968b9994..5fa21ed74 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java @@ -33,6 +33,12 @@ default void addIntegerUnlessEmpty(Map map, String key, Integer } } + default void maybeAddBlankLine(List content) { + if (content.size() > 0 && !content.get(content.size() - 1).isBlank()) { + content.add(""); + } + } + /** Remove leading '+' */ default Map mapOfNumbers(Map map) { Map result = new HashMap<>(); diff --git a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java index f258dfdf0..10d9e6072 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java @@ -186,20 +186,31 @@ default JsonNode getFieldFrom(JsonNode source, JsonNodeReader field) { default List getListOfStrings(JsonNode source, Tui tui) { JsonNode target = getFrom(source); - if (target == null) { + if (target == null || target.isNull()) { return List.of(); - } else if (target.isTextual()) { + } + if (target.isTextual()) { return List.of(target.asText()); } + if (target.isObject()) { + throw new IllegalArgumentException( + "Unexpected object when creating list of strings: %s".formatted( + source)); + } List list = fieldFromTo(source, Tui.LIST_STRING, tui); return list == null ? List.of() : list; } default Map getMapOfStrings(JsonNode source, Tui tui) { JsonNode target = getFrom(source); - if (target == null) { + if (target == null || target.isNull()) { return Map.of(); } + if (target.isTextual() || target.isArray()) { + throw new IllegalArgumentException( + "Unexpected text or array when creating map of strings: %s".formatted( + source)); + } Map map = fieldFromTo(source, Tui.MAP_STRING_STRING, tui); return map == null ? Map.of() : map; } @@ -387,10 +398,6 @@ default String value() { return name(); } - default String toAnchorTag(String x) { - return Tui.toAnchorTag(x); - } - default boolean isValueOfField(JsonNode source, JsonNodeReader field) { return matches(field.getTextOrEmpty(source)); } @@ -450,7 +457,7 @@ default ArrayNode ensureArrayIn(JsonNode target) { if (target == null) { return Tui.MAPPER.createArrayNode(); } - return target.withArray(this.nodeName()); + return target.withArrayProperty(this.nodeName()); } default JsonNode copyFrom(JsonNode source) { diff --git a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java index f951017c9..e80dfc21e 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java @@ -2,6 +2,7 @@ import static dev.ebullient.convert.StringUtil.isPresent; import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.nio.file.Path; import java.util.ArrayDeque; @@ -664,18 +665,21 @@ default Stream streamOfFieldNames(JsonNode source) { return StreamSupport.stream(iterableFieldNames(source).spliterator(), false); } + default Stream> streamProps(JsonNode source) { + return streamPropsExcluding(source, (JsonNodeReader[]) null); + } + default Stream> streamPropsExcluding(JsonNode source, JsonNodeReader... excludingKeys) { if (source == null || !source.isObject()) { return Stream.of(); } + if (excludingKeys == null || excludingKeys.length == 0) { + return source.properties().stream(); + } return source.properties().stream() .filter(e -> Arrays.stream(excludingKeys).noneMatch(s -> e.getKey().equalsIgnoreCase(s.name()))); } - default String toAnchorTag(String x) { - return Tui.toAnchorTag(x); - } - /** {@link #createLink(String, Path, String, String)} with an empty title */ default String createLink(String displayText, Path target, String anchor) { return createLink(displayText, target, anchor, null); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java index c82f3085e..81acbd8b9 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.Collection; import java.util.Comparator; @@ -12,6 +13,7 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; public record ItemMastery( String name, @@ -32,24 +34,19 @@ public String linkify(String linkText) { boolean included = isPresent(indexKey) ? index.isIncluded(indexKey) - : index.customRulesIncluded(); + : index.customContentIncluded(); return included ? "[%s](%sitem-mastery.md#%s)".formatted( - linkText, index.rulesVaultRoot(), Tui.toAnchorTag(name)) + linkText, index.rulesVaultRoot(), toAnchorTag(name)) : linkText; } public static final Comparator comparator = Comparator.comparing(ItemMastery::name); private static final Map masteryMap = new HashMap<>(); - public static ItemMastery fromKey(String key, Tools5eIndex index) { - String finalKey = index.getAliasOrDefault(key); - JsonNode node = index.getNode(finalKey); - return node == null ? null : fromNode(finalKey, node); - } - - public static ItemMastery fromNode(String key, JsonNode mastery) { + public static ItemMastery fromNode(JsonNode mastery) { + String key = TtrpgValue.indexKey.getTextOrEmpty(mastery); // Create the ItemType object once return masteryMap.computeIfAbsent(key, k -> { String name = SourceField.name.getTextOrEmpty(mastery); @@ -69,4 +66,8 @@ public static List asLinks(Collection itemMasteries) { .map(ItemMastery::linkify) .toList(); } + + public static void clear() { + masteryMap.clear(); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java index ea0455ba7..6d91c08a8 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.Collection; import java.util.Comparator; @@ -13,6 +14,7 @@ import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; @@ -37,12 +39,12 @@ public String linkify(String linkText) { boolean included = isPresent(indexKey) ? index.isIncluded(indexKey) - : index.customRulesIncluded(); + : index.customContentIncluded(); return included ? "[%s](%sitem-properties.md#%s)".formatted( linkText, index.rulesVaultRoot(), - Tui.toAnchorTag(isPresent(sectionName) ? sectionName : name)) + toAnchorTag(isPresent(sectionName) ? sectionName : name)) : linkText; } @@ -53,13 +55,12 @@ public String linkify(String linkText) { public static final ItemProperty SILVERED = ItemProperty.customProperty("Silvered", "Silvered Weapons", "="); public static final ItemProperty POISON = ItemProperty.customProperty("Poison", "="); - public static ItemProperty fromKey(String key, Tools5eIndex index) { - String finalKey = index.getAliasOrDefault(key); - JsonNode node = index.getNode(finalKey); - return node == null ? null : ItemProperty.fromNode(finalKey, node); - } - - public static ItemProperty fromNode(String key, JsonNode property) { + public static ItemProperty fromNode(JsonNode property) { + String key = TtrpgValue.indexKey.getTextOrEmpty(property); + if (key.isEmpty()) { + Tui.instance().warnf(Msg.NOT_SET.wrap("Index key not found for property %s"), property); + return null; + } // Create the ItemType object once return ItemProperty.propertyMap.computeIfAbsent(key, k -> { String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(property); @@ -138,6 +139,10 @@ public static ItemProperty customProperty(String name, String sectionName, Strin public static ItemProperty customProperty(String name, String abbreviation) { return ItemProperty.customProperty(name, name, abbreviation); } + + public static void clear() { + propertyMap.clear(); + } } // Parser.ITM_PROP_ABV__TWO_HANDED = "2H"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java index 4947d23e3..5e33e01f4 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.HashMap; import java.util.Map; @@ -10,6 +11,7 @@ import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; @@ -41,23 +43,22 @@ public String linkify(String linkText) { boolean included = isPresent(indexKey) ? index.isIncluded(indexKey) - : index.customRulesIncluded(); + : index.customContentIncluded(); return included ? "[%s](%sitem-types.md#%s)".formatted( - linkText, index.rulesVaultRoot(), Tui.toAnchorTag(name)) + linkText, index.rulesVaultRoot(), toAnchorTag(name)) : linkText; } public static final Map typeMap = new HashMap<>(); - public static ItemType fromKey(String key, Tools5eIndex index) { - String finalKey = index.getAliasOrDefault(key); - JsonNode node = index.getNode(finalKey); - return node == null ? null : fromNode(finalKey, node); - } - - public static ItemType fromNode(String typeKey, JsonNode typeNode) { + public static ItemType fromNode(JsonNode typeNode) { + String typeKey = TtrpgValue.indexKey.getTextOrEmpty(typeNode); + if (typeKey.isEmpty()) { + Tui.instance().warnf(Msg.NOT_SET.wrap("Index key not found for property %s"), typeNode); + return null; + } // Create the ItemType object once return typeMap.computeIfAbsent(typeKey, k -> { String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(typeNode); @@ -160,6 +161,10 @@ private static ItemTypeGroup mapGroup(String abbreviation, String lowercase, Jso } }; } + + public static void clear() { + typeMap.clear(); + } } // Parser.ITM_TYP_ABV__TREASURE = "$"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java index 55905d747..28b9a8597 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java @@ -56,8 +56,8 @@ protected Tools5eQuteBase buildQuteResource() { backgroundName, getSourceText(sources), listPrerequisites(rootNode), - String.join("\n", text), images, + String.join("\n", text), tags); } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java index 4322a79fd..5dd9fbd36 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java @@ -33,7 +33,7 @@ protected Tools5eQuteBase buildQuteResource() { List fluffImages = new ArrayList<>(); List text = getFluff(Tools5eIndexType.facilityFluff, "##", fluffImages); - appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + appendToText(text, rootNode, "##"); String type = BastionFields.facilityType.getTextOrThrow(rootNode); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java index cca389a8e..37f946648 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java @@ -1,23 +1,33 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.joinConjunct; +import static dev.ebullient.convert.StringUtil.markdownLinkToHtml; +import static dev.ebullient.convert.StringUtil.toAnchorTag; +import static dev.ebullient.convert.StringUtil.toTitleCase; +import static dev.ebullient.convert.StringUtil.uppercaseFirst; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.qute.QuteClass; +import dev.ebullient.convert.tools.dnd5e.qute.QuteClass.HitPointDie; +import dev.ebullient.convert.tools.dnd5e.qute.QuteClass.Multiclassing; +import dev.ebullient.convert.tools.dnd5e.qute.QuteClass.StartingEquipment; import dev.ebullient.convert.tools.dnd5e.qute.QuteSubclass; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -26,35 +36,22 @@ public class Json2QuteClass extends Json2QuteCommon { final static Map keyToClassFeature = new HashMap<>(); final Map> startingText = new HashMap<>(); - final List classFeatures = new ArrayList<>(); - final List subclasses = new ArrayList<>(); - boolean additionalFromBackground; + final boolean isSidekick; + final String decoratedClassName; final String classSource; final String subclassTitle; - final String decoratedClassName; + final String primaryAbility; String filename = null; + SidekickProficiencies sidekickProficiencies = null; Json2QuteClass(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) { super(index, type, jsonNode); - boolean pushed = parseState().push(getSources(), rootNode); // store state - try { - if (!isSidekick()) { - findClassHitDice(); - findStartingEquipment(); - findClassProficiencies(); - } - - decoratedClassName = type.decoratedName(jsonNode); - classSource = jsonNode.get("source").asText(); - subclassTitle = getTextOrEmpty(rootNode, "subclassTitle"); - - // class features can be text elements or object elements (classFeature field) - findClassFeatures(Tools5eIndexType.classfeature, jsonNode.get("classFeatures"), classFeatures, "classFeature"); - findSubclasses(); - } finally { - parseState().pop(pushed); // restore state - } + decoratedClassName = type.decoratedName(jsonNode); + classSource = jsonNode.get("source").asText(); + isSidekick = ClassFields.isSidekick.booleanOrDefault(jsonNode, false); + subclassTitle = ClassFields.subclassTitle.getTextOrEmpty(jsonNode); + primaryAbility = buildPrimaryAbility(); } @Override @@ -66,691 +63,955 @@ public String getFileName() { @Override protected QuteClass buildQuteResource() { + // Some compensation for sidekicks. Do this first + List features = findClassFeatures( + Tools5eIndexType.classfeature, + ClassFields.classFeatures.ensureArrayIn(rootNode), + ClassFields.classFeature); + Tags tags = new Tags(getSources()); tags.add("class", getName()); - // May have fluff text. Does not have separate fluff images - List text = getFluff(Tools5eIndexType.classFluff, "##", new ArrayList<>()); + List progression = buildProgressionTable(features, rootNode, ClassFields.classTableGroups); + + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.classFluff, "##", images); maybeAddBlankLine(text); text.add("## Class Features"); - for (ClassFeature cf : classFeatures) { + for (ClassFeature cf : features) { cf.appendText(this, text, getSources().primarySource()); } - addOptionalFeatureText(rootNode, text); - - List progression = new ArrayList<>(); - buildFeatureProgression(progression); - maybeAddBlankLine(progression); - buildClassProgression(rootNode, progression, "classTableGroups"); + addOptionalFeatureText(rootNode, getSources().primarySource(), text); return new QuteClass(getSources(), decoratedClassName, getSourceText(getSources()), - startingHitDice(), String.join("\n", progression), + primaryAbility, + buildHitDie(), buildStartingEquipment(), - buildStartMulticlassing(), + buildMulticlassing(), String.join("\n", text), + images, tags); } public List buildSubclasses() { List quteSc = new ArrayList<>(); - for (Subclass sc : subclasses) { - boolean pushed = parseState().push(sc.sources); - filename = Tools5eQuteBase.fixFileName(sc.getName(), sc.sources); + for (String scKey : ClassFields.subclassKeys.getListOfStrings(rootNode, tui())) { + JsonNode scNode = index.getNode(scKey); + Tools5eSources scSources = Tools5eSources.findSources(scKey); + String scName = scSources.getName(); + String scShortName = ClassFields.shortName.getTextOrDefault(scNode, scName); + + List scFeatures = findClassFeatures( + Tools5eIndexType.subclassFeature, + ClassFields.subclassFeatures.ensureArrayIn(scNode), + ClassFields.subclassFeature); + + filename = Tools5eQuteBase.fixFileName(scName, scSources); + boolean pushed = parseState().push(scSources); try { - Tags tags = new Tags(sc.sources); - tags.add("subclass", getName(), sc.shortName); + Tags tags = new Tags(scSources); + tags.add("subclass", getName(), scShortName); - if (tags.toString().contains("cleric")) { - tags.add("domain", sc.shortName); + if (getName().matches(".*[Cc]leric.*")) { + tags.add("domain", scShortName); } - List text = new ArrayList<>(); + List progression = buildProgressionTable(scFeatures, scNode, ClassFields.subclassTableGroups); + + List images = new ArrayList<>(); + List text = getFluff(scNode, Tools5eIndexType.subclassFluff, "##", images); text.add("## Class Features"); - for (ClassFeature scf : sc.classFeatures) { - scf.appendText(this, text, sc.sources.primarySource()); + for (ClassFeature scf : scFeatures) { + scf.appendText(this, text, scSources.primarySource()); } - addOptionalFeatureText(sc.subclassNode, text); - - List progression = new ArrayList<>(); - buildClassProgression(sc.subclassNode, progression, "subclassTableGroups"); + addOptionalFeatureText(scNode, scSources.primarySource(), text); - quteSc.add(new QuteSubclass(sc.sources, - sc.getName(), - getSourceText(sc.sources), + quteSc.add(new QuteSubclass(scSources, + scName, + getSourceText(scSources), getName(), // parentClassName String.format("[%s](%s.md)", decoratedClassName, - Tools5eQuteBase.getClassResource(getName(), sc.parentClassSource)), - sc.parentClassSource, // parentClassSource + Tools5eQuteBase.getClassResource(getName(), getSources().primarySource())), + getSources().primarySource(), subclassTitle, String.join("\n", progression), String.join("\n", text), + images, tags)); + } finally { parseState().pop(pushed); filename = null; } } - return quteSc; } - void buildFeatureProgression(List progression) { - List> row_levels = new ArrayList<>(21); - for (int i = 0; i < 21; i++) { - row_levels.add(new ArrayList<>()); + private ClassFeature getClassFeature(String featureKey, Tools5eIndexType featureType) { + return keyToClassFeature.computeIfAbsent(featureKey, k -> { + JsonNode featureNode = index().getOriginNoFallback(featureKey); + return new ClassFeature(featureType, featureKey, featureNode); + }); + } + + List findClassFeatures(Tools5eIndexType featureType, JsonNode featureElements, + ClassFields field) { + List features = new ArrayList<>(); + for (JsonNode featureNode : featureElements) { + + String featureKey = featureNode.isTextual() + ? featureType.fromTagReference(featureNode.asText()) + : featureType.fromTagReference(ClassFields.classFeature.getTextOrEmpty(featureNode)); + + var classFeature = getClassFeature(featureKey, featureType); + + if (isSidekick && "1".equals(classFeature.level()) + && "Bonus Proficiencies".equalsIgnoreCase(classFeature.getName())) { + // sidekick classes don't have startingProficiencies, they have a bonus + // proficiency feature instead. + sidekickProficiencies = new SidekickProficiencies(classFeature.cfNode()); + } + // Add to list of features (for content rendering later) + features.add(classFeature); } + return features; + } - Map> levelToFeature = classFeatures.stream() - .collect(Collectors.groupingBy(x -> Integer.valueOf(x.level))); + void addOptionalFeatureText(JsonNode optFeatures, String primarySource, List text) { + JsonNode optionalFeatureProgression = ClassFields.optionalfeatureProgression.getFrom(optFeatures); + if (optionalFeatureProgression == null) { + return; + } - // Headings are in row 0 - row_levels.get(0).add("Level"); - row_levels.get(0).add("PB"); - row_levels.get(0).add("Features"); - // Values - for (int level = 1; level < row_levels.size(); level++) { - final int featureLevel = level; - row_levels.get(level).add(JsonSource.levelToString(level)); - row_levels.get(level).add("+" + levelToPb(level)); + String relativePath = Tools5eIndexType.optionalFeatureTypes.getRelativePath(); + String source = SourceField.source.getTextOrDefault(optionalFeatureProgression, primarySource); - List features = levelToFeature.get(level); - if (features == null || features.isEmpty()) { - row_levels.get(level).add("⏤"); - } else { - row_levels.get(level).add(features.stream() - .map(x -> markdownLinkify(x.getName(), featureLevel)) - .collect(Collectors.joining(", "))); + maybeAddBlankLine(text); + text.add("## Optional Features"); + for (JsonNode ofp : iterableElements(optionalFeatureProgression)) { + for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) { + OptionalFeatureType oft = index.getOptionalFeatureType(featureType, source); + if (oft != null) { + maybeAddBlankLine(text); + text.add("> [!example]- " + oft.title); + text.add(String.format("> ![%s](%s%s/%s.md#%s)", + oft.title, + index().compendiumVaultRoot(), relativePath, + oft.getFilename(), + toAnchorTag(oft.title))); + text.add("^list-" + slugify(oft.title)); + } else { + tui().errorf( + Msg.UNRESOLVED, "Can not find optional feature type %s for progression. Source: %s; Reference: %s", + featureType, ofp, parseState().getSource()); + } } } + } + + String buildPrimaryAbility() { + // primary ability only exists in 2024 class versions + if (!ClassFields.primaryAbility.existsIn(rootNode)) { + return null; + } + + // Array of objects with multiple properties + List abilities = ClassFields.primaryAbility.streamProps(rootNode) + .map(e -> { + return streamPropsExcluding(rootNode) + .filter(x -> x.getValue().asBoolean()) + .map(x -> SkillOrAbility.format(x.getKey(), index(), getSources())) + .toList(); + }) + .map(l -> joinConjunct("and", l)) + .toList(); + return joinConjunct("or", abilities); + } + + HitPointDie buildHitDie() { + if (isSidekick) { + // Sidekicks do not have a hit die. Hit points depend on creature statblock + return new HitPointDie(getName(), 0, 0, this.sources.isClassic(), isSidekick); + } + JsonNode hdNode = ClassFields.hd.getFrom(rootNode); + if (hdNode != null) { + // both attributes are required. Default should not be necessary + return new HitPointDie( + getName(), + ClassFields.number.intOrDefault(hdNode, 1), + ClassFields.faces.intOrDefault(hdNode, 1), + this.sources.isClassic(), + isSidekick); + } + return null; + } + + StartingEquipment buildStartingEquipment() { + if (isSidekick) { + if (sidekickProficiencies == null) { + tui().warnf(Msg.UNKNOWN, "Sidekick class %s has no proficiencies", getName()); + return null; + } + return new StartingEquipment( + sidekickProficiencies.savingThrows(), + sidekickProficiencies.skills(), + sidekickProficiencies.weapons(), + sidekickProficiencies.tools(), + sidekickProficiencies.armor(), + "", + sources.isClassic()); + } + + List savingThrows = ClassFields.proficiency.streamFrom(rootNode) + .map(n -> SkillOrAbility.format(n.asText(), index(), getSources())) + .sorted() + .toList(); + + JsonNode startingProficiencies = ClassFields.startingProficiencies.getFrom(rootNode); - progression.addAll( - convertRowsToTable(row_levels, "Feature progression", - List.of("- PB: Proficiency Bonus"), - "feature-progression")); - maybeAddBlankLine(progression); + var armor = listOfArmorProficiencies(startingProficiencies); + var skills = listOfSkillProfiencies(startingProficiencies); + var tools = listOfToolProfiencies(startingProficiencies); + var weapons = listOfWeaponProfiencies(startingProficiencies); + + return new StartingEquipment(savingThrows, skills, weapons, tools, armor, + equipmentDescription(ClassFields.startingEquipment.getFrom(rootNode)), + sources.isClassic()); } - void buildClassProgression(JsonNode node, List progression, String field) { - if (!node.has(field)) { - return; + Multiclassing buildMulticlassing() { + JsonNode multiclassing = ClassFields.multiclassing.getFrom(rootNode); + if (multiclassing == null || multiclassing.isEmpty()) { + return null; } + // primary ablity only exists in 2024 class versions (or homebrew) + // requirements only exist in 2014 class versions (or homebrew) + String requirements = null; + if (ClassFields.requirements.existsIn(multiclassing)) { + List reqContents = new ArrayList<>(); - List> row_levels = new ArrayList<>(21); - for (int i = 0; i < 21; i++) { - row_levels.add(new ArrayList<>()); - if (i == 0) { - row_levels.get(i).add("Level"); + JsonNode reqNode = ClassFields.requirements.getFrom(multiclassing); + JsonNode orNode = ClassFields.or.getFrom(reqNode); + if (orNode == null) { + reqContents.add("**Ability Score Minimum:**" + abilityRequirements(reqNode, ", ")); } else { - row_levels.get(i).add(JsonSource.levelToString(i)); + reqContents.add("**Ability Score Minimum:**" + streamOf(orNode) + .map(n -> abilityRequirements(n, " or ")) + .collect(Collectors.joining("; "))); } + appendToText(reqContents, SourceField.entries.getFrom(reqNode), null); + requirements = String.join("\n", reqContents); } - for (JsonNode table : iterableElements(node.get(field))) { - // Headings - for (JsonNode c : iterableElements(table.get("colLabels"))) { - String label = c.asText(); - if (label.contains("|spells|")) { - row_levels.get(0).add( - label.substring(label.indexOf(" ") + 1, label.indexOf("|"))); - } else { - row_levels.get(0).add(replaceText(label)); + JsonNode reqSpecialNode = ClassFields.requirementsSpecial.getFrom(multiclassing); + String requirementsSpecial = reqSpecialNode == null + ? null + : replaceText(reqSpecialNode); + + JsonNode profGained = ClassFields.proficienciesGained.getFrom(multiclassing); + + List skillsGained = listOfSkillProfiencies(profGained); + List weaponsGained = listOfWeaponProfiencies(profGained); + List toolsGained = listOfToolProfiencies(profGained); + List armorGained = listOfArmorProficiencies(profGained); + + return new Multiclassing( + primaryAbility, + requirements, + requirementsSpecial, + join(", ", skillsGained), + join(", ", weaponsGained), + join(", ", toolsGained), + join(", ", armorGained), + flattenToString(SourceField.entries.getFrom(multiclassing), "\n"), + getSources().isClassic()); + } + + List buildProgressionTable(List features, JsonNode sourceNode, ClassFields field) { + List headings = new ArrayList<>(); + List spellCasting = new ArrayList<>(); + + Map levels = new HashMap<>(); + for (int i = 1; i < 21; i++) { + // Create LevelProgression for each level + // this sets level and proficiency bonus for level + levels.put(i, new LevelProgression(i)); + } + + for (ClassFeature feature : features) { + var lp = levels.get(Integer.valueOf(feature.level())); + lp.addFeature(feature); + } + + for (JsonNode tableNode : field.iterateArrayFrom(sourceNode)) { + if (ClassFields.rows.existsIn(tableNode)) { + // headings for other/middle columns + for (JsonNode label : ClassFields.colLabels.iterateArrayFrom(tableNode)) { + headings.add(markdownLinkToHtml(replaceText(label))); } - } - // Values - if (table.has("rows")) { - ArrayNode rows = table.withArray("rows"); - for (int i = 0; i < rows.size(); i++) { - int level = i + 1; - if (level >= row_levels.size()) { - tui().errorf("Badly formed class-progression table in %s: %s", sources, table.toString()); - break; + // values for other/middle columns + int i = 1; + for (JsonNode row : ClassFields.rows.iterateArrayFrom(tableNode)) { + var lp = levels.get(Integer.valueOf(i)); + for (JsonNode col : iterableElements(row)) { + lp.addValue(progressionColumnValue(col)); } - rows.get(i).forEach(c -> row_levels.get(level).add(columnValue(c))); + i++; } - } else if (table.has("rowsSpellProgression")) { - ArrayNode rows = table.withArray("rowsSpellProgression"); - for (int i = 0; i < rows.size(); i++) { - int level = i + 1; - if (level >= row_levels.size()) { - tui().errorf("Badly formed spell-progression table in %s: %s", sources, table.toString()); - break; + } else if (ClassFields.rowsSpellProgression.existsIn(tableNode)) { + // headings for other/middle columns + for (JsonNode label : ClassFields.colLabels.iterateArrayFrom(tableNode)) { + spellCasting.add(replaceText(label)); + } + int i = 1; + for (JsonNode row : ClassFields.rowsSpellProgression.iterateArrayFrom(tableNode)) { + var lp = levels.get(Integer.valueOf(i)); + for (JsonNode col : iterableElements(row)) { + lp.addSpellSlot(progressionColumnValue(col)); } - rows.get(i).forEach(c -> row_levels.get(level).add(columnValue(c))); + i++; } } } - progression.addAll(convertRowsToTable(row_levels, "Class progression", - List.of("- 1st-9th: Spell slots per level"), "class-progression")); + return progressionAsTable(headings, spellCasting, levels); } - List convertRowsToTable(List> row_levels, String title, List footer, String blockid) { + List progressionAsTable(List headings, + List spellCasting, + Map levels) { List text = new ArrayList<>(); - // Convert each row to markdown columns - row_levels.forEach(r -> text.add("| " + String.join(" | ", r) + " |")); - - // insert a header delimiting row (copy row 0, replace everything not a "|" with a "-") - text.add(1, text.get(0).replaceAll("[^|]", "-")); - - if (footer != null && !footer.isEmpty()) { - maybeAddBlankLine(text); - text.addAll(footer); - } + text.add("[!tldr] Class and Feature Progression"); + text.add(""); + text.add(""); + text.add(""); + // Top-level heading row to group spell casting columns + text.add("%s" + .formatted( + 3 + headings.size(), + spellCasting.isEmpty() + ? "" + : "" + .formatted(spellCasting.size()))); + + text.add( + "%s%s" + .formatted( + headings.isEmpty() + ? "" + : "", + spellCasting.isEmpty() + ? "" + : "")); + + text.add(""); + + for (int i = 1; i < 21; i++) { + var lp = levels.get(Integer.valueOf(i)); + text.add( + "%s%s" + .formatted( + lp.level, lp.pb, + join(", ", lp.features), + lp.values.isEmpty() + ? "" + : "", + spellCasting.isEmpty() + ? "" + : "")); + } + + text.add("
Spell Slots per Spell Level
LevelPBFeatures
" + join("", headings) + + "" + + join("", spellCasting) + "
%s%s%s
" + join("", lp.values) + + "" + + join("", lp.spellSlots) + "
"); // Move everything into a callout box text.replaceAll(s -> "> " + s); - - // add start of block - text.add(0, "> [!tldr]- " + title); - text.add(1, "> "); // must have a blank line before table starts - - text.add("^" + blockid); + text.add("^class-progession"); return text; } - String buildStartingEquipment() { - List startingEquipment = new ArrayList<>(); - startingEquipment.add(String.format("You are proficient with the following items%s.", - additionalFromBackground - ? ", in addition to any proficiencies provided by your race or background" - : "")); - maybeAddBlankLine(startingEquipment); - - if (startingText.containsKey("saves")) { - startingEquipment.add(String.format("- **Saving Throws**: %s", startingTextJoinOrDefault("saves", "none"))); - } - startingEquipment.add(String.format("- **Armor**: %s", startingTextJoinOrDefault("armor", "none"))); - startingEquipment.add(String.format("- **Weapons**: %s", startingTextJoinOrDefault("weapons", "none"))); - startingEquipment.add(String.format("- **Tools**: %s", startingTextJoinOrDefault("tools", "none"))); - startingEquipment.add(String.format("- **Skills**: %s", startingTextJoinOrDefault("skills", "none"))); - - if (!isSidekick()) { - maybeAddBlankLine(startingEquipment); - startingEquipment.add(String.format("You begin play with the following equipment%s.", - additionalFromBackground ? ", in addition to any equipment provided by your background" : "")); - maybeAddBlankLine(startingEquipment); - List equipment = startingText.get("equipment"); - if (equipment == null) { - startingEquipment.add("- None"); - } else { - startingEquipment.addAll(equipment); - } - String wealth = startingTextJoinOrDefault("wealth", ""); - if (!wealth.isEmpty()) { - maybeAddBlankLine(startingEquipment); - startingEquipment.add(String.format("Alternatively, you may start with %s gp and choose your own equipment.", - startingTextJoinOrDefault("wealth", "3d4 x 10"))); - } - } - return String.join("\n", startingEquipment); - } - - String buildStartMulticlassing() { - JsonNode multiclassing = rootNode.get("multiclassing"); - if (multiclassing == null) { - return null; + String progressionColumnValue(JsonNode c) { + if (c == null || c.isNull()) { + return "⏤"; } + if (c.isObject()) { + String type = ClassFields.type.getTextOrEmpty(c); + return switch (type) { + case "dice" -> { + List rolls = new ArrayList<>(); + for (JsonNode roll : ClassFields.toRoll.iterateArrayFrom(c)) { + rolls.add("%sd%s".formatted( + ClassFields.number.getTextOrEmpty(roll), + ClassFields.faces.getTextOrEmpty(roll))); + } + yield join(",", rolls); + } + case "bonus", "bonusSpeed" -> { + yield "+" + ClassFields.value.getTextOrEmpty(c); + } + default -> throw new IllegalArgumentException("Unknown column object value: " + c.toPrettyString()); + }; + } + String value = c.asText(); + return value.isEmpty() || value.equals("0") + ? "⏤" + : replaceText(value); + } + + String abilityRequirements(JsonNode reqNode, String joiner) { + return streamProps(reqNode) + .filter(n -> SkillOrAbility.fromTextValue(n.getKey()) != null) + .map(e -> "%s %s".formatted( + SkillOrAbility.format(e.getKey(), index(), getSources()), + e.getValue().asText())) + .sorted() + .collect(Collectors.joining(joiner)); + } + + List listOfArmorProficiencies(JsonNode containingNode) { + return ClassFields.armor.streamFrom(containingNode) + .map(n -> { + if (n.isTextual()) { + return armorToLink(n.asText()); + } + return armorToLink(ClassFields.full.getTextOrDefault(n, + ClassFields.proficiency.getTextOrEmpty(n))); + }) + .toList(); + } + + List listOfSkillProfiencies(JsonNode containingNode) { + if (isSidekick) { + return List.of(); + } + + return ClassFields.skills.streamFrom(containingNode) + // ARRAY of objects + .map(n -> { + String choose = null; + List baseSkills = new ArrayList<>(); + // any: integer + // choose: { + // count: integer, + // from: [ + // ... + // ] + // } + // skillName: true + for (var x : iterableFields(n)) { + if ("any".equals(x.getKey())) { + choose = skillChoices(List.of(), + x.getValue().asInt()); + } else if ("choose".equals(x.getKey())) { + choose = skillChoices( + ClassFields.from.getListOfStrings(x.getValue(), tui()), + ClassFields.count.intOrDefault(x.getValue(), 1)); + } else { + SkillOrAbility skill = index.findSkillOrAbility(n.asText(), getSources()); + if (skill != null) { + baseSkills.add(linkifySkill(skill)); + } + } + } - final List startMulticlass = new ArrayList<>(); - startMulticlass.add(String.format("To multiclass as a %s, you must meet the following prerequisites:", getName())); + String allSkills = joinConjunct("and", baseSkills); + if (baseSkills.size() > 0 && choose != null) { + return "%s; and %s".formatted(allSkills, choose); + } + return choose == null ? allSkills : choose; + }) + .toList(); + } - maybeAddBlankLine(startMulticlass); - JsonNode requirements = multiclassing.get("requirements"); - if (requirements == null) { - tui().warnf(Msg.NOT_SET, "No requirements specified to multiclass %s: %s", getSources().getKey(), multiclassing); - } else if (requirements.has("or")) { - List options = new ArrayList<>(); - requirements.get("or").get(0).fields().forEachRemaining(ability -> options.add(String.format("%s %s", - SkillOrAbility.format(ability.getKey(), index(), getSources()), ability.getValue().asText()))); - startMulticlass.add("- " + String.join(", or ", options)); - } else { - requirements.fields().forEachRemaining( - ability -> startMulticlass.add(String.format("- %s %s", - SkillOrAbility.format(ability.getKey(), index(), getSources()), ability.getValue().asText()))); - } - - JsonNode gained = multiclassing.get("proficienciesGained"); - if (gained != null) { - maybeAddBlankLine(startMulticlass); - startMulticlass.add("You gain the following proficiencies:"); - maybeAddBlankLine(startMulticlass); - - startMulticlass.add(String.format("- **Armor**: %s", - startingTextJoinOrDefault(gained, "armor"))); - startMulticlass.add(String.format("- **Weapons**: %s", - startingTextJoinOrDefault(gained, "weapons"))); - startMulticlass.add(String.format("- **Tools**: %s", - startingTextJoinOrDefault(gained, "tools"))); - - if (gained.has("skills")) { - startMulticlass.add(String.format("- **Skills**: %s", - classSkills(gained, sources))); - } - } - return String.join("\n", startMulticlass); + List listOfWeaponProfiencies(JsonNode containingNode) { + return ClassFields.weapons.streamFrom(containingNode) + .map(n -> { + if (ClassFields.optional.booleanOrDefault(n, false)) { + return "%s (optional)".formatted(replaceText(ClassFields.proficiency.getTextOrEmpty(n))); + } + String weaponType = n.asText(); + if (weaponType.matches("(?i)(simple|martial)")) { + return "%s weapons".formatted( + sources.isClassic() ? weaponType : toTitleCase(weaponType)); + } + return replaceText(weaponType); + }) + .toList(); } - boolean isSidekick() { - return getSources().getName().toLowerCase().contains("sidekick"); + List listOfToolProfiencies(JsonNode containingNode) { + return ClassFields.tools.streamFrom(containingNode) + .map(this::replaceText) + .toList(); } - void findClassHitDice() { - JsonNode hd = rootNode.get("hd"); - if (hd != null) { - put("hd", List.of(hd.get("faces").asText())); - } + String armorToLink(String armor) { + return armor + .replaceAll("^light", linkify(Tools5eIndexType.itemType, + sources.isClassic() ? "la|PHB|light armor" : "la|XPHB|Light armor")) + .replaceAll("^medium", linkify(Tools5eIndexType.itemType, + sources.isClassic() ? "ma|PHB|medium armor" : "ma|XPHB|Medium armor")) + .replaceAll("^heavy", linkify(Tools5eIndexType.itemType, + sources.isClassic() ? "ha|PHB|heavy armor" : "ha|XPHB|Heavy armor")) + .replaceAll("^shields?", linkify(Tools5eIndexType.item, + sources.isClassic() ? "shield|PHB|shields" : "shield|XPHB|Shields")); } - void findClassFeatures(Tools5eIndexType type, JsonNode arrayElement, List features, String fieldName) { - for (JsonNode cf : iterableElements(arrayElement)) { + String skillChoices(Collection skills, int numSkills) { + if (skills.isEmpty() || skills.size() >= 18) { + String link = "||skill%s".formatted(numSkills == 1 ? "" : "s"); + String linkToSkills = linkifyRules(Tools5eIndexType.skill, link, "skills"); + return sources.isClassic() + ? "choose any %s %s".formatted(numSkills, linkToSkills) + : "Choose %s %s".formatted(numSkills, linkToSkills); + } + + List formatted = skills.stream().map(x -> index.findSkillOrAbility(x, getSources())) + .filter(x -> x != null) + .sorted(SkillOrAbility.comparator) + .map(x -> linkifySkill(x)) + .toList(); + return sources.isClassic() + ? "choose %s from %s".formatted(numSkills, + joinConjunct(" and ", formatted)) + : "*Choose %s:* %s".formatted(numSkills, + joinConjunct(" or ", formatted)); + } + + String equipmentDescription(JsonNode startingEquipment) { + List text = new ArrayList<>(); - ClassFeature feature = findClassFeature(this, type, cf, fieldName); - if (feature == null) { - continue; + if (ClassFields.additionalFromBackground.existsIn(startingEquipment) + && ClassFields.defaultEquipment.existsIn(startingEquipment)) { + // Older default format. + if (ClassFields.additionalFromBackground.booleanOrDefault(startingEquipment, false)) { + text.add("You start with the following items, plus anything provided by your background."); + text.add(""); } - features.add(feature); + for (JsonNode item : ClassFields.defaultEquipment.iterateArrayFrom(startingEquipment)) { + text.add("- %s".formatted(replaceText(item))); + } - if (isSidekick() && "1".equals(feature.level) && feature.getName().equals("Bonus Proficiencies")) { - sidekickProficiencies(feature.cfNode); + String goldAlternative = ClassFields.goldAlternative.getTextOrNull(startingEquipment); + if (isPresent(goldAlternative)) { + text.add(""); + text.add("Alternatively, you may start with %s gp to buy your own equipment." + .formatted(replaceText(goldAlternative))); } + } else { + JsonNode entries = SourceField.entries.getFrom(startingEquipment); + appendToText(text, entries, null); } + return String.join("\n", text); } static ClassFeature findClassFeature(JsonSource converter, Tools5eIndexType type, JsonNode cf, String fieldName) { String lookup = cf.isTextual() ? cf.asText() : cf.get(fieldName).asText(); String finalKey = type.fromTagReference(lookup); - JsonNode featureJson = finalKey == null ? null : converter.index().resolveClassFeatureNode(finalKey); - - if (featureJson == null) { - return null; // skipped or not found - } - ClassFeature feature = keyToClassFeature.get(finalKey); if (feature == null) { - feature = new ClassFeature(); - feature.cfNode = featureJson; - feature.cfType = type; - feature.level = lookup.replaceAll(".*\\|(\\d+)\\|?.*", "$1"); - feature.cfSources = Tools5eSources.findSources(finalKey); - - keyToClassFeature.put(finalKey, feature); - } - // check inclusion of class feature sources - if (!converter.cfg().sourceIncluded(feature.cfSources)) { - return null; // skipped + JsonNode cfNode = converter.index().getNode(finalKey); + if (cfNode == null) { + return null; // skipped or not found + } + feature = new ClassFeature(type, finalKey, cfNode); + keyToClassFeature.putIfAbsent(finalKey, feature); } return feature; } - void findSubclasses() { - Map scNodes = new HashMap<>(); - for (JsonNode x : index().classElementsMatching(Tools5eIndexType.subclass, getSources().getName(), classSource)) { - scNodes.put(x, classSource); + static record ClassFeature( + Tools5eIndexType cfType, + JsonNode cfNode, + Tools5eSources cfSources, + KeyData keyData) { + public ClassFeature(Tools5eIndexType cfType, String key, JsonNode cfNode) { + this(cfType, cfNode, Tools5eSources.findSources(key), + cfType == Tools5eIndexType.classfeature + ? new ClassFeatureKeyData(key) + : new SubclassFeatureKeyData(key)); } - for (String aliasKey : index.getAliasesFor(getSources().getKey())) { - int lastSegment = aliasKey.lastIndexOf('|'); - String aliasSource = aliasKey.substring(lastSegment + 1); - for (JsonNode x : index().classElementsMatching(Tools5eIndexType.subclass, getSources().getName(), aliasSource)) { - scNodes.put(x, aliasSource); - } + + public String getName() { + return cfSources.getName(); } - for (JsonNode scNode : scNodes.keySet()) { - String parentClassSource = scNodes.get(scNode); - String scKey = Tools5eIndexType.subclass.createKey(scNode); - JsonNode resolved = index.resolveClassFeatureNode(scKey, scNode); + public String level() { + return keyData.level(); + } - Subclass sc = new Subclass(); - sc.subclassNode = resolved; - sc.parentClassSource = parentClassSource; // e.g. PHB or DMG - sc.parentKey = getSources().getKey(); - sc.shortName = resolved.get("shortName").asText(); - sc.sources = Tools5eSources.findSources(scKey); + void appendLink(JsonSource converter, List text, String pageSource) { + converter.maybeAddBlankLine(text); + String x = converter.decoratedFeatureTypeName(cfSources, cfNode); + text.add(String.format("[%s](#%s)", x, toAnchorTag(x + " (Level " + level() + ")"))); + } - // If parent sources does not contain subclass source... - if (!getSources().contains(sc.sources) && index.isExcluded(scKey)) { - continue; // excluded + public void appendListItemText(JsonSource converter, List text, String pageSource) { + boolean pushed = converter.parseState().pushFeatureType(); + try { + text.add("**" + converter.decoratedFeatureTypeName(cfSources, cfNode) + "**"); + if (!cfSources.primarySource().equalsIgnoreCase(pageSource)) { + text.add(converter.getLabeledSource(cfSources)); + } + text.add(""); + converter.appendToText(text, SourceField.entries.getFrom(cfNode), null); + text.add(""); + } finally { + converter.parseState().pop(pushed); } + } - // subclass features are text elements (null field) - findClassFeatures(Tools5eIndexType.subclassFeature, resolved.get("subclassFeatures"), sc.classFeatures, null); - - subclasses.add(sc); + void appendText(JsonSource converter, List text, String primarySource) { + boolean pushed = converter.parseState().pushFeatureType(); + try { + converter.maybeAddBlankLine(text); + text.add("### " + converter.decoratedFeatureTypeName(cfSources, cfNode) + " (Level " + level() + ")"); + if (!cfSources.primarySource().equalsIgnoreCase(primarySource)) { + text.add(converter.getLabeledSource(cfSources)); + } + converter.maybeAddBlankLine(text); + converter.appendToText(text, SourceField.entries.getFrom(cfNode), "####"); + } finally { + converter.parseState().pop(pushed); + } } } - void addOptionalFeatureText(JsonNode entry, List text) { - JsonNode optionalFeatureProgession = entry.get("optionalfeatureProgression"); - if (optionalFeatureProgession == null) { - return; - } + static interface KeyData { + String name(); - maybeAddBlankLine(text); - text.add("## Optional Features"); + String parentName(); - String relativePath = Tools5eIndexType.optionalFeatureTypes.getRelativePath(); - String source = entry.get("source").asText(); - for (JsonNode ofp : iterableElements(optionalFeatureProgession)) { - for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) { - OptionalFeatureType oft = index.getOptionalFeatureType(featureType, source); + String parentSource(); - if (oft != null) { - maybeAddBlankLine(text); - text.add("> [!example]- " + oft.title); - text.add(String.format("> ![%s](%s%s/%s.md#%s)", - oft.title, - index().compendiumVaultRoot(), relativePath, - oft.getFilename(), - toAnchorTag(oft.title))); - text.add("^list-" + slugify(oft.title)); - } else { - tui().errorf( - Msg.UNRESOLVED, "Can not find optional feature type %s for progression. Source: %s", - featureType, ofp); - } - } - } + String level(); + + String itemSource(); } - void findStartingEquipment() { - JsonNode equipment = rootNode.get("startingEquipment"); - if (equipment != null) { - String wealth = getWealth(equipment); - put("wealth", List.of(wealth)); - put("equipment", defaultEquipment(equipment)); - additionalFromBackground = booleanOrDefault(equipment, "additionalFromBackground", true); + static class ClassFeatureKeyData implements KeyData { + final String cfName; + final String className; + final String classSource; + final String level; + final String cfSource; + + public ClassFeatureKeyData(String key) { + String[] parts = key.split("\\|"); + this.cfName = parts[1]; + this.className = parts[2]; + this.classSource = parts[3]; + this.level = parts[4]; + this.cfSource = parts[5]; } - } - public void findClassProficiencies() { - if (rootNode.has("proficiency")) { - List savingThrows = new ArrayList<>(); - rootNode.withArray("proficiency").forEach(n -> savingThrows.add(asAbilityEnum(n))); - put("saves", savingThrows); + @Override + public String name() { + return cfName; } - JsonNode startingProf = rootNode.get("startingProficiencies"); - if (startingProf == null) { - tui().errorf("%s has no starting proficiencies", sources); - } else { - if (startingProf.has("armor")) { - put("armor", findAndReplace(startingProf, "armor")); - } - if (startingProf.has("weapons")) { - put("weapons", findAndReplace(startingProf, "weapons")); - } - if (startingProf.has("tools")) { - put("tools", findAndReplace(startingProf, "tools")); - } - if (startingProf.has("skills")) { - put("skills", List.of(classSkills(startingProf, sources))); - } + @Override + public String parentName() { + return className; } - } - void sidekickProficiencies(JsonNode sidekickClassFeature) { - for (JsonNode e : iterableEntries(sidekickClassFeature)) { - String line = e.asText(); - if (line.contains("saving throw")) { - //"The sidekick gains proficiency in one saving throw of your choice: Dexterity, Intelligence, or Charisma.", - //"The sidekick gains proficiency in one saving throw of your choice: Wisdom, Intelligence, or Charisma.", - //"The sidekick gains proficiency in one saving throw of your choice: Strength, Dexterity, or Constitution.", - String text = line.replaceAll(".*in one saving throw of your choice: (.*)", "$1") - .replaceAll("or ", "").replace(".", ""); - put("saves", List.of(text)); - } - if (line.contains("skills")) { - // "In addition, the sidekick gains proficiency in five skills of your choice, and it gains proficiency with light armor. If it is a humanoid or has a simple or martial weapon in its stat block, it also gains proficiency with all simple weapons and with two tools of your choice." - // "In addition, the sidekick gains proficiency in two skills of your choice from the following list: {@skill Arcana}, {@skill History}, {@skill Insight}, {@skill Investigation}, {@skill Medicine}, {@skill Performance}, {@skill Persuasion}, and {@skill Religion}.", - // "In addition, the sidekick gains proficiency in two skills of your choice from the following list: {@skill Acrobatics}, {@skill Animal Handling}, {@skill Athletics}, {@skill Intimidation}, {@skill Nature}, {@skill Perception}, and {@skill Survival}.", - String numSkills = line.replaceAll(".* proficiency in (.*) skills .*", "$1"); - int count = Integer.parseInt(textToInt(numSkills)); - put("numSkills", List.of(count + "")); - - Collection skills; - int start = line.indexOf("list:"); - if (start >= 0) { - int end = line.indexOf('.'); - String text = line.substring(start + 5, end).trim() - .replaceAll("\\{@skill ([^}]+)}", "$1") - .replace(".", "") - .replace("and ", ""); - skills = Set.of(text.split("\\s*,\\s*")); - } else { - skills = SkillOrAbility.allSkills; - } - put("skills", List.of(skillChoices(skills, count))); - } - if (line.contains("armor")) { - // "In addition, the sidekick gains proficiency in five skills of your choice, and it gains proficiency with light armor. If it is a humanoid or has a simple or martial weapon in its stat block, it also gains proficiency with all simple weapons and with two tools of your choice." - // "The sidekick gains proficiency with light armor, and if it is a humanoid or has a simple or martial weapon in its stat block, it also gains proficiency with all simple weapons." - // "The sidekick gains proficiency with all armor, and if it is a humanoid or has a simple or martial weapon in its stat block, it gains proficiency with shields and all simple and martial weapons." - if (line.contains("all armor")) { // Warrior Sidekick - put("armor", List.of("light, medium, heavy, shields")); - put("weapons", List.of("martial")); - } else { - put("armor", List.of("light")); - put("weapons", List.of("simple")); - } - } - if (line.contains("tools")) { - put("tools", List.of("two tools of your choice")); - } + @Override + public String parentSource() { + return classSource; } - } - String skillChoices(Collection skills, int numSkills) { - return String.format("Choose %s from %s", - numSkills, - skills.stream().map(x -> index.findSkillOrAbility(x, getSources())) - .filter(x -> x != null) - .sorted(SkillOrAbility.comparator) - .map(x -> "*" + x.value() + "*") - .collect(Collectors.joining(", "))); + @Override + public String level() { + return level; + } + + @Override + public String itemSource() { + return cfSource; + } } - String chooseSkillListFrom(JsonNode choose) { - int count = choose.has("count") - ? choose.get("count").asInt() - : 1; + // Unpack a subclass key + static class SubclassKeyData implements KeyData { + final String scName; + final String className; + final String classSource; + final String scSource; - ArrayNode from = choose.withArray("from"); - return skillChoices(toListOfStrings(from), count); - } + public SubclassKeyData(String key) { + String[] parts = key.split("\\|"); + this.scName = parts[1]; + this.className = parts[2]; + this.classSource = parts[3]; + this.scSource = parts[4]; + } - String classSkills(JsonNode source, Tools5eSources sources) { - List result = new ArrayList<>(); + @Override + public String name() { + return scName; + } - ArrayNode skillNode = source.withArray("skills"); - if (skillNode.size() > 1) { - tui().errorf("Multivalue skill array in %s: %s", sources, source.toPrettyString()); + @Override + public String parentName() { + return className; } - JsonNode skills = skillNode.get(0); - for (Entry e : iterableFields(skills)) { - String skill = e.getKey(); - if ("choose".equals(skill)) { - result.add(chooseSkillListFrom(e.getValue())); - } else if ("any".equals(skill)) { - int count = skills.get("any").asInt(); - result.add(skillChoices(SkillOrAbility.allSkills, count)); - } else { - SkillOrAbility custom = index.findSkillOrAbility(skill, getSources()); - if (custom == null) { - tui().errorf("Unexpected skills in starting proficiencies for %s: %s", - sources, source.toPrettyString()); - } - result.add("*" + custom.value() + "*"); - } + @Override + public String parentSource() { + return classSource; } - return String.join("; ", result); - } - List defaultEquipment(JsonNode equipment) { - List text = new ArrayList<>(); - appendList(text, equipment.withArray("default"), ListType.unordered); - return text; - } + @Override + public String level() { + return ""; + } - String getWealth(JsonNode equipment) { - return replaceText(getTextOrEmpty(equipment, "goldAlternative")); + @Override + public String itemSource() { + return scSource; + } } - void put(String key, List value) { - startingText.put(key, value); - } + // Unpack a subclass feature key + static class SubclassFeatureKeyData implements KeyData { + final String scfName; + final String className; + final String classSource; + final String scName; + final String scSource; + final String level; + final String scfSource; - int startingHitDice() { - List text = startingText.get("hd"); - if (text == null || text.isEmpty()) { - return 0; + public SubclassFeatureKeyData(String key) { + String[] parts = key.split("\\|"); + this.scfName = parts[1]; + this.className = parts[2]; + this.classSource = parts[3]; + this.scName = parts[4]; + this.scSource = parts[5]; + this.level = parts[6]; + this.scfSource = parts[7]; } - if (text.size() > 1) { - throw new IllegalArgumentException("Unable to parse int from starting text field: " + text); + + @Override + public String name() { + return scfName; } - return Integer.parseInt(text.get(0)); - } - String startingTextJoinOrDefault(String field, String value) { - List text = startingText.get(field); - return text == null ? value : String.join(", ", text); - } + @Override + public String parentName() { + return scName; + } - String startingTextJoinOrDefault(JsonNode source, String field) { - return startingTextJoinOrDefault(source, field, s -> s); - } + @Override + public String parentSource() { + return scSource; + } - String startingTextJoinOrDefault(JsonNode source, String field, Function replacements) { - List text = findAndReplace(source, field, replacements); - return text == null || text.isEmpty() ? "none" : String.join(", ", text); - } + @Override + public String level() { + return level; + } - String textToInt(String text) { - switch (text) { - case "two" -> { - return "2"; - } - case "three" -> { - return "3"; - } - case "five" -> { - return "5"; - } - default -> { - tui().errorf("Unknown number of skills (%s) listed in sidekick class features (%s)", text, sources); - return "1"; - } + @Override + public String itemSource() { + return scfSource; } } - static class ClassFeature { - JsonNode cfNode; - String level; + static class LevelProgression { + final String level; + final String pb; + List features = new ArrayList<>(); + List values = new ArrayList<>(); + List spellSlots = new ArrayList<>(); - Tools5eSources cfSources; - Tools5eIndexType cfType; + LevelProgression(int level) { + this.level = JsonSource.levelToString(level); + this.pb = "+" + JsonSource.levelToPb(level); + } - public String getName() { - return cfSources.getName(); + void addFeature(ClassFeature cf) { + features.add(toHtmlLink(cf.getName(), cf.level())); } - void appendLink(JsonSource converter, List text, String pageSource) { - converter.maybeAddBlankLine(text); - String x = converter.decoratedFeatureTypeName(cfSources, cfNode); - text.add(String.format("[%s](#%s)", x, converter.toAnchorTag(x + " (Level " + level + ")"))); + void addValue(String value) { + values.add(value); } - void appendText(JsonSource converter, List text, String pageSource) { - boolean pushed = converter.parseState().pushFeatureType(); - try { - converter.maybeAddBlankLine(text); - text.add("### " + converter.decoratedFeatureTypeName(cfSources, cfNode) + " (Level " + level + ")"); - if (!cfSources.primarySource().equalsIgnoreCase(pageSource)) { - text.add(converter.getLabeledSource(cfSources)); - } - text.add(""); - converter.appendToText(text, cfNode.get("entries"), "####"); - } finally { - converter.parseState().pop(pushed); - } + void addSpellSlot(String value) { + spellSlots.add(value); } - public void appendListItemText(JsonSource converter, List text, String pageSource) { - boolean pushed = converter.parseState().pushFeatureType(); - try { - text.add("**" + converter.decoratedFeatureTypeName(cfSources, cfNode) + "**"); - if (!cfSources.primarySource().equalsIgnoreCase(pageSource)) { - text.add(converter.getLabeledSource(cfSources)); - } - text.add(""); - converter.appendToText(text, cfNode.get("entries"), null); - text.add(""); - } finally { - converter.parseState().pop(pushed); - } + String toHtmlLink(String x, String level) { + return String.format("%s", + toAnchorTag(x + " (Level " + level + ")"), + x); } } - static class Subclass { - public String parentKey; - JsonNode subclassNode; - String shortName; + // Not static. Relies on Json2QuteClass members + class SidekickProficiencies { + static final Pattern sidekickArmor = Pattern.compile("(?<=with ).*? armor"); + static final Pattern sidekickHumanoid = Pattern.compile("[Ii]f it is a humanoid[^.]+\\.($| )"); + static final Pattern sidekickSavingThrows = Pattern.compile("\\b[^ ]+ saving throw of your choice.*"); + static final Pattern sidekickSkills = Pattern + .compile("\\b[^ ]+ skills of your choice(,| from the following list.*)"); + static final Pattern sidekickTools = Pattern.compile("\\b[^ ]+ tools of your choice"); + static final Pattern sidekickWeapons = Pattern.compile("(?<=(with|and) )all simple( and martial)? weapons"); - Tools5eSources sources; - String parentClassSource; + private String armor; + private String skills; + private String savingThrows; + private String tools; + private String weapons; - final List classFeatures = new ArrayList<>(); + SidekickProficiencies(JsonNode node) { + String text = String.join("\n", SourceField.entries.replaceTextFromList(node, index())); + String humanoidClause = null; - public String getName() { - return sources.getName(); - } - } + Matcher humanoidMatcher = sidekickHumanoid.matcher(text); + if (humanoidMatcher.find()) { + humanoidClause = humanoidMatcher.group(0); + // Remove the humanoid clause from the text + text = humanoidMatcher.replaceAll(""); + } - String markdownLinkify(String x, int level) { - return String.format("[%s](#%s)", x, toAnchorTag(x + " (Level " + level + ")")); - } + Matcher armorMatcher = sidekickArmor.matcher(text); + if (armorMatcher.find()) { + armor = uppercaseFirst(armorMatcher.group()); + if (humanoidClause.contains("shields")) { + armor += "; and shields if [humanoid](%s)".formatted(toAnchorTag("Bonus Progression (Level 1)")); + } + } - String columnValue(JsonNode c) { - if (c.isTextual() || c.isIntegralNumber()) { - String value = c.asText(); - if (value.isEmpty() || value.equals("0")) { - return "⏤"; - } else { - return replaceText(value); + Matcher savingThrowsMatcher = sidekickSavingThrows.matcher(text); + if (savingThrowsMatcher.find()) { + savingThrows = uppercaseFirst(savingThrowsMatcher.group()); } - } else if (c.isObject()) { - String type = getTextOrEmpty(c, "type"); - switch (type) { - case "dice" -> { - JsonNode toRoll = c.get("toRoll"); - List rolls = new ArrayList<>(); - toRoll.forEach(f -> rolls.add(String.format("%sd%s", f.get("number").asText(), f.get("faces").asText()))); - return String.join(", ", rolls); - } - case "bonus", "bonusSpeed" -> { - return "+" + c.get("value").asText(); - } + + Matcher skillsMatcher = sidekickSkills.matcher(text); + if (skillsMatcher.find()) { + skills = uppercaseFirst(skillsMatcher.group()).replaceAll(",$", ""); } - throw new IllegalArgumentException("Unknown column object value: " + c.toPrettyString()); - } else { - throw new IllegalArgumentException("Unknown column value: " + c.toPrettyString()); + + // Only present in the humanoid clause + Matcher toolMatcher = sidekickTools.matcher(humanoidClause); + if (toolMatcher.find()) { + tools = "%s if [humanoid](%s)".formatted( + uppercaseFirst(toolMatcher.group()), + toAnchorTag("Bonus Progression (Level 1)")); + } + + // Only present in the humanoid clause + Matcher weaponsMatcher = sidekickWeapons.matcher(humanoidClause); + if (weaponsMatcher.find()) { + weapons = "%s if [humanoid](%s)".formatted( + uppercaseFirst(weaponsMatcher.group()), + toAnchorTag("Bonus Progression (Level 1)")); + } + } + + List armor() { + return isPresent(armor) ? List.of(armor) : List.of(); + } + + List savingThrows() { + return isPresent(savingThrows) ? List.of(savingThrows) : List.of(); + } + + List skills() { + return isPresent(skills) ? List.of(skills) : List.of(); + } + + List tools() { + return isPresent(tools) ? List.of(tools) : List.of(); + } + + List weapons() { + return isPresent(weapons) ? List.of(weapons) : List.of(); } } enum ClassFields implements JsonNodeReader { + additionalFromBackground, + additionalProperties, + any, + armor, + choose, + classFeature, + classFeatures, + classTableGroups, + colLabels, + count, + defaultEquipment("default"), // default is a reserved word + faces, + featureKeys, + from, + full, + gainSubclassFeature, + goldAlternative, + hd, + isSidekick, + multiclassing, + number, + optional, optionalfeatureProgression, - subclassSource, + or, + primaryAbility, + proficienciesGained, + proficiency, + properties, + required, + requirements, + requirementsSpecial, + rows, + rowsSpellProgression, + shortName, + skills, + startingEquipment, + startingProficiencies, + subclassFeature, + subclassFeatures, + subclassKeys, subclassShortName, + subclassSource, + subclassTableGroups, + subclassTitle, + toRoll, + tools, + type, + value, + weapons, + ; + + final String nodeName; + + ClassFields() { + nodeName = name(); + } + + ClassFields(String nodeName) { + this.nodeName = nodeName; + } + + @Override + public String nodeName() { + return nodeName; + } } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java index 3e0bcf316..c6443b6df 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java @@ -95,27 +95,35 @@ public String getText(String heading) { } public String getFluffDescription(Tools5eIndexType fluffType, String heading, List images) { - List text = getFluff(fluffType, heading, images); + return getFluffDescription(rootNode, fluffType, heading, images); + } + + public String getFluffDescription(JsonNode fromNode, Tools5eIndexType fluffType, String heading, List images) { + List text = getFluff(fromNode, fluffType, heading, images); return text.isEmpty() ? null : String.join("\n", text); } public List getFluff(Tools5eIndexType fluffType, String heading, List images) { + return getFluff(rootNode, fluffType, heading, images); + } + + public List getFluff(JsonNode fromNode, Tools5eIndexType fluffType, String heading, List images) { List text = new ArrayList<>(); JsonNode fluffNode = null; - if (TtrpgValue.indexFluffKey.existsIn(rootNode)) { + if (TtrpgValue.indexFluffKey.existsIn(fromNode)) { // Specific variant - String fluffKey = TtrpgValue.indexFluffKey.getTextOrEmpty(rootNode); + String fluffKey = TtrpgValue.indexFluffKey.getTextOrEmpty(fromNode); fluffNode = index.getOrigin(fluffKey); - } else if (Tools5eFields.fluff.existsIn(rootNode)) { - fluffNode = Tools5eFields.fluff.getFrom(rootNode); + } else if (Tools5eFields.fluff.existsIn(fromNode)) { + fluffNode = Tools5eFields.fluff.getFrom(fromNode); JsonNode monsterFluff = Tools5eFields._monsterFluff.getFrom(fluffNode); if (monsterFluff != null) { String fluffKey = fluffType.createKey(monsterFluff); fluffNode = index.getOrigin(fluffKey); } - } else if (Tools5eFields.hasFluff.booleanOrDefault(rootNode, false) - || Tools5eFields.hasFluffImages.booleanOrDefault(rootNode, false)) { - String fluffKey = fluffType.createKey(rootNode); + } else if (Tools5eFields.hasFluff.booleanOrDefault(fromNode, false) + || Tools5eFields.hasFluffImages.booleanOrDefault(fromNode, false)) { + String fluffKey = fluffType.createKey(fromNode); fluffNode = index.getOrigin(fluffKey); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java index 1ed193018..bbb45a102 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java @@ -228,15 +228,19 @@ private void appendItemProperties(List text, Tags tags) { final JsonNode srdEntries = TtrpgConfig.activeGlobalConfig("srdEntries").get("properties"); for (JsonNode srdEntry : iterableElements(srdEntries)) { - // FIXME: "edition" test for srd entries - currentSources = Tools5eSources.findOrTemporary(srdEntry); + String finalKey = TtrpgValue.indexKey.getTextOrEmpty(srdEntry); + if (index().isExcluded(finalKey)) { + continue; + } + currentSources = Tools5eSources.findSources(finalKey); + boolean p2 = parseState().push(srdEntry); try { - String name = srdEntry.get("name").asText(); + String name = currentSources.getName(); maybeAddBlankLine(text); text.add("## " + name); - if (!srdEntry.has("srd")) { + if (!currentSources.isSrdOrFreeRules()) { text.add(getLabeledSource(srdEntry)); } text.add(""); @@ -254,10 +258,15 @@ private void appendItemProperties(List text, Tags tags) { } maybeAddBlankLine(text); text.add("### " + propName); - if (!property.has("srd")) { + if (!Tools5eSources.isSrd(property)) { text.add(getLabeledSource(property)); } - appendToText(text, SourceField.entries.getFrom(property), null); + List inner = new ArrayList<>(); + appendToText(inner, SourceField.entries.getFrom(property), null); + if (!inner.isEmpty()) { + inner.set(0, inner.get(0).replaceAll("^\\*\\*.*?\\.\\*\\* ", "")); + text.addAll(inner); + } } } else { appendToText(text, SourceField.entries.getFrom(srdEntry), "###"); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java index 3f5ba141d..b7e40bf05 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java @@ -1,7 +1,11 @@ package dev.ebullient.convert.tools.dnd5e; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteFeat; @@ -17,13 +21,18 @@ protected Tools5eQuteBase buildQuteResource() { Tags tags = new Tags(getSources()); tags.add("feat"); + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.featFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + // TODO: update w/ category, ability, additionalSpells return new QuteFeat(sources, type.decoratedName(rootNode), getSourceText(sources), listPrerequisites(rootNode), null, // Level coming someday.. - getText("##"), + images, + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java index 4f8a25858..e28d72c82 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java @@ -1,7 +1,11 @@ package dev.ebullient.convert.tools.dnd5e; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteHazard; @@ -22,11 +26,16 @@ protected Tools5eQuteBase buildQuteResource() { tags.add("hazard", hazardType); } + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.trapFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + return new QuteHazard(getSources(), getSources().getName(), getSourceText(getSources()), getHazardType(hazardType), - getText("##"), + images, + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java index e090c379f..53aac4143 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java @@ -57,9 +57,9 @@ protected Tools5eQuteBase buildQuteResource() { return new QuteItem(sources, getSourceText(sources), rootVariant, - text, - fluffImages, variants, + fluffImages, + text, tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java index 374e2c4d5..7de1bd780 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java @@ -30,7 +30,7 @@ protected Tools5eQuteBase buildQuteResource() { } List fluffImages = new ArrayList<>(); - List text = getFluff(Tools5eIndexType.monsterFluff, "##", fluffImages); + List text = getFluff(Tools5eIndexType.objectFluff, "##", fluffImages); appendToText(text, SourceField.entries.getFrom(rootNode), "##"); return new QuteObject(sources, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java index 0c3b26ff1..8ad07bde6 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java @@ -1,7 +1,11 @@ package dev.ebullient.convert.tools.dnd5e; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteFeat; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -19,13 +23,17 @@ protected Tools5eQuteBase buildQuteResource() { tags.add("optional-feature", featureType); } - // set the template to use + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.optionalfeatureFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + return new QuteFeat(getSources(), getSources().getName(), getSourceText(sources), listPrerequisites(rootNode), null, - getText("##"), + images, + String.join("\n", text), tags); } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java index 83ac498c4..779e8c886 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.qute.QuteReward; @@ -23,8 +24,11 @@ protected QuteReward buildQuteResource() { tags.add("reward", type); } - List details = new ArrayList<>(); + List images = new ArrayList<>(); + List text = getFluff(Tools5eIndexType.rewardFluff, "##", images); + appendToText(text, SourceField.entries.getFrom(rootNode), "##"); + List details = new ArrayList<>(); String type = RewardField.type.getTextOrNull(rootNode); if (type != null) { details.add(type); @@ -41,7 +45,8 @@ protected QuteReward buildQuteResource() { RewardField.ability.transformTextFrom(rootNode, "\n", index), getSources().getName().startsWith(detail) ? "" : detail, RewardField.signaturespells.transformTextFrom(rootNode, "\n", index), - getText("##"), + images, + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java index 97dc5f119..9ec9a3b10 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java @@ -60,8 +60,8 @@ protected Tools5eQuteBase buildQuteResource() { spellComponents(), spellDuration(), String.join(", ", classes), - String.join("\n", text), getFluffImages(Tools5eIndexType.spellFluff), + String.join("\n", text), tags); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java index b588f5756..9379d7e6a 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java @@ -292,7 +292,7 @@ default void appendClassFeatureRef(List text, JsonNode entry, Tools5eInd return; // skipped or not found } if (parseState().featureTypeDepth() > 2) { - tui().errorf("Cycle in class or subclass features found in %s", cf.cfSources); + tui().errorf("Cycle in class or subclass features found in %s", cf.cfSources()); // this is within an existing feature description. Emit as a link cf.appendLink(this, text, parseState().getSource(featureType)); } else if (parseState().inList()) { @@ -648,7 +648,7 @@ default void embedReference(List text, JsonNode entry, Tools5eIndexType } } else { text.add(link); - tui().warnf(Msg.UNRESOLVED, "unable to find statblock target: %s", entry); + tui().warnf(Msg.UNRESOLVED, "unable to find statblock target %s from %s", entry, getSources()); } } @@ -961,7 +961,7 @@ default String mapAlignmentToString(String a) { }; } - default int levelToPb(int level) { + static int levelToPb(int level) { // 2 + (¼ * (Level – 1)) return 2 + ((int) (.25 * (level - 1))); } @@ -1284,6 +1284,7 @@ enum Tools5eFields implements JsonNodeReader { number, // speed optionalfeature, otherSources, + parentSource, prop, // statblock race, regionalEffects, // legendary group diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index 4a3707207..6e6da9cae 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -1,10 +1,13 @@ package dev.ebullient.convert.tools.dnd5e; import static dev.ebullient.convert.StringUtil.isPresent; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.regex.MatchResult; import java.util.regex.Matcher; @@ -41,10 +44,12 @@ public interface JsonTextReplacement extends JsonTextConverter static final Pattern quickRefPattern = Pattern.compile("\\{@quickref ([^}]+)}"); static final Pattern notePattern = Pattern.compile("\\{@(note|tip) ([^}]+)}"); static final Pattern footnotePattern = Pattern.compile("\\{@footnote ([^}]+)}"); - static final Pattern abilitySavePattern = Pattern.compile("\\{@(ability|savingThrow) ([^}]+)}"); // {@ability str 20} + static final Pattern abilitySavePattern = Pattern.compile("\\{@(ability|savingThrow) ([^}]+)}"); // {@ability str + // 20} static final Pattern savingThrowPattern = Pattern.compile("\\{@actSave ([^}]+)}"); static final Pattern attackPattern = Pattern.compile("\\{@atkr? ([^}]+)}"); - static final Pattern skillCheckPattern = Pattern.compile("\\{@skillCheck ([^}]+)}"); // {@skillCheck animal_handling 5} + static final Pattern skillCheckPattern = Pattern.compile("\\{@skillCheck ([^}]+)}"); // {@skillCheck animal_handling + // 5} static final Pattern optionalFeaturesFilter = Pattern.compile("\\{@filter ([^|}]+)\\|optionalfeatures\\|([^}]*)}"); static final Pattern featureTypePattern = Pattern.compile("(?:[Ff]eature )?[Tt]ype=([^|}]+)"); static final Pattern featureSourcePattern = Pattern.compile("source=([^|}]+)"); @@ -52,6 +57,8 @@ public interface JsonTextReplacement extends JsonTextConverter static final Pattern promptPattern = Pattern.compile("#\\$prompt_number(?::(.*?))?\\$#"); static final String subclassFeatureMask = "subclassfeature\\|(.*)\\|.*?\\|.*?\\|.*?\\|.*?\\|(\\d+)\\|.*"; + static final Set missingKeys = new HashSet<>(); + Tools5eIndex index(); Tools5eSources getSources(); @@ -178,12 +185,14 @@ default String _replaceTokenText(String input, boolean nested) { try { result = replacePromptStrings(result); - // {@dice .. }, {@damage ..}{@hit ..}, {@d20 ..}, {@initiative ...}, {@scaledice..}, {@scaledamage..} + // {@dice .. }, {@damage ..}{@hit ..}, {@d20 ..}, {@initiative ...}, + // {@scaledice..}, {@scaledamage..} result = replaceWithDiceRoller(result); result = chancePattern.matcher(result).replaceAll((match) -> { // "Chance tags; similar to dice roller tags, but output success/failure. - // {@chance 50}; {@chance 50|display text}; {@chance 50|display text|rolled by name}; + // {@chance 50}; {@chance 50|display text}; {@chance 50|display text|rolled by + // name}; // {@chance 50|display text|rolled by name|on success text}; // {@chance 50|display text|rolled by name|on success text|on failure text}.", String[] parts = match.group(1).split("\\|"); @@ -213,7 +222,8 @@ default String _replaceTokenText(String input, boolean nested) { }); result = homebrewPattern.matcher(result).replaceAll((match) -> { - // {@homebrew changes|modifications}, {@homebrew additions} or {@homebrew |removals} + // {@homebrew changes|modifications}, {@homebrew additions} or {@homebrew + // |removals} String s = match.group(1); int pos = s.indexOf('|'); if (pos == 0) { // removal @@ -281,8 +291,10 @@ default String _replaceTokenText(String input, boolean nested) { List type = new ArrayList<>(); String method = ""; // render.js Renderer.attackTagToFull - // const ptType = tags.includes("m") ? "Melee " : tags.includes("r") ? "Ranged " : tags.includes("g") ? "Magical " : tags.includes("a") ? "Area " : ""; - // const ptMethod = tags.includes("w") ? "Weapon " : tags.includes("s") ? "Spell " : tags.includes("p") ? "Power " : ""; + // const ptType = tags.includes("m") ? "Melee " : tags.includes("r") ? "Ranged " + // : tags.includes("g") ? "Magical " : tags.includes("a") ? "Area " : ""; + // const ptMethod = tags.includes("w") ? "Weapon " : tags.includes("s") ? "Spell + // " : tags.includes("p") ? "Power " : ""; if (match.group(1).contains("m")) { type.add("Melee "); } @@ -311,7 +323,8 @@ default String _replaceTokenText(String input, boolean nested) { .replaceAll("\\{@hitYourSpellAttack ([^}]+)}", "$1") .replaceAll("\\{@hitYourSpellAttack}", "the summoner's spell attack modifier") // "Internal links: {@5etools This Is Your Life|lifegen.html}", - // "External links: {@link https://discord.gg/5etools} or {@link Discord|https://discord.gg/5etools}" + // "External links: {@link https://discord.gg/5etools} or {@link + // Discord|https://discord.gg/5etools}" .replaceAll("\\{@link ([^}|]+)\\|([^}]+)}", "$1 ($2)") // this must come first .replaceAll("\\{@link ([^}|]+)}", "$1") // this must come first .replaceAll("\\{@5etools ([^}|]+)\\|?[^}]*}", "$1") @@ -375,9 +388,12 @@ default String _replaceTokenText(String input, boolean nested) { } result = footnotePattern.matcher(result).replaceAll((match) -> { - // {@footnote directly in text|This is primarily for homebrew purposes, as the official texts (so far) avoid using footnotes}, - // {@footnote optional reference information|This is the footnote. References are free text.|Footnote 1, page 20}.", - // We're converting these to _inline_ markdown footnotes, as numbering is difficult to track + // {@footnote directly in text|This is primarily for homebrew purposes, as the + // official texts (so far) avoid using footnotes}, + // {@footnote optional reference information|This is the footnote. References + // are free text.|Footnote 1, page 20}.", + // We're converting these to _inline_ markdown footnotes, as numbering is + // difficult to track String[] parts = match.group(1).split("\\|"); if (parts[0].contains("")) { // This already assumes what the footnote name will be @@ -436,9 +452,9 @@ default String replaceSavingThrow(MatchResult match) { default String replaceSkillOrAbility(MatchResult match) { // format: {@ability str 20} or {@ability str 20|Display Text} - // or {@ability str 20|Display Text|Roll Name Text} + // or {@ability str 20|Display Text|Roll Name Text} // format: {@savingThrow str 5} or {@savingThrow str 5|Display Text} - // or {@savingThrow str 5|Display Text|Roll Name Text} + // or {@savingThrow str 5|Display Text|Roll Name Text} String[] parts = match.group(2).split("\\|"); String[] score = parts[0].split(" "); @@ -469,8 +485,9 @@ default String replaceSkillOrAbility(MatchResult match) { } default String replaceSkillCheck(MatchResult match) { - // format: {@skillCheck animal_handling 5} or {@skillCheck animal_handling 5|Display Text} - // or {@skillCheck animal_handling 5|Display Text|Roll Name Text} + // format: {@skillCheck animal_handling 5} or {@skillCheck animal_handling + // 5|Display Text} + // or {@skillCheck animal_handling 5|Display Text|Roll Name Text} String[] parts = match.group(1).split("\\|"); String[] score = parts[0].split(" "); SkillOrAbility skill = index().findSkillOrAbility(score[0], getSources()); @@ -493,16 +510,27 @@ default String replaceSkillCheck(MatchResult match) { return "%s (%s)".formatted(text, dice); } + default String linkifySkill(SkillOrAbility skill) { + String source = skill.source(); + return linkify(Tools5eIndexType.skill, + "%s|%s|%s".formatted(skill.value(), source, skill.value())); + } + default String linkifyRules(Tools5eIndexType type, String text, String rules) { // {@condition stunned} assumes PHB by default, - // {@condition stunned|PHB} can have sources added with a pipe (not that it's ever useful), + // {@condition stunned|PHB} can have sources added with a pipe (not that it's + // ever useful), // {@condition stunned|PHB|and optional link text added with another pipe}.", String[] parts = text.split("\\|"); String name = parts[0]; - String source = parts.length > 1 ? parts[1] : "PHB"; + String source = parts.length > 1 ? parts[1] : type.defaultSourceString(); String linkText = parts.length > 2 ? parts[2] : name; + if (name.isBlank()) { + return "[%s](%s%s.md)".formatted(linkText, index().rulesVaultRoot(), rules); + } + String aliasKey = index().getAliasOrDefault(type.createKey(name, source)); if (index().isExcluded(aliasKey)) { return linkText; @@ -536,7 +564,8 @@ default String linkify(Tools5eIndexType type, String s) { return switch (type) { // {@background Charlatan} assumes PHB by default, // {@background Anthropologist|toa} can have sources added with a pipe, - // {@background Anthropologist|ToA|and optional link text added with another pipe}.", + // {@background Anthropologist|ToA|and optional link text added with another + // pipe}.", // {@feat Alert} assumes PHB by default, // {@feat Elven Accuracy|xge} can have sources added with a pipe, // {@feat Elven Accuracy|xge|and optional link text added with another pipe}.", @@ -554,10 +583,13 @@ default String linkify(Tools5eIndexType type, String s) { // {@object Ballista|DMG|and optional link text added with another pipe}.", // {@optfeature Agonizing Blast} assumes PHB by default, // {@optfeature Aspect of the Moon|xge} can have sources added with a pipe, - // {@optfeature Aspect of the Moon|xge|and optional link text added with another pipe}.", + // {@optfeature Aspect of the Moon|xge|and optional link text added with another + // pipe}.", // {@psionic Mastery of Force} assumes UATheMysticClass by default - // {@psionic Mastery of Force|UATheMysticClass} can have sources added with a pipe - // {@psionic Mastery of Force|UATheMysticClass|and optional link text added with another pipe}.", + // {@psionic Mastery of Force|UATheMysticClass} can have sources added with a + // pipe + // {@psionic Mastery of Force|UATheMysticClass|and optional link text added with + // another pipe}.", // {@race Human} assumes PHB by default, // {@race Aasimar (Fallen)|VGM} // {@race Aasimar|DMG|racial traits for the aasimar} @@ -566,16 +598,19 @@ default String linkify(Tools5eIndexType type, String s) { // {@race dwarf (hill)||Dwarf, hill} // {@reward Blessing of Health} assumes DMG by default, // {@reward Blessing of Health} can have sources added with a pipe, - // {@reward Blessing of Health|DMG|and optional link text added with another pipe}.", + // {@reward Blessing of Health|DMG|and optional link text added with another + // pipe}.", // {@spell acid splash} assumes PHB by default, // {@spell tiny servant|xge} can have sources added with a pipe, // {@spell tiny servant|xge|and optional link text added with another pipe}.", // {@table 25 gp Art Objects} assumes DMG by default, // {@table Adventuring Gear|phb} can have sources added with a pipe, - // {@table Adventuring Gear|phb|and optional link text added with another pipe}.", + // {@table Adventuring Gear|phb|and optional link text added with another + // pipe}.", // {@trap falling net} assumes DMG by default, // {@trap falling portcullis|xge} can have sources added with a pipe, - // {@trap falling portcullis|xge|and optional link text added with another pipe}.", + // {@trap falling portcullis|xge|and optional link text added with another + // pipe}.", // {@vehicle Galley} assumes GoS by default, // {@vehicle Galley|UAOfShipsAndSea} can have sources added with a pipe, // {@vehicle Galley|GoS|and optional link text added with another pipe}.", @@ -588,6 +623,7 @@ default String linkify(Tools5eIndexType type, String s) { legendaryGroup, object, optfeature, + psionic, race, reward, spell, @@ -638,12 +674,17 @@ default String linkifyType(Tools5eIndexType type, String aliasKey, String linkTe String dirName = type.getRelativePath(); JsonNode jsonSource = index().getNode(aliasKey); // filtered if (jsonSource == null) { - if (index().getOrigin(aliasKey) == null) { - // sources can be excluded, that's fine.. but if this is something that doesn't exist at all.. - tui().debugf(Msg.UNRESOLVED, "unresolvable {@%s %s} as [%s] from %s", - type, match, aliasKey, parseState().getSource()); + jsonSource = index().getHomebrewNode(type, aliasKey, parseState().getSource()); + if (jsonSource == null) { + if (index().getOrigin(aliasKey) == null) { + // sources can be excluded, that's fine.. but if this is something that doesn't + // exist at all.. + tui().debugf(Msg.UNRESOLVED, "unresolvable {@%s %s} as [%s] from %s", + type, match, aliasKey, parseState().getSource()); + // log a stack trace of how we got here + } + return linkText; } - return linkText; } Tools5eSources linkSource = Tools5eSources.findSources(jsonSource); return linkOrText(linkText, aliasKey, dirName, @@ -701,7 +742,8 @@ default String linkifyClass(String match) { // {@class artificer|uaartificer} can have sources added with a pipe, // {@class fighter|phb|optional link text added with another pipe}, // {@class fighter|phb|subclasses added|Eldritch Knight} with another pipe, - // {@class fighter|phb|and class feature added|Eldritch Knight|phb|2-0} with another pipe + // {@class fighter|phb|and class feature added|Eldritch Knight|phb|2-0} with + // another pipe // {@class Barbarian|phb|Path of the Ancestral Guardian|Ancestral Guardian|xge} // {@class Fighter|phb|Samurai|Samurai|xge} String[] parts = match.split("\\|"); @@ -714,7 +756,8 @@ default String linkifyClass(String match) { String relativePath = Tools5eIndexType.classtype.getRelativePath(); if (subclass != null) { String key = index() - .getAliasOrDefault(Tools5eIndexType.getSubclassKey(className, classSource, subclass, subclassSource)); + .getAliasOrDefault( + Tools5eIndexType.getSubclassKey(className, classSource, subclass, subclassSource)); // "subclass|path of wild magic|barbarian|phb|" int first = key.indexOf('|'); int second = key.indexOf('|', first + 1); @@ -729,7 +772,8 @@ default String linkifyClass(String match) { } default String linkifyClassFeature(String match) { - // "Class Features: Class source is assumed to be PHB, class feature source is assumed to be the same as class source" + // "Class Features: Class source is assumed to be PHB, class feature source is + // assumed to be the same as class source" // {@classFeature Rage|Barbarian||1}, // {@classFeature Infuse Item|Artificer|TCE|2}, // {@classFeature Survival Instincts|Barbarian||2|UAClassFeatureVariants}, @@ -789,13 +833,16 @@ default String linkifyOptionalFeatureType(MatchResult match) { } default String linkifySubclassFeature(String match) { - //"Subclass Features: + // "Subclass Features: // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3}, // {@subclassFeature Alchemist|Artificer|TCE|Alchemist|TCE|3}, - // {@subclassFeature Path of the Battlerager|Barbarian||Battlerager|SCAG|3}, --> "barbarian-path-of-the-... " - // {@subclassFeature Blessed Strikes|Cleric||Life||8|UAClassFeatureVariants}, --> "-domain" + // {@subclassFeature Path of the Battlerager|Barbarian||Battlerager|SCAG|3}, --> + // "barbarian-path-of-the-... " + // {@subclassFeature Blessed Strikes|Cleric||Life||8|UAClassFeatureVariants}, + // --> "-domain" // {@subclassFeature Blessed Strikes|Cleric|PHB|Twilight|TCE|8|TCE} - // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3||optional display text}. + // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3||optional + // display text}. // Class source is assumed to be PHB. // Subclass source is assumed to be PHB. // Subclass feature source is assumed to be the same as subclass source.", @@ -824,8 +871,10 @@ default String linkifySubclassFeature(String match) { String subclassKey = Tools5eIndexType.subclass.fromChildKey(featureKey); // look up alias for subclass so link is correct, but don't follow reprints - // "subclass|redemption|paladin|phb|" : "subclass|oath of redemption|paladin|phb|", - // "subclass|twilight|cleric|phb|tce" : "subclass|twilight domain|cleric|phb|tce" + // "subclass|redemption|paladin|phb|" : "subclass|oath of + // redemption|paladin|phb|", + // "subclass|twilight|cleric|phb|tce" : "subclass|twilight + // domain|cleric|phb|tce" subclassKey = index().getAliasOrDefault(subclassKey, false); JsonNode subclassNode = index().getNode(subclassKey); @@ -839,8 +888,10 @@ default String linkifySubclassFeature(String match) { return linkText; } // Examine new subclass node's features, to see if there is a match - // e.g. for "subclassfeature|primal companion|ranger|phb|beast master|phb|3|tce", - // consider "subclassfeature|primal companion|ranger|xphb|beast master|xphb|3|xphb" + // e.g. for "subclassfeature|primal companion|ranger|phb|beast + // master|phb|3|tce", + // consider "subclassfeature|primal companion|ranger|xphb|beast + // master|xphb|3|xphb" String test = featureKey.replaceAll(subclassFeatureMask, "$1-$2"); boolean found = false; for (String fkey : Tools5eFields.classFeatureKeys.getListOfStrings(subclassNode, tui())) { @@ -852,7 +903,8 @@ default String linkifySubclassFeature(String match) { } } if (!found) { - tui().warnf(Msg.UNRESOLVED, "No equivalent subclass feature found for {@subclassfeature %s} in %s (from %s)", + tui().warnf(Msg.UNRESOLVED, + "No equivalent subclass feature found for {@subclassfeature %s} in %s (from %s)", match, subclassKey, getSources().getKey()); return linkText; } @@ -922,28 +974,40 @@ default String linkifyItemAttribute(Tools5eIndexType type, String s) { String source = parts.length > 1 ? parts[1] : type.defaultSourceString(); String linkText = parts.length > 2 ? parts[2] : parts[0]; String lookup = "%s|%s".formatted(parts[0], source); + if (missingKeys.contains(lookup)) { + return linkText; + } return switch (type) { case itemType -> { - ItemType itemType = index().findItemType(lookup, getSources()); + String key = Tools5eIndexType.itemType.fromTagReference(lookup); + ItemType itemType = index().findItemType(key, getSources()); if (itemType == null) { - tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); - yield s; + if (missingKeys.add(lookup) && index().isIncluded(key)) { + tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); + } + yield linkText; } yield itemType.linkify(linkText); } case itemProperty -> { - ItemProperty itemProperty = index().findItemProperty(lookup, getSources()); + String key = Tools5eIndexType.itemProperty.fromTagReference(lookup); + ItemProperty itemProperty = index().findItemProperty(key, getSources()); if (itemProperty == null) { - tui().warnf(Msg.UNRESOLVED, "Item property %s not found from %s", s, getSources().getKey()); - yield s; + if (missingKeys.add(lookup) && index().isIncluded(key)) { + tui().warnf(Msg.UNRESOLVED, "Item property %s not found from %s", s, getSources().getKey()); + } + yield linkText; } yield itemProperty.linkify(linkText); } case itemMastery -> { - ItemMastery itemMastery = index().findItemMastery(lookup, getSources()); + String key = Tools5eIndexType.itemMastery.fromTagReference(lookup); + ItemMastery itemMastery = index().findItemMastery(key, getSources()); if (itemMastery == null) { - tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); - yield s; + if (missingKeys.add(lookup) && index().isIncluded(key)) { + tui().warnf(Msg.UNRESOLVED, "Item mastery %s not found from %s", s, getSources().getKey()); + } + yield linkText; } yield itemMastery.linkify(linkText); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java index 193f8384b..15218548e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java @@ -127,10 +127,13 @@ public Map getMap() { * This is included in all-index.json */ static class OptionalFeatureType { + + @JsonIgnore + final HomebrewMetaTypes homebrewMeta; + final String lookupKey; final String featureTypeKey; final String abbreviation; - final HomebrewMetaTypes homebrewMeta; final String title; final String source; final List features = new ArrayList<>(); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java index e9c03b8d6..06d8c6242 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.toTitleCase; + import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -16,6 +18,8 @@ public interface SkillOrAbility { String value(); + String source(); + int ordinal(); public static SkillOrAbility fromTextValue(String v) { @@ -53,17 +57,20 @@ public class CustomSkillOrAbility implements SkillOrAbility { final String name; final String lower; final String key; + final String source; public CustomSkillOrAbility(String name) { this.name = name; this.lower = name.toLowerCase(); this.key = null; + this.source = ""; } public CustomSkillOrAbility(JsonNode skill) { - this.name = SourceField.name.getTextOrEmpty(skill); + this.name = toTitleCase(SourceField.name.getTextOrEmpty(skill)); this.lower = this.name.toLowerCase(); this.key = Tools5eIndexType.skill.createKey(skill); + this.source = SourceField.source.getTextOrEmpty(skill); } @Override @@ -71,6 +78,11 @@ public String value() { return name; } + @Override + public String source() { + return source; + } + public int ordinal() { return 99; } @@ -120,5 +132,9 @@ enum SkillOrAbilityEnum implements SkillOrAbility { public String value() { return longValue; } + + public String source() { + return Tools5eIndexType.skill.defaultSourceString(); + } } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java index 56dd5011e..6fac93fbd 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java @@ -62,8 +62,7 @@ public ItemType findHomebrewType(String fragment, Tools5eSources sources) { if (meta != null) { JsonNode homebrewNode = meta.getItemProperty(fragment); if (homebrewNode != null) { - String key = Tools5eIndexType.itemType.createKey(homebrewNode); - return ItemType.fromNode(key, homebrewNode); + return ItemType.fromNode(homebrewNode); } } return null; @@ -74,8 +73,7 @@ public ItemMastery findHomebrewMastery(String fragment, Tools5eSources sources) if (meta != null) { JsonNode homebrewNode = meta.getItemMastery(fragment); if (homebrewNode != null) { - String key = Tools5eIndexType.itemMastery.createKey(homebrewNode); - return ItemMastery.fromNode(key, homebrewNode); + return ItemMastery.fromNode(homebrewNode); } } return null; @@ -86,8 +84,7 @@ public ItemProperty findHomebrewProperty(String fragment, Tools5eSources sources if (meta != null) { JsonNode homebrewNode = meta.getItemProperty(fragment); if (homebrewNode != null) { - String key = Tools5eIndexType.itemProperty.createKey(homebrewNode); - return ItemProperty.fromNode(key, homebrewNode); + return ItemProperty.fromNode(homebrewNode); } } return null; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 0a3335c93..1afc6a3a2 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -14,10 +14,10 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.function.BiConsumer; -import java.util.function.Function; import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -30,6 +30,7 @@ import dev.ebullient.convert.qute.SourceAndPage; import dev.ebullient.convert.tools.MarkdownConverter; import dev.ebullient.convert.tools.ToolsIndex; +import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField; import dev.ebullient.convert.tools.dnd5e.Json2QuteRace.RaceFields; import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; @@ -125,7 +126,9 @@ private void indexTypes(String filename, JsonNode node) { Tools5eIndexType.objectFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.optionalfeatureFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.raceFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.rewardFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.spellFluff.withArrayFrom(node, this::addToIndex); + Tools5eIndexType.subclassFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.trapFluff.withArrayFrom(node, this::addToIndex); Tools5eIndexType.vehicleFluff.withArrayFrom(node, this::addToIndex); @@ -288,9 +291,10 @@ public void prepare() { // Add missing/frequently-used aliases TtrpgConfig.addDefaultAliases(aliases); + TtrpgConfig.addReferenceEntries((n) -> addToIndex(Tools5eIndexType.reference, n)); - tui().progressf("Importing homebrew sources"); // Properly import homebrew sources + tui().progressf("Importing homebrew sources"); homebrewIndex.importBrew(this::importHomebrewTree); tui().debugf("Preparing index using configuration:\n%s", Tui.jsonStringify(config)); @@ -307,6 +311,19 @@ public void prepare() { .filter(n -> !ItemField.packContents.existsIn(n)) .toList(); + // Bring in parent adventures (before sources are created) + nodeIndex.values().stream() + .filter(n -> Tools5eFields.parentSource.existsIn(n)) + .forEach(n -> { + String source = SourceField.source.getTextOrEmpty(n); + String parentSource = Tools5eFields.parentSource.getTextOrNull(n); + if (TtrpgConfig.getConfig().sourceIncluded(source)) { + // include the parent source if you include an adventure (related rules) + tui().debugf(Msg.SOURCE, "including %s due to %s", parentSource, source); + TtrpgConfig.includeAdditionalSource(parentSource); + } + }); + List variants = new ArrayList<>(); // For each node: handle copies, link sources @@ -392,7 +409,7 @@ public void prepare() { filteredIndex.put(key, e.getValue()); } else if (sources.includedByConfig()) { filteredIndex.put(key, e.getValue()); - logThis.accept(msgType, "(KEEP) " + key); + logThis.accept(msgType, "------ " + key); } else if (type.isOutputType()) { logThis.accept(msgType, "(drop) " + key); } @@ -407,6 +424,33 @@ public void prepare() { tui().progressf("Removing dependent and dangling resources"); filteredIndex.keySet().removeIf(k -> otherwiseExcluded(k)); + // Populate classes and subclasses with related (included) + // features + for (var entry : filteredIndex.entrySet()) { + String entryKey = entry.getKey(); + var type = Tools5eIndexType.getTypeFromKey(entryKey); + if (type == Tools5eIndexType.subclass + || type == Tools5eIndexType.classfeature + || type == Tools5eIndexType.subclassFeature) { + var parentType = switch (type) { + case subclass -> Tools5eIndexType.classtype; + case classfeature -> Tools5eIndexType.classtype; + case subclassFeature -> Tools5eIndexType.subclass; + default -> null; + }; + ClassFields targetField = switch (type) { + case subclass -> ClassFields.subclassKeys; + case classfeature -> ClassFields.featureKeys; + case subclassFeature -> ClassFields.featureKeys; + default -> null; + }; + String parentKey = getAliasOrDefault(parentType.fromChildKey(entryKey)); + JsonNode parent = getOriginNoFallback(parentKey); + ArrayNode target = targetField.ensureArrayIn(parent); + target.add(entryKey); + } + } + // Deities have their own glorious reprint mess, which we only need to deal with // when we aren't hoarding all the things. if (config.reprintBehavior() != ReprintBehavior.all) { @@ -593,34 +637,39 @@ private boolean otherwiseExcluded(String key) { Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); return switch (type) { - case card -> removeIfParentExcluded(key, Tools5eIndexType.deck, Msg.DECK); - case classfeature, subclassFeature -> removeIfParentExcluded(key, Tools5eIndexType.classtype, - Msg.CLASSES); + case card -> removeIfParentExcluded(key, type, Tools5eIndexType.deck, Msg.DECK); + case classfeature -> removeIfParentExcluded(key, type, + Tools5eIndexType.classtype, Msg.CLASSES); + case subclassFeature -> removeIfParentExcluded(key, type, + Tools5eIndexType.subclass, Msg.CLASSES) + || removeIfParentExcluded(key, type, + Tools5eIndexType.classtype, Msg.CLASSES); case optfeature, optionalFeatureTypes -> removeUnusedOptionalFeatures(type, key); - case subclass -> !sources.includedByConfig() || removeIfParentExcluded(key, Tools5eIndexType.classtype, - Msg.CLASSES); - case subrace -> !sources.includedByConfig() || removeIfParentExcluded(key, Tools5eIndexType.race, Msg.RACES); + case subclass -> !sources.includedByConfig() + || removeIfParentExcluded(key, type, Tools5eIndexType.classtype, Msg.CLASSES); + case subrace -> !sources.includedByConfig() + || removeIfParentExcluded(key, type, Tools5eIndexType.race, Msg.RACES); default -> false; // does not have a parent }; } - private boolean removeIfParentExcluded(String key, Tools5eIndexType parentType, Msg msg) { + private boolean removeIfParentExcluded(String key, Tools5eIndexType type, Tools5eIndexType parentType, Msg msg) { String parentKey = parentType.fromChildKey(key); Tools5eSources parentSources = Tools5eSources.findSources(parentKey); if (parentSources == null) { - tui().warnf(Msg.UNRESOLVED, "%35s :: unresolved parent of [%s]", parentKey, key); // allow for corrections (aliases), not reprints parentKey = getAliasOrDefault(parentKey, false); parentSources = Tools5eSources.findSources(parentKey); if (parentSources == null) { + tui().warnf(Msg.UNRESOLVED, "%35s :: unresolved parent of [%s]", parentKey, key); return true; // has a parent, it is missing (dangling resource) } } - boolean included = parentSources.includedByConfig(); - if (!included) { + boolean filterIncluded = filteredIndex.containsKey(parentKey); + if (!filterIncluded) { tui().debugf(msg, "(drop) %43s :: %s", parentKey, key); } - return !included; + return !filterIncluded; } private boolean removeUnusedOptionalFeatures(Tools5eIndexType type, String key) { @@ -651,12 +700,6 @@ public boolean notPrepared() { return filteredIndex == null; } - public List classElementsMatching(Tools5eIndexType type, String className, String classSource) { - String pattern = String.format("%s\\|[^|]+\\|%s\\|%s\\|.*", type, className, classSource) - .toLowerCase(); - return nodesMatching(pattern); - } - public List elementsMatching(Tools5eIndexType type, String middle) { String pattern = String.format("%s\\|%s\\|.*", type, middle) .toLowerCase(); @@ -747,59 +790,64 @@ public JsonNode getNode(String finalKey) { return filteredIndex.get(finalKey); } - public ItemProperty findItemProperty(String fragment, Tools5eSources sources) { - if (fragment == null || fragment.isEmpty()) { + public JsonNode getHomebrewNode(Tools5eIndexType type, String finalKey, String currentSource) { + HomebrewMetaTypes meta = homebrewIndex.getHomebrewMetaTypes(currentSource); + if (meta == null) { return null; } - if (fragment.contains("|")) { - String finalKey = Tools5eIndexType.itemProperty.fromTagReference(fragment); - return ItemProperty.fromKey(finalKey, this); - } + String adaptKey = finalKey.replace(type.defaultSourceString().toLowerCase(), currentSource.toLowerCase()); + return filteredIndex.get(adaptKey); + } - // We could have a default property (phb), or we could have a homebrew property - ItemProperty property = homebrewIndex.findHomebrewProperty(fragment, sources); - if (property == null) { - // Then we'll try the default source - String key = Tools5eIndexType.itemProperty.fromTagReference(fragment); - return ItemProperty.fromKey(key, this); + public ItemProperty findItemProperty(String key, Tools5eSources activeSources) { + if (key == null || key.isEmpty()) { + return null; + } + JsonNode propertyNode = findTypePropertyNode(key); // check alias & phb/xphb + if (propertyNode != null) { + return ItemProperty.fromNode(propertyNode); } - return property; + // try homebrew property + return homebrewIndex.findHomebrewProperty(key, activeSources); } - public ItemType findItemType(String fragment, Tools5eSources sources) { - if (fragment == null || fragment.isEmpty()) { + public ItemType findItemType(String key, Tools5eSources activeSources) { + if (key == null || key.isEmpty()) { return null; } - if (fragment.contains("|")) { - String finalKey = Tools5eIndexType.itemType.fromTagReference(fragment); - return ItemType.fromKey(finalKey, this); - } - // We could have a default property (phb), or we could have a homebrew property - ItemType type = homebrewIndex.findHomebrewType(fragment, sources); - if (type == null) { - // Then we'll try the default source - String key = Tools5eIndexType.itemType.fromTagReference(fragment); - return ItemType.fromKey(key, this); + JsonNode typeNode = findTypePropertyNode(key); // check alias & phb/xphb + if (typeNode != null) { + return ItemType.fromNode(typeNode); } - return type; + // try homebrew property + return homebrewIndex.findHomebrewType(key, activeSources); } - public ItemMastery findItemMastery(String fragment, Tools5eSources sources) { - if (fragment == null || fragment.isEmpty()) { + public ItemMastery findItemMastery(String key, Tools5eSources activeSources) { + if (key == null || key.isEmpty()) { return null; } - if (fragment.contains("|")) { - String finalKey = Tools5eIndexType.itemMastery.fromTagReference(fragment); - return ItemMastery.fromKey(finalKey, this); + JsonNode masteryNode = getNode(getAliasOrDefault(key)); + if (masteryNode != null) { + return ItemMastery.fromNode(masteryNode); } - // We could have a default property, or we could have a homebrew property - ItemMastery mastery = homebrewIndex.findHomebrewMastery(fragment, sources); - if (mastery == null) { - // Then we'll try the default source - String key = Tools5eIndexType.itemMastery.fromTagReference(fragment); - return ItemMastery.fromKey(key, this); + // try homebrew property + return homebrewIndex.findHomebrewMastery(key, activeSources); + } + + private JsonNode findTypePropertyNode(String key) { + String aliasKey = getAliasOrDefault(key); + JsonNode node = getNode(aliasKey); + if (node == null && aliasKey.endsWith("phb")) { + aliasKey = aliasKey.contains("|xphb") + ? aliasKey.replace("|xphb", "|phb") + : aliasKey.replace("|phb", "|xphb"); + node = getNode(aliasKey); + if (node != null) { + addAlias(key, aliasKey); + } } - return mastery; + return node; } public HomebrewMetaTypes getHomebrewMetaTypes(Tools5eSources sources) { @@ -861,13 +909,6 @@ private Optional matchTable(String rowData, JsonNode table) { return Optional.empty(); } - public List originNodesMatching(Function filter) { - return nodeIndex.entrySet().stream() - .filter(e -> filter.apply(e.getValue())) - .map(Entry::getValue) - .collect(Collectors.toList()); - } - public JsonNode getOriginNoFallback(String finalKey) { JsonNode result = nodeIndex.get(finalKey); return result; @@ -898,10 +939,9 @@ public JsonNode getOrigin(String finalKey) { result = nodeIndex.get(lookup); } } - } - if (result == null) { - tui().debugf(Msg.UNRESOLVED, "No element found for %s", - finalKey); + if (result == null) { + tui().log(new Exception("No element found for " + finalKey), false); + } } return result; } @@ -962,7 +1002,7 @@ public String linkifyByName(Tools5eIndexType type, String name) { }); } - public boolean customRulesIncluded() { + public boolean customContentIncluded() { // The biggest hack of all time (not really). // I have some custom content for types/property/mastery that // should be included, but only if: @@ -1006,20 +1046,6 @@ public Set> includedEntries() { return filteredIndex.entrySet(); } - public JsonNode resolveClassFeatureNode(String finalKey) { - JsonNode featureNode = getOrigin(finalKey); - if (featureNode == null) { - tui().debugf(Msg.UNRESOLVED, "unresolved class feature %s", finalKey); - return null; // skip this - } - return resolveClassFeatureNode(finalKey, featureNode); - } - - public JsonNode resolveClassFeatureNode(String finalKey, JsonNode featureNode) { - // TODO: Handle copies or other fill-in / fluff? - return featureNode; - } - public Collection classesForSpell(String spellKey) { return spellClassIndex.get(spellKey); } @@ -1132,6 +1158,9 @@ public void cleanup() { // affiliated sources cache, too Tools5eSources.clear(); + ItemMastery.clear(); + ItemProperty.clear(); + ItemType.clear(); } static class Tuple { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java index 79a44b53f..275c8e308 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java @@ -49,6 +49,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { itemType, itemTypeAdditionalEntries, language, + languageFluff, legendaryGroup, magicvariant, monster, @@ -75,6 +76,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { status, subclass, subclassFeature, + subclassFluff, subrace("race"), table, tableGroup, @@ -381,7 +383,7 @@ public String decoratedName(JsonNode entry) { public String decoratedName(String name, JsonNode entry) { Tools5eSources sources = Tools5eSources.findOrTemporary(entry); if (sources.isPrimarySource("DMG") - && !sources.type.defaultSourceString().equalsIgnoreCase("DMG") + && !sources.getType().defaultSourceString().equalsIgnoreCase("DMG") && !name.contains("(DMG)")) { return name + " (DMG)"; } @@ -620,16 +622,18 @@ boolean hasVariants() { boolean isFluffType() { return switch (this) { case backgroundFluff, - facilityFluff, classFluff, conditionFluff, + facilityFluff, featFluff, itemFluff, + languageFluff, monsterFluff, objectFluff, optionalfeatureFluff, raceFluff, rewardFluff, + subclassFluff, trapFluff, vehicleFluff -> true; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java index 6f9619796..c9e60ca1a 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java @@ -186,15 +186,15 @@ public static String srdName(JsonNode node) { return "true".equalsIgnoreCase(name) ? null : name; } - final boolean srd; - final boolean basicRules; - final boolean srd52; - final boolean freeRules2024; - final Tools5eIndexType type; - final String edition; + private final boolean srd; + private final boolean basicRules; + private final boolean srd52; + private final boolean freeRules2024; + private final Tools5eIndexType type; + private final String edition; - boolean filterRule; - boolean cfgIncluded; + private boolean filterRule; + private boolean cfgIncluded; private Tools5eSources(Tools5eIndexType type, String key, JsonNode jsonElement) { super(type, key, jsonElement); @@ -207,6 +207,10 @@ private Tools5eSources(Tools5eIndexType type, String key, JsonNode jsonElement) testSourceRules(); } + public boolean isSrdOrFreeRules() { + return srd || basicRules || srd52 || freeRules2024; + } + /** * Is this included by configuration (source list, include/exclude rules)? * Content may be suppressed for other reasons (reprints) diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java index 17bcccced..241291e08 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java @@ -14,16 +14,13 @@ */ @TemplateData public class QuteBackground extends Tools5eQuteBase { - /** List of images for this background (as {@link dev.ebullient.convert.qute.ImageRef}) */ - public final List fluffImages; /** Formatted text listing other prerequisite conditions (optional) */ public final String prerequisite; public QuteBackground(Tools5eSources sources, String name, String source, String prerequisite, - String text, List images, Tags tags) { - super(sources, name, source, text, tags); - this.fluffImages = images; + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.prerequisite = prerequisite; // optional } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java index 70cde921b..bef221e81 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java @@ -36,15 +36,13 @@ public class QuteBastion extends Tools5eQuteBase { public final String type; /** Formatted text listing other prerequisite conditions (optional) */ public final String prerequisite; - /** List of images for this bastion (as {@link dev.ebullient.convert.qute.ImageRef}, optional) */ - public final List fluffImages; public QuteBastion(Tools5eSources sources, String name, String source, List hirelings, String level, List orders, String prerequisite, List space, String type, String text, List images, Tags tags) { - super(sources, name, source, text, tags); - this.fluffImages = images; // optional + super(sources, name, source, images, text, tags); + this.hirelings = hirelings; // optional this.level = level; // optional this.orders = orders; // optional diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java index 793615413..d5f1b2b9c 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java @@ -1,5 +1,14 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.joinConjunct; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import dev.ebullient.convert.qute.ImageRef; +import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; @@ -12,34 +21,365 @@ @TemplateData public class QuteClass extends Tools5eQuteBase { + /** Formatted string describing the primary abilities for this class */ + public final String primaryAbility; + /** Hit dice for this class as a single digit: 8 */ public final int hitDice; + /** Average Hit dice roll as a single digit */ + public final int hitRollAverage; + + /** + * Hit point die for this class as + * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.HitPointDie} + */ + public final HitPointDie hitPointDie; + /** Formatted callout containing class and feature progressions. */ public final String classProgression; - /** Formatted text describing starting equipment */ - public final String startingEquipment; + /** + * Formatted text describing starting equipment as + * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.StartingEquipment} + */ + public final StartingEquipment startingEquipment; - /** Formatted text section describing how to multiclass with this class */ - public final String multiclassing; + /** + * Multiclassing requirements and proficiencies for this class as + * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.Multiclassing} + */ + public final Multiclassing multiclassing; public QuteClass(Tools5eSources sources, String name, String source, - int hitDice, String classProgression, - String startingEquipment, String multiclassing, - String text, Tags tags) { - super(sources, name, source, text, tags); - - this.hitDice = hitDice; + String classProgression, + String primaryAbility, HitPointDie hitPointDie, + StartingEquipment startingEquipment, Multiclassing multiclassing, + String text, List images, Tags tags) { + super(sources, name, source, images, text, tags); + this.primaryAbility = primaryAbility; + this.hitPointDie = hitPointDie; + // compat with previous version. Sidekicks do not have a hitPointDie + this.hitDice = hitPointDie == null || hitPointDie.isSidekick() + ? 0 + : hitPointDie.face(); + this.hitRollAverage = hitPointDie == null || hitPointDie.isSidekick() + ? 0 + : hitPointDie.average(); this.classProgression = classProgression; this.startingEquipment = startingEquipment; this.multiclassing = multiclassing; } /** - * The average roll for a hit die of this class, for example: `add {resource.hitRollAverage}...` + * Describes the multiclassing information for the class. + * + * If referenced as a unit (ignoring inner attributes), it will render + * formatted text describing multiclassing requirements and proficiencies. + * + * @param primaryAbility Primary ability for multiclassing as formatted + * string (optional) + * @param requirements Prerequisites for multiclassing as formatted + * string (optional) + * @param requirementsSpecial Special prerequisites for multiclassing as + * formatted string (optional) + * @param skills Skill proficiencies gained as formatted string + * (optional) + * @param weapons Weapon proficiencies gained as formatted string + * (optional) + * @param tools Tool proficiencies gained as formatted string + * (optional) + * @param armor Armor proficiencies gained as formatted string + * (optional) + * @param text Formatted text describing this multiclass + * (optional) + * @param isClassic True if this class is from the 2014 edition + */ + @TemplateData + public record Multiclassing( + String primaryAbility, + String requirements, + String requirementsSpecial, + String skills, + String weapons, + String tools, + String armor, + String text, + boolean isClassic) implements QuteUtil { + public String prereq() { + if (isPresent(this.primaryAbility)) { + return "To qualify for a new class, you must have a score of at least 13 in the primary ability of the new class (%s) and your current classes." + .formatted(primaryAbility); + } + if (isPresent(requirements)) { + return requirements; + } + return ""; + } + + public String prereqSpecial() { + if (isPresent(requirementsSpecial)) { + List content = new ArrayList<>(); + if (isPresent(requirements)) { + content.add( + "To qualify for a new class, you must meet the %sprerequisites for both your current class and your new one." + .formatted(isPresent(requirementsSpecial) ? "" : "ability score ")); + } + maybeAddBlankLine(content); + content.add("**%sPrerequisites:** %s".formatted( + isPresent(requirements) ? "Other " : "", + requirementsSpecial)); + return String.join("\n", content); + } + return ""; + } + + public String profIntro() { + return "When you gain a level in a class other than your first, you gain only some of that class's starting proficiencies."; + } + + @Override + public String toString() { + boolean hasRequirements = isPresent(primaryAbility) || isPresent(requirements) + || isPresent(requirementsSpecial); + boolean hasProficiencies = isPresent(armor) || isPresent(weapons) || isPresent(tools) || isPresent(skills); + + List content = new ArrayList<>(); + if (hasRequirements) { + content.add(prereq()); + if (isPresent(requirementsSpecial)) { + maybeAddBlankLine(content); + content.add(prereqSpecial()); + } + } + if (isPresent(text)) { + maybeAddBlankLine(content); + content.add(text); + } + if (hasProficiencies) { + if (isPresent(requirements)) { + maybeAddBlankLine(content); + content.add(profIntro()); + } + maybeAddBlankLine(content); + if (isClassic) { + if (isPresent(armor)) { + content.add("- **Armor**: " + armor); + } + if (isPresent(weapons)) { + content.add("- **Weapons**: " + weapons); + } + if (isPresent(tools)) { + content.add("- **Tools**: " + tools); + } + if (isPresent(skills)) { + content.add("- **Skills**: " + skills); + } + } else { + if (isPresent(skills)) { + content.add("- **Skill Proficiencies**: " + skills); + } + if (isPresent(weapons)) { + content.add("- **Weapon Proficiencies**: " + weapons); + } + if (isPresent(tools)) { + content.add("- **Tool Proficiencies**: " + tools); + } + if (isPresent(armor)) { + content.add("- **Armor Training**: " + armor); + } + } + } + + return String.join("\n", content); + } + } + + /** + * Describes the starting equipment for the class. + * + * If referenced as a unit (ignoring inner attributes), it will render + * structured text describing starting proficiencies and equipment *2014* vs + * *2024*. + * + * @param savingThrows List of saving throws + * @param skills List of skills as formatted strings (links) + * @param weapons List of weapons as formatted strings (links) + * @param tools List of tools as formatted strings (links) + * @param armor List of armor as formatted strings (links) + * @param equipment List of equipment as formatted strings (links) + * @param isClassic True if this class is from the 2014 edition */ - public int getHitRollAverage() { - return (hitDice + 1) / 2; + @TemplateData + public record StartingEquipment( + List savingThrows, + List skills, + List weapons, + List tools, + List armor, + String equipment, + boolean isClassic) implements QuteUtil { + + @Override + public String toString() { + List text = new ArrayList<>(); + text.add(getProficiencies()); + if (isPresent(equipment)) { + maybeAddBlankLine(text); + text.add((isClassic ? "" : "**Starting Equipment:** ") + equipment); + } + return String.join("\n", text); + } + + /** Formatted string of class proficiencies */ + public String getProficiencies() { + List text = new ArrayList<>(); + if (isClassic) { + text.add("- **Saving Throws**: " + getJoinOrDefault(savingThrows, null)); + text.add("- **Armor**: " + (isPresent(armor) ? getArmorString() : "none")); + text.add("- **Weapons**: " + getJoinOrDefault(weapons, isClassic ? null : " and ")); + text.add("- **Tools**: " + getJoinOrDefault(tools, isClassic ? null : " and ")); + text.add("- **Skills**: " + join(" *or* ", skills)); + } else { + text.add("- **Saving Throw Proficiencies**: " + getJoinOrDefault(savingThrows, null)); + text.add("- **Skill Proficiencies**: " + join(" *or* ", skills)); + text.add("- **Weapon Proficiencies**: " + getJoinOrDefault(weapons, isClassic ? null : " and ")); + if (isPresent(tools)) { + text.add("- **Tool Proficiencies**: " + getJoinOrDefault(tools, isClassic ? null : " and ")); + } + if (isPresent(armor)) { + text.add("- **Armor Training**: " + getArmorString()); + } + } + return String.join("\n", text); + } + + /** + * Create a structured string describing armor training. + * Slighly different formatting and joining for 2014 vs 2024 materials. + * + * @return formatted string with links to armor item types and shield items + */ + public String getArmorString() { + if (isClassic) { + return join(", ", armor); + } + List armorLinks = armor.stream() + .filter(s -> s.matches("Light|Medium|Heavy")) + .collect(Collectors.toCollection(ArrayList::new)); + List otherLinks = armor.stream() + .filter(s -> !s.matches("Light|Medium|Heavy")) + .toList(); + if (armorLinks.size() > 1) { + // remove " armor" from all but the last item + for (int i = 0; i < armorLinks.size() - 1; i++) { + armorLinks.set(i, armorLinks.get(i).replace(" armor", "")); + } + String joined = joinConjunct("and", armorLinks); + armorLinks.clear(); + armorLinks.add(joined); + } + armorLinks.addAll(otherLinks); + return joinConjunct(" and ", armorLinks); + } + + /** + * Given a list of strings, return a formatted string with a conjunction. + * + * @param value List of strings. + * @param conjunct Conjunction (and, or). If null, elements will be + * comma-separated. + * Otherwise, the first n elements comma-separated and the last + * element will be joined with conjunction. + * @return Formatted string. If value is empty, will return "none". + */ + public String getJoinOrDefault(List value, String conjunct) { + if (value == null || value.isEmpty()) { + return "none"; + } + return conjunct == null + ? join(", ", value) + : joinConjunct(conjunct, value); + } + } + + /** + * Describes the hit point die used by the class. + * + * If referenced as a unit (ignoring inner attributes), it will render + * formatted strings based on the class version (2024 or not). + * + * @param number How many dice to roll (pretty much always 1) + * @param face Die to roll (8, 10); This will be 0 for sidekicks + * @param average The average value of a hit dice roll + * @param isClassic True if this is a 2014 class + * @param isSidekick Explicit test for sidekick (alternate to 0 face) + */ + @TemplateData + public record HitPointDie( + String name, + int number, + int face, + int average, + boolean isClassic, + boolean isSidekick) { + public HitPointDie(String name, int number, int face, boolean isClassic, boolean isSidekick) { + this(name, number, face, (number * face) / 2 + 1, isClassic, isSidekick); + } + + @Override + public String toString() { + // return + // `
Hit Point Die: + // ${renderer.render(Renderer.class.getHitDiceEntry(cls.hd, {styleHint}))} per + // ${cls.name} level
+ //
Hit Points at Level 1: + // ${Renderer.class.getHitPointsAtFirstLevel(cls.hd, {styleHint})}
+ //
Hit Points per additional ${cls.name} Level: + // ${Renderer.class.getHitPointsAtHigherLevels(cls.name, cls.hd, + // {styleHint})}
`; + // return styleHint === "classic" -- hit dice entry + // ? `{@dice ${clsHd.number}d${clsHd.faces}||Hit die}` + // : `{@dice ${clsHd.number}d${clsHd.faces}|${clsHd.number === 1 ? "" : + // clsHd.number}D${clsHd.faces}|Hit die}`; + if (isSidekick) { + String suffix = isClassic ? "its Constitution modifier" : "its Con. modifier"; + return """ + - **Hit Point Die**: *x*; specified in the sidekick's statblock (human, gnome, kobold, etc.) + - **Hit Points at Level 1:** 1d*x* + %s + - **Hit Points per additional %s lvel:** 1d*x* + %s (minimum of 1 hit point per level) + """ + .stripIndent() + .formatted(suffix, name, suffix); + } + + String dieEntry = isClassic + ? "%sd%s".formatted(number, face) + : "%sD%s".formatted(number == 1 ? "" : number, face); + + // classic ? `${clsHd.number * clsHd.faces} + your Constitution modifier` + // : `${clsHd.number * clsHd.faces} + Con. modifier`; + String level1 = "%s + %s".formatted( + number * face, + isClassic ? "your Constitution modifier" : "Con. modifier"); + + // classic ? `${Renderer.get().render(Renderer.class.getHitDiceEntry(clsHd, + // {styleHint}))} (or ${((clsHd.number * clsHd.faces) / 2 + 1)}) + your + // Constitution modifier per ${className} level after 1st` + // : `${Renderer.get().render(Renderer.class.getHitDiceEntry(clsHd, + // {styleHint}))} + your Con. modifier, or, ${((clsHd.number * clsHd.faces) / 2 + // + 1)} + your Con. modifier`; + String levelUp = isClassic + ? "%s (or %s) + your Constitution modifier".formatted( + dieEntry, average) + : "%s + your Con. modifier or %s + your Con. modifier".formatted( + dieEntry, average); // average + + return """ + - **Hit Point Die:** %s per %s level + - **Hit Points at Level 1:** %s + - **Hit Points per additional %s Level:** %s + """.formatted(dieEntry, name, level1, name, levelUp); + } } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java index 47095e475..43a8a82a4 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java @@ -25,7 +25,7 @@ public class QuteDeck extends Tools5eQuteBase { public QuteDeck(CompendiumSources sources, String name, String source, ImageRef cardBack, List cards, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, List.of(), text, tags); this.cardBack = cardBack; this.cards = cards; } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java index 7a40cd9a2..e27743a13 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java @@ -39,7 +39,7 @@ public QuteDeity(Tools5eSources sources, String name, String source, String title, String cateogry, String domains, String province, String symbol, ImageRef symbolImg, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, List.of(), text, tags); this.altNames = altNames; this.pantheon = pantheon; this.alignment = alignment; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java index 84d02cf21..877f8ecb7 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; @@ -19,8 +22,8 @@ public class QuteFeat extends Tools5eQuteBase { public QuteFeat(Tools5eSources sources, String name, String source, String prerequisite, String level, - String text, Tags tags) { - super(sources, name, source, text, tags); + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); withTemplate("feat2md.txt"); // Feat and OptionalFeature this.level = level; this.prerequisite = prerequisite; // optional diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java index 573791d27..4db9fb51d 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.Tags; import io.quarkus.qute.TemplateData; @@ -17,8 +20,8 @@ public class QuteHazard extends Tools5eQuteBase { public QuteHazard(CompendiumSources sources, String name, String source, String hazardType, - String text, Tags tags) { - super(sources, name, source, text, tags); + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.hazardType = hazardType; withTemplate("hazard2md.txt"); // not trap or hazard (types) } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java index b17e23e98..913217f66 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java @@ -1,11 +1,11 @@ package dev.ebullient.convert.tools.dnd5e.qute; import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import java.util.List; import java.util.stream.Collectors; -import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; @@ -22,19 +22,16 @@ public class QuteItem extends Tools5eQuteBase { /** Detailed information about this item as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant} */ public final Variant rootVariant; - /** List of images for this item as {@link dev.ebullient.convert.qute.ImageRef} */ - public final List fluffImages; /** List of magic item variants as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant}. Optional. */ public final List variants; public QuteItem(Tools5eSources sources, String source, - Variant rootVariant, String text, List images, - List variants, Tags tags) { - super(sources, rootVariant.name, source, text, tags); + Variant rootVariant, List variants, List images, + String text, Tags tags) { + super(sources, rootVariant.name, source, images, text, tags); withTemplate("item2md.txt"); this.rootVariant = rootVariant; - this.fluffImages = images == null ? List.of() : images; this.variants = variants == null ? List.of() : variants; } @@ -130,7 +127,7 @@ public String getVariantSectionLinks() { return ""; } return variants.stream() - .map(x -> String.format("- [%s](#%s)", x.name, Tui.toAnchorTag(x.name))) + .map(x -> String.format("- [%s](#%s)", x.name, toAnchorTag(x.name))) .collect(Collectors.joining("\n")); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java index 7d1e64e7c..d38a558b1 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java @@ -91,8 +91,6 @@ public class QuteMonster extends Tools5eQuteBase { public final String environment; /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */ public final ImageRef token; - /** List of {@link dev.ebullient.convert.qute.ImageRef} related to the creature */ - public final List fluffImages; public QuteMonster(Tools5eSources sources, String name, String source, boolean isNpc, String size, String type, String subtype, String alignment, @@ -105,9 +103,9 @@ public QuteMonster(Tools5eSources sources, String name, String source, boolean i Collection legendary, Collection legendaryGroup, String legendaryGroupLink, List spellcasting, String description, String environment, - ImageRef tokenImage, List fluffImages, Tags tags) { + ImageRef tokenImage, List images, Tags tags) { - super(sources, name, source, description, tags); + super(sources, name, source, images, description, tags); this.isNpc = isNpc; this.size = size; @@ -137,7 +135,6 @@ public QuteMonster(Tools5eSources sources, String name, String source, boolean i this.description = description; this.environment = environment; this.token = tokenImage; - this.fluffImages = fluffImages; } @Override diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java index d0d122312..059d1d18f 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java @@ -45,8 +45,6 @@ public class QuteObject extends Tools5eQuteBase { /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */ public final ImageRef token; - /** List of {@link dev.ebullient.convert.qute.ImageRef} related to the creature */ - public final List fluffImages; public QuteObject(CompendiumSources sources, String name, String source, @@ -57,9 +55,9 @@ public QuteObject(CompendiumSources sources, String senses, ImmuneResist immuneResist, Collection actions, - ImageRef tokenImage, List fluffImages, + ImageRef tokenImage, List images, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, images, text, tags); this.isNpc = isNpc; this.size = size; this.creatureType = creatureType; @@ -74,7 +72,6 @@ public QuteObject(CompendiumSources sources, this.action = actions; this.token = tokenImage; - this.fluffImages = fluffImages; } /** List of source books (abbreviated name). Fantasy statblock uses this list. */ diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java index 1adf6cc6f..602e0f4f2 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.dnd5e.qute; import java.util.Collection; +import java.util.List; import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.tools.CompendiumSources; @@ -25,7 +26,7 @@ public class QutePsionic extends Tools5eQuteBase { public QutePsionic(CompendiumSources sources, String name, String source, String typeOrder, String focus, Collection modes, String text, Tags tags) { - super(sources, name, source, text, tags); + super(sources, name, source, List.of(), text, tags); this.typeOrder = typeOrder; this.focus = focus; this.modes = modes; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java index 895b079c1..5668a6403 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java @@ -29,14 +29,12 @@ public class QuteRace extends Tools5eQuteBase { public final String traits; /** Formatted text describing the race. Optional. Same as {resource.text} */ public final String description; - /** List of images for this race (as {@link dev.ebullient.convert.qute.ImageRef}) */ - public final List fluffImages; public QuteRace(Tools5eSources sources, String name, String source, String ability, String type, String size, String speed, String spellcasting, String traits, String description, List images, Tags tags) { - super(sources, name, source, description, tags); + super(sources, name, source, images, description, tags); this.ability = ability; this.type = type; this.size = size; @@ -44,6 +42,5 @@ public QuteRace(Tools5eSources sources, String name, String source, this.spellcasting = spellcasting; this.traits = traits; this.description = description; - this.fluffImages = images; } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java index 104bbfafc..bfc92f791 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.Tags; import io.quarkus.qute.TemplateData; @@ -23,8 +26,8 @@ public class QuteReward extends Tools5eQuteBase { public QuteReward(CompendiumSources sources, String name, String source, String ability, String detail, String signatureSpells, - String text, Tags tags) { - super(sources, name, source, text, tags); + List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); withTemplate("reward2md.txt"); this.ability = ability; this.detail = detail; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java index 62dcc85a4..0182b3325 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java @@ -32,14 +32,12 @@ public class QuteSpell extends Tools5eQuteBase { public final String duration; /** String: rendered list of links to classes that can use this spell. May be incomplete or empty. */ public final String classes; - /** List of images for this spell (as {@link dev.ebullient.convert.qute.ImageRef}) */ - public final List fluffImages; public QuteSpell(Tools5eSources sources, String name, String source, String level, String school, boolean ritual, String time, String range, String components, String duration, - String classes, String text, List fluffImages, Tags tags) { - super(sources, name, source, text, tags); + String classes, List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.level = level; this.school = school; @@ -49,7 +47,6 @@ public QuteSpell(Tools5eSources sources, String name, String source, String leve this.components = components; this.duration = duration; this.classes = classes; - this.fluffImages = fluffImages; } /** List of class names that can use this spell. May be incomplete or empty. */ diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java index d98f7e6bf..37837f42e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java @@ -1,5 +1,8 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.List; + +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; import io.quarkus.qute.TemplateData; @@ -11,7 +14,6 @@ */ @TemplateData public class QuteSubclass extends Tools5eQuteBase { - /** Name of the parent class */ public final String parentClass; /** Markdown link to the parent class */ @@ -30,9 +32,8 @@ public QuteSubclass(Tools5eSources sources, String parentClassSource, String subclassTitle, String classProgression, - String text, Tags tags) { - super(sources, name, source, text, tags); - + String text, List images, Tags tags) { + super(sources, name, source, images, text, tags); this.parentClass = parentClass; this.parentClassLink = parentClassLink; this.parentClassSource = parentClassSource; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java index 57c650e5f..8cb957426 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java @@ -54,8 +54,6 @@ public class QuteVehicle extends Tools5eQuteBase { /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */ public final ImageRef token; - /** List of {@link dev.ebullient.convert.qute.ImageRef} related to the creature */ - public final List fluffImages; public QuteVehicle(CompendiumSources sources, String name, String source, String vehicleType, String terrain, @@ -64,9 +62,8 @@ public QuteVehicle(CompendiumSources sources, String name, String source, ShipCrewCargoPace shipCrewCargoPace, List shipSections, Collection action, - ImageRef token, List fluffImages, - String text, Tags tags) { - super(sources, name, source, text, tags); + ImageRef token, List images, String text, Tags tags) { + super(sources, name, source, images, text, tags); this.vehicleType = vehicleType; this.terrain = terrain; @@ -81,7 +78,6 @@ public QuteVehicle(CompendiumSources sources, String name, String source, this.action = action; this.token = token; - this.fluffImages = fluffImages; } /** True if this vehicle is a Ship */ diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java index 31cd42c8b..7179a8e19 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java @@ -1,11 +1,13 @@ package dev.ebullient.convert.tools.dnd5e.qute; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.QuteBase; import dev.ebullient.convert.tools.CompendiumSources; import dev.ebullient.convert.tools.Tags; @@ -24,12 +26,76 @@ @TemplateData public class Tools5eQuteBase extends QuteBase { + /** List of images as {@link dev.ebullient.convert.qute.ImageRef} (optional) */ + public final List fluffImages; + String targetPath; String filename; String template; - public Tools5eQuteBase(CompendiumSources sources, String name, String source, String text, Tags tags) { + public Tools5eQuteBase(CompendiumSources sources, String name, String source, List fluffImages, String text, + Tags tags) { super(sources, name, source, text, tags); + this.fluffImages = isPresent(fluffImages) ? fluffImages : List.of(); + } + + /** + * Return true if any images are present + */ + public boolean getHasImages() { + return !fluffImages.isEmpty(); + } + + /** + * Return true if more than one image is present + */ + public boolean getHasMoreImages() { + return fluffImages.size() > 1; + } + + /** + * Return an embedded wikilink to the first image + * Will have the "right" anchor tag. + */ + public String getShowPortraitImage() { + if (fluffImages.isEmpty()) { + return ""; + } + return fluffImages.get(0).getEmbeddedLink("right"); + } + + /** + * Return embedded wikilinks for all images + * If there is more than one, they will be displayed in a gallery. + */ + public String getShowAllImages() { + return createImageLinks(false); + } + + /** + * Return embedded wikilinks for all but the first image + * If there is more than one, they will be displayed in a gallery. + */ + public String getShowMoreImages() { + return createImageLinks(true); + } + + private String createImageLinks(boolean omitFirst) { + if (fluffImages.isEmpty()) { + return ""; + } + if (fluffImages.size() == 1 && !omitFirst) { + return fluffImages.get(0).getEmbeddedLink("center"); + } + if (fluffImages.size() == 2 && omitFirst) { + return fluffImages.get(1).getEmbeddedLink("center"); + } + List lines = new ArrayList<>(); + lines.add("> [!gallery]"); + for (int i = omitFirst ? 1 : 0; i < fluffImages.size(); i++) { + lines.add(fluffImages.get(i).getEmbeddedLink("")); // no anchor + } + return String.join("\n", lines); } public static String fixFileName(String name, Tools5eSources sources) { diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java index f1104d55a..684cb185c 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java @@ -1,5 +1,6 @@ package dev.ebullient.convert.tools.pf2e; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import static dev.ebullient.convert.StringUtil.toTitleCase; import java.nio.file.Path; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java index 9d05096bf..f97106934 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.pf2e; +import static dev.ebullient.convert.StringUtil.toAnchorTag; + import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java index 8d890ec47..24dc0c4b2 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java @@ -1,6 +1,7 @@ package dev.ebullient.convert.tools.pf2e; import static dev.ebullient.convert.StringUtil.join; +import static dev.ebullient.convert.StringUtil.toAnchorTag; import static dev.ebullient.convert.StringUtil.toTitleCase; import java.util.ArrayList; diff --git a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java index 7dd32c940..f5c01b970 100644 --- a/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java @@ -1,12 +1,13 @@ package dev.ebullient.convert.tools.pf2e.qute; +import static dev.ebullient.convert.StringUtil.toAnchorTag; + import java.util.Collection; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; -import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.pf2e.Pf2eIndexType; import dev.ebullient.convert.tools.pf2e.Pf2eSources; import io.quarkus.qute.TemplateData; @@ -43,7 +44,7 @@ public QuteTraitIndex(Pf2eSources sources, Map> categ /** List of category anchor links */ public List getCategoryLinks() { return categoryToTraits.keySet().stream() - .map(x -> "[" + x + "](#" + Tui.toAnchorTag(x) + ")") + .map(x -> "[" + x + "](#" + toAnchorTag(x) + ")") .toList(); } diff --git a/src/main/resources/convertData.json b/src/main/resources/convertData.json index fb68a2841..30dfa0883 100644 --- a/src/main/resources/convertData.json +++ b/src/main/resources/convertData.json @@ -30,6 +30,16 @@ "type": "entries", "name": "Attunement", "edition": "classic", + "source": "DMG", + "page": 136, + "basicRules": true, + "srd": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Attunement|XPHB" + } + ], "entries": [ "Some magic items require a creature to form a bond with them before their magical properties can be used. This bond is called attunement, and certain items have a prerequisite for it. If the prerequisite is a class, a creature must be a member of that class to attune to the item. (If the class is a spellcasting class, a monster qualifies if it has spell slots and uses that class's spell list.) If the prerequisite is to be a spellcaster, a creature qualifies if it can cast at least one spell using its traits or features, not using a magic item or the like.", "Without becoming attuned to an item that requires attunement, a creature gains only its nonmagical benefits, unless its description states otherwise. For example, a magic shield that requires attunement provides the benefits of a normal shield to a creature not attuned to it, but none of its magical properties.", @@ -59,6 +69,8 @@ "source": "XPHB", "page": 232, "id": "717", + "freerules2024": true, + "srd52": true, "entries": [ "Some magic items require a creature to form a bond\u2014called Attunement\u2014with them before the creature can use an item's magical properties. Without becoming attuned to an item that requires Attunement, you gain only its nonmagical benefits unless its description states otherwise. For example, a magic Shield that requires Attunement provides the benefits of a normal Shield if you aren't attuned to it, but none of its magical properties.", { @@ -107,7 +119,9 @@ { "name": "General and Weapon Properties", "srd": true, + "srd52": true, "basicRules": true, + "freeRules2024": true, "entries": [ ] }, @@ -118,6 +132,12 @@ "page": 147, "srd": true, "basicRules": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Improvised Weapons|XPHB" + } + ], "entries": [ "Sometimes characters don't have their weapons and have to attack with whatever is close at hand. An improvised weapon includes any object you can wield in one or two hands, such as broken glass, a table leg, a frying pan, a wagon wheel, or a dead goblin.", "In many cases, an improvised weapon is similar to an actual weapon and can be treated as such. For example, a table leg is akin to a club. At the DM's option, a character proficient with a weapon can use a similar object as if it were that weapon and use his or her proficiency bonus.", @@ -174,16 +194,41 @@ "name": "Cursed Items", "source": "DMG", "page": 138, + "basicRules": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Cursed Items|XDMG" + } + ], "entries": [ "Some magic items bear curses that bedevil their users, sometimes long after a user has stopped using an item. Most methods of identifying items, including the identify spell, fail to reveal the presence of a curse, although lore might hint at it.", "Attunement to a cursed item can't be ended voluntarily unless the curse is broken first, such as with the {@spell remove curse} spell." ] }, + { + "type": "entries", + "name": "Cursed Items", + "source": "XDMG", + "page": 220, + "freeRules2024": true, + "entries": [ + "A magic item’s description specifies whether it bears a curse. Most methods of identifying items, including the Identify spell, fail to reveal such a curse.", + "Attunement to a cursed item can't be ended voluntarily unless the curse is broken first, such as with the {@spell Remove Curse} spell." + ] + }, { "type": "entries", "name": "Poison", "source": "DMG", "page": 257, + "srd": true, + "reprintedAs": [ + { + "tag": "reference", + "uid": "Poison|XDMG" + } + ], "entries": [ "Given their insidious and deadly nature, poisons are illegal in most societies but are a favorite tool among assassins, drow, and other evil creatures.", "Poisons come in the following four types.", @@ -224,6 +269,59 @@ "id": "2fc" } ] + }, + { + "name": "Poison", + "source": "XDMG", + "page": 90, + "entries": [ + "Given their insidious and deadly nature, poisons are a favorite tool among assassins and evil creatures.", + "Poisons come in the following four types:", + { + "type": "list", + "style": "list-hang-notitle", + "items": [ + { + "type": "item", + "name": "Contact", + "entry": "Contact poison can be smeared on an object and remains potent until it is touched or washed off. A creature that touches contact poison with exposed skin suffers its effects." + }, + { + "type": "item", + "name": "Ingested", + "entry": "A creature must swallow an entire dose of ingested poison to suffer its effects. The dose can be delivered in food or a liquid. You may decide that a partial dose has a reduced effect, such as allowing {@variantrule Advantage|XPHB} on the saving throw or dealing only half as much damage on a failed save." + }, + { + "type": "item", + "name": "Inhaled", + "entry": "Poisonous powders and gases take effect when inhaled. Blowing the powder or releasing the gas subjects creatures in a 5-foot {@variantrule Cube [Area of Effect]|XPHB|Cube} to its effect. The resulting cloud dissipates immediately afterward. Holding one's breath is ineffective against inhaled poisons, as they affect nasal membranes, tear ducts, and other parts of the body." + }, + { + "type": "item", + "name": "Injury", + "entry": "Injury poison can be applied as a Bonus Action to a weapon, a piece of ammunition, or similar object. The poison remains potent until delivered through a wound or washed off. A creature that takes Piercing or Slashing damage from an object coated with the poison is exposed to its effects." + } + ] + }, + { + "type": "entries", + "name": "Purchasing Poison", + "page": 90, + "id": "1be", + "entries": [ + "In some settings, laws prohibit the possession and use of poison, but an illicit dealer or unscrupulous apothecary might keep a hidden stash. Characters with criminal contacts might be able to acquire poison easily. Other characters might have to make extensive inquiries and pay bribes before they acquire the poison they seek." + ] + }, + { + "type": "entries", + "name": "Harvesting Poison", + "page": 90, + "id": "1bf", + "entries": [ + "A character can attempt to harvest poison from a venomous creature that is dead or has the {@condition Incapacitated|XPHB} condition. The effort takes {@dice 1d6} minutes, after which the character makes a {@dc 20} Intelligence ({@skill Nature|XPHB}) check using a {@item Poisoner's Kit|XPHB}. On a successful check, the character harvests enough poison for a single dose, and no additional poison can be harvested from that creature. On a failed check, the character is unable to extract any poison. If the character fails the check by 5 or more, the character is subjected to the creature's poison." + ] + } + ] } ] }, @@ -565,6 +663,7 @@ "aliases": { "item|alchemist's tools|phb": "item|alchemist's supplies|phb", "item|alchemists' supplies|phb": "item|alchemist's supplies|phb", + "item|arrow of slaying (generic)|dmg": "item|arrow of slaying|dmg", "item|backpack|dmg": "item|backpack|phb", "item|breastplate|dmg": "item|breastplate|phb", "item|caltrops (20)|phb": "item|caltrops (bag of 20)|phb", @@ -589,6 +688,7 @@ "spell|deception|phb": "skill|deception|phb", "spell|detect good and evil|phb": "spell|detect evil and good|phb", "spell|enlarge|phb": "spell|enlarge/reduce|phb", + "spell|fire wall|phb": "spell|wall of fire|phb", "spell|frostbite|phb": "spell|frostbite|xge", "spell|history|phb": "skill|history|phb", "spell|infestation|phb": "spell|infestation|xge", @@ -605,52 +705,6 @@ "trap|quicksand|dmg": "hazard|quicksand|dmg" }, "fixes": { - "adventure/adventure-lr.json": [ - { - "match": " \\(see the \\\"\\{@condition Bluerot\\|GoS}\\\" sidebar\\)", - "replace": "" - } - ], - "adventure/adventure-rot.json": [ - { - "match": "\\{@i \\{@i The Rise of Tiamat}.}", - "replace": "{@i The Rise of Tiamat}." - } - ], - "class/class-artificer.json": [ - { - "match": "(\"page\":\\s*\\d+,)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"Alchemist)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Alchemical Formula\", \"featureType\": [ \"AF\" ] } ],$2$3" - } - ], - "class/class-bard.json": [ - { - "match": "(\"isReprinted\": true,)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"College of Swords)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Fighting Style\", \"featureType\": [ \"FS:B\" ], \"progression\": { \"3\": 1 } } ],$2$3" - }, - { - "match": "(\"page\":\\s*\\d+,)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"College of Swords)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Fighting Style\", \"featureType\": [ \"FS:B\" ], \"progression\": { \"3\": 1 } } ],$2$3" - } - ], - "class/class-wizard.json": [ - { - "match": "(\\],)([\\r\\n\\s]+)(\"subclassFeatures\": \\[[\\s\\r\\n]+\"Onomancy)", - "replace": "$1$2\"optionalfeatureProgression\": [ { \"name\": \"Onomancy Resonant\", \"featureType\": [ \"OR\" ] } ],$2$3" - } - ], - "class/D&D Wiki; Swashbuckler.json": [ - { - "match": "\t\t\t\t\"MB\"", - "replace": "\t\t\t\t\"TB\"" - } - ], - "deities.json": [ - { - "match": "deities/TDCSR/StormLord.webp", - "replace": "deities/TDCSR/Stormlord.webp" - } - ] }, "sources": [ "actions.json", @@ -663,6 +717,7 @@ "books.json", "class", "conditionsdiseases.json", + "cultsboons.json", "decks.json", "deities.json", "feats.json", @@ -671,6 +726,7 @@ "fluff-conditionsdiseases.json", "fluff-feats.json", "fluff-items.json", + "fluff-languages.json", "fluff-objects.json", "fluff-optionalfeatures.json", "fluff-races.json", @@ -680,6 +736,7 @@ "generated/gendata-tables.json", "items-base.json", "items.json", + "languages.json", "magicvariants.json", "objects.json", "optionalfeatures.json", diff --git a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java index e7ad85b49..ab6f1d98f 100644 --- a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java +++ b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java @@ -2,12 +2,18 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import dev.ebullient.convert.io.Tui; @@ -19,9 +25,11 @@ @QuarkusMainTest public class CustomTemplatesTest { - static Path testOutput; + static Path rootTestOutput; static Tui tui; + Path testOutput; + @BeforeAll public static void setupDir() { setupDir("templates"); @@ -30,8 +38,8 @@ public static void setupDir() { public static void setupDir(String name) { tui = new Tui(); tui.init(null, false, false); - testOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name); - testOutput.toFile().mkdirs(); + rootTestOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name); + rootTestOutput.toFile().mkdirs(); } @AfterAll @@ -39,9 +47,33 @@ public static void cleanup() { System.out.println("Done."); } + @BeforeEach + public void setup() { + testOutput = null; // test should set this to something readable + } + + @AfterEach + public void moveLogFile() throws IOException { + assertThat(testOutput).isNotNull(); // make sure test set this + + Path logFile = Path.of("ttrpg-convert.out.txt"); + if (Files.exists(logFile) && Files.exists(testOutput)) { + String content = Files.readString(logFile, StandardCharsets.UTF_8); + + Path filePath = testOutput.resolve(logFile); + Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING); + + if (content.contains("Exception")) { + tui.errorf("Exception found in %s", filePath); + } + } + TestUtils.cleanupReferences(); + } + @Test @Launch({ "--help" }) void testCommandHelp(LaunchResult result) { + testOutput = rootTestOutput.resolve("help"); result.echoSystemOut(); assertThat(result.getOutput()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) @@ -51,6 +83,7 @@ void testCommandHelp(LaunchResult result) { @Test @Launch({ "--version" }) void testCommandVersion(LaunchResult result) { + testOutput = rootTestOutput.resolve("version"); result.echoSystemOut(); assertThat(result.getOutput()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) @@ -58,13 +91,13 @@ void testCommandVersion(LaunchResult result) { } @Test - void testCommandBadTemplates(QuarkusMainLauncher launcher) { + void testCommandBadTemplates(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("bad-templates"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("bad-templates"); - + TestUtils.deleteDir(testOutput); LaunchResult result = launcher.launch("--index", "--background=garbage.txt", - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); @@ -75,12 +108,13 @@ void testCommandBadTemplates(QuarkusMainLauncher launcher) { } @Test - void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) { + void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("bad-templates-json"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("bad-templates-json"); + TestUtils.deleteDir(testOutput); LaunchResult result = launcher.launch("--index", - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.PATH_5E_TOOLS_DATA.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.TEST_RESOURCES.resolve("sources-bad-template.json").toString()); @@ -92,13 +126,13 @@ void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) { } @Test - void testCommandTemplates_5e(QuarkusMainLauncher launcher) { + void testCommandTemplates_5e(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("srd-templates"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("srd-templates"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); // SRD only, just templates - LaunchResult result = launcher.launch( + LaunchResult result = launcher.launch("--log", "--index", "--background", TestUtils.TEST_RESOURCES.resolve("other/background.txt").toString(), "--class", TestUtils.TEST_RESOURCES.resolve("other/class.txt").toString(), "--deity", TestUtils.TEST_RESOURCES.resolve("other/deity.txt").toString(), @@ -108,7 +142,7 @@ void testCommandTemplates_5e(QuarkusMainLauncher launcher) { "--race", TestUtils.TEST_RESOURCES.resolve("other/race.txt").toString(), "--spell", TestUtils.TEST_RESOURCES.resolve("other/spell.txt").toString(), "--subclass", TestUtils.TEST_RESOURCES.resolve("other/subclass.txt").toString(), - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); @@ -117,14 +151,14 @@ void testCommandTemplates_5e(QuarkusMainLauncher launcher) { .isEqualTo(0); List.of( - target.resolve("compendium/backgrounds"), - target.resolve("compendium/classes"), - target.resolve("compendium/deities"), - target.resolve("compendium/feats"), - target.resolve("compendium/items"), - target.resolve("compendium/races"), - target.resolve("compendium/spells"), - target.resolve("rules")) + testOutput.resolve("compendium/backgrounds"), + testOutput.resolve("compendium/classes"), + testOutput.resolve("compendium/deities"), + testOutput.resolve("compendium/feats"), + testOutput.resolve("compendium/items"), + testOutput.resolve("compendium/races"), + testOutput.resolve("compendium/spells"), + testOutput.resolve("rules")) .forEach(directory -> TestUtils.assertDirectoryContents(directory, tui, (p, content) -> { List errors = new ArrayList<>(); boolean index = false; @@ -155,14 +189,14 @@ void testCommandTemplates_5e(QuarkusMainLauncher launcher) { } @Test - void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) { + void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) throws IOException { + testOutput = rootTestOutput.resolve("json-templates"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { - Path target = testOutput.resolve("json-templates"); - TestUtils.deleteDir(target); + TestUtils.deleteDir(testOutput); - LaunchResult result = launcher.launch("--debug", "--index", + LaunchResult result = launcher.launch("--log", "--index", "-c", TestUtils.TEST_RESOURCES.resolve("5e/sources-templates.json").toString(), - "-o", target.toString(), + "-o", testOutput.toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); @@ -171,19 +205,19 @@ void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) { .isEqualTo(0); // test extra cp value attribute in yaml frontmatter - Path abacus = target.resolve("compendium/items/abacus.md"); + Path abacus = testOutput.resolve("compendium/items/abacus.md"); assertThat(abacus).exists(); assertThat(abacus).content().contains("cost: 200"); List.of( - target.resolve("compendium/backgrounds"), - target.resolve("compendium/classes"), - target.resolve("compendium/deities"), - target.resolve("compendium/feats"), - target.resolve("compendium/items"), - target.resolve("compendium/races"), - target.resolve("compendium/spells"), - target.resolve("rules")) + testOutput.resolve("compendium/backgrounds"), + testOutput.resolve("compendium/classes"), + testOutput.resolve("compendium/deities"), + testOutput.resolve("compendium/feats"), + testOutput.resolve("compendium/items"), + testOutput.resolve("compendium/races"), + testOutput.resolve("compendium/spells"), + testOutput.resolve("rules")) .forEach(directory -> TestUtils.assertDirectoryContents(directory, tui, (p, content) -> { List errors = new ArrayList<>(); boolean frontmatter = false; diff --git a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java index 26b5ca0ad..106f83a26 100644 --- a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java +++ b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java @@ -293,7 +293,7 @@ void testLiveData_5eUA(QuarkusMainLauncher launcher) { } @Test - void testCommand_5eBookAdventureInJson(QuarkusMainLauncher launcher) { + void testLiveData_5eBookAdventureInJson(QuarkusMainLauncher launcher) { testOutput = rootTestOutput.resolve("json-book-adventure"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { TestUtils.deleteDir(testOutput); @@ -340,7 +340,7 @@ void testCommand_5eBookAdventureInJson(QuarkusMainLauncher launcher) { } @Test - void testCommand_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { + void testLiveData_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { testOutput = rootTestOutput.resolve("yaml-adventure"); if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { TestUtils.deleteDir(testOutput); @@ -365,4 +365,23 @@ void testCommand_5eBookAdventureMinimalYaml(QuarkusMainLauncher launcher) { }); } } + + @Test + void testLiveData_Sample(QuarkusMainLauncher launcher) { + testOutput = rootTestOutput.resolve("sample"); + if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) { + + TestUtils.deleteDir(testOutput); + + Tui.instance().infof("--- Sample content ----- "); + + LaunchResult result = launcher.launch("--log", "--index", + "-o", testOutput.toString(), + "-c", TestUtils.TEST_RESOURCES.resolve("5e/sample.yaml").toString(), + TestUtils.PATH_5E_TOOLS_DATA.toString()); + assertThat(result.exitCode()) + .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) + .isEqualTo(0); + } + } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java index 9cdbdc7eb..59fe8c6ba 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java @@ -40,8 +40,8 @@ public void cleanup() throws Exception { public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); - // All sources, but reprints will be followed. - // PHB elements should be missing/replaced by XPHB equivalents (e.g.) + // All sources, but things that have been reprinted will be replaced by the newest version + // e.g. PHB elements should be missing/replaced by XPHB equivalents if (commonTests.dataPresent) { commonTests.assert_MISSING("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); @@ -51,6 +51,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|artificer|tce"); commonTests.assert_MISSING("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); @@ -197,7 +198,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); commonTests.assert_MISSING("subrace|human|human|phb|phb"); commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_Present("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); commonTests.assert_MISSING("trap|collapsing roof|dmg"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java index 0c4e9cc12..f4614a761 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java @@ -56,6 +56,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_Present("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java index e9e7b3761..88da1889b 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java index c6db87eca..c13a72d81 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_MISSING("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java index 1a75c0e43..ec8fe990b 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_MISSING("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_MISSING("classtype|bard|xphb"); commonTests.assert_Present("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java index 44706f0c4..e7f763f55 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_MISSING("classtype|artificer|tce"); commonTests.assert_MISSING("classtype|bard|phb"); commonTests.assert_Present("classtype|bard|xphb"); commonTests.assert_MISSING("condition|blinded|phb"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java index 68fd4f0c0..440a86aa8 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java @@ -49,6 +49,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("background|sage|phb"); commonTests.assert_MISSING("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + commonTests.assert_Present("classtype|artificer|tce"); commonTests.assert_Present("classtype|bard|phb"); commonTests.assert_MISSING("classtype|bard|xphb"); commonTests.assert_Present("condition|blinded|phb"); diff --git a/src/test/resources/5e/sample.yaml b/src/test/resources/5e/sample.yaml new file mode 100644 index 000000000..cb9b51655 --- /dev/null +++ b/src/test/resources/5e/sample.yaml @@ -0,0 +1,93 @@ +sources: + adventure: + - CM + - DC + - DIP + - FS + - GoS + - IDRotF + - LMoP + - LOX + - OoW + - PotA + - SDW + - SLW + - TftYP-AtG + - TftYP-DiT + - TftYP-TFoF + - TftYP-THSoT + - TftYP-TSC + - TftYP-ToH + - TftYP-WPM + - WBtW + - WDH + - WDMM + book: + - AAG + - AI + - BAM + - DMG + - DoD + - EGW + - FTD + - MaBJoV + - MM + - MPMM + - MTF + - PHB + - SCAG + - SCC + - TCE + - TDCSR + - VGM + - XGE + reference: + - AWM + - EEPC + - ESK + - TftYP + - SaF + homebrew: + - sources/5e-homebrew/collection/MCDM Productions; Strongholds and Followers.json + +paths: + compendium: /compendium/5e/ + rules: /compendium/5e/rules/ + +include: + - race|genasi|eepc + - racefluff|genasi|eepc + - subrace|air|genasi|eepc + - subrace|earth|genasi|eepc + - subrace|fire|genasi|eepc + - subrace|water|genasi|eepc + +excludePattern: + - race\|.*\|dmg + +exclude: + - monster|expert|dc + - monster|expert|sdw + - monster|expert|slw + +template: + background: examples/templates/tools5e/mixed/mixed-background2md.txt + class: examples/templates/tools5e/mixed/mixed-class2md.txt + deity: examples/templates/tools5e/mixed/mixed-deity2md.txt + feat: examples/templates/tools5e/mixed/mixed-feat2md.txt + hazard: examples/templates/tools5e/mixed/mixed-hazard2md.txt + item: examples/templates/tools5e/mixed/mixed-item2md.txt + monster: examples/templates/tools5e/mixed/mixed-monster2md.txt + object: examples/templates/tools5e/mixed/mixed-object2md.txt + race: examples/templates/tools5e/mixed/mixed-race2md.txt + reward: examples/templates/tools5e/mixed/mixed-reward2md.txt + spell: examples/templates/tools5e/mixed/mixed-spell2md.txt + subclass: examples/templates/tools5e/mixed/mixed-subclass2md.txt + vehicle: examples/templates/tools5e/mixed/mixed-vehicle2md.txt + +images: + copyInternal: true + internalRoot: sources/5etools-img + +useDiceRoller: true +yamlStatblocks: false diff --git a/src/test/resources/5e/sources-book-adventure.json b/src/test/resources/5e/sources-book-adventure.json index 2da661e0c..561706b0f 100644 --- a/src/test/resources/5e/sources-book-adventure.json +++ b/src/test/resources/5e/sources-book-adventure.json @@ -6,7 +6,8 @@ "convert": { "adventure": [ "WBtW", - "MOT-NSS" + "MOT-NSS", + "DIP" ], "book": [ "PHB", diff --git a/src/test/resources/5e/sources-images.yaml b/src/test/resources/5e/sources-images.yaml index 3dc9c7001..8e1932214 100644 --- a/src/test/resources/5e/sources-images.yaml +++ b/src/test/resources/5e/sources-images.yaml @@ -34,10 +34,12 @@ include: - classtype|wizard|xphb template: background: "examples/templates/tools5e/images-background2md.txt" + class: "examples/templates/tools5e/images-class2md.txt" item: "examples/templates/tools5e/images-item2md.txt" monster: "examples/templates/tools5e/images-monster2md.txt" object: "examples/templates/tools5e/images-object2md.txt" race: "examples/templates/tools5e/images-race2md.txt" spell: "examples/templates/tools5e/images-spell2md.txt" + subclass: "examples/templates/tools5e/images-subclass2md.txt" vehicle: "examples/templates/tools5e/images-vehicle2md.txt" useDiceRoller: true From 8634d706bc7179b117593f71675c5765532d5c0d Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Fri, 24 Jan 2025 11:40:14 -0500 Subject: [PATCH 091/119] Update pf2e-tools-data.yml --- .github/workflows/pf2e-tools-data.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 9451a92ea..8d9d07c99 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -2,7 +2,7 @@ name: Pf2e Tools Data on: schedule: # At 09:07 on Saturday (because why not) - - cron: "7 9 * * 6" + - cron: "7 9 * * */5" workflow_dispatch: From d2913731fe9b9a4a6be51a815d2a73c0be0fb9f6 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Fri, 24 Jan 2025 11:40:32 -0500 Subject: [PATCH 092/119] Update tools-data.yml --- .github/workflows/tools-data.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 2ea1d1248..beec9b66d 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -1,8 +1,7 @@ name: Test Tools Data on: schedule: - # At 03:07 on Saturday (because why not) - - cron: "7 3 * * 6" + - cron: "7 9 * * */5" workflow_dispatch: From c8169550320a0dfe4edd2272ae3f6cc84130e2e0 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Sat, 18 Jan 2025 13:33:46 -0500 Subject: [PATCH 093/119] =?UTF-8?q?=E2=9C=A8=20=F0=9F=93=9D=20=20New=20fun?= =?UTF-8?q?ction=20to=20display=20extra=20sources=20in=20a=20footnote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/ebullient/convert/qute/QuteBase.java | 43 +++++++++++++++++++ .../ebullient/convert/qute/SourceAndPage.java | 2 +- .../convert/tools/dnd5e/qute/QuteMonster.java | 7 --- .../convert/tools/dnd5e/qute/QuteObject.java | 7 --- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/qute/QuteBase.java b/src/main/java/dev/ebullient/convert/qute/QuteBase.java index 6a8a6cd42..53d5f40a2 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteBase.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteBase.java @@ -1,5 +1,6 @@ package dev.ebullient.convert.qute; +import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -59,6 +60,15 @@ public Collection getSourceAndPage() { return sources.getSourceAndPage(); } + /** + * List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + */ + public final List getBooks() { + return getSourceAndPage().stream() + .map(x -> x.source) + .toList(); + } + /** List of content superceded by this note (as {@link dev.ebullient.convert.qute.Reprinted}) */ public Collection getReprintOf() { if (sources == null) { @@ -67,6 +77,39 @@ public Collection getReprintOf() { return sources.getReprints(); } + /** + * Get Sources as a footnote. + * + * Calling this method will return an italicised string with the primary source + * followed by a footnote listing all other sources. Useful for types + * that tend to have many sources. + */ + public String getSourcesWithFootnote() { + if (sources == null) { + return ""; + } + if (sources.getSources().size() == 1) { + SourceAndPage sp = sources.getSourceAndPage().iterator().next(); + String txt = sp.toString(); + if (!txt.isEmpty()) { + return "_Source: " + txt + "_"; + } + } + String primary = null; + List srcTxt = new ArrayList<>(); + for(var sp : sources.getSourceAndPage()) { + String txt = sp.toString(); + if (!txt.isEmpty()) { + if (primary == null) { + primary = txt; + } else { + srcTxt.add(txt); + } + } + } + return "_Source: %s_ ^[%s]".formatted(primary, String.join(", ", srcTxt)); + } + /** True if the content (text) contains sections */ public boolean getHasSections() { return text != null && !text.isEmpty() && text.contains("\n## "); diff --git a/src/main/java/dev/ebullient/convert/qute/SourceAndPage.java b/src/main/java/dev/ebullient/convert/qute/SourceAndPage.java index e925d9831..adae397e6 100644 --- a/src/main/java/dev/ebullient/convert/qute/SourceAndPage.java +++ b/src/main/java/dev/ebullient/convert/qute/SourceAndPage.java @@ -42,7 +42,7 @@ public String toString() { } return book; } - return null; + return ""; } @Override diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java index d38a558b1..aecdaa9e7 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java @@ -142,13 +142,6 @@ public String targetPath() { return Tools5eQuteBase.monsterPath(isNpc, type); } - /** List of source books (abbreviated name). Fantasy statblock uses this list. */ - public final List getBooks() { - return getSourceAndPage().stream() - .map(x -> x.source) - .toList(); - } - /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hp} */ public String getHp() { return acHp.getHp(); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java index 059d1d18f..877f99a53 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java @@ -74,13 +74,6 @@ public QuteObject(CompendiumSources sources, this.token = tokenImage; } - /** List of source books (abbreviated name). Fantasy statblock uses this list. */ - public final List getBooks() { - return getSourceAndPage().stream() - .map(x -> x.source) - .toList(); - } - /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hp} */ public String getHp() { return acHp.getHp(); From 40ad078e0397e27d23de1b873037a6fcac09a5ad Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Sat, 18 Jan 2025 20:12:58 -0500 Subject: [PATCH 094/119] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20streamline=20write?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tools/dnd5e/Tools5eMarkdownConverter.java | 324 +++++++++--------- 1 file changed, 154 insertions(+), 170 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java index aa0b299e9..336bd3d3e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java @@ -4,7 +4,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import com.fasterxml.jackson.databind.JsonNode; @@ -41,35 +40,29 @@ public Tools5eMarkdownConverter writeFiles(IndexType type) { return writeFiles(List.of(type)); } + static class WritingQueue { + List baseCompendium = new ArrayList<>(); + List baseRules = new ArrayList<>(); + List noteCompendium = new ArrayList<>(); + List noteRules = new ArrayList<>(); + + // Some state for combining notes + Map combinedDocs = new HashMap<>(); + } + public Tools5eMarkdownConverter writeFiles(List types) { if (index.notPrepared()) { throw new IllegalStateException("Index must be prepared before writing files"); } - if (types != null) { - writeQuteBaseFiles(types.stream() - .map(x -> (Tools5eIndexType) x) - .filter(x -> x.writeFile()) - .toList()); - writeQuteNoteFiles(types.stream() - .map(x -> (Tools5eIndexType) x) - .filter(x -> x.isOutputType() && x.useQuteNote()) - .toList()); - } - return this; - } - - private void writeQuteBaseFiles(List types) { - if (types.isEmpty()) { - return; + if (types == null || types.isEmpty()) { + return this; } index.tui().progressf("Converting data: %s", types); - List compendium = new ArrayList<>(); - List rules = new ArrayList<>(); - - for (Entry e : index.includedEntries()) { - final String key = e.getKey(); - final JsonNode jsonSource = e.getValue(); + WritingQueue queue = new WritingQueue(); + for (var entry : index.includedEntries()) { + final String key = entry.getKey(); + final JsonNode jsonSource = entry.getValue(); Tools5eIndexType nodeType = Tools5eIndexType.getTypeFromKey(key); if (types.contains(Tools5eIndexType.race) && nodeType == Tools5eIndexType.subrace) { @@ -78,171 +71,162 @@ private void writeQuteBaseFiles(List types) { continue; } - if (nodeType == Tools5eIndexType.classtype) { - Json2QuteClass jsonClass = new Json2QuteClass(index, nodeType, jsonSource); - QuteBase converted = jsonClass.build(); - if (converted != null) { - compendium.add(converted); - compendium.addAll(jsonClass.buildSubclasses()); - } - } else { - QuteBase converted = json2qute(nodeType, jsonSource); - if (converted != null) { - append(nodeType, converted, compendium, rules); - } + if (nodeType.writeFile()) { + writeQuteBaseFiles(nodeType, key, jsonSource, queue); + } else if (nodeType.isOutputType() && nodeType.useQuteNote()) { + writeQuteNoteFiles(nodeType, key, jsonSource, queue); } } - writer.writeFiles(index.compendiumFilePath(), compendium); - writer.writeFiles(index.rulesFilePath(), rules); - } + writer.writeFiles(index.compendiumFilePath(), queue.baseCompendium); + writer.writeFiles(index.rulesFilePath(), queue.baseRules); - private QuteBase json2qute(Tools5eIndexType type, JsonNode jsonSource) { - return switch (type) { - case background -> new Json2QuteBackground(index, type, jsonSource).build(); - case deck -> new Json2QuteDeck(index, type, jsonSource).build(); - case deity -> new Json2QuteDeity(index, type, jsonSource).build(); - case facility -> new Json2QuteBastion(index, type, jsonSource).build(); - case feat -> new Json2QuteFeat(index, type, jsonSource).build(); - case hazard, trap -> new Json2QuteHazard(index, type, jsonSource).build(); - case item, itemGroup -> new Json2QuteItem(index, type, jsonSource).build(); - case monster -> new Json2QuteMonster(index, type, jsonSource).build(); - case object -> new Json2QuteObject(index, type, jsonSource).build(); - case optfeature -> new Json2QuteOptionalFeature(index, type, jsonSource).build(); - case psionic -> new Json2QutePsionicTalent(index, type, jsonSource).build(); - case race, subrace -> new Json2QuteRace(index, type, jsonSource).build(); - case reward -> new Json2QuteReward(index, type, jsonSource).build(); - case spell -> new Json2QuteSpell(index, type, jsonSource).build(); - case vehicle -> new Json2QuteVehicle(index, type, jsonSource).build(); - default -> throw new IllegalArgumentException("Unsupported type " + type); - }; - } - - private void writeQuteNoteFiles(List types) { - if (types.isEmpty()) { - return; + for (Json2QuteCommon value : queue.combinedDocs.values()) { + append(value.type, value.buildNote(), queue.noteCompendium, queue.noteRules); } - index.tui().progressf("Converting data: %s", types); - - final String vrDir = Tools5eIndexType.variantrule.getRelativePath(); - List compendium = new ArrayList<>(); - List rules = new ArrayList<>(); + if (!Json2QuteBackground.traits.isEmpty()) { + queue.noteCompendium.addAll(new BackgroundTraits2Note(index).buildNotes()); + } - Map combinedDocs = new HashMap<>(); + writer.writeNotes(index.compendiumFilePath(), queue.noteCompendium, true); + writer.writeNotes(index.rulesFilePath(), queue.noteRules, false); - for (Entry e : index.includedEntries()) { - final String key = e.getKey(); - final JsonNode node = e.getValue(); + return this; + } - Tools5eIndexType nodeType = Tools5eIndexType.getTypeFromKey(key); - if (!types.contains(nodeType)) { - continue; + private void writeQuteBaseFiles(Tools5eIndexType type, String key, JsonNode jsonSource, WritingQueue queue) { + var compendium = queue.baseCompendium; + var rules = queue.baseRules; + if (type == Tools5eIndexType.classtype) { + Json2QuteClass jsonClass = new Json2QuteClass(index, type, jsonSource); + QuteBase converted = jsonClass.build(); + if (converted != null) { + compendium.add(converted); + compendium.addAll(jsonClass.buildSubclasses()); } + } else { + QuteBase converted = switch (type) { + case background -> new Json2QuteBackground(index, type, jsonSource).build(); + case deck -> new Json2QuteDeck(index, type, jsonSource).build(); + case deity -> new Json2QuteDeity(index, type, jsonSource).build(); + case facility -> new Json2QuteBastion(index, type, jsonSource).build(); + case feat -> new Json2QuteFeat(index, type, jsonSource).build(); + case hazard, trap -> new Json2QuteHazard(index, type, jsonSource).build(); + case item, itemGroup -> new Json2QuteItem(index, type, jsonSource).build(); + case monster -> new Json2QuteMonster(index, type, jsonSource).build(); + case object -> new Json2QuteObject(index, type, jsonSource).build(); + case optfeature -> new Json2QuteOptionalFeature(index, type, jsonSource).build(); + case psionic -> new Json2QutePsionicTalent(index, type, jsonSource).build(); + case race, subrace -> new Json2QuteRace(index, type, jsonSource).build(); + case reward -> new Json2QuteReward(index, type, jsonSource).build(); + case spell -> new Json2QuteSpell(index, type, jsonSource).build(); + case vehicle -> new Json2QuteVehicle(index, type, jsonSource).build(); + default -> throw new IllegalArgumentException("Unsupported type " + type); + }; + if (converted != null) { + append(type, converted, compendium, rules); + } + } + } - switch (nodeType) { - case action -> { - Json2QuteCompose action = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, - t -> new Json2QuteCompose(nodeType, index, "Actions")); - action.add(node); - } - case adventureData, bookData -> { - String metadataKey = key.replace("data|", "|"); - JsonNode metadata = index.getOrigin(metadataKey); - if (!node.has("data")) { - index.tui().errorf("No data for %s", key); - } else if (metadata == null) { - index.tui().errorf("Unable to find metadata (%s) for %s", metadataKey, key); - } else if (index.isIncluded(metadataKey)) { - compendium.addAll(new Json2QuteBook(index, nodeType, metadata, node).buildBook()); - } else { - index.tui().debugf(Msg.FILTER, "%s is excluded", metadataKey); - } - } - case status, condition -> { - Json2QuteCompose conditions = (Json2QuteCompose) combinedDocs.computeIfAbsent( - Tools5eIndexType.condition, - t -> new Json2QuteCompose(nodeType, index, "Conditions")); - conditions.add(node); - } - case disease -> { - Json2QuteCompose disease = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, - t -> new Json2QuteCompose(nodeType, index, "Diseases")); - disease.add(node); - } - case itemMastery -> { - Json2QuteCompose itemMastery = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, - t -> new Json2QuteCompose(nodeType, index, "Item Mastery")); - itemMastery.add(node); - } - case itemProperty -> { - Json2QuteCompose itemProperty = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, - t -> new Json2QuteCompose(nodeType, index, "Item Properties")); - itemProperty.add(node); - } - case itemType -> { - Json2QuteCompose itemTypes = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, - t -> new Json2QuteCompose(nodeType, index, "Item Types")); - itemTypes.add(node); - } - case legendaryGroup -> { - QuteNote converted = new Json2QuteLegendaryGroup(index, nodeType, node).buildNote(); - if (converted != null) { - compendium.add(converted); - } - } - case optionalFeatureTypes -> { - OptionalFeatureType oft = index.getOptionalFeatureType(node); - if (oft == null) { - index.tui().errorf("Unable to find optional feature type for %s", key); - continue; - } - QuteNote converted = new Json2QuteOptionalFeatureType(index, node, oft).buildNote(); - if (converted != null) { - compendium.add(converted); - } + private void writeQuteNoteFiles(Tools5eIndexType nodeType, String key, JsonNode node, WritingQueue queue) { + var compendiumDocs = queue.noteCompendium; + var ruleDocs = queue.noteRules; + var combinedDocs = queue.combinedDocs; + final var vrDir = Tools5eIndexType.variantrule.getRelativePath(); + + switch (nodeType) { + case action -> { + Json2QuteCompose action = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, + t -> new Json2QuteCompose(nodeType, index, "Actions")); + action.add(node); + } + case adventureData, bookData -> { + String metadataKey = key.replace("data|", "|"); + JsonNode metadata = index.getOrigin(metadataKey); + if (!node.has("data")) { + index.tui().errorf("No data for %s", key); + } else if (metadata == null) { + index.tui().errorf("Unable to find metadata (%s) for %s", metadataKey, key); + } else if (index.isIncluded(metadataKey)) { + compendiumDocs.addAll(new Json2QuteBook(index, nodeType, metadata, node).buildBook()); + } else { + index.tui().debugf(Msg.FILTER, "%s is excluded", metadataKey); } - case sense -> { - Json2QuteCompose sense = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, - t -> new Json2QuteCompose(nodeType, index, "Senses")); - sense.add(node); + } + case status, condition -> { + Json2QuteCompose conditions = (Json2QuteCompose) combinedDocs.computeIfAbsent( + Tools5eIndexType.condition, + t -> new Json2QuteCompose(nodeType, index, "Conditions")); + conditions.add(node); + } + case disease -> { + Json2QuteCompose disease = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, + t -> new Json2QuteCompose(nodeType, index, "Diseases")); + disease.add(node); + } + case itemMastery -> { + Json2QuteCompose itemMastery = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, + t -> new Json2QuteCompose(nodeType, index, "Item Mastery")); + itemMastery.add(node); + } + case itemProperty -> { + Json2QuteCompose itemProperty = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, + t -> new Json2QuteCompose(nodeType, index, "Item Properties")); + itemProperty.add(node); + } + case itemType -> { + Json2QuteCompose itemTypes = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, + t -> new Json2QuteCompose(nodeType, index, "Item Types")); + itemTypes.add(node); + } + case legendaryGroup -> { + QuteNote converted = new Json2QuteLegendaryGroup(index, nodeType, node).buildNote(); + if (converted != null) { + compendiumDocs.add(converted); } - case skill -> { - Json2QuteCompose skill = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, - t -> new Json2QuteCompose(nodeType, index, "Skills")); - skill.add(node); + } + case optionalFeatureTypes -> { + OptionalFeatureType oft = index.getOptionalFeatureType(node); + if (oft == null) { + index.tui().errorf("Unable to find optional feature type for %s", key); + return; } - case table, tableGroup -> { - Tools5eQuteNote tableNote = new Json2QuteTable(index, nodeType, node).buildNote(); - if (tableNote.getName().equals("Damage Types")) { - rules.add(tableNote); - } else { - compendium.add(tableNote); - } + QuteNote converted = new Json2QuteOptionalFeatureType(index, node, oft).buildNote(); + if (converted != null) { + compendiumDocs.add(converted); } - case variantrule -> append(nodeType, - new Json2QuteNote(index, nodeType, node) - .useSuffix(true) - .withImagePath(vrDir) - .buildNote() - .withTargetPath(vrDir), - compendium, rules); - default -> { - // skip it + } + case sense -> { + Json2QuteCompose sense = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, + t -> new Json2QuteCompose(nodeType, index, "Senses")); + sense.add(node); + } + case skill -> { + Json2QuteCompose skill = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType, + t -> new Json2QuteCompose(nodeType, index, "Skills")); + skill.add(node); + } + case table, tableGroup -> { + Tools5eQuteNote tableNote = new Json2QuteTable(index, nodeType, node).buildNote(); + if (tableNote.getName().equals("Damage Types")) { + ruleDocs.add(tableNote); + } else { + compendiumDocs.add(tableNote); } } + case variantrule -> append(nodeType, + new Json2QuteNote(index, nodeType, node) + .useSuffix(true) + .withImagePath(vrDir) + .buildNote() + .withTargetPath(vrDir), + compendiumDocs, ruleDocs); + default -> { + // skip it + } } - - for (Json2QuteCommon value : combinedDocs.values()) { - append(value.type, value.buildNote(), compendium, rules); - } - - if (!Json2QuteBackground.traits.isEmpty()) { - compendium.addAll(new BackgroundTraits2Note(index).buildNotes()); - } - - writer.writeNotes(index.compendiumFilePath(), compendium, true); - writer.writeNotes(index.rulesFilePath(), rules, false); } void append(Tools5eIndexType type, T note, List compendium, List rules) { From 8501bea13e7d858a4cc86069f1a7c58ccb785f2a Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Tue, 21 Jan 2025 21:41:35 -0500 Subject: [PATCH 095/119] =?UTF-8?q?=E2=9C=A8=F0=9F=94=A5=F0=9F=90=9B=20New?= =?UTF-8?q?=20Spell=20Lists=20and=20a=20million=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored discovery of homebrew metadata - Fix resolving types and properties for homebrew item variants - Consistent reference and cleanup of optional features, feature types, - Fixed propagation of homebrew sources through magic variants. - New lists for spells (by class, by school, by other thing) - Reference links provided to spells point to those lists and feats - Refactor some tag reference parsing - template methods: sources with footnotes --- docs/templates/QuteBase.md | 14 +- docs/templates/QuteNote.md | 14 +- docs/templates/dnd5e/QuteBackground.md | 14 +- docs/templates/dnd5e/QuteBastion/README.md | 14 +- docs/templates/dnd5e/QuteClass/README.md | 14 +- docs/templates/dnd5e/QuteDeck/README.md | 14 +- docs/templates/dnd5e/QuteDeity.md | 14 +- docs/templates/dnd5e/QuteFeat.md | 14 +- docs/templates/dnd5e/QuteHazard.md | 14 +- docs/templates/dnd5e/QuteItem/README.md | 14 +- docs/templates/dnd5e/QuteMonster/README.md | 12 +- docs/templates/dnd5e/QuteObject.md | 12 +- docs/templates/dnd5e/QutePsionic.md | 14 +- docs/templates/dnd5e/QuteRace.md | 14 +- docs/templates/dnd5e/QuteReward.md | 14 +- docs/templates/dnd5e/QuteSpell.md | 20 +- docs/templates/dnd5e/QuteSubclass.md | 14 +- docs/templates/dnd5e/QuteVehicle/README.md | 14 +- docs/templates/dnd5e/Tools5eQuteBase.md | 14 +- docs/templates/dnd5e/Tools5eQuteNote.md | 14 +- docs/templates/pf2e/Pf2eQuteBase.md | 14 +- docs/templates/pf2e/Pf2eQuteNote.md | 14 +- docs/templates/pf2e/QuteAbility.md | 14 +- docs/templates/pf2e/QuteAction/README.md | 14 +- docs/templates/pf2e/QuteAffliction/README.md | 14 +- docs/templates/pf2e/QuteArchetype.md | 14 +- docs/templates/pf2e/QuteBackground.md | 14 +- docs/templates/pf2e/QuteBook/README.md | 14 +- docs/templates/pf2e/QuteCreature/README.md | 14 +- docs/templates/pf2e/QuteDeity/README.md | 14 +- docs/templates/pf2e/QuteFeat.md | 14 +- docs/templates/pf2e/QuteHazard/README.md | 14 +- docs/templates/pf2e/QuteItem/README.md | 14 +- docs/templates/pf2e/QuteRitual/README.md | 14 +- docs/templates/pf2e/QuteSpell/README.md | 14 +- docs/templates/pf2e/QuteTrait.md | 14 +- docs/templates/pf2e/QuteTraitIndex.md | 14 +- .../dev/ebullient/convert/StringUtil.java | 6 + .../java/dev/ebullient/convert/io/Msg.java | 1 + .../java/dev/ebullient/convert/io/Tui.java | 3 + .../dev/ebullient/convert/qute/QuteBase.java | 2 +- .../dev/ebullient/convert/qute/QuteUtil.java | 16 +- .../convert/tools/JsonTextConverter.java | 7 + .../ebullient/convert/tools/ToolsIndex.java | 2 + .../convert/tools/dnd5e/HomebrewIndex.java | 313 ++++++++++ .../convert/tools/dnd5e/ItemProperty.java | 10 +- .../convert/tools/dnd5e/ItemTag.java | 34 ++ .../convert/tools/dnd5e/ItemType.java | 22 +- .../convert/tools/dnd5e/Json2QuteClass.java | 23 +- .../convert/tools/dnd5e/Json2QuteCommon.java | 7 +- .../convert/tools/dnd5e/Json2QuteItem.java | 392 ++++++------- .../dnd5e/Json2QuteOptionalFeatureType.java | 15 +- .../tools/dnd5e/Json2QutePsionicTalent.java | 14 +- .../convert/tools/dnd5e/Json2QuteSpell.java | 135 +---- .../tools/dnd5e/Json2QuteSpellIndex.java | 214 +++++++ .../convert/tools/dnd5e/JsonSource.java | 31 +- .../tools/dnd5e/JsonTextReplacement.java | 162 +++--- .../convert/tools/dnd5e/MagicVariant.java | 10 +- .../tools/dnd5e/OptionalFeatureIndex.java | 241 ++++---- .../convert/tools/dnd5e/PsionicType.java | 14 +- .../convert/tools/dnd5e/SpellEntry.java | 346 +++++++++++ .../convert/tools/dnd5e/SpellIndex.java | 544 ++++++++++++++++++ .../convert/tools/dnd5e/SpellSchool.java | 42 +- .../tools/dnd5e/Tools5eHomebrewIndex.java | 282 --------- .../convert/tools/dnd5e/Tools5eIndex.java | 190 +++--- .../convert/tools/dnd5e/Tools5eIndexType.java | 145 ++--- .../tools/dnd5e/Tools5eMarkdownConverter.java | 9 +- .../convert/tools/dnd5e/Tools5eSources.java | 34 ++ .../convert/tools/dnd5e/qute/QuteMonster.java | 22 +- .../convert/tools/dnd5e/qute/QuteSpell.java | 18 +- .../tools/dnd5e/qute/Tools5eQuteBase.java | 31 + src/main/resources/convertData.json | 2 +- .../java/dev/ebullient/convert/TestUtils.java | 5 + .../convert/Tools5eDataConvertTest.java | 37 +- .../convert/tools/dnd5e/CommonDataTests.java | 2 +- .../convert/tools/dnd5e/FilterAllTest.java | 2 +- .../tools/dnd5e/FilterNoneEditionTest.java | 2 +- src/test/resources/5e/sources-homebrew.json | 9 +- 78 files changed, 2820 insertions(+), 1096 deletions(-) create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/HomebrewIndex.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTag.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpellIndex.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/SpellEntry.java create mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/SpellIndex.java delete mode 100644 src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java diff --git a/docs/templates/QuteBase.md b/docs/templates/QuteBase.md index c0835efe4..fd7d8274e 100644 --- a/docs/templates/QuteBase.md +++ b/docs/templates/QuteBase.md @@ -7,9 +7,13 @@ for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### hasSections True if the content (text) contains sections @@ -34,6 +38,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/QuteNote.md b/docs/templates/QuteNote.md index c73e53c84..5f2eef79d 100644 --- a/docs/templates/QuteNote.md +++ b/docs/templates/QuteNote.md @@ -8,9 +8,13 @@ named `note2md.txt` by default. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### hasSections True if the content (text) contains sections @@ -35,6 +39,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteBackground.md b/docs/templates/dnd5e/QuteBackground.md index 489d88f8e..a1b45a9e3 100644 --- a/docs/templates/dnd5e/QuteBackground.md +++ b/docs/templates/dnd5e/QuteBackground.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### fluffImages List of images as [ImageRef](../ImageRef.md) (optional) @@ -64,6 +68,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteBastion/README.md b/docs/templates/dnd5e/QuteBastion/README.md index 3d1c45910..2f064029c 100644 --- a/docs/templates/dnd5e/QuteBastion/README.md +++ b/docs/templates/dnd5e/QuteBastion/README.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath) +[books](#books), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### fluffImages List of images as [ImageRef](../../ImageRef.md) (optional) @@ -81,6 +85,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### space List of possible spaces this bastion can occupy (as [Space](Space.md), diff --git a/docs/templates/dnd5e/QuteClass/README.md b/docs/templates/dnd5e/QuteClass/README.md index 13086717b..36bda39e7 100644 --- a/docs/templates/dnd5e/QuteClass/README.md +++ b/docs/templates/dnd5e/QuteClass/README.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hitPointDie](#hitpointdie), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [primaryAbility](#primaryability), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hitPointDie](#hitpointdie), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [primaryAbility](#primaryability), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### classProgression Formatted callout containing class and feature progressions. @@ -86,6 +90,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### startingEquipment Formatted text describing starting equipment as diff --git a/docs/templates/dnd5e/QuteDeck/README.md b/docs/templates/dnd5e/QuteDeck/README.md index d9603bdce..7b1d6a850 100644 --- a/docs/templates/dnd5e/QuteDeck/README.md +++ b/docs/templates/dnd5e/QuteDeck/README.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[cardBack](#cardback), [cards](#cards), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [cardBack](#cardback), [cards](#cards), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### cardBack Image from the back of the card as [ImageRef](../../ImageRef.md) (optional) @@ -68,6 +72,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteDeity.md b/docs/templates/dnd5e/QuteDeity.md index cc4e89dfb..506e92285 100644 --- a/docs/templates/dnd5e/QuteDeity.md +++ b/docs/templates/dnd5e/QuteDeity.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[alignment](#alignment), [altNames](#altnames), [category](#category), [domains](#domains), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) +[alignment](#alignment), [altNames](#altnames), [books](#books), [category](#category), [domains](#domains), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath) ### alignment @@ -17,6 +17,10 @@ Alignment of this deity List of alternative names +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### category Category of this deity: Lesser Idols, Prime Deities @@ -88,6 +92,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### symbol Text description of deity's symbol: Wave of white water on green diff --git a/docs/templates/dnd5e/QuteFeat.md b/docs/templates/dnd5e/QuteFeat.md index bbbbcd922..e2672666e 100644 --- a/docs/templates/dnd5e/QuteFeat.md +++ b/docs/templates/dnd5e/QuteFeat.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### fluffImages List of images as [ImageRef](../ImageRef.md) (optional) @@ -68,6 +72,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteHazard.md b/docs/templates/dnd5e/QuteHazard.md index 0edfaf173..390d2616c 100644 --- a/docs/templates/dnd5e/QuteHazard.md +++ b/docs/templates/dnd5e/QuteHazard.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### fluffImages List of images as [ImageRef](../ImageRef.md) (optional) @@ -64,6 +68,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteItem/README.md b/docs/templates/dnd5e/QuteItem/README.md index 6727caa13..54fd55aef 100644 --- a/docs/templates/dnd5e/QuteItem/README.md +++ b/docs/templates/dnd5e/QuteItem/README.md @@ -6,13 +6,17 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[armorClass](#armorclass), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) +[armorClass](#armorclass), [books](#books), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight) ### armorClass Changes to armor class provided by the item, if applicable +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### cost Cost of the item (gp, sp, cp). Optional. @@ -104,6 +108,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### stealthPenalty True if the item imposes a stealth penalty, if applicable diff --git a/docs/templates/dnd5e/QuteMonster/README.md b/docs/templates/dnd5e/QuteMonster/README.md index 00d815372..9a3e0536e 100644 --- a/docs/templates/dnd5e/QuteMonster/README.md +++ b/docs/templates/dnd5e/QuteMonster/README.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml @@ -46,7 +46,7 @@ Creature bonus actions as a list of [NamedText](../../NamedText.md) ### books -List of source books (abbreviated name). Fantasy statblock uses this list. +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. ### conditionImmune @@ -203,6 +203,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### speed Creature speed as a comma-separated list diff --git a/docs/templates/dnd5e/QuteObject.md b/docs/templates/dnd5e/QuteObject.md index fcc39cba8..a8a9504ab 100644 --- a/docs/templates/dnd5e/QuteObject.md +++ b/docs/templates/dnd5e/QuteObject.md @@ -6,7 +6,7 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) +[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable) ### 5eInitiativeYaml @@ -38,7 +38,7 @@ Object actions as a list of [NamedText](../NamedText.md) ### books -List of source books (abbreviated name). Fantasy statblock uses this list. +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. ### conditionImmune @@ -143,6 +143,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### speed Object speed as a comma-separated list diff --git a/docs/templates/dnd5e/QutePsionic.md b/docs/templates/dnd5e/QutePsionic.md index 438cbf891..f7aab8907 100644 --- a/docs/templates/dnd5e/QutePsionic.md +++ b/docs/templates/dnd5e/QutePsionic.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[fluffImages](#fluffimages), [focus](#focus), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) +[books](#books), [fluffImages](#fluffimages), [focus](#focus), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### fluffImages List of images as [ImageRef](../ImageRef.md) (optional) @@ -68,6 +72,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteRace.md b/docs/templates/dnd5e/QuteRace.md index 5ff714a2e..8cf46d96a 100644 --- a/docs/templates/dnd5e/QuteRace.md +++ b/docs/templates/dnd5e/QuteRace.md @@ -6,13 +6,17 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [description](#description), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) +[ability](#ability), [books](#books), [description](#description), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath) ### ability Ability scores associated with this race (comma-separated list of scores or choices) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### description Formatted text describing the race. Optional. Same as {resource.text} @@ -72,6 +76,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### speed Speed: 30 ft. May include additional values, like flight or swim speed. diff --git a/docs/templates/dnd5e/QuteReward.md b/docs/templates/dnd5e/QuteReward.md index b6af1f0d2..89e852314 100644 --- a/docs/templates/dnd5e/QuteReward.md +++ b/docs/templates/dnd5e/QuteReward.md @@ -6,13 +6,17 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[ability](#ability), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[ability](#ability), [books](#books), [detail](#detail), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### ability Description of special ability granted by this reward, if defined separately. This is usually included in reward text. +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### detail Reward detail string (similar to item detail). May include the reward type and rarity if either are defined. @@ -72,6 +76,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteSpell.md b/docs/templates/dnd5e/QuteSpell.md index 4851be787..ce574bf2c 100644 --- a/docs/templates/dnd5e/QuteSpell.md +++ b/docs/templates/dnd5e/QuteSpell.md @@ -6,12 +6,16 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) +[books](#books), [classList](#classlist), [classes](#classes), [components](#components), [duration](#duration), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [range](#range), [references](#references), [reprintOf](#reprintof), [ritual](#ritual), [school](#school), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### classList -List of class names that can use this spell. May be incomplete or empty. +List of resource names (not links) that can use this spell. ### classes @@ -57,6 +61,10 @@ Note name Formatted: spell range +### references + +List of links to resources (classes, subclasses, feats, etc.) that have access to this spell + ### reprintOf List of content superceded by this note (as [Reprinted](../Reprinted.md)) @@ -92,6 +100,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/QuteSubclass.md b/docs/templates/dnd5e/QuteSubclass.md index 63bdd02e5..badc235e3 100644 --- a/docs/templates/dnd5e/QuteSubclass.md +++ b/docs/templates/dnd5e/QuteSubclass.md @@ -6,9 +6,13 @@ Extension of [Tools5eQuteBase](Tools5eQuteBase.md). ## Attributes -[classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [classProgression](#classprogression), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### classProgression A pre-foramatted markdown callout describing subclass spell or feature progression @@ -76,6 +80,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### subclassTitle Title of subclass: "Bard College", or "Primal Path" diff --git a/docs/templates/dnd5e/QuteVehicle/README.md b/docs/templates/dnd5e/QuteVehicle/README.md index 0a8548a7c..bfc7a2e04 100644 --- a/docs/templates/dnd5e/QuteVehicle/README.md +++ b/docs/templates/dnd5e/QuteVehicle/README.md @@ -10,13 +10,17 @@ Extension of [Tools5eQuteBase](../Tools5eQuteBase.md). ## Attributes -[action](#action), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) +[action](#action), [books](#books), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype) ### action List of vehicle actions as a collection of [NamedText](../../NamedText.md) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### fluffImages List of images as [ImageRef](../../ImageRef.md) (optional) @@ -110,6 +114,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/Tools5eQuteBase.md b/docs/templates/dnd5e/Tools5eQuteBase.md index 99bc792bd..531bec5e3 100644 --- a/docs/templates/dnd5e/Tools5eQuteBase.md +++ b/docs/templates/dnd5e/Tools5eQuteBase.md @@ -8,9 +8,13 @@ for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [fluffImages](#fluffimages), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### fluffImages List of images as [ImageRef](../ImageRef.md) (optional) @@ -62,6 +66,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/dnd5e/Tools5eQuteNote.md b/docs/templates/dnd5e/Tools5eQuteNote.md index 2b7e27efc..918c3696a 100644 --- a/docs/templates/dnd5e/Tools5eQuteNote.md +++ b/docs/templates/dnd5e/Tools5eQuteNote.md @@ -7,9 +7,13 @@ Notes created from `Tools5eQuteNote` will use the `note2md.txt` template. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### hasSections True if the content (text) contains sections @@ -34,6 +38,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/Pf2eQuteBase.md b/docs/templates/pf2e/Pf2eQuteBase.md index a087f6f54..39377d875 100644 --- a/docs/templates/pf2e/Pf2eQuteBase.md +++ b/docs/templates/pf2e/Pf2eQuteBase.md @@ -8,9 +8,13 @@ for the type. For example, `QuteBackground` will use `background2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### hasSections True if the content (text) contains sections @@ -35,6 +39,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/Pf2eQuteNote.md b/docs/templates/pf2e/Pf2eQuteNote.md index 61b5ecd6d..407ba34de 100644 --- a/docs/templates/pf2e/Pf2eQuteNote.md +++ b/docs/templates/pf2e/Pf2eQuteNote.md @@ -8,9 +8,13 @@ unless otherwise noted. Folder index notes use `index2md.txt`. ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### hasSections True if the content (text) contains sections @@ -35,6 +39,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteAbility.md b/docs/templates/pf2e/QuteAbility.md index 16d29dbc0..53b988552 100644 --- a/docs/templates/pf2e/QuteAbility.md +++ b/docs/templates/pf2e/QuteAbility.md @@ -13,7 +13,7 @@ Extension of [Pf2eQuteNote](Pf2eQuteNote.md) ## Attributes -[activity](#activity), [bareTraitList](#baretraitlist), [components](#components), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasActivity](#hasactivity), [hasAttributes](#hasattributes), [hasDetails](#hasdetails), [hasEffect](#haseffect), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [note](#note), [prerequisites](#prerequisites), [range](#range), [reference](#reference), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[activity](#activity), [bareTraitList](#baretraitlist), [books](#books), [components](#components), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasActivity](#hasactivity), [hasAttributes](#hasattributes), [hasDetails](#hasdetails), [hasEffect](#haseffect), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [note](#note), [prerequisites](#prerequisites), [range](#range), [reference](#reference), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### activity @@ -24,6 +24,10 @@ Ability ([activity/activation details](QuteDataActivity.md)) Return a comma-separated list of de-styled trait links (no title attributes) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### components List of formatted strings. Activation components for this ability, e.g. command, envision @@ -107,6 +111,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### special Special notes for this ability - usually requirements or caveats relating to its use. diff --git a/docs/templates/pf2e/QuteAction/README.md b/docs/templates/pf2e/QuteAction/README.md index ebee7051d..073f30b5b 100644 --- a/docs/templates/pf2e/QuteAction/README.md +++ b/docs/templates/pf2e/QuteAction/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[actionType](#actiontype), [activity](#activity), [aliases](#aliases), [basic](#basic), [cost](#cost), [frequency](#frequency), [hasSections](#hassections), [item](#item), [labeledSource](#labeledsource), [name](#name), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[actionType](#actiontype), [activity](#activity), [aliases](#aliases), [basic](#basic), [books](#books), [cost](#cost), [frequency](#frequency), [hasSections](#hassections), [item](#item), [labeledSource](#labeledsource), [name](#name), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### actionType @@ -25,6 +25,10 @@ Aliases for this note True if this is a basic action. Same as `{resource.actionType.basic}`. +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### cost The cost of using this action @@ -70,6 +74,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteAffliction/README.md b/docs/templates/pf2e/QuteAffliction/README.md index d41e46373..41447acbe 100644 --- a/docs/templates/pf2e/QuteAffliction/README.md +++ b/docs/templates/pf2e/QuteAffliction/README.md @@ -6,13 +6,17 @@ Extension of [Pf2eQuteNote](../Pf2eQuteNote.md) ## Attributes -[aliases](#aliases), [category](#category), [effect](#effect), [hasSections](#hassections), [isEmbedded](#isembedded), [labeledSource](#labeledsource), [level](#level), [maxDuration](#maxduration), [name](#name), [notes](#notes), [onset](#onset), [reprintOf](#reprintof), [savingThrow](#savingthrow), [source](#source), [sourceAndPage](#sourceandpage), [stages](#stages), [tags](#tags), [temptedCurse](#temptedcurse), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[aliases](#aliases), [books](#books), [category](#category), [effect](#effect), [hasSections](#hassections), [isEmbedded](#isembedded), [labeledSource](#labeledsource), [level](#level), [maxDuration](#maxduration), [name](#name), [notes](#notes), [onset](#onset), [reprintOf](#reprintof), [savingThrow](#savingthrow), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [stages](#stages), [tags](#tags), [temptedCurse](#temptedcurse), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### aliases Aliases for this note. Only populated if not embedded. +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### category Category of affliction (Curse or Disease). Usually shown alongside the level. @@ -70,6 +74,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### stages Affliction stages: map of name to stage data as diff --git a/docs/templates/pf2e/QuteArchetype.md b/docs/templates/pf2e/QuteArchetype.md index 70c88229c..de08c0434 100644 --- a/docs/templates/pf2e/QuteArchetype.md +++ b/docs/templates/pf2e/QuteArchetype.md @@ -6,12 +6,16 @@ Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[benefits](#benefits), [dedicationLevel](#dedicationlevel), [feats](#feats), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[benefits](#benefits), [books](#books), [dedicationLevel](#dedicationlevel), [feats](#feats), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### benefits +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### dedicationLevel @@ -42,6 +46,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteBackground.md b/docs/templates/pf2e/QuteBackground.md index f881724b7..963a264e9 100644 --- a/docs/templates/pf2e/QuteBackground.md +++ b/docs/templates/pf2e/QuteBackground.md @@ -6,9 +6,13 @@ Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### hasSections True if the content (text) contains sections @@ -33,6 +37,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteBook/README.md b/docs/templates/pf2e/QuteBook/README.md index 85ed0273d..dad4cf18d 100644 --- a/docs/templates/pf2e/QuteBook/README.md +++ b/docs/templates/pf2e/QuteBook/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteNote](../Pf2eQuteNote.md) ## Attributes -[aliases](#aliases), [bookInfo](#bookinfo), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[aliases](#aliases), [bookInfo](#bookinfo), [books](#books), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### aliases @@ -17,6 +17,10 @@ Aliases for this note Information about the book as `dev.ebullient.convert.tools.pf2e.qute.QuteBook.BookInfo` +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### hasSections True if the content (text) contains sections @@ -41,6 +45,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteCreature/README.md b/docs/templates/pf2e/QuteCreature/README.md index 2bca8690f..4df45be82 100644 --- a/docs/templates/pf2e/QuteCreature/README.md +++ b/docs/templates/pf2e/QuteCreature/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[abilities](#abilities), [abilityMods](#abilitymods), [aliases](#aliases), [attacks](#attacks), [defenses](#defenses), [description](#description), [hasSections](#hassections), [items](#items), [labeledSource](#labeledsource), [languages](#languages), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [ritualCasting](#ritualcasting), [senses](#senses), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[abilities](#abilities), [abilityMods](#abilitymods), [aliases](#aliases), [attacks](#attacks), [books](#books), [defenses](#defenses), [description](#description), [hasSections](#hassections), [items](#items), [labeledSource](#labeledsource), [languages](#languages), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [ritualCasting](#ritualcasting), [senses](#senses), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### abilities @@ -26,6 +26,10 @@ Aliases for this note (optional) The creature's attacks, as a list of [QuteInlineAttack](../QuteInlineAttack/README.md) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### defenses Defenses (AC, saves, etc) as [QuteDataDefenses](../QuteDataDefenses/README.md) @@ -86,6 +90,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### speed The creature's speed, as an [QuteDataSpeed](../QuteDataSpeed.md) diff --git a/docs/templates/pf2e/QuteDeity/README.md b/docs/templates/pf2e/QuteDeity/README.md index 2c50f4ba3..d321c131c 100644 --- a/docs/templates/pf2e/QuteDeity/README.md +++ b/docs/templates/pf2e/QuteDeity/README.md @@ -13,7 +13,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [alignment](#alignment), [anathema](#anathema), [areasOfConcern](#areasofconcern), [avatar](#avatar), [category](#category), [cleric](#cleric), [edicts](#edicts), [followerAlignment](#followeralignment), [hasSections](#hassections), [intercession](#intercession), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[aliases](#aliases), [alignment](#alignment), [anathema](#anathema), [areasOfConcern](#areasofconcern), [avatar](#avatar), [books](#books), [category](#category), [cleric](#cleric), [edicts](#edicts), [followerAlignment](#followeralignment), [hasSections](#hassections), [intercession](#intercession), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### aliases @@ -32,6 +32,10 @@ Aliases for this note ### avatar +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### category @@ -74,6 +78,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteFeat.md b/docs/templates/pf2e/QuteFeat.md index d396e78fb..8793cb89b 100644 --- a/docs/templates/pf2e/QuteFeat.md +++ b/docs/templates/pf2e/QuteFeat.md @@ -13,7 +13,7 @@ Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[access](#access), [activity](#activity), [aliases](#aliases), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasSections](#hassections), [labeledSource](#labeledsource), [leadsTo](#leadsto), [level](#level), [name](#name), [note](#note), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[access](#access), [activity](#activity), [aliases](#aliases), [books](#books), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [hasSections](#hassections), [labeledSource](#labeledsource), [leadsTo](#leadsto), [level](#level), [name](#name), [note](#note), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### access @@ -27,6 +27,10 @@ Activity/Activation cost (as [QuteDataActivity](QuteDataActivity.md)) Aliases for this note +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### cost @@ -81,6 +85,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### special diff --git a/docs/templates/pf2e/QuteHazard/README.md b/docs/templates/pf2e/QuteHazard/README.md index e192070ec..cb85cf146 100644 --- a/docs/templates/pf2e/QuteHazard/README.md +++ b/docs/templates/pf2e/QuteHazard/README.md @@ -13,7 +13,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[abilities](#abilities), [actions](#actions), [attacks](#attacks), [complexity](#complexity), [defenses](#defenses), [disable](#disable), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [reset](#reset), [routine](#routine), [routineAdmonition](#routineadmonition), [source](#source), [sourceAndPage](#sourceandpage), [stealth](#stealth), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[abilities](#abilities), [actions](#actions), [attacks](#attacks), [books](#books), [complexity](#complexity), [defenses](#defenses), [disable](#disable), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [reset](#reset), [routine](#routine), [routineAdmonition](#routineadmonition), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [stealth](#stealth), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### abilities @@ -47,6 +47,10 @@ ability. Example: The attacks available to the hazard, as a list of [QuteInlineAttack](../QuteInlineAttack/README.md) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### complexity @@ -97,6 +101,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### stealth The hazard's stealth, as a diff --git a/docs/templates/pf2e/QuteItem/README.md b/docs/templates/pf2e/QuteItem/README.md index dab5364b5..ff1cafb45 100644 --- a/docs/templates/pf2e/QuteItem/README.md +++ b/docs/templates/pf2e/QuteItem/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[access](#access), [activate](#activate), [aliases](#aliases), [ammunition](#ammunition), [armor](#armor), [category](#category), [contract](#contract), [craftReq](#craftreq), [duration](#duration), [group](#group), [hands](#hands), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [onset](#onset), [price](#price), [reprintOf](#reprintof), [shield](#shield), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [traits](#traits), [usage](#usage), [variants](#variants), [vaultPath](#vaultpath), [weapons](#weapons) +[access](#access), [activate](#activate), [aliases](#aliases), [ammunition](#ammunition), [armor](#armor), [books](#books), [category](#category), [contract](#contract), [craftReq](#craftreq), [duration](#duration), [group](#group), [hands](#hands), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [onset](#onset), [price](#price), [reprintOf](#reprintof), [shield](#shield), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [traits](#traits), [usage](#usage), [variants](#variants), [vaultPath](#vaultpath), [weapons](#weapons) ### access @@ -29,6 +29,10 @@ Formatted string. Ammunition required Item armor attributes as [QuteItemArmorData](QuteItemArmorData.md) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### category Formatted string. Item category @@ -93,6 +97,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteRitual/README.md b/docs/templates/pf2e/QuteRitual/README.md index de4bc4407..ebb510950 100644 --- a/docs/templates/pf2e/QuteRitual/README.md +++ b/docs/templates/pf2e/QuteRitual/README.md @@ -6,13 +6,17 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [casting](#casting), [checks](#checks), [duration](#duration), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [ritualType](#ritualtype), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [targeting](#targeting), [text](#text), [traits](#traits), [vaultPath](#vaultpath) +[aliases](#aliases), [books](#books), [casting](#casting), [checks](#checks), [duration](#duration), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [ritualType](#ritualtype), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [targeting](#targeting), [text](#text), [traits](#traits), [vaultPath](#vaultpath) ### aliases Aliases for this note +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### casting Casting attributes as [QuteRitualCasting](QuteRitualCasting.md) @@ -65,6 +69,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteSpell/README.md b/docs/templates/pf2e/QuteSpell/README.md index bd23672d1..28ca1ebf7 100644 --- a/docs/templates/pf2e/QuteSpell/README.md +++ b/docs/templates/pf2e/QuteSpell/README.md @@ -6,7 +6,7 @@ Extension of [Pf2eQuteBase](../Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [amp](#amp), [castDuration](#castduration), [components](#components), [cost](#cost), [domains](#domains), [duration](#duration), [formattedComponents](#formattedcomponents), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [save](#save), [source](#source), [sourceAndPage](#sourceandpage), [spellLists](#spelllists), [spellType](#spelltype), [subclass](#subclass), [tags](#tags), [targeting](#targeting), [text](#text), [traditions](#traditions), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) +[aliases](#aliases), [amp](#amp), [books](#books), [castDuration](#castduration), [components](#components), [cost](#cost), [domains](#domains), [duration](#duration), [formattedComponents](#formattedcomponents), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [save](#save), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [spellLists](#spelllists), [spellType](#spelltype), [subclass](#subclass), [tags](#tags), [targeting](#targeting), [text](#text), [traditions](#traditions), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath) ### aliases @@ -17,6 +17,10 @@ Aliases for this note Psi amp behavior as [QuteSpellAmp](QuteSpellAmp.md) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### castDuration The time it takes to cast the spell, as a [QuteDataDuration](../QuteDataDuration.md) which is either a [QuteDataActivity](../QuteDataActivity.md) @@ -88,6 +92,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### spellLists Spell lists containing this spell diff --git a/docs/templates/pf2e/QuteTrait.md b/docs/templates/pf2e/QuteTrait.md index ae1f85af9..59d9f168d 100644 --- a/docs/templates/pf2e/QuteTrait.md +++ b/docs/templates/pf2e/QuteTrait.md @@ -6,13 +6,17 @@ Extension of [Pf2eQuteBase](Pf2eQuteBase.md) ## Attributes -[aliases](#aliases), [categories](#categories), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[aliases](#aliases), [books](#books), [categories](#categories), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) ### aliases Aliases for this note +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### categories List of categories to which this trait belongs @@ -41,6 +45,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/docs/templates/pf2e/QuteTraitIndex.md b/docs/templates/pf2e/QuteTraitIndex.md index 8008ad819..e37fe7e67 100644 --- a/docs/templates/pf2e/QuteTraitIndex.md +++ b/docs/templates/pf2e/QuteTraitIndex.md @@ -10,9 +10,13 @@ Extension of [Pf2eQuteNote](Pf2eQuteNote.md) ## Attributes -[categoryLinks](#categorylinks), [categoryToTraits](#categorytotraits), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +[books](#books), [categoryLinks](#categorylinks), [categoryToTraits](#categorytotraits), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath) +### books + +List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example. + ### categoryLinks List of category anchor links @@ -45,6 +49,14 @@ String describing the content's source(s) Book sources as list of [SourceAndPage](../SourceAndPage.md) +### sourcesWithFootnote + +Get Sources as a footnote. + +Calling this method will return an italicised string with the primary source +followed by a footnote listing all other sources. Useful for types +that tend to have many sources. + ### tags Collected tags for inclusion in frontmatter diff --git a/src/main/java/dev/ebullient/convert/StringUtil.java b/src/main/java/dev/ebullient/convert/StringUtil.java index 028861265..0481bd4df 100644 --- a/src/main/java/dev/ebullient/convert/StringUtil.java +++ b/src/main/java/dev/ebullient/convert/StringUtil.java @@ -37,6 +37,12 @@ public static String valueOrDefault(String value, String fallback) { return value == null || value.isEmpty() ? fallback : value; } + public static String valueOrDefault(String[] parts, int index, String fallback) { + return index < 0 || index >= parts.length + ? fallback + : valueOrDefault(parts[index], fallback); + } + public static String uppercaseFirst(String value) { return value == null || value.isEmpty() ? value : Character.toUpperCase(value.charAt(0)) + value.substring(1); } diff --git a/src/main/java/dev/ebullient/convert/io/Msg.java b/src/main/java/dev/ebullient/convert/io/Msg.java index 57682c2a5..af3749fc2 100644 --- a/src/main/java/dev/ebullient/convert/io/Msg.java +++ b/src/main/java/dev/ebullient/convert/io/Msg.java @@ -21,6 +21,7 @@ public enum Msg { REPRINT(Character.toString(0x1F4F0)), // 📰 SOMEDAY(Character.toString(0x1F6A7)), // 🚧 SOURCE(Character.toString(0x1F4D8)), // 📘 + SPELL(Character.toString(0x1F4AB)), // 💫 TARGET(Character.toString(0x1F3AF)), // 🎯 UNKNOWN(Character.toString(0x1F47B)), // 👻 UNRESOLVED(Character.toString(0x1FAE3)), // 🫣 diff --git a/src/main/java/dev/ebullient/convert/io/Tui.java b/src/main/java/dev/ebullient/convert/io/Tui.java index 37d5af99d..2ab66d554 100644 --- a/src/main/java/dev/ebullient/convert/io/Tui.java +++ b/src/main/java/dev/ebullient/convert/io/Tui.java @@ -330,6 +330,9 @@ public void logf(String output, Object... params) { public void logf(Msg msg, String output, Object... params) { if (log != null) { output = format(msg.wrap(output), params); + if (msg == Msg.UNKNOWN || msg == Msg.UNRESOLVED) { + log(new Exception(output), false); + } log.println(output); } } diff --git a/src/main/java/dev/ebullient/convert/qute/QuteBase.java b/src/main/java/dev/ebullient/convert/qute/QuteBase.java index 53d5f40a2..a4b6c014d 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteBase.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteBase.java @@ -97,7 +97,7 @@ public String getSourcesWithFootnote() { } String primary = null; List srcTxt = new ArrayList<>(); - for(var sp : sources.getSourceAndPage()) { + for (var sp : sources.getSourceAndPage()) { String txt = sp.toString(); if (!txt.isEmpty()) { if (primary == null) { diff --git a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java index 5fa21ed74..0bbd42c8d 100644 --- a/src/main/java/dev/ebullient/convert/qute/QuteUtil.java +++ b/src/main/java/dev/ebullient/convert/qute/QuteUtil.java @@ -7,7 +7,6 @@ import dev.ebullient.convert.io.JavadocIgnore; import dev.ebullient.convert.tools.IndexType; -import dev.ebullient.convert.tools.pf2e.Pf2eIndexType; @JavadocIgnore public interface QuteUtil { @@ -73,12 +72,25 @@ default void addUnlessEmpty(Map map, String key, List value) } } + default String levelToString(String level) { + switch (level) { + case "1": + return "1st"; + case "2": + return "2nd"; + case "3": + return "3rd"; + default: + return level + "th"; + } + } + default String template() { throw new UnsupportedOperationException("Tried to call template() on a class which does not have a template defined"); } default IndexType indexType() { - return Pf2eIndexType.syntheticGroup; + throw new UnsupportedOperationException("Tried to call indexType() on a class which does not have a template defined"); } @JavadocIgnore diff --git a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java index e80dfc21e..e3d5bcb7f 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java @@ -76,6 +76,13 @@ default JsonNode createNode(String source) { } } + default boolean isEmpty(JsonNode node) { + return node == null || node.isNull() + || (node.isTextual() && node.asText().isBlank() + || (node.isArray() && node.size() == 0) + || (node.isObject() && node.size() == 0)); + } + default boolean isArrayNode(JsonNode node) { return node != null && node.isArray(); } diff --git a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java index 6e284877f..fe6442ad3 100644 --- a/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/ToolsIndex.java @@ -23,6 +23,8 @@ enum TtrpgValue implements JsonNodeReader { indexParentKey, indexVersionKeys, isHomebrew, + homebrewSource, + homebrewBaseSource, } static ToolsIndex createIndex() { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/HomebrewIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/HomebrewIndex.java new file mode 100644 index 000000000..7e8f3ee0e --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/HomebrewIndex.java @@ -0,0 +1,313 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static dev.ebullient.convert.StringUtil.isPresent; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.config.CompendiumConfig; +import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; +import dev.ebullient.convert.tools.dnd5e.PsionicType.CustomPsionicType; +import dev.ebullient.convert.tools.dnd5e.SkillOrAbility.CustomSkillOrAbility; +import dev.ebullient.convert.tools.dnd5e.SpellSchool.CustomSpellSchool; + +public class HomebrewIndex implements JsonSource { + + private final Map homebrewMetaTypes = new HashMap<>(); + private final Tools5eIndex index; + + HomebrewIndex(Tools5eIndex index) { + this.index = index; + } + + public void importBrew(Consumer processHomebrewTree) { + for (HomebrewMetaTypes homebrew : homebrewMetaTypes.values()) { + processHomebrewTree.accept(homebrew); + for (var featureType : homebrew.optionalFeatureTypes.keySet()) { + index.optFeatureIndex.addOptionalFeatureType(featureType, homebrew); + } + } + } + + public Collection getHomebrewMetaTypes(Tools5eSources sources) { + Map metaTypes = new HashMap<>(); + for (String src : sources.getSources()) { + HomebrewMetaTypes meta = homebrewMetaTypes.get(src); + if (meta != null) { + metaTypes.put(meta.primary, meta); + } + } + return metaTypes.values(); + } + + public HomebrewMetaTypes getHomebrewMetaTypes(String source) { + return homebrewMetaTypes.get(source); + } + + public SkillOrAbility findHomebrewSkillOrAbility(String key, Tools5eSources sources) { + Collection metaTypes = getHomebrewMetaTypes(sources); + for (HomebrewMetaTypes meta : metaTypes) { + SkillOrAbility skill = meta.getSkillType(key); + if (skill != null) { + return skill; + } + } + return null; + } + + public SpellSchool findHomebrewSpellSchool(String code, Tools5eSources sources) { + Collection metaTypes = getHomebrewMetaTypes(sources); + for (HomebrewMetaTypes meta : metaTypes) { + SpellSchool school = meta.getSpellSchool(code); + if (school != null) { + return school; + } + } + return SpellSchool.SchoolEnum.None; + } + + public ItemType findHomebrewType(String abbreviation, Tools5eSources sources) { + Collection metaTypes = getHomebrewMetaTypes(sources); + for (HomebrewMetaTypes meta : metaTypes) { + // key is lowercase abbreviation + JsonNode node = meta.getItemType(abbreviation); + if (node != null) { + return ItemType.fromNode(node); + } + } + return null; + } + + public ItemMastery findHomebrewMastery(String name, Tools5eSources sources) { + Collection metaTypes = getHomebrewMetaTypes(sources); + for (HomebrewMetaTypes meta : metaTypes) { + // key is lowercase name + JsonNode node = meta.getItemMastery(name); + if (node != null) { + return ItemMastery.fromNode(node); + } + } + return null; + } + + public ItemProperty findHomebrewProperty(String code, Tools5eSources sources) { + Collection metaTypes = getHomebrewMetaTypes(sources); + for (HomebrewMetaTypes meta : metaTypes) { + JsonNode node = meta.getItemProperty(code); + if (node != null) { + return ItemProperty.fromNode(node); + } + } + return null; + } + + public void clear() { + homebrewMetaTypes.clear(); + } + + public boolean addHomebrewSourcesIfPresent(String filename, JsonNode brewNode) { + JsonNode meta = SourceField._meta.getFrom(brewNode); + JsonNode sources = HomebrewFields.sources.getFrom(meta); + if (sources == null || sources.size() == 0) { + return false; + } + Set definedSources = new HashSet<>(); + + for (JsonNode s : iterableElements(sources)) { + String json = HomebrewFields.json.getTextOrNull(s); + if (json == null) { + tui().errorf(Msg.BREW, "Source does not define json id: %s", s); + continue; + } + String fullName = HomebrewFields.full.getTextOrEmpty(s); + String abbreviation = HomebrewFields.abbreviation.getTextOrEmpty(s); + if (fullName == null) { + tui().warnf(Msg.BREW, "Homebrew source %s missing full name: %s", json, fullName); + } + TtrpgConfig.addHomebrewSource(fullName, json, abbreviation); // define source + TtrpgConfig.includeAdditionalSource(json); // include source + definedSources.add(json); + } + + HomebrewMetaTypes metaTypes = new HomebrewMetaTypes(definedSources, + filename, brewNode, Tools5eFields.edition.getTextOrDefault(meta, "classic")); + + for (var src : definedSources) { + homebrewMetaTypes.compute(src, (k, v) -> { + if (v == null) { + return metaTypes; + } + tui().errorf(Msg.BREW, "Shared homebrew id %s: %s and %s; ignoring definition in %s", + src, v.filename, v.filename); + return v; + }); + } + + // --- From meta of homebrew --- + + for (Entry entry : HomebrewFields.optionalFeatureTypes.iterateFieldsFrom(meta)) { + metaTypes.setOptionalFeatureType(entry.getKey(), entry.getValue().asText()); + } + + // ignoring short names for spell schools and psionic types + for (Entry entry : HomebrewFields.spellSchools.iterateFieldsFrom(meta)) { + metaTypes.setSpellSchool(entry.getKey(), entry.getValue()); + } + + for (Entry entry : HomebrewFields.psionicTypes.iterateFieldsFrom(meta)) { + metaTypes.setPsionicType(entry.getKey(), entry.getValue()); + } + + Tools5eSources.addFonts(meta, HomebrewFields.fonts); + + return true; + } + + static class HomebrewMetaTypes { + final String primary; + final Set sourceKeys; + final String filename; + final JsonNode homebrewNode; + final String edition; + + // name, long name + final Map optionalFeatureTypes = new HashMap<>(); + final Map psionicTypes = new HashMap<>(); + final Map skillOrAbility = new HashMap<>(); + final Map spellSchoolTypes = new HashMap<>(); + final Map itemTypes = new HashMap<>(); + final Map itemProperties = new HashMap<>(); + final Map itemMastery = new HashMap<>(); + + HomebrewMetaTypes(Set sourceKeys, String filename, JsonNode homebrewNode, String edition) { + this.primary = sourceKeys.iterator().next(); + this.sourceKeys = sourceKeys; + this.filename = filename; + this.homebrewNode = homebrewNode; + this.edition = edition; + } + + public String getOptionalFeatureType(String key) { + return optionalFeatureTypes.get(key.toLowerCase()); + } + + public void setOptionalFeatureType(String key, String value) { + optionalFeatureTypes.put(key.toLowerCase(), value); + } + + public PsionicType getPsionicType(String key) { + return psionicTypes.get(key.toLowerCase()); + } + + public void setPsionicType(String key, JsonNode value) { + try { + CustomPsionicType psionicType = Tui.MAPPER.convertValue(value, CustomPsionicType.class); + psionicTypes.put(key.toLowerCase(), psionicType); + } catch (IllegalArgumentException e) { + Tui.instance().errorf(Msg.BREW, "Error reading psionic type %s: %s", key, value); + } + } + + public SkillOrAbility getSkillType(String key) { + return skillOrAbility.get(key.toLowerCase()); + } + + public void setSkillType(String key, JsonNode skillNode) { + try { + CustomSkillOrAbility skill = new CustomSkillOrAbility(skillNode); + skillOrAbility.put(key.toLowerCase(), skill); + } catch (IllegalArgumentException e) { + Tui.instance().errorf(Msg.BREW, "Error reading skill %s: %s", key, skillNode); + } + } + + public SpellSchool getSpellSchool(String key) { + return spellSchoolTypes.get(key.toLowerCase()); + } + + public void setSpellSchool(String key, JsonNode spellNode) { + try { + CustomSpellSchool school = new CustomSpellSchool(key, + HomebrewFields.full.getTextOrEmpty(spellNode)); + spellSchoolTypes.put(key.toLowerCase(), school); + } catch (IllegalArgumentException e) { + Tui.instance().errorf(Msg.BREW, "Error reading skill %s: %s", key, spellNode); + } + } + + public JsonNode getItemType(String abbreviation) { + return itemTypes.get(abbreviation.toLowerCase()); + } + + public JsonNode getItemProperty(String abbreviation) { + return itemProperties.get(abbreviation.toLowerCase()); + } + + public JsonNode getItemMastery(String name) { + return itemMastery.get(name.toLowerCase()); + } + + public void addCrossReference(Tools5eIndexType type, String key, JsonNode value) { + String name = SourceField.name.getTextOrNull(value); + String abbreviation = Tools5eFields.abbreviation.getTextOrNull(value); + + // Done before copies & variants are made + TtrpgValue.homebrewBaseSource.setIn(value, SourceField.source.getTextOrEmpty(value)); + TtrpgValue.homebrewSource.setIn(value, SourceField.source.getTextOrEmpty(value)); + + if (isPresent(abbreviation)) { + // Make sure the key and sources have been constructed/assigned + if (type == Tools5eIndexType.itemType) { + itemTypes.put(abbreviation.toLowerCase(), value); + } else if (type == Tools5eIndexType.itemProperty) { + itemProperties.put(abbreviation.toLowerCase(), value); + } + } else if (isPresent(name)) { + if (type == Tools5eIndexType.itemMastery) { + itemMastery.put(name.toLowerCase(), value); + } else if (type == Tools5eIndexType.skill) { + setSkillType(name.toLowerCase(), value); + } + } + } + } + + enum HomebrewFields implements JsonNodeReader { + abbreviation, + fonts, + full, + json, + optionalFeatureTypes, + psionicTypes, + skill, + sources, + spellSchools, + spellDistanceUnits + } + + @Override + public CompendiumConfig cfg() { + return index.config; + } + + @Override + public Tools5eIndex index() { + return index; + } + + @Override + public Tools5eSources getSources() { + return null; + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java index 6d91c08a8..f1b411528 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java @@ -15,7 +15,6 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; -import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; record ItemProperty( @@ -143,6 +142,15 @@ public static ItemProperty customProperty(String name, String abbreviation) { public static void clear() { propertyMap.clear(); } + + public static String defaultItemSource(String code) { + // reprint will handle PHB -> XPHB, DMG -> XDMG, etc. + return switch (code) { + case "AF", "BF", "RLD" -> "DMG"; + case "ER", "Vst" -> "TDCSR"; + default -> "PHB"; + }; + } } // Parser.ITM_PROP_ABV__TWO_HANDED = "2H"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTag.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTag.java new file mode 100644 index 000000000..12aadd991 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTag.java @@ -0,0 +1,34 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.Tags; + +enum ItemTag { + age, + armor, + attunement, + gear, + mastery, + property, + rarity, + shield, + tier, + vehicle, + weapon, + wondrous, + ; + + void add(Tags tags, String... segments) { + tags.addRaw(build(segments)); + } + + String build(String... segments) { + return Stream.concat(Stream.of("item", name()), Arrays.stream(segments)) + .map(Tui::slugify) + .collect(Collectors.joining("/")); + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java index 5e33e01f4..9e815b9b7 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java @@ -8,11 +8,11 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.ebullient.convert.config.TtrpgConfig; import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonTextConverter.SourceField; import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; -import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemTag; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; /** @@ -56,7 +56,7 @@ public String linkify(String linkText) { public static ItemType fromNode(JsonNode typeNode) { String typeKey = TtrpgValue.indexKey.getTextOrEmpty(typeNode); if (typeKey.isEmpty()) { - Tui.instance().warnf(Msg.NOT_SET.wrap("Index key not found for property %s"), typeNode); + Tui.instance().warnf(Msg.NOT_SET.wrap("Index key not found for type %s"), typeNode); return null; } // Create the ItemType object once @@ -165,6 +165,22 @@ private static ItemTypeGroup mapGroup(String abbreviation, String lowercase, Jso public static void clear() { typeMap.clear(); } + + public static String defaultItemSource(String code) { + boolean xphbAvailable = TtrpgConfig.getConfig().sourceIncluded("XPHB"); + boolean xdmgAvailable = TtrpgConfig.getConfig().sourceIncluded("XDMG"); + return switch (code) { + case "$", "$A", "$G" -> "DMG"; // treasure + case "AF", "EXP" -> "DMG"; // ammunition, explosives + case "AIR", "SC", "SHP" -> xphbAvailable ? "XPHB" : "DMG"; // airship + case "GV", "RD", "RG", "WD" -> "DMG"; // generic variant / magic item + case "IDG" -> "TDCSR"; // illegal drug + case "SPC" -> "AAG"; // spelljammer + case "TB" -> "XDMG"; + case "TG" -> xdmgAvailable ? "XDMG" : "PHB"; // trade good + default -> "PHB"; + }; + } } // Parser.ITM_TYP_ABV__TREASURE = "$"; @@ -199,6 +215,7 @@ public static void clear() { // Parser.ITM_TYP_ABV__VEHICLE_SPACE = "SPC"; // Parser.ITM_TYP_ABV__TOOL = "T"; // Parser.ITM_TYP_ABV__TACK_AND_HARNESS = "TAH"; +// Parser.ITM_TYP_ABV__TRADE_BAR = "TB"; // Parser.ITM_TYP_ABV__TRADE_GOOD = "TG"; // Parser.ITM_TYP_ABV__VEHICLE_LAND = "VEH"; // Parser.ITM_TYP_ABV__WAND = "WD"; @@ -267,6 +284,7 @@ public static void clear() { // Parser.ITM_TYP__ODND_VEHICLE_WATER = "SHP|XPHB"; // Parser.ITM_TYP__ODND_TOOL = "T|XPHB"; // Parser.ITM_TYP__ODND_TACK_AND_HARNESS = "TAH|XPHB"; +// Parser.ITM_TYP__ODND_TRADE_BAR = "TB|XDMG"; // Parser.ITM_TYP__ODND_TRADE_GOOD = "TG|XDMG"; // Parser.ITM_TYP__ODND_VEHICLE_LAND = "VEH|XPHB"; // Parser.ITM_TYP__ODND_WAND = "WD|XDMG"; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java index 37f946648..f6f3a1ebb 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java @@ -5,6 +5,7 @@ import static dev.ebullient.convert.StringUtil.joinConjunct; import static dev.ebullient.convert.StringUtil.markdownLinkToHtml; import static dev.ebullient.convert.StringUtil.toAnchorTag; +import static dev.ebullient.convert.StringUtil.toOrdinal; import static dev.ebullient.convert.StringUtil.toTitleCase; import static dev.ebullient.convert.StringUtil.uppercaseFirst; @@ -192,22 +193,22 @@ void addOptionalFeatureText(JsonNode optFeatures, String primarySource, List [!example]- " + oft.title); + String title = oft.getTitle(); // this could be long if homebrew mixed + text.add("> [!example]- Optional Features: " + title); text.add(String.format("> ![%s](%s%s/%s.md#%s)", - oft.title, + title, index().compendiumVaultRoot(), relativePath, oft.getFilename(), - toAnchorTag(oft.title))); - text.add("^list-" + slugify(oft.title)); + toAnchorTag(title))); + text.add("^list-optfeature-" + slugify(oft.abbreviation)); } else { tui().errorf( Msg.UNRESOLVED, "Can not find optional feature type %s for progression. Source: %s; Reference: %s", @@ -560,13 +561,13 @@ List listOfToolProfiencies(JsonNode containingNode) { String armorToLink(String armor) { return armor .replaceAll("^light", linkify(Tools5eIndexType.itemType, - sources.isClassic() ? "la|PHB|light armor" : "la|XPHB|Light armor")) + sources.isClassic() ? "la|phb|light armor" : "la|xphb|Light armor")) .replaceAll("^medium", linkify(Tools5eIndexType.itemType, - sources.isClassic() ? "ma|PHB|medium armor" : "ma|XPHB|Medium armor")) + sources.isClassic() ? "ma|phb|medium armor" : "ma|xphb|Medium armor")) .replaceAll("^heavy", linkify(Tools5eIndexType.itemType, - sources.isClassic() ? "ha|PHB|heavy armor" : "ha|XPHB|Heavy armor")) + sources.isClassic() ? "ha|phb|heavy armor" : "ha|xphb|Heavy armor")) .replaceAll("^shields?", linkify(Tools5eIndexType.item, - sources.isClassic() ? "shield|PHB|shields" : "shield|XPHB|Shields")); + sources.isClassic() ? "shield|phb|shields" : "shield|xphb|Shields")); } String skillChoices(Collection skills, int numSkills) { @@ -841,7 +842,7 @@ static class LevelProgression { List spellSlots = new ArrayList<>(); LevelProgression(int level) { - this.level = JsonSource.levelToString(level); + this.level = toOrdinal(level); this.pb = "+" + JsonSource.levelToPb(level); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java index c6443b6df..be1f1c77e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java @@ -2,6 +2,7 @@ import static dev.ebullient.convert.StringUtil.isPresent; import static dev.ebullient.convert.StringUtil.joinConjunct; +import static dev.ebullient.convert.StringUtil.toOrdinal; import java.nio.file.Path; import java.text.Normalizer; @@ -359,7 +360,7 @@ private String levelPrereq(JsonNode levelPrereq) { tui().errorf("levelPrereq: Array parameter"); if (levelPrereq.isNumber()) { - return levelToText(levelPrereq.asText()); + return toOrdinal(levelPrereq.asInt()); } String level = Tools5eFields.level.getTextOrThrow(levelPrereq); @@ -368,7 +369,7 @@ private String levelPrereq(JsonNode levelPrereq) { // neither class nor subclass is defined if (classNode == null && subclassNode == null) { - return levelToText(level); + return toOrdinal(level); } boolean isLevelVisible = !"1".equals(level); // hide implied first level @@ -388,7 +389,7 @@ private String levelPrereq(JsonNode levelPrereq) { } return String.format("%s%s", - isLevelVisible ? levelToText(level) : "", + isLevelVisible ? toOrdinal(level) : "", isClassVisible ? " " + classPart : ""); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java index 53aac4143..f7a5ed7cd 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java @@ -6,17 +6,13 @@ import static dev.ebullient.convert.StringUtil.uppercaseFirst; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.TreeSet; -import java.util.stream.Collectors; -import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; @@ -64,177 +60,185 @@ protected Tools5eQuteBase buildQuteResource() { } private Variant createVariant(JsonNode variantNode, Tags tags) { - ItemType itemType = getItemType(variantNode, ItemField.type); - ItemType itemTypeAlt = getItemType(variantNode, ItemField.typeAlt); - - Set itemProperties = new TreeSet<>(ItemProperty.comparator); - findProperties(variantNode, itemProperties, itemType, itemTypeAlt); - - Set itemMasteries = new TreeSet<>(ItemMastery.comparator); - findMastery(variantNode, itemMasteries); - - String damage = null; - String damage2h = null; - if (variantNode.has("dmgType")) { - String dmg1 = getTextOrDefault(variantNode, "dmg1", null); - String dmg2 = getTextOrDefault(variantNode, "dmg2", null); - String dmgType = getTextOrDefault(variantNode, "dmgType", null); - damage = dmg1 + " " + dmgType; - if (dmg2 != null && !dmg2.isBlank()) { - damage2h = dmg2 + " " + dmgType; + Tools5eSources variantSources = Tools5eSources.findOrTemporary(variantNode); + boolean pushed = parseState().push(variantNode); + try { + ItemType itemType = getItemType(variantSources, variantNode, ItemField.type); + ItemType itemTypeAlt = getItemType(variantSources, variantNode, ItemField.typeAlt); + + Set itemProperties = new TreeSet<>(ItemProperty.comparator); + findProperties(variantSources, variantNode, itemProperties, itemType, itemTypeAlt); + + Set itemMasteries = new TreeSet<>(ItemMastery.comparator); + findMastery(variantSources, variantNode, itemMasteries); + + String damage = null; + String damage2h = null; + if (variantNode.has("dmgType")) { + String dmg1 = getTextOrDefault(variantNode, "dmg1", null); + String dmg2 = getTextOrDefault(variantNode, "dmg2", null); + String dmgType = getTextOrDefault(variantNode, "dmgType", null); + damage = dmg1 + " " + dmgType; + if (dmg2 != null && !dmg2.isBlank()) { + damage2h = dmg2 + " " + dmgType; + } } - } - String baseItemKey = ItemField.baseItem.getTextOrEmpty(variantNode); - String baseItem = linkify(Tools5eIndexType.item, baseItemKey); - boolean baseItemIncluded = false; - - boolean ammo = ItemField.ammo.booleanOrDefault(variantNode, false); - boolean cursed = ItemField.curse.booleanOrDefault(variantNode, false); - boolean firearm = ItemField.firearm.booleanOrDefault(variantNode, false); - boolean poison = ItemField.poison.booleanOrDefault(variantNode, false); - boolean staff = ItemField.staff.booleanOrDefault(variantNode, false); - boolean tattoo = ItemField.tattoo.booleanOrDefault(variantNode, false); - boolean wondrous = ItemField.wondrous.booleanOrDefault(variantNode, false); - - boolean focus = ItemField.focus.existsIn(variantNode) - || ItemField.scfType.existsIn(variantNode); - - String age = ItemField.age.getTextOrEmpty(variantNode); - String weaponCategory = ItemField.weaponCategory.getTextOrEmpty(variantNode); - - String attunement = attunement(variantNode); - String rarity = ItemField.rarity.getTextOrEmpty(variantNode); - String tier = ItemField.tier.getTextOrEmpty(variantNode); - - String poisonTypes = poison - ? joinConjunct(" or ", ItemField.poisonTypes.getListOfStrings(variantNode, tui())) - : null; - - // -- render.js ------------------------- - // const [typeListText, typeHtml, subTypeHtml] = Renderer.item.getHtmlAndTextTypes(item); - // Building typeDescription and subtypeDescription in a stable order - List typeDescription = new ArrayList<>(); - List subTypeDescription = new ArrayList<>(); - - if (wondrous) { - typeDescription.add("wondrous item" + (tattoo ? " (tattoo)" : "")); - if (tattoo) { - ItemTag.wondrous.add(tags, "tattoo"); + String baseItemKey = ItemField.baseItem.getTextOrEmpty(variantNode); + String baseItem = linkify(Tools5eIndexType.item, baseItemKey); + boolean baseItemIncluded = false; + + boolean ammo = ItemField.ammo.booleanOrDefault(variantNode, false); + boolean cursed = ItemField.curse.booleanOrDefault(variantNode, false); + boolean firearm = ItemField.firearm.booleanOrDefault(variantNode, false); + boolean poison = ItemField.poison.booleanOrDefault(variantNode, false); + boolean staff = ItemField.staff.booleanOrDefault(variantNode, false); + boolean tattoo = ItemField.tattoo.booleanOrDefault(variantNode, false); + boolean wondrous = ItemField.wondrous.booleanOrDefault(variantNode, false); + + boolean focus = ItemField.focus.existsIn(variantNode) + || ItemField.scfType.existsIn(variantNode); + + String age = ItemField.age.getTextOrEmpty(variantNode); + String weaponCategory = ItemField.weaponCategory.getTextOrEmpty(variantNode); + + String attunement = attunement(variantNode); + String rarity = ItemField.rarity.getTextOrEmpty(variantNode); + String tier = ItemField.tier.getTextOrEmpty(variantNode); + + String poisonTypes = poison + ? joinConjunct(" or ", ItemField.poisonTypes.getListOfStrings(variantNode, tui())) + : null; + + // -- render.js ------------------------- + // const [typeListText, typeHtml, subTypeHtml] = + // Renderer.item.getHtmlAndTextTypes(item); + // Building typeDescription and subtypeDescription in a stable order + List typeDescription = new ArrayList<>(); + List subTypeDescription = new ArrayList<>(); + + if (wondrous) { + typeDescription.add("wondrous item" + (tattoo ? " (tattoo)" : "")); + if (tattoo) { + ItemTag.wondrous.add(tags, "tattoo"); + } + } + if (staff) { + typeDescription.add("staff"); + } + if (ammo) { + typeDescription.add("ammunition"); + } + if (isPresent(age)) { + ItemTag.age.add(tags, age); + subTypeDescription.add(age); + } + if (isPresent(weaponCategory)) { + ItemTag.weapon.add(tags, weaponCategory); + baseItemIncluded = isPresent(baseItem); + typeDescription.add("weapon" + + (baseItemIncluded ? " (" + baseItem + ")" : "")); + subTypeDescription.add(weaponCategory + " weapon"); + } + if (staff && (EncodedType.M.typeIn(itemType, itemTypeAlt))) { + // "M" --> Type: Melee weapon + // DMG p140: "Unless a staff's description says otherwise, a staff can be used + // as a quarterstaff." + subTypeDescription.add("melee weapon"); + } + if (itemType != null) { + tags.addRaw(ItemType.tagForType(itemType, tui())); + processType(itemType, typeDescription, subTypeDescription, baseItem, baseItemIncluded); + subTypeDescription.add(itemType.linkify()); + } + if (itemTypeAlt != null) { + tags.addRaw(ItemType.tagForType(itemTypeAlt, tui())); + processType(itemTypeAlt, typeDescription, subTypeDescription, baseItem, baseItemIncluded); + subTypeDescription.add(itemTypeAlt.linkify()); + } + if (firearm) { + subTypeDescription.add("firearm"); + } + if (poison) { + itemProperties.add(ItemProperty.POISON); + typeDescription.add("poison" + (isPresent(poisonTypes) ? " (" + poisonTypes + ")" : "")); + } + if (cursed) { + itemProperties.add(ItemProperty.CURSED); + typeDescription.add("cursed item"); } - } - if (staff) { - typeDescription.add("staff"); - } - if (ammo) { - typeDescription.add("ammunition"); - } - if (isPresent(age)) { - ItemTag.age.add(tags, age); - subTypeDescription.add(age); - } - if (isPresent(weaponCategory)) { - ItemTag.weapon.add(tags, weaponCategory); - baseItemIncluded = isPresent(baseItem); - typeDescription.add("weapon" - + (baseItemIncluded ? " (" + baseItem + ")" : "")); - subTypeDescription.add(weaponCategory + " weapon"); - } - if (staff && (EncodedType.M.typeIn(itemType, itemTypeAlt))) { - // "M" --> Type: Melee weapon - // DMG p140: "Unless a staff's description says otherwise, a staff can be used as a quarterstaff." - subTypeDescription.add("melee weapon"); - } - if (itemType != null) { - tags.addRaw(ItemType.tagForType(itemType, tui())); - processType(itemType, typeDescription, subTypeDescription, baseItem, baseItemIncluded); - subTypeDescription.add(itemType.linkify()); - } - if (itemTypeAlt != null) { - tags.addRaw(ItemType.tagForType(itemTypeAlt, tui())); - processType(itemTypeAlt, typeDescription, subTypeDescription, baseItem, baseItemIncluded); - subTypeDescription.add(itemTypeAlt.linkify()); - } - if (firearm) { - subTypeDescription.add("firearm"); - } - if (poison) { - itemProperties.add(ItemProperty.POISON); - typeDescription.add("poison" + (isPresent(poisonTypes) ? " (" + poisonTypes + ")" : "")); - } - if (cursed) { - itemProperties.add(ItemProperty.CURSED); - typeDescription.add("cursed item"); - } - // Begin creation of detail string; - // render.js getAttunementAndAttunementCatText(item); - // getTypeRarityAndAttunementText(item); - // getTypeRarityAndAttunementHtml - String detail = join(", ", typeDescription); - if ("other".equals(detail)) { - detail = ""; - } + // Begin creation of detail string; + // render.js getAttunementAndAttunementCatText(item); + // getTypeRarityAndAttunementText(item); + // getTypeRarityAndAttunementHtml + String detail = join(", ", typeDescription); + if ("other".equals(detail)) { + detail = ""; + } - if (isPresent(tier)) { - ItemTag.tier.add(tags, tier); - detail += (detail.isBlank() ? "" : ", ") + tier; - } - if (isPresent(rarity)) { - ItemTag.rarity.add(tags, rarity - .replace("very rare", "very-rare") - .replaceAll("[()]", "") // unknown (magic) -> unknown magic - .split(" ")); - if (!hiddenRarity.contains(rarity)) { - detail += (detail.isBlank() ? "" : ", ") + rarity; + if (isPresent(tier)) { + ItemTag.tier.add(tags, tier); + detail += (detail.isBlank() ? "" : ", ") + tier; + } + if (isPresent(rarity)) { + ItemTag.rarity.add(tags, rarity + .replace("very rare", "very-rare") + .replaceAll("[()]", "") // unknown (magic) -> unknown magic + .split(" ")); + if (!hiddenRarity.contains(rarity)) { + detail += (detail.isBlank() ? "" : ", ") + rarity; + } + } + if (isPresent(attunement)) { + ItemTag.attunement.add(tags, + attunement.equals("optional") ? "optional" : "required"); + + detail += (detail.isBlank() ? "" : " ") + + switch (attunement) { + case "required" -> "(requires attunement)"; + case "optional" -> "(attunement optional)"; + default -> "(requires attunement " + attunement + ")"; + }; } - } - if (isPresent(attunement)) { - ItemTag.attunement.add(tags, - attunement.equals("optional") ? "optional" : "required"); - - detail += (detail.isBlank() ? "" : " ") - + switch (attunement) { - case "required" -> "(requires attunement)"; - case "optional" -> "(attunement optional)"; - default -> "(requires attunement " + attunement + ")"; - }; - } - return new Variant( - itemName(variantNode), - uppercaseFirst(detail), - uppercaseFirst(join(", ", subTypeDescription)), - baseItem, - itemType == null ? "" : itemType.name(), - itemTypeAlt == null ? "" : itemTypeAlt.name(), - ItemProperty.asLinks(itemProperties), - ItemMastery.asLinks(itemMasteries), - armorClass(variantNode, itemType, itemTypeAlt), - weaponCategory, - damage, - damage2h, - ItemField.range.getTextOrNull(variantNode), - ItemField.strength.intOrNull(variantNode), - ItemField.stealth.booleanOrDefault(variantNode, false), - listPrerequisites(variantNode), - age, - coinValue(variantNode), - ItemField.value.intOrNull(variantNode), - ItemField.weight.doubleOrNull(variantNode), - rarity, - tier, - attunement, - ammo, - firearm, - cursed, - focus, - focus ? focusType(variantNode) : "", - poison, - poisonTypes, - staff, - tattoo, - wondrous); + return new Variant( + itemName(variantNode), + uppercaseFirst(detail), + uppercaseFirst(join(", ", subTypeDescription)), + baseItem, + itemType == null ? "" : itemType.name(), + itemTypeAlt == null ? "" : itemTypeAlt.name(), + ItemProperty.asLinks(itemProperties), + ItemMastery.asLinks(itemMasteries), + armorClass(variantNode, itemType, itemTypeAlt), + weaponCategory, + damage, + damage2h, + ItemField.range.getTextOrNull(variantNode), + ItemField.strength.intOrNull(variantNode), + ItemField.stealth.booleanOrDefault(variantNode, false), + listPrerequisites(variantNode), + age, + coinValue(variantNode), + ItemField.value.intOrNull(variantNode), + ItemField.weight.doubleOrNull(variantNode), + rarity, + tier, + attunement, + ammo, + firearm, + cursed, + focus, + focus ? focusType(variantNode) : "", + poison, + poisonTypes, + staff, + tattoo, + wondrous); + } finally { + parseState().pop(pushed); + } } // render.js _getHtmlAndTextTypes_type @@ -358,11 +362,13 @@ String armorClass(JsonNode variantNode, ItemType type, ItemType typeAlt) { return null; } // - If you wear light armor, you add your Dexterity modifier to the base number - // from your armor type to determine your Armor Class. - // - If you wear medium armor, you add your Dexterity modifier, to a maximum of +2, - // to the base number from your armor type to determine your Armor Class. - // - Heavy armor does not let you add your Dexterity modifier to your Armor Class, - // but it also does not penalize you if your Dexterity modifier is negative. + // from your armor type to determine your Armor Class. + // - If you wear medium armor, you add your Dexterity modifier, to a maximum of + // +2, + // to the base number from your armor type to determine your Armor Class. + // - Heavy armor does not let you add your Dexterity modifier to your Armor + // Class, + // but it also does not penalize you if your Dexterity modifier is negative. if (EncodedType.LA.typeIn(type, typeAlt)) { ac += " + Dex modifier"; } else if (EncodedType.MA.typeIn(type, typeAlt)) { @@ -371,19 +377,19 @@ String armorClass(JsonNode variantNode, ItemType type, ItemType typeAlt) { return ac; } - ItemType getItemType(JsonNode node, ItemField typeField) { - String fragment = typeField.getTextOrEmpty(node); - return index.findItemType(fragment, sources); + ItemType getItemType(Tools5eSources variantSources, JsonNode node, ItemField typeField) { + String abbv = typeField.getTextOrEmpty(node); + return index.findItemType(abbv, variantSources); } - void findProperties(JsonNode variantNode, + void findProperties(Tools5eSources variantSources, JsonNode variantNode, Set itemProperties, ItemType type, ItemType typeAlt) { JsonNode propertyList = ItemField.property.getFrom(variantNode); if (propertyList != null && propertyList.isArray()) { // List of properties: abbreviation, or abbreviation|source for (JsonNode x : iterableElements(propertyList)) { - ItemProperty p = index.findItemProperty(x.asText(), sources); + ItemProperty p = index.findItemProperty(x.asText(), variantSources); if (p != null) { itemProperties.add(p); } @@ -397,11 +403,12 @@ void findProperties(JsonNode variantNode, } } - void findMastery(JsonNode variantNode, Set itemMasteries) { + void findMastery(Tools5eSources variantSources, JsonNode variantNode, + Set itemMasteries) { JsonNode masteryList = ItemField.mastery.getFrom(variantNode); if (masteryList != null && masteryList.isArray()) { for (JsonNode x : iterableElements(masteryList)) { - ItemMastery mastery = index.findItemMastery(x.asText(), sources); + ItemMastery mastery = index.findItemMastery(x.asText(), variantSources); if (mastery != null) { itemMasteries.add(mastery); } @@ -409,31 +416,6 @@ void findMastery(JsonNode variantNode, Set itemMasteries) { } } - enum ItemTag { - age, - armor, - attunement, - gear, - property, - rarity, - shield, - tier, - vehicle, - weapon, - wondrous, - ; - - void add(Tags tags, String... segments) { - tags.addRaw(build(segments)); - } - - String build(String... segments) { - return Stream.concat(Stream.of("item", name()), Arrays.stream(segments)) - .map(Tui::slugify) - .collect(Collectors.joining("/")); - } - } - enum EncodedType { GV, // generic variant LA, // light armor diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java index bd88a54eb..fa62e84a3 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java @@ -12,13 +12,13 @@ public class Json2QuteOptionalFeatureType extends Json2QuteCommon { - final OptionalFeatureType optionalFeatures; + final OptionalFeatureType oft; final String title; - Json2QuteOptionalFeatureType(Tools5eIndex index, JsonNode node, OptionalFeatureType optionalFeatures) { + Json2QuteOptionalFeatureType(Tools5eIndex index, JsonNode node, OptionalFeatureType optionalFeatureType) { super(index, Tools5eIndexType.optionalFeatureTypes, node); - this.optionalFeatures = optionalFeatures; - this.title = optionalFeatures.title; + this.oft = optionalFeatureType; + this.title = optionalFeatureType.getTitle(); } @Override @@ -28,7 +28,7 @@ public String getName() { @Override protected Tools5eQuteNote buildQuteNote() { - List featureKeys = optionalFeatures.features; + List featureKeys = oft.features; List nodes = featureKeys.stream() .map(index::getAliasOrDefault) .map(index::getNode) @@ -45,7 +45,8 @@ protected Tools5eQuteNote buildQuteNote() { List text = new ArrayList<>(); for (JsonNode entry : nodes) { - text.add("- " + linkify(Tools5eIndexType.optfeature, Tools5eIndexType.optfeature.toTagReference(entry))); + Tools5eIndexType type = Tools5eIndexType.getTypeFromNode(entry); + text.add("- " + type.linkify(index, entry)); } if (text.isEmpty()) { return null; @@ -53,7 +54,7 @@ protected Tools5eQuteNote buildQuteNote() { String sourceText = super.sources.getSourceText(index().srdOnly()); return new Tools5eQuteNote(title, sourceText, text, tags) - .withTargetFile(optionalFeatures.getFilename()) + .withTargetFile(oft.getFilename()) .withTargetPath(Tools5eIndexType.optionalFeatureTypes.getRelativePath()); } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java index 150e8b165..b63141908 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java @@ -9,8 +9,8 @@ import dev.ebullient.convert.qute.NamedText; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes; import dev.ebullient.convert.tools.dnd5e.PsionicType.PsionicTypeEnum; -import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewMetaTypes; import dev.ebullient.convert.tools.dnd5e.qute.QutePsionic; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -39,13 +39,21 @@ protected Tools5eQuteBase buildQuteResource() { String getPsionicTypeOrder() { String order = PsionicFields.order.replaceTextFrom(rootNode, this); - HomebrewMetaTypes meta = index.getHomebrewMetaTypes(sources); + Collection metas = index.getHomebrewMetaTypes(sources); String typeName = PsionicFields.type.getTextOrDefault(rootNode, "\u2014"); PsionicType type = switch (typeName) { case "D" -> PsionicTypeEnum.Discipline; case "T" -> PsionicTypeEnum.Talent; - default -> meta.getPsionicType(typeName); + default -> { + for (var meta : metas) { + var t = meta.getPsionicType(typeName); + if (t != null) { + yield t; + } + } + yield null; + } }; return type == null ? order : type.combineWith(order); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java index 9ec9a3b10..431599c38 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java @@ -1,17 +1,17 @@ package dev.ebullient.convert.tools.dnd5e; import java.util.ArrayList; -import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; -import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.dnd5e.SpellEntry.SpellReference; import dev.ebullient.convert.tools.dnd5e.qute.QuteSpell; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; @@ -26,20 +26,28 @@ public class Json2QuteSpell extends Json2QuteCommon { @Override protected Tools5eQuteBase buildQuteResource() { - boolean ritual = spellIsRitual(); - SpellSchool school = getSchool(); - String level = SpellFields.level.getTextOrEmpty(rootNode); + SpellEntry spellEntry = index().getSpellIndex().getSpellEntry(getSources().getKey()); Tags tags = new Tags(getSources()); - tags.add("spell", "school", school.name()); - tags.add("spell", "level", (level.equals("0") ? "cantrip" : level)); - if (ritual) { + tags.add("spell", "school", spellEntry.school.name()); + tags.add("spell", "level", JsonSource.spellLevelToText(spellEntry.level)); + if (spellEntry.ritual) { tags.add("spell", "ritual"); } - Set classes = indexedSpellClasses(tags); - classes.addAll(spellClasses(school, tags)); // legacy + // 🔧 Spell: spell|fireball|phb, + // references: {subclass|destruction domain|cleric|phb|vss=subclass|destruction domain|cleric|phb|vss;c:5;s:null;null, ...} + // expanded: {subclass|the fiend|warlock|phb|phb=subclass|the fiend|warlock|phb|phb;c:null;s:3;null, ...} + List referenceLinks = new ArrayList<>(); + Set allRefs = new TreeSet<>(Comparator.comparing(x -> x.refererKey)); + allRefs.addAll(spellEntry.references.values()); + allRefs.addAll(spellEntry.expandedList.values()); + + for (var r : allRefs) { + tags.addRaw(r.tagifyReference()); + referenceLinks.add(r.linkifyReference()); + } List text = new ArrayList<>(); appendToText(text, rootNode, "##"); @@ -52,14 +60,14 @@ protected Tools5eQuteBase buildQuteResource() { return new QuteSpell(sources, decoratedName, getSourceText(sources), - levelToText(level), - school.name(), - ritual, + JsonSource.spellLevelToText(spellEntry.level), + spellEntry.school.name(), + spellEntry.ritual, spellCastingTime(), spellRange(), spellComponents(), spellDuration(), - String.join(", ", classes), + referenceLinks, getFluffImages(Tools5eIndexType.spellFluff), String.join("\n", text), tags); @@ -97,6 +105,7 @@ String spellComponents() { list.add(replaceText(f.getValue().asText())); } } + case "r" -> list.add("R"); // Royalty. Acquisitions Incorporated } } return String.join(", ", list); @@ -185,103 +194,6 @@ String spellCastingTime() { SpellFields.unit.getTextOrEmpty(time)); } - // FIXME: spell lists are pretty broken. - Set indexedSpellClasses(Tags tags) { - Collection list = index().classesForSpell(this.sources.getKey()); - if (list == null) { - // tui().debugf("No classes found for %s", this.sources.getKey()); - return new TreeSet<>(); - } - - return list.stream() - .filter(k -> index().isIncluded(k)) - .map(k -> { - Tools5eSources sources = Tools5eSources.findSources(k); - Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(k); - if (type == Tools5eIndexType.subclass) { - JsonNode subclassNode = index().getOrigin(k); - String subclassName = sources.getName(); - String className = SpellFields.className.getTextOrEmpty(subclassNode).trim(); - String classSource = SpellFields.classSource.getTextOrEmpty(subclassNode).trim(); - return getSubclass(tags, className, classSource, subclassName, sources.primarySource(), k); - } - String className = sources.getName(); - return getClass(tags, className, sources.primarySource(), k); - }) - .collect(Collectors.toCollection(TreeSet::new)); - } - - Set spellClasses(SpellSchool school, Tags tags) { - JsonNode classesNode = SpellFields.classes.getFrom(rootNode); - if (classesNode == null || classesNode.isNull()) { - return Set.of(); - } - Set classes = new TreeSet<>(); - classesNode.withArray("fromClassList").forEach(c -> { - String className = SourceField.name.getTextOrEmpty(c); - String classSource = SourceField.source.getTextOrEmpty(c); - String finalKey = Tools5eIndexType.classtype.createKey(className, classSource); - if (index().isIncluded(finalKey)) { - classes.add(getClass(tags, className, classSource, finalKey)); - } - }); - classesNode.withArray("fromClassListVariant").forEach(c -> { - String definedInSource = SpellFields.definedInSource.getTextOrEmpty(c); - String className = SourceField.name.getTextOrEmpty(c); - String classSource = SourceField.source.getTextOrEmpty(c); - String finalKey = Tools5eIndexType.classtype.createKey(className, classSource); - if (index.sourceIncluded(definedInSource) && index().isIncluded(finalKey)) { - classes.add(getClass(tags, className, classSource, finalKey)); - } - }); - classesNode.withArray("fromSubclass").forEach(s -> { - String className = s.get("class").get("name").asText().trim(); - String classSource = s.get("class").get("source").asText(); - String subclassName = s.get("subclass").get("name").asText(); - String subclassSource = s.get("subclass").get("source").asText(); - String finalKey = Tools5eIndexType.getSubclassKey(className.trim(), classSource.trim(), subclassName.trim(), - subclassSource.trim()); - if (index().isIncluded(finalKey)) { - classes.add(getSubclass(tags, className, classSource, subclassName, subclassSource, finalKey)); - } - }); - if (classes.contains("Wizard")) { - // FIXME. Spell schools are busted (PHB/XPHB for these two) - if (school == SpellSchool.SchoolEnum.Abjuration || school == SpellSchool.SchoolEnum.Evocation) { - String finalKey = Tools5eIndexType.getSubclassKey("Fighter", "PHB", "Eldritch Knight", "PHB"); - if (index().isIncluded(finalKey)) { - classes.add(getSubclass(tags, "Fighter", "PHB", "Eldritch Knight", "PHB", finalKey)); - } - } - if (school == SpellSchool.SchoolEnum.Enchantment || school == SpellSchool.SchoolEnum.Illusion) { - String finalKey = Tools5eIndexType.getSubclassKey("Rogue", "PHB", "Arcane Trickster", "PHB"); - if (index().isIncluded(finalKey)) { - classes.add(getSubclass(tags, "Rogue", "PHB", "Arcane Trickster", "PHB", finalKey)); - } - } - } - return classes; - } - - private String getClass(Tags tags, String className, String classSource, String classKey) { - tags.add("spell", "class", className); - return linkOrText( - className, - classKey, - Tools5eIndexType.classtype.getRelativePath(), - Tools5eQuteBase.getClassResource(className, classSource)); - } - - private String getSubclass(Tags tags, String className, String classSource, String subclassName, - String subclassSource, String subclassKey) { - tags.add("spell", "class", className, subclassName); - return linkOrText( - String.format("%s (%s)", className, subclassName), - subclassKey, - Tools5eIndexType.classtype.getRelativePath(), - Tools5eQuteBase.getSubclassResource(subclassName, className, classSource, subclassSource)); - } - enum SpellFields implements JsonNodeReader { amount, className, @@ -306,5 +218,6 @@ enum SpellFields implements JsonNodeReader { unit, unlimited, definedInSource, + spellAttack, } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpellIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpellIndex.java new file mode 100644 index 000000000..471e9ad81 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpellIndex.java @@ -0,0 +1,214 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static dev.ebullient.convert.StringUtil.toTitleCase; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import dev.ebullient.convert.qute.QuteNote; +import dev.ebullient.convert.tools.Tags; +import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; +import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote; + +/** + * Read the spell index: create a variety of lists and indexes for spells + */ +public class Json2QuteSpellIndex extends Json2QuteCommon { + static final List SPELL_LEVELS = List.of( + "Cantrip", "1st Level", "2nd Level", "3rd Level", "4th Level", "5th Level", "6th Level", "7th Level", "8th Level", + "9th Level"); + + final SpellIndex spellIndex; + + Json2QuteSpellIndex(Tools5eIndex index) { + super(index, Tools5eIndexType.spellIndex, null); + this.spellIndex = index.getSpellIndex(); + } + + public Collection buildNotes() { + List notes = new ArrayList<>(); + + SpellByLevel spellsByLevel = new SpellByLevel(); + Map spellsByClass = new HashMap<>(); + Map spellsByOther = new HashMap<>(); + Map spellsBySchool = new HashMap<>(); + + // Spells by all the things. + for (var entry : spellIndex.spellsByKey.values()) { + spellsByLevel.add(entry); + spellsBySchool.computeIfAbsent(entry.school, k -> new SpellByLevel()).add(entry); + + for (var name : entry.classes) { + spellsByClass.computeIfAbsent(name, k -> new SpellByLevel()).add(entry); + } + + for (var ref : entry.references.values()) { + if (ref.refererType == Tools5eIndexType.classtype) { + continue; + } + spellsByOther.computeIfAbsent(ref.refererKey, k -> new SpellRefByLevel(ref)) + .add(entry); + } + + for (var ref : entry.expandedList.values()) { + if (ref.refererType == Tools5eIndexType.classtype) { + continue; + } + spellsByOther.computeIfAbsent(ref.refererKey, k -> new SpellRefByLevel(ref)) + .add(entry); + } + } + + // Create school spell list + for (var schoolList : spellsBySchool.entrySet()) { + QuteNote note = createSchoolList(schoolList.getKey(), schoolList.getValue()); + if (note != null) { + notes.add(note); + } + } + + // Create class spell list + for (var classList : spellsByClass.entrySet()) { + QuteNote note = createClassList(classList.getKey(), classList.getValue()); + if (note != null) { + notes.add(note); + } + } + + // Create other spell list + for (var otherList : spellsByOther.entrySet()) { + QuteNote note = createOtherList(otherList.getKey(), otherList.getValue()); + if (note != null) { + notes.add(note); + } + } + + return notes; + } + + private QuteNote createClassList(String className, SpellByLevel spellsByClass) { + if (spellsByClass.isEmpty()) { + return null; + } + + Tags tags = new Tags(); + tags.add("spell", "list", "class", className); + List text = new ArrayList<>(); + + for (int i = 0; i < SPELL_LEVELS.size(); i++) { + Set levelSpells = spellsByClass.getLevel(String.valueOf(i)); + if (levelSpells.isEmpty()) { + continue; + } + maybeAddBlankLine(text); + String levelHeading = SPELL_LEVELS.get(i); + text.add("## " + levelHeading); + text.add(""); + for (var entry : levelSpells) { + text.add("- " + entry.linkify() + (entry.isExpanded(className) ? " (\\*)" : "")); + } + } + maybeAddBlankLine(text); + + return new Tools5eQuteNote(toTitleCase(className) + " Spells", "", text, tags) + .withTargetFile(Tools5eQuteBase.getClassSpellList(className)) + .withTargetPath(Tools5eIndexType.spellIndex.getRelativePath()); + } + + private QuteNote createSchoolList(SpellSchool spellSchool, SpellByLevel spellsByClass) { + if (spellsByClass.isEmpty()) { + return null; + } + + Tags tags = new Tags(); + tags.add("spell", "list", "school", spellSchool.name()); + List text = new ArrayList<>(); + + for (int i = 0; i < SPELL_LEVELS.size(); i++) { + Set levelSpells = spellsByClass.getLevel(String.valueOf(i)); + if (levelSpells.isEmpty()) { + continue; + } + maybeAddBlankLine(text); + String levelHeading = SPELL_LEVELS.get(i); + text.add("## " + levelHeading); + text.add(""); + for (var entry : levelSpells) { + text.add("- " + entry.linkify()); + } + } + maybeAddBlankLine(text); + + return new Tools5eQuteNote(spellSchool.name() + " Spells", "", text, tags) + .withTargetFile("list-spells-school-" + spellSchool.name()) + .withTargetPath(Tools5eIndexType.spellIndex.getRelativePath()); + } + + private QuteNote createOtherList(String key, SpellRefByLevel spellsByOther) { + if (spellsByOther.isEmpty()) { + return null; + } + Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); + String name = type.decoratedName(spellsByOther.reference.refererNode); + + Tags tags = new Tags(); + tags.add("spell", "list", type.name(), name); + List text = new ArrayList<>(); + + for (int i = 0; i < SPELL_LEVELS.size(); i++) { + Set levelSpells = spellsByOther.getLevel(String.valueOf(i)); + if (levelSpells.isEmpty()) { + continue; + } + maybeAddBlankLine(text); + String levelHeading = SPELL_LEVELS.get(i); + text.add("## " + levelHeading); + text.add(""); + for (var entry : levelSpells) { + text.add("- " + entry.linkify() + " " + spellsByOther.reference.describe()); + } + } + maybeAddBlankLine(text); + + return new Tools5eQuteNote("Spells for " + name, "", text, tags) + .withTargetFile(spellsByOther.reference.listFileName()) + .withTargetPath(Tools5eIndexType.spellIndex.getRelativePath()); + } + + private class SpellRefByLevel extends SpellByLevel { + final SpellEntry.SpellReference reference; + + SpellRefByLevel(SpellEntry.SpellReference reference) { + this.reference = reference; + } + + boolean isEmpty() { + return spellsByLevel.isEmpty(); + } + } + + private class SpellByLevel { + final Map> spellsByLevel = new HashMap<>(); + + void add(SpellEntry entry) { + spellsByLevel.computeIfAbsent(entry.getLevel(), + k -> new TreeSet<>(Comparator.comparing(SpellEntry::getName))).add(entry); + } + + Set getLevel(String level) { + Set spells = spellsByLevel.get(level); + return spells == null ? Set.of() : spells; + } + + boolean isEmpty() { + return spellsByLevel.isEmpty(); + } + } + +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java index 9379d7e6a..b7cc610a8 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java @@ -925,7 +925,7 @@ default String mapAlignmentToString(String a) { case "C" -> "Chaotic"; case "CE" -> "Chaotic Evil"; case "CG" -> "Chaotic Good"; - case "CECG" -> "Chaotic Evil or Chaotic Good"; + case "CECG", "CGCE" -> "Chaotic Evil or Chaotic Good"; case "CGCN" -> "Chaotic Good or Chaotic Neutral"; case "CGNE" -> "Chaotic Good or Neutral Evil"; case "CECN" -> "Chaotic Evil or Chaotic Neutral"; @@ -1061,25 +1061,6 @@ default String sizeToString(String size) { }; } - default String levelToText(String level) { - return switch (level) { - case "0" -> "cantrip"; - case "1" -> "1st-level"; - case "2" -> "2nd-level"; - case "3" -> "3rd-level"; - default -> level + "th-level"; - }; - } - - static String levelToString(int level) { - return switch (level) { - case 1 -> "1st"; - case 2 -> "2nd"; - case 3 -> "3rd"; - default -> level + "th"; - }; - } - static String crToTagValue(String cr) { return switch (cr) { case "1/8" -> "⅛"; @@ -1191,6 +1172,16 @@ default String convertCurrency(int cp) { return String.join(", ", result); } + public static String spellLevelToText(String level) { + return switch (level) { + case "0", "c" -> "cantrip"; + case "1" -> "1st-level"; + case "2" -> "2nd-level"; + case "3" -> "3rd-level"; + default -> level + "th-level"; + }; + } + @RegisterForReflection @JsonIgnoreProperties(ignoreUnknown = true) class JsonMediaHref { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index 6e6da9cae..ea4647ff7 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -2,6 +2,7 @@ import static dev.ebullient.convert.StringUtil.isPresent; import static dev.ebullient.convert.StringUtil.toAnchorTag; +import static dev.ebullient.convert.StringUtil.valueOrDefault; import java.util.ArrayList; import java.util.HashSet; @@ -35,7 +36,7 @@ public interface JsonTextReplacement extends JsonTextConverter static final Pattern linkifyPattern = Pattern.compile("\\{@(" + "|action|background|card|class|condition|creature|deck|deity|disease|facility" + "|feat|hazard|item|itemMastery|itemProperty|itemType|legroup|object|psionic|race|reward" - + "|sense|skill|spell|status|table|variantrule|vehicle" + + "|sense|skill|spell|status|subclass|table|variantrule|vehicle" + "|optfeature|classFeature|subclassFeature|trap) ([^}]+)}"); static final Pattern chancePattern = Pattern.compile("\\{@chance ([^}]+)}"); static final Pattern fontPattern = Pattern.compile("\\{@font ([^}]+)}"); @@ -274,9 +275,6 @@ default String _replaceTokenText(String input, boolean nested) { return parts[0]; }); - result = linkifyPattern.matcher(result) - .replaceAll(this::linkify); - result = fontPattern.matcher(result).replaceAll((match) -> { String[] parts = match.group(1).split("\\|"); String fontFamily = Tools5eSources.getFontReference(parts[1]); @@ -459,7 +457,7 @@ default String replaceSkillOrAbility(MatchResult match) { String[] score = parts[0].split(" "); boolean abilityCheck = match.group(1).equals("ability"); - String text = parts.length > 1 && !parts[1].isBlank() ? parts[1] : null; + String text = valueOrDefault(parts, 1, null); SkillOrAbility ability = index().findSkillOrAbility(score[0], getSources()); @@ -491,9 +489,8 @@ default String replaceSkillCheck(MatchResult match) { String[] parts = match.group(1).split("\\|"); String[] score = parts[0].split(" "); SkillOrAbility skill = index().findSkillOrAbility(score[0], getSources()); - String text = parts.length > 1 && !parts[1].isBlank() - ? parts[1] - : linkifyRules(Tools5eIndexType.skill, skill.value(), "skills"); + String text = valueOrDefault(parts, 1, + linkifyRules(Tools5eIndexType.skill, skill.value(), "skills")); String dice = score[1]; if (score[1].matches("\\d+")) { @@ -524,8 +521,8 @@ default String linkifyRules(Tools5eIndexType type, String text, String rules) { String[] parts = text.split("\\|"); String name = parts[0]; - String source = parts.length > 1 ? parts[1] : type.defaultSourceString(); - String linkText = parts.length > 2 ? parts[2] : name; + String source = valueOrDefault(parts, 1, type.defaultSourceString()); + String linkText = valueOrDefault(parts, 2, name); if (name.isBlank()) { return "[%s](%s%s.md)".formatted(linkText, index().rulesVaultRoot(), rules); @@ -639,7 +636,8 @@ default String linkify(Tools5eIndexType type, String s) { case skill -> linkifyRules(type, s, "skills"); case itemMastery, itemProperty, itemType -> linkifyItemAttribute(type, s); case monster -> linkifyCreature(s); - case subclass, classtype -> linkifyClass(s); + case subclass -> linkifySubclass(s); // RARE!! + case classtype -> linkifyClass(s); case deity -> linkifyDeity(s); case card -> linkifyCardType(s); case classfeature -> linkifyClassFeature(s); @@ -663,8 +661,8 @@ linkText, index().compendiumVaultRoot(), dirName, slugify(resourceName) default String linkifyType(Tools5eIndexType type, String match) { String[] parts = match.split("\\|"); - String source = parts.length > 1 && !parts[1].isBlank() ? parts[1] : type.defaultSourceString(); - String linkText = parts.length > 2 ? parts[2] : parts[0]; + String source = valueOrDefault(parts, 1, type.defaultSourceString()); + String linkText = valueOrDefault(parts, 2, parts[0]); String key = index().getAliasOrDefault(type.createKey(parts[0].trim(), source)); return linkifyType(type, key, linkText, match); @@ -674,17 +672,14 @@ default String linkifyType(Tools5eIndexType type, String aliasKey, String linkTe String dirName = type.getRelativePath(); JsonNode jsonSource = index().getNode(aliasKey); // filtered if (jsonSource == null) { - jsonSource = index().getHomebrewNode(type, aliasKey, parseState().getSource()); - if (jsonSource == null) { - if (index().getOrigin(aliasKey) == null) { - // sources can be excluded, that's fine.. but if this is something that doesn't - // exist at all.. - tui().debugf(Msg.UNRESOLVED, "unresolvable {@%s %s} as [%s] from %s", - type, match, aliasKey, parseState().getSource()); - // log a stack trace of how we got here - } - return linkText; + if (index().getOrigin(aliasKey) == null) { + // sources can be excluded, that's fine.. but if this is something that doesn't + // exist at all.. + tui().debugf(Msg.UNRESOLVED, "unresolvable {@%s %s} as [%s] from %s", + type, match, aliasKey, parseState().getSource()); + // log a stack trace of how we got here } + return linkText; } Tools5eSources linkSource = Tools5eSources.findSources(jsonSource); return linkOrText(linkText, aliasKey, dirName, @@ -698,7 +693,7 @@ default String linkifyCardType(String match) { String[] parts = match.split("\\|"); String cardName = parts[0]; String deckName = parts[1]; - String source = parts.length < 3 || parts[2].isBlank() ? Tools5eIndexType.card.defaultSourceString() : parts[2]; + String source = valueOrDefault(parts, 2, Tools5eIndexType.card.defaultSourceString()); String key = index().getAliasOrDefault(Tools5eIndexType.deck.createKey(deckName, source)); if (index().isExcluded(key)) { @@ -718,19 +713,9 @@ default String linkifyDeity(String match) { // {@deity Ioun|dawn war|dmg|and optional link text added with another pipe}.", String[] parts = match.split("\\|"); String deity = parts[0]; - String source = "phb"; - String linkText = deity; - String pantheon = "Faerûnian"; - - if (parts.length > 3) { - linkText = parts[3]; - } - if (parts.length > 2) { - source = parts[2]; - } - if (parts.length > 1) { - pantheon = parts[1]; - } + String pantheon = valueOrDefault(parts, 1, "Forotten Realms"); + String source = valueOrDefault(parts, 2, Tools5eIndexType.deity.defaultSourceString()); + String linkText = valueOrDefault(parts, 3, deity); String key = index().getAliasOrDefault(Tools5eIndexType.deity.createKey(parts[0], source)); return linkOrText(linkText, key, Tools5eIndexType.deity.getRelativePath(), @@ -748,10 +733,10 @@ default String linkifyClass(String match) { // {@class Fighter|phb|Samurai|Samurai|xge} String[] parts = match.split("\\|"); String className = parts[0]; - String classSource = parts.length < 2 || parts[1].isEmpty() ? "phb" : parts[1]; - String linkText = parts.length < 3 || parts[2].isEmpty() ? className : parts[2]; - String subclass = parts.length < 4 || parts[3].isEmpty() ? null : parts[3]; - String subclassSource = parts.length < 5 || parts[4].isEmpty() ? classSource : parts[4]; + String classSource = valueOrDefault(parts, 1, Tools5eIndexType.classtype.defaultSourceString()); + String linkText = valueOrDefault(parts, 2, className); + String subclass = valueOrDefault(parts, 3, null); + String subclassSource = valueOrDefault(parts, 4, Tools5eIndexType.classtype.defaultSourceString()); String relativePath = Tools5eIndexType.classtype.getRelativePath(); if (subclass != null) { @@ -759,11 +744,11 @@ default String linkifyClass(String match) { .getAliasOrDefault( Tools5eIndexType.getSubclassKey(className, classSource, subclass, subclassSource)); // "subclass|path of wild magic|barbarian|phb|" - int first = key.indexOf('|'); - int second = key.indexOf('|', first + 1); - subclass = key.substring(first + 1, second); + Tools5eSources scSources = Tools5eSources.findSources(key); return linkOrText(linkText, key, relativePath, - Tools5eQuteBase.getSubclassResource(subclass, className, classSource, subclassSource)); + Tools5eQuteBase.getSubclassResource( + scSources == null ? subclass : scSources.getName(), + className, classSource, subclassSource)); } else { String key = index().getAliasOrDefault(Tools5eIndexType.classtype.createKey(className, classSource)); return linkOrText(linkText, key, relativePath, @@ -771,6 +756,30 @@ default String linkifyClass(String match) { } } + default String linkifySubclass(String match) { + // Only used in homebrew (so far) + // "Subclasses:{@subclass Berserker|Barbarian}, + // {@subclass Berserker|Barbarian}, + // {@subclass Ancestral Guardian|Barbarian||XGE}, + // {@subclass Artillerist|Artificer|TCE|TCE}. + // Class and subclass source is assumed to be PHB." + String[] parts = match.split("\\|"); + String scShortName = parts[0]; + String className = parts[1]; + String classSource = valueOrDefault(parts, 2, Tools5eIndexType.classtype.defaultSourceString()); + String scSource = valueOrDefault(parts, 3, Tools5eIndexType.subclass.defaultSourceString()); + String linkText = valueOrDefault(parts, 4, scShortName); + + // "subclass|path of wild magic|barbarian|phb|phb" + String key = index() + .getAliasOrDefault( + Tools5eIndexType.getSubclassKey(className, classSource, scShortName, scSource)); + Tools5eSources scSources = Tools5eSources.findSources(key); + return linkOrText(linkText, key, + Tools5eIndexType.classtype.getRelativePath(), + Tools5eQuteBase.getSubclassResource(scSources.getName(), className, classSource, scSource)); + } + default String linkifyClassFeature(String match) { // "Class Features: Class source is assumed to be PHB, class feature source is // assumed to be the same as class source" @@ -823,7 +832,7 @@ default String linkifyOptionalFeatureType(MatchResult match) { featureSource = parseState().getSource(); } - OptionalFeatureType oft = index().getOptionalFeatureType(featureType, featureSource); + OptionalFeatureType oft = index().getOptionalFeatureType(featureType); if (oft == null) { return linkText; } @@ -935,8 +944,8 @@ default String linkifyCreature(String match) { // {@creature cow|vgm} can have sources added with a pipe, // {@creature cow|vgm|and optional link text added with another pipe}.", String[] parts = match.trim().split("\\|"); - String source = parts.length > 1 && !parts[1].isBlank() ? parts[1] : indexType.defaultSourceString(); - String linkText = parts.length > 2 ? parts[2] : parts[0]; + String source = valueOrDefault(parts, 1, indexType.defaultSourceString()); + String linkText = valueOrDefault(parts, 2, parts[0]); String key = index().getAliasOrDefault(indexType.createKey(parts[0], source)); if (index().isExcluded(key)) { @@ -959,7 +968,7 @@ default String linkifyVariant(String variant) { // "fromVariant": "Action Options", // "fromVariant": "Spellcasting|XGE", String[] parts = variant.trim().split("\\|"); - String source = parts.length > 1 ? parts[1] : Tools5eIndexType.variantrule.defaultSourceString(); + String source = valueOrDefault(parts, 1, Tools5eIndexType.variantrule.defaultSourceString()); if (!index().sourceIncluded(source)) { return "%s".formatted(TtrpgConfig.sourceToLongName(source), parts[0]); } else { @@ -970,48 +979,29 @@ parts[0], index().rulesVaultRoot(), } default String linkifyItemAttribute(Tools5eIndexType type, String s) { - String parts[] = s.split("\\|"); - String source = parts.length > 1 ? parts[1] : type.defaultSourceString(); - String linkText = parts.length > 2 ? parts[2] : parts[0]; - String lookup = "%s|%s".formatted(parts[0], source); - if (missingKeys.contains(lookup)) { - return linkText; - } + String[] parts = s.split("\\|"); + String name = parts[0]; + String linkText = valueOrDefault(parts, 2, name); return switch (type) { - case itemType -> { - String key = Tools5eIndexType.itemType.fromTagReference(lookup); - ItemType itemType = index().findItemType(key, getSources()); - if (itemType == null) { - if (missingKeys.add(lookup) && index().isIncluded(key)) { - tui().warnf(Msg.UNRESOLVED, "Item type %s not found from %s", s, getSources().getKey()); - } - yield linkText; - } - yield itemType.linkify(linkText); + case itemMastery -> { + ItemMastery mastery = index().findItemMastery(s, getSources()); + yield mastery == null + ? linkText + : mastery.linkify(linkText); } case itemProperty -> { - String key = Tools5eIndexType.itemProperty.fromTagReference(lookup); - ItemProperty itemProperty = index().findItemProperty(key, getSources()); - if (itemProperty == null) { - if (missingKeys.add(lookup) && index().isIncluded(key)) { - tui().warnf(Msg.UNRESOLVED, "Item property %s not found from %s", s, getSources().getKey()); - } - yield linkText; - } - yield itemProperty.linkify(linkText); + ItemProperty property = index().findItemProperty(s, getSources()); + yield property == null + ? linkText + : property.linkify(linkText); } - case itemMastery -> { - String key = Tools5eIndexType.itemMastery.fromTagReference(lookup); - ItemMastery itemMastery = index().findItemMastery(key, getSources()); - if (itemMastery == null) { - if (missingKeys.add(lookup) && index().isIncluded(key)) { - tui().warnf(Msg.UNRESOLVED, "Item mastery %s not found from %s", s, getSources().getKey()); - } - yield linkText; - } - yield itemMastery.linkify(linkText); + case itemType -> { + ItemType itemType = index().findItemType(s, getSources()); + yield itemType == null + ? linkText + : itemType.linkify(linkText); } - default -> linkify(type, s); // should never happen + default -> linkText; }; } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java index da343a363..01c32f5ce 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java @@ -163,7 +163,10 @@ private List findVariants(Tools5eIndex index, Tools5eIndexType type, if (fluffKey != null) { TtrpgValue.indexFluffKey.setIn(specficVariant, fluffKey); } - Tools5eSources.constructSources(newKey, specficVariant); + Tools5eSources variantSources = Tools5eSources.constructSources(newKey, specficVariant); + variantSources.amendHomebrewSources(baseItem); + variantSources.amendHomebrewSources(genericVariant); + if (spawnNewItems) { variants.add(specficVariant); if (key.replace(" (*)", "").replace("magicvariant", "item").equals(newKey)) { @@ -506,6 +509,11 @@ private JsonNode createSpecificVariant(JsonNode baseItem, JsonNode genericVarian // TODO: // Renderer.item._createSpecificVariants_mergeVulnerableResistImmune({specificVariant, inherits}); + // Carry over any homebrew sources from the base or GV item + // so that any properties or types can be rendered properly + if (TtrpgValue.homebrewSource.existsIn(genericVariant)) { + TtrpgValue.homebrewSource.copy(genericVariant, specificVariant); + } return specificVariant; } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java index 15218548e..d7a8843d6 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java @@ -4,6 +4,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; @@ -13,9 +16,8 @@ import dev.ebullient.convert.io.Msg; import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.tools.JsonNodeReader; -import dev.ebullient.convert.tools.ToolsIndex.TtrpgValue; +import dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes; import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; -import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewMetaTypes; import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -28,40 +30,44 @@ public class OptionalFeatureIndex implements JsonSource { this.index = index; } + public OptionalFeatureType addOptionalFeatureType(String featureType, HomebrewMetaTypes homebrew) { + // scope the optional feature key (homebrew may conflict) + try { + var oft = optFeatureIndex.computeIfAbsent(featureType.toLowerCase(), + k -> new OptionalFeatureType(featureType, homebrew, index())); + oft.addHomebrewMeta(homebrew); + return oft; + } catch (IllegalArgumentException e) { + tui().errorf(e, "Unable to define optional feature"); + } + return null; + } + public void addOptionalFeature(String finalKey, JsonNode optFeatureNode, HomebrewMetaTypes homebrew) { - String lookup = null; - for (String ft : toListOfStrings(optFeatureNode.get("featureType"))) { - try { - boolean homebrewType = homebrew != null && homebrew.getOptionalFeatureType(ft) != null; - // scope the optional feature key (homebrew may conflict) - String featKey = (homebrewType ? ft + "-" + homebrew.jsonKey : ft).toLowerCase(); - - optFeatureIndex.computeIfAbsent(featKey, k -> new OptionalFeatureType(ft, k, homebrew, index())).add(finalKey); - lookup = lookup == null ? featKey : lookup; - } catch (IllegalArgumentException e) { - tui().errorf(e, "Unable to define optional feature"); + for (String ft : OftFields.featureType.getListOfStrings(optFeatureNode, tui())) { + var oft = addOptionalFeatureType(ft, homebrew); + if (oft != null) { + oft.addFeature(finalKey); } } - if (lookup != null) { - OftFields.oftLookup.setIn(optFeatureNode, lookup); - OftFields.oftIndexKey.setIn(optFeatureNode, optFeatureIndex.get(lookup).getKey()); - } } - public void amendSources(String key, JsonNode jsonSource, Tools5eHomebrewIndex homebrewIndex) { + public void amendSources(String key, JsonNode jsonSource) { Tools5eSources sources = Tools5eSources.findSources(key); if (sources.getType() == Tools5eIndexType.optfeature) { - OptionalFeatureType oft = get(jsonSource); - if (oft == null) { - tui().warnf(Msg.UNRESOLVED, "OptionalFeatureType %s not found for %s", jsonSource, key); - } else { - oft.amendSources(sources); + for (String featureType : OftFields.featureType.getListOfStrings(jsonSource, tui())) { + OptionalFeatureType oft = get(featureType); + if (oft == null) { + tui().warnf(Msg.UNRESOLVED, "OptionalFeatureType %s not found for %s", jsonSource, key); + } else { + oft.amendSources(sources); + } } } else { for (JsonNode ofp : ClassFields.optionalfeatureProgression.iterateArrayFrom(jsonSource)) { for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) { // class/subclass source matters for homebrew scope (if necessary) - OptionalFeatureType oft = get(featureType, sources.primarySource(), homebrewIndex); + OptionalFeatureType oft = get(featureType); if (oft == null) { tui().warnf(Msg.UNRESOLVED, "OptionalFeatureType %s not found for %s", featureType, key); @@ -74,45 +80,32 @@ public void amendSources(String key, JsonNode jsonSource, Tools5eHomebrewIndex h } } - public OptionalFeatureType get(Tools5eIndexType type, String key) { - return switch (type) { - case optfeature -> { - JsonNode ofNode = index().getOrigin(key); - String oftKey = OftFields.oftIndexKey.getTextOrNull(ofNode); - JsonNode oftNode = index().getOrigin(oftKey); - yield get(oftNode); + public void removeUnusedOptionalFeatures( + Function testInUse, + Consumer remove) { + for (var oft : optFeatureIndex.values()) { + // Test to see if any of the features using this type are still active. + if (oft.testFeaturesInUse(testInUse) || oft.testConsumersInUse(testInUse)) { + continue; } - case optionalFeatureTypes -> { - JsonNode node = index().getOrigin(key); - yield get(node); - } - default -> null; - }; + + // Remove the feature type + remove.accept(oft.getKey()); + // Remove all features associated with this type + oft.features.forEach(remove); + } } public OptionalFeatureType get(JsonNode node) { if (node == null) { return null; } - String lookup = OftFields.oftLookup.getTextOrNull(node); - return lookup == null ? null : optFeatureIndex.get(lookup); + String lookup = SourceField.name.getTextOrEmpty(node); + return lookup == null ? null : optFeatureIndex.get(lookup.toLowerCase()); } - public OptionalFeatureType get(String ft, String source, Tools5eHomebrewIndex homebrewIndex) { - HomebrewMetaTypes metaTypes = homebrewIndex.getHomebrewMetaTypes(source); - String homebrewType = metaTypes == null - ? null - : metaTypes.getOptionalFeatureType(ft); - - OptionalFeatureType oft = optFeatureIndex.get(ft.toLowerCase()); - if (homebrewType != null) { - String homebrewScoped = ft + "-" + metaTypes.jsonKey; - OptionalFeatureType homebrewOft = optFeatureIndex.get(homebrewScoped.toLowerCase()); - return homebrewOft == null - ? oft - : homebrewOft; - } - return oft; + public OptionalFeatureType get(String featureType) { + return optFeatureIndex.get(featureType.toLowerCase()); } public void clear() { @@ -128,83 +121,49 @@ public Map getMap() { */ static class OptionalFeatureType { - @JsonIgnore - final HomebrewMetaTypes homebrewMeta; - - final String lookupKey; final String featureTypeKey; final String abbreviation; - final String title; - final String source; + final Tools5eSources sources; final List features = new ArrayList<>(); final List consumers = new ArrayList<>(); @JsonIgnore final ObjectNode featureTypeNode; - OptionalFeatureType(String abbreviation, String scopedAbv, HomebrewMetaTypes homebrewMeta, Tools5eIndex index) { + @JsonIgnore + final Map homebrewMeta = new HashMap<>(); + + OptionalFeatureType(String abbreviation, HomebrewMetaTypes homebrewMeta, Tools5eIndex index) { this.abbreviation = abbreviation; - this.lookupKey = scopedAbv; - this.homebrewMeta = homebrewMeta; - String tmpTitle = null; - if (homebrewMeta != null) { - tmpTitle = homebrewMeta.getOptionalFeatureType(abbreviation); - } - if (tmpTitle == null) { - tmpTitle = switch (abbreviation) { - case "AI" -> "Artificer Infusion"; - case "ED" -> "Elemental Discipline"; - case "EI" -> "Eldritch Invocation"; - case "MM" -> "Metamagic"; - case "MV" -> "Maneuver"; - case "MV:B" -> "Maneuver, Battle Master"; - case "MV:C2-UA" -> "Maneuver, Cavalier V2 (UA)"; - case "AS:V1-UA" -> "Arcane Shot, V1 (UA)"; - case "AS:V2-UA" -> "Arcane Shot, V2 (UA)"; - case "AS" -> "Arcane Shot"; - case "OTH" -> "Other"; - case "FS:F" -> "Fighting Style, Fighter"; - case "FS:B" -> "Fighting Style, Bard"; - case "FS:P" -> "Fighting Style, Paladin"; - case "FS:R" -> "Fighting Style, Ranger"; - case "PB" -> "Pact Boon"; - case "OR" -> "Onomancy Resonant"; - case "RN" -> "Rune Knight Rune"; - case "AF" -> "Alchemical Formula"; - case "TT" -> "Traveler's Trick"; - default -> null; - }; - } - if (tmpTitle == null) { - index.tui().warnf(Msg.NOT_SET.wrap("Missing title for OptionalFeatureType in %s from %s"), - abbreviation, - homebrewMeta == null ? "unknown/core" : homebrewMeta.filename); - tmpTitle = abbreviation; - } - title = tmpTitle; - source = getSource(homebrewMeta); + String primarySource = getSource(homebrewMeta); featureTypeNode = Tui.MAPPER.createObjectNode(); - featureTypeNode.put("name", scopedAbv); - featureTypeNode.put("source", source); - OftFields.oftLookup.setIn(featureTypeNode, lookupKey); + featureTypeNode.put("name", abbreviation); + featureTypeNode.put("source", primarySource); if (inSRD(abbreviation)) { featureTypeNode.put("srd", true); + featureTypeNode.put("srd52", true); } // KNOCK-ON: Add to index + this.featureTypeKey = Tools5eIndexType.optionalFeatureTypes.createKey(featureTypeNode); index.addToIndex(Tools5eIndexType.optionalFeatureTypes, featureTypeNode); - featureTypeKey = TtrpgValue.indexKey.getTextOrThrow(featureTypeNode); - Tools5eSources.constructSources(featureTypeKey, featureTypeNode); + this.sources = Tools5eSources.constructSources(featureTypeKey, featureTypeNode); } public void amendSources(Tools5eSources otherSources) { // Update sources from those of a consuming/using class or subclass // Optional features will always add to sources of types - Tools5eSources mySources = Tools5eSources.findSources(featureTypeNode); if (otherSources.getType() == Tools5eIndexType.optfeature - || otherSources.contains(mySources)) { - mySources.amendSources(otherSources); + || otherSources.contains(this.sources)) { + this.sources.amendSources(otherSources); + } + } + + public void addHomebrewMeta(HomebrewMetaTypes homebrew) { + if (homebrew != null) { + homebrewMeta.put(homebrew.primary, homebrew); + this.sources.amendSources(homebrew.sourceKeys); } } @@ -212,29 +171,69 @@ public void addConsumer(String key) { consumers.add(key); } - public void add(String key) { + public void addFeature(String key) { features.add(key); } public String getFilename() { - return "list-" + Tools5eQuteBase.fixFileName(title, source, Tools5eIndexType.optionalFeatureTypes); + return Tools5eQuteBase.getOptionalFeatureTypeResource(abbreviation); } public Tools5eSources getSources() { return Tools5eSources.findSources(featureTypeKey); } - public boolean inUse() { + public boolean testConsumersInUse(Function test) { + return consumers.stream() + .map(k -> test.apply(k)) + .reduce(Boolean::logicalOr) + .orElse(false); + } + + public boolean testFeaturesInUse(Function test) { return features.stream() - .map(k -> Tools5eSources.includedByConfig(k)) + .map(k -> test.apply(k)) .reduce(Boolean::logicalOr) .orElse(false); } + public String getTitle() { + return switch (abbreviation) { + case "AI" -> "Artificer Infusion"; + case "ED" -> "Elemental Discipline"; + case "EI" -> "Eldritch Invocation"; + case "MM" -> "Metamagic"; + case "MV" -> "Maneuver"; + case "MV:B" -> "Maneuver, Battle Master"; + case "MV:C2-UA" -> "Maneuver, Cavalier V2 (UA)"; + case "AS:V1-UA" -> "Arcane Shot, V1 (UA)"; + case "AS:V2-UA" -> "Arcane Shot, V2 (UA)"; + case "AS" -> "Arcane Shot"; + case "OTH" -> "Other"; + case "FS:F" -> "Fighting Style, Fighter"; + case "FS:B" -> "Fighting Style, Bard"; + case "FS:P" -> "Fighting Style, Paladin"; + case "FS:R" -> "Fighting Style, Ranger"; + case "PB" -> "Pact Boon"; + case "OR" -> "Onomancy Resonant"; + case "RN" -> "Rune Knight Rune"; + case "AF" -> "Alchemical Formula"; + case "TT" -> "Traveler's Trick"; + default -> { + if (!homebrewMeta.isEmpty()) { + yield homebrewMeta.values().stream() + .map(hb -> hb.getOptionalFeatureType(abbreviation)) + .distinct() + .collect(Collectors.joining("; ")); + } + Tui.instance().warnf(Msg.NOT_SET, "Missing title for OptionalFeatureType in %s", + abbreviation); + yield abbreviation; + } + }; + } + private String getSource(HomebrewMetaTypes homebrewMeta) { - if (homebrewMeta != null) { - return homebrewMeta.jsonKey; - } return switch (abbreviation) { case "AF" -> "UAA"; case "AI", "RN" -> "TCE"; @@ -243,7 +242,13 @@ private String getSource(HomebrewMetaTypes homebrewMeta) { case "AS:V2-UA" -> "UARSC"; case "MV:C2-UA" -> "UARCO"; case "OR" -> "UACDW"; - default -> "PHB"; + case "TT" -> "HWCS"; + default -> { + if (homebrewMeta != null) { + yield homebrewMeta.primary; + } + yield "PHB"; + } }; } @@ -276,7 +281,7 @@ public Tools5eSources getSources() { } enum OftFields implements JsonNodeReader { - oftLookup, - oftIndexKey, + featureType, + optionalFeatureTypes, } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/PsionicType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/PsionicType.java index 23da4f418..93d93d4ef 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/PsionicType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/PsionicType.java @@ -40,19 +40,21 @@ public boolean isAltDisplay() { enum PsionicTypeEnum implements PsionicType { - Discipline("Discipline", "D"), - Talent("Talent", "T"); + Discipline("D"), + Talent("T"); - private String fullName; private String shortName; - PsionicTypeEnum(String fullName, String shortName) { - this.fullName = fullName; + PsionicTypeEnum(String shortName) { this.shortName = shortName; } public String getFullName() { - return fullName; + return name(); + } + + public String getShortName() { + return shortName; } public boolean isAltDisplay() { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellEntry.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellEntry.java new file mode 100644 index 000000000..58b948c5b --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellEntry.java @@ -0,0 +1,346 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static dev.ebullient.convert.StringUtil.isPresent; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.JsonTextConverter.SourceField; +import dev.ebullient.convert.tools.dnd5e.SpellIndex.SpellIndexFields; +import dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase; + +public class SpellEntry { + final String level; + final String spellKey; + final Map references = new TreeMap<>(); + final Map expandedList = new TreeMap<>(); + final Set classes = new HashSet<>(); + final Set classExpanded = new HashSet<>(); + final SpellSchool school; + final boolean ritual; + final List components; + final List spellAttack; + + @JsonIgnore + final JsonNode spellNode; + + /** + * Created as spells are read from index + * Needed for filtering spells for indexes + */ + public SpellEntry(String key, JsonNode spellNode) { + this.spellKey = key; + this.spellNode = spellNode; + this.level = SpellIndexFields.level.getTextOrEmpty(spellNode); + this.ritual = spellIsRitual(spellNode); + this.components = spellComponents(spellNode); + this.spellAttack = spellAttack(spellNode); + this.school = spellSchool(spellNode); + } + + public String getLevelText() { + return JsonSource.spellLevelToText(level); + } + + public String getLevel() { + return level; + } + + public String getName() { + return Tools5eIndexType.spell.decoratedName(spellNode); + } + + /** + * Added when reading legacy spell definitions from the spell node + * Considered an expansion if the variantNode is present + * (i.e. TCE expands the spell list for ranger... ) + */ + public SpellReference addSpellReference(String refererKey, boolean expanded) { + SpellReference ref = new SpellReference(refererKey, expanded); + if (expanded) { + expandedList.put(refererKey, ref); + } else { + references.put(refererKey, ref); + } + if (ref.refererType == Tools5eIndexType.classtype) { + // Create class index without source (Wizard) for filters -> spellEntry + String className = SourceField.name.getTextOrEmpty(ref.refererNode).toLowerCase(); + classes.add(className); + if (expanded) { + classExpanded.add(className); + } + } + return ref; + } + + public SpellReference addReference(String refererKey, String constraint, String asLevel, boolean expanded) { + SpellReference ref = new SpellReference(refererKey, constraint, asLevel, expanded); + return addReference(ref); + } + + /** + * Add a reference to this spell from the `additionalSpells` attribute. + * There is more information here: is there a class-level requirement, or + * a spell-slot level requirement; does this expand the class spell list; etc. + * Given these have more detail (and are newer), they may replace a more basic + * reference. + * + * @param ref + */ + public SpellReference addReference(SpellReference ref) { + return addOrReplace(ref, ref.expanded ? expandedList : references); + } + + private SpellReference addOrReplace(SpellReference spellRef, Map set) { + return set.compute(spellRef.refererKey, (k, existingRef) -> { + if (existingRef != null && existingRef.isSpecific()) { + return existingRef; // Keep the existing specific reference + } + return spellRef; // Add new or replace non-specific + }); + } + + private boolean spellIsRitual(JsonNode spellNode) { + boolean ritual = false; + JsonNode meta = SpellIndexFields.meta.getFrom(spellNode); + if (meta != null) { + ritual = SpellIndexFields.ritual.booleanOrDefault(meta, false); + } + return ritual; + } + + private List spellComponents(JsonNode spellNode) { + JsonSource converter = Tools5eIndex.getInstance(); + List list = new ArrayList<>(); + for (Entry f : SpellIndexFields.components.iterateFieldsFrom(spellNode)) { + switch (f.getKey().toLowerCase()) { + case "v" -> list.add("V"); + case "s" -> list.add("S"); + case "m" -> { + if (f.getValue().isObject()) { + list.add(SpellIndexFields.text.replaceTextFrom(f.getValue(), converter)); + } else { + list.add(converter.replaceText(f.getValue())); + } + } + case "r" -> list.add("R"); // Royalty. Acquisitions Incorporated + } + } + return list; + } + + private List spellAttack(JsonNode spellNode) { + List list = new ArrayList<>(); + for (Entry f : SpellIndexFields.spellAttack.iterateFieldsFrom(spellNode)) { + switch (f.getKey().toLowerCase()) { + case "m" -> list.add("M"); // melee + case "r" -> list.add("R"); // ranged + case "o" -> list.add("O"); // other/unknown + default -> { + } + } + } + return list; + } + + private SpellSchool spellSchool(JsonNode spellNode) { + String school = SpellIndexFields.school.getTextOrEmpty(spellNode); + return Tools5eIndex.getInstance().findSpellSchool(school, Tools5eSources.findSources(spellNode)); + } + + public SpellReference getReference(String key) { + SpellReference ref = references.get(key); + if (ref == null) { + return expandedList.get(key); + } + return ref; + } + + public boolean inClassList(String x) { + return classes.contains(x.toLowerCase()); + } + + public boolean isExpanded(String className) { + return classExpanded.contains(className.toLowerCase()); + } + + public String linkify() { + Tools5eSources sources = Tools5eSources.findSources(spellNode); + String name = Tools5eIndexType.spell.decoratedName(spellNode); + String resource = Tools5eQuteBase.fixFileName(name, sources); + return "[%s](%s \"%s\")".formatted(name, resource, sources.primarySource()); + } + + @Override + public String toString() { + return "spellEntry[" + spellKey + ";l=" + level + ";classes=" + classes + ";classExp=" + classExpanded + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((spellKey == null) ? 0 : spellKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SpellEntry other = (SpellEntry) obj; + if (spellKey == null) { + if (other.spellKey != null) { + return false; + } + } else if (!spellKey.equals(other.spellKey)) { + return false; + } + return true; + } + + public static class SpellReference { + final Tools5eIndexType refererType; + final JsonNode refererNode; + final String refererName; + + final String refererKey; + final String classLevel; + final String spellLevel; + final String asLevel; // special case for known spells castable as cantrips + final boolean expanded; + + public SpellReference(String key, boolean expanded) { + this(key, "", null, expanded); + } + + public SpellReference(String key, String constraint, String asLevel, boolean expanded) { + this.refererKey = key; + this.asLevel = asLevel; + this.expanded = expanded; + if (constraint.contains("_")) { + this.classLevel = null; + this.spellLevel = null; + } else if (constraint.matches("^\\d+")) { + this.classLevel = constraint; + this.spellLevel = null; + } else if (constraint.matches("^s\\d+")) { + this.classLevel = null; + this.spellLevel = constraint.substring(1); + } else { + if (isPresent(constraint)) { + Tui.instance().logf(Msg.UNKNOWN, "%s: Unknown constraint [%s]", key, constraint); + } + this.classLevel = null; + this.spellLevel = null; + } + this.refererType = Tools5eIndexType.getTypeFromKey(key); + this.refererNode = Tools5eIndex.getInstance().getOriginNoFallback(key); + this.refererName = refererType.decoratedName(refererNode); + } + + boolean isSpecific() { + return classLevel != null + || spellLevel != null + || asLevel != null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((refererKey == null) ? 0 : refererKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SpellReference other = (SpellReference) obj; + if (refererKey == null) { + if (other.refererKey != null) { + return false; + } + } else if (!refererKey.equals(other.refererKey)) { + return false; + } + return true; + } + + @Override + public String toString() { + return refererKey + ";c:" + classLevel + ";s:" + spellLevel + ";" + asLevel; + } + + public String tagifyReference() { + String type = refererType.name().replace("type", ""); + return Stream.of("spell", type, refererName) + .map(Tui::slugify) + .collect(Collectors.joining("/")); + } + + public String listFileName() { + return Tools5eQuteBase.getSpellList(refererName, Tools5eSources.findSources(refererNode)); + } + + public String linkifyReference() { + Tools5eSources sources = Tools5eSources.findSources(refererNode); + Tools5eIndex index = Tools5eIndex.getInstance(); + String name = refererType.decoratedName(refererNode); + String resource = Tools5eQuteBase.getSpellList(name, sources); + if (refererType == Tools5eIndexType.subclass) { + String classKey = Tools5eIndexType.classtype.fromChildKey(refererKey); + JsonNode classNode = index.getOriginNoFallback(classKey); + String className = Tools5eIndexType.classtype.decoratedName(classNode); + name = "%s (%s)".formatted(className, name); + } + return "[%s](%s)".formatted(name, resource); + } + + public String describe() { + List append = new ArrayList<>(); + if (isPresent(asLevel)) { + append.add("as " + JsonSource.spellLevelToText(asLevel)); + } else if (isPresent(spellLevel)) { + String display = JsonSource.spellLevelToText(spellLevel); + if ("cantrip".equals(display)) { + display = "cantrips"; + } else { + display += " spells"; + } + append.add("with access to " + display); + } + if (isPresent(classLevel) && !"1".equals(classLevel)) { + append.add("at class level " + classLevel); + } + return String.join(", ", append); + } + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellIndex.java new file mode 100644 index 000000000..44d168d17 --- /dev/null +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellIndex.java @@ -0,0 +1,544 @@ +package dev.ebullient.convert.tools.dnd5e; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.io.Tui; +import dev.ebullient.convert.tools.JsonNodeReader; +import dev.ebullient.convert.tools.dnd5e.SpellEntry.SpellReference; +import dev.ebullient.convert.tools.dnd5e.Tools5eIndexType.IndexFields; + +public class SpellIndex implements JsonSource { + private static final Set skipReferences = Set.of( + "subclass|college of lore|bard|phb|phb"); + + final Map spellsByKey = new TreeMap<>(); + + private final Tools5eIndex index; + + public SpellIndex(Tools5eIndex index) { + this.index = index; + } + + public void clear() { + spellsByKey.clear(); + } + + public SpellEntry getSpellEntry(String key) { + key = index.getAliasOrDefault(key); + return spellsByKey.get(key); + } + + /** + * Add a spell to the index + * (while iterating elements in prepare) + * + * @param key + * @param spellNode + */ + public SpellEntry addSpell(String key, JsonNode spellNode) { + key = index.getAliasOrDefault(key); + return spellsByKey.compute(key, + (k, v) -> v == null ? new SpellEntry(k, spellNode) : v); + } + + /** + * Read spells/sources.json. + * Called at the end of {@link Tools5eIndex#prepare()} + * read additional spell sources to generate reference indexes of included + * spells + */ + public void buildSpellIndex(Collection allNodes) { + // Remove excluded spells ahead of any other iteration + spellsByKey.entrySet().removeIf(e -> index.isExcluded(e.getKey())); + + // Read spells/sources.json; generate class index for filters + readSpellSources(); + // now process additionalSpells nodes + processAdditionalSpells(allNodes); + } + + /** + * Called from {@link #buildSpellIndex(Collection)} to read + * the spells/sources.json file and create class indexes for filtering + * by class. + */ + private void readSpellSources() { + // Iterate the contents of spells/sources.json + // This file is organized source -> spellName -> "class" or "classVariant"-> + // array of { name, source } + JsonNode spellClassMap = TtrpgConfig.readIndex("spell-source"); + // source -> spellName + for (var sourceToSpells : iterableFields(spellClassMap)) { + final String spellSource = sourceToSpells.getKey(); + // spellName -> "class" or "classVariant" + for (var nameToReferences : iterableFields(sourceToSpells.getValue())) { + final String spellName = nameToReferences.getKey(); + + String spellKey = Tools5eIndexType.spell.createKey(spellName, spellSource); + if (index.isExcluded(spellKey)) { + continue; + } + + final JsonNode spellNode = index().getOriginNoFallback(spellKey); + SpellEntry spellEntry = addSpell(spellKey, spellNode); + + final JsonNode referenceNode = nameToReferences.getValue(); + // "class" or "classVariant" -> { name, source } + for (var typeToReference : iterableFields(referenceNode)) { + if (!typeToReference.getKey().matches("^class.*")) { + // maybe something besides class or classVariant.. + // that would be unexpected + tui().logf(Msg.UNKNOWN, "Unknown reference type: %s", typeToReference.getKey()); + continue; + } + Tools5eIndexType refType = Tools5eIndexType.classtype; + // "class" or "classVariant" -> array of { name, source } + for (var reference : iterableElements(typeToReference.getValue())) { + readClassType(spellEntry, reference, refType); + } + } + } + } + } + + /** + * Called from {@link #buildSpellIndex(Collection)} to process + * `additionalSpells` + * nodes present in included content (can occur in a variety of types) + */ + private void processAdditionalSpells(Collection allNodes) { + // iterate over all included nodes + for (var node : allNodes) { + Tools5eSources sources = Tools5eSources.findSources(node); + final String nodeKey = sources.getKey(); + if (skipReferences.contains(nodeKey) || index.isExcluded(nodeKey)) { + // beyond excluded classes, there are some hard-coded notes to skip... + continue; + } + // get the type of the node (spell, race, class, feat, ...) + final Tools5eIndexType type = sources.getType(); + if (type == Tools5eIndexType.spell) { + // look for legacy specification of classes that can use a spell + // this method is used by homebrew + readClassesFromSpell(nodeKey, node); + } else { + // otherwise look for `additionalSpells` in the node + readAdditionalSpells(nodeKey, node); + } + } + } + + /** + * Called from {@link #processAdditionalSpells(Collection)} to read + * + * @param spellKey the spell key + * @param spellNode the spell node + */ + private void readClassesFromSpell(String spellKey, JsonNode spellNode) { + JsonNode classes = SpellIndexFields.classes.getFrom(spellNode); + if (classes == null || classes.isNull()) { + return; + } + // Find the created spellEntry (by key) + SpellEntry spellEntry = getSpellEntry(spellKey); + // Legacy / homebrew + for (var n : SpellIndexFields.fromClassList.iterateArrayFrom(classes)) { + tui().logf(Msg.SPELL, "readClasses/fromClassList: %s :: %s", spellKey, n); + readClassType(spellEntry, n, Tools5eIndexType.classtype); + } + for (var n : SpellIndexFields.fromClassListVariant.iterateArrayFrom(classes)) { + tui().logf(Msg.SPELL, "readClasses/fromClassList: %s :: %s", spellKey, n); + readClassType(spellEntry, n, Tools5eIndexType.classtype); + } + for (var n : SpellIndexFields.fromSubclass.iterateArrayFrom(classes)) { + tui().logf(Msg.SPELL, "readClasses/fromSubclass: %s :: %s", spellKey, n); + JsonNode classNode = SpellIndexFields.classNode.getFrom(n); + JsonNode subclassNode = SpellIndexFields.subclass.getFrom(n); + // Add class attributes to the subclass node so the key can + // be created as usual + IndexFields.className.setIn(subclassNode, + SourceField.name.getTextOrNull(classNode)); + IndexFields.classSource.setIn(subclassNode, + SourceField.source.getTextOrNull(classNode)); + readClassType(spellEntry, subclassNode, Tools5eIndexType.subclass); + } + } + + /** + * Called from {@link #readClassesFromSpell(String, JsonNode)} + * + * @param spellEntry the spell entry + * @param reference the class or subclass node + * @param refType Tools5eIndexType.classtype or Tools5eIndexType.subclass + */ + private void readClassType(SpellEntry spellEntry, JsonNode reference, Tools5eIndexType refType) { + final String refKey = refType.createKey(reference); + + // A book: TCE, for example, which made changes to Bard and Ranger.. + String variantSource = SpellIndexFields.definedInSource.getTextOrNull(reference); + + // skip (a) if reference is excluded, or + // (b) this is a variant and the variant source is excluded + if (index().isExcluded(refKey) + || (variantSource != null && !index().sourceIncluded(variantSource))) { + return; + } + + spellEntry.addSpellReference(refKey, variantSource != null); + } + + /** + * Called from {@link #processAdditionalSpells(Collection)} to read + * + * from: "util-additionalspells.json" + * schema: additionalSpellsArray + * + * @param refererKey the key of the referring node + * @param refererNode the referring node + */ + private void readAdditionalSpells(String refererKey, JsonNode refererNode) { + final JsonNode additionalNode = SpellIndexFields.additionalSpells.getFrom(refererNode); + if (index.isExcluded(refererKey) || additionalNode == null || additionalNode.isNull()) { + // skip excluded nodes and nodes without an additionalSpells attribute + return; + } + // Collect all spells referenced by this element + for (var additionalSpells : iterableElements(additionalNode)) { + gatherSpells(refererKey, SpellIndexFields.innate.getFrom(additionalSpells), false); + gatherSpells(refererKey, SpellIndexFields.known.getFrom(additionalSpells), false); + gatherSpells(refererKey, SpellIndexFields.prepared.getFrom(additionalSpells), false); + gatherSpells(refererKey, SpellIndexFields.expanded.getFrom(additionalSpells), true); + } + } + + /** + * Called from {@link #readAdditionalSpells(String, JsonNode)} + * from: "util-additionalspells.json" + * schema: _additionalSpellObject + * + * @param refererKey the key of the referring node + * @param spellList the _additionalSpellObject data from the referring node + * @param expanded true if this data expands/extends the class spell list + */ + private void gatherSpells(String refererKey, JsonNode spellList, boolean expanded) { + // This is a rough ride. We need to handle a variety of formats + // Ultimately, we're just looking to find a list of touched/referenced spells + for (var properties : iterableFields(spellList)) { + toSpellList(refererKey, properties.getValue(), properties.getKey(), expanded); + } + } + + /** + * Called from {@link #gatherSpells(String, JsonNode, boolean)} + * + * from: "util-additionalspells.json", schema noted below + * + * @param refererKey the key of the referring node + * @param spellList list of nodes (of various types) that reference spells + * @param constraint the constraint (the key: 1, s1, etc.) + * @param expanded true if this data expands/extends the class spell list + */ + private void toSpellList(String refererKey, JsonNode spellList, String constraint, boolean expanded) { + if (spellList == null || spellList.isNull()) { + return; + } + if (spellList.isObject()) { + // _additionalSpellLevelObject -> _additionalSpellRechargeObject + resolveRechargeSpells(refererKey, SpellIndexFields.rest.getFrom(spellList), constraint, expanded); + resolveRechargeSpells(refererKey, SpellIndexFields.daily.getFrom(spellList), constraint, expanded); + resolveRechargeSpells(refererKey, SpellIndexFields.resource.getFrom(spellList), constraint, expanded); + + // recurse: these keys hold arrays of spells: + // _additionalSpellArrayOfStringOrFilterObject + toSpellList(refererKey, SpellIndexFields.ritual.getFrom(spellList), constraint, expanded); + toSpellList(refererKey, SpellIndexFields.will.getFrom(spellList), constraint, expanded); + toSpellList(refererKey, SpellIndexFields.others.getFrom(spellList), constraint, expanded); + } else if (spellList.isArray()) { + // _additionalSpellArrayOfStringOrFilterObject + for (var reference : iterableElements(spellList)) { + if (reference.isTextual()) { + addFromText(refererKey, reference.asText(), constraint, expanded); + } else if (reference.isObject()) { + // a filter defining referenced spells (where all would be included) + resolveFilter(refererKey, SpellIndexFields.all.getFrom(reference), constraint, expanded); + + // a filter defining referenced spells (where some would be chosen) + JsonNode choose = SpellIndexFields.choose.getFrom(reference); + if (SpellIndexFields.from.existsIn(choose)) { + // choose from a list of spell reference tags.. + for (var x : SpellIndexFields.from.iterateArrayFrom(choose)) { + addFromText(refererKey, x.asText(), constraint, expanded); + } + } else { + // handle the filter describing the spells to include + resolveFilter(refererKey, choose, constraint, expanded); + } + } + } + } + } + + /** + * Called from {@link #toSpellList(String, JsonNode, String, boolean)} + * + * schema: _additionalSpellRechargeObject + * + * @param refererKey the key of the referring node + * @param rechargeNode the _additionalSpellRechargeObject data from the + * referring node + * @param constraint the constraint (the key: 1, s1, etc.) + * @param expanded true if this data expands/extends the class spell list + */ + public void resolveRechargeSpells(String refererKey, JsonNode rechargeNode, String constraint, + boolean expanded) { + if (rechargeNode == null || rechargeNode.isNull()) { + return; + } + // we're ignoring the key here: 1, 2, 3, 4, ...; 1e, 2e, 3e... + // the value is _additionalSpellArrayOfStringOrFilterObject + for (var x : iterableFields(rechargeNode)) { + toSpellList(refererKey, x.getValue(), constraint, expanded); + } + } + + /** + * A range of spells to be added, formatted similarly to the options in a + * {@literal {@filter ...}} tag. For example: {@code level=0|class=Wizard} + * + * @param refererKey the key of the referring node + * @param filter the filter node (should be text) + * @param constraint the constraint (the key: 1, s1, etc.) + * @param expanded true if this data expands/extends the class spell list + */ + public void resolveFilter(String refererKey, JsonNode filter, String constraint, boolean expanded) { + if (filter == null || filter.isNull()) { + return; + } + if (!filter.isTextual()) { + tui().logf(Msg.UNKNOWN, "resolveFilter unknown value %s from %s", filter, refererKey); + return; + } + tui().logf(Msg.SPELL, "resolveFilter (%2s) %s :: %s", constraint, refererKey, filter); + // level=1;2;3;4;5|class=Cleric;Druid;Wizard|school=D + String[] filterParts = filter.asText().split("\\|"); + FilterConditions filterConditions = new FilterConditions(); + + for (String f : filterParts) { + String[] parts = f.split("="); + if (parts.length == 2) { + switch (parts[0].toLowerCase()) { + case "class" -> { + filterConditions.setClasses(parts[1].split(";")); + } + case "level" -> { + filterConditions.setLevels(parts[1].split(";")); + } + case "school" -> { + filterConditions.setSchools(parts[1].split(";")); + } + case "source" -> { + filterConditions.setSources(parts[1].split(";")); + } + case "spell attack" -> { + filterConditions.setSpellAttack(parts[1].split(";")); + } + case "components & miscellaneous" -> { + filterConditions.setComponentsMisc(parts[1].split(";")); + } + default -> + tui().logf(Msg.UNKNOWN, "resolveFilter unknown part: %s", parts[0]); + } + } + } + for (SpellEntry spell : spellsByKey.values()) { + if (filterConditions.matchAll(spell)) { + spell.addReference(new SpellReference(refererKey, constraint, null, expanded)); + } + } + } + + /** + * Called from {@link #toSpellList(String, JsonNode, String, boolean)} + * + * @param refererKey the key of the referring node + * @param tag the spell reference tag + * @param constraint the constraint (the key: 1, s1, etc.) + * @param expanded true if this data expands/extends the class spell list + */ + private void addFromText(String refererKey, String tag, String constraint, boolean expanded) { + int pos = tag.indexOf("#"); + String asLevel = pos > 0 ? tag.substring(pos + 1) : null; + tag = pos > 0 ? tag.substring(0, pos) : tag; + + String spellKey = Tools5eIndexType.spell.fromTagReference(tag); + if (index.isExcluded(spellKey)) { + return; + } + var spellEntry = getSpellEntry(spellKey); + if (spellEntry != null) { + spellEntry.addReference(refererKey, constraint, asLevel, expanded); + } else { + tui().logf(Msg.UNRESOLVED, "Missing spell reference: %s", spellKey); + } + } + + static class FilterConditions { + Set classes = Set.of(); + Set levels = Set.of(); + Set schools = Set.of(); + Set sources = Set.of(); + Set spellAttack = Set.of(); + Set componentsMisc = Set.of(); + + /** + * @param class the class to set + */ + public void setClasses(String[] classList) { + this.classes = Arrays.stream(classList) + .map(x -> x.toLowerCase()) + .collect(Collectors.toSet()); + } + + /** + * @param level the level to set + */ + public void setLevels(String[] level) { + this.levels = Set.of(level); + } + + /** + * @param school the school to set + */ + public void setSchools(String[] school) { + this.schools = Set.of(school); + } + + /** + * @param source the source to set + */ + public void setSources(String[] source) { + this.sources = Set.of(source); + } + + /** + * @param source the source to set + */ + public void setSpellAttack(String[] source) { + this.spellAttack = Arrays.stream(source) + .map(x -> x.toUpperCase()) + .collect(Collectors.toSet()); + } + + public void setComponentsMisc(String[] components) { + this.componentsMisc = Arrays.stream(components) + .map(x -> x.toLowerCase()) + .collect(Collectors.toSet()); + } + + boolean matchAll(SpellEntry spell) { + Tools5eSources spellSources = Tools5eSources.findSources(spell.spellNode); + return testClasses(spell) + && testLevels(spell) + && testSchools(spell) + && testSources(spell, spellSources) + && testSpellAttack(spell) + && testSpellComponents(spell); + } + + private boolean testClasses(SpellEntry spell) { + return classes.isEmpty() || classes.stream().anyMatch(x -> spell.inClassList(x)); + } + + private boolean testLevels(SpellEntry spell) { + return levels.isEmpty() || levels.contains(spell.level); + } + + private boolean testSchools(SpellEntry spell) { + return schools.isEmpty() || schools.contains(spell.school.code()); + } + + private boolean testSources(SpellEntry spell, Tools5eSources spellSources) { + return sources.isEmpty() || spellSources.includedBy(sources); + } + + private boolean testSpellAttack(SpellEntry spell) { + return spellAttack.isEmpty() + || spell.spellAttack.stream().anyMatch(x -> spellAttack.contains(x.toUpperCase())); + } + + private boolean testSpellComponents(SpellEntry spell) { + if (componentsMisc.contains("ritual")) { + return spell.ritual; + } else if (!componentsMisc.isEmpty()) { + Tui.instance().logf(Msg.UNKNOWN, "Unknown components & miscellaneous value: %s", componentsMisc); + } + return true; + } + } + + enum SpellIndexFields implements JsonNodeReader { + ability, + additionalSpells, + all, + choose, + classes, + classNode("class"), + components, + daily, + definedInSource, + expanded, + from, + fromClassList, + fromClassListVariant, + fromSubclass, + innate, + known, + level, + meta, + others("_"), + prepared, + resource, + rest, + ritual, + school, + spellAttack, + subclass, + text, + will, + ; + + private final String nodeName; + + SpellIndexFields(String nodeName) { + this.nodeName = nodeName; + } + + SpellIndexFields() { + this.nodeName = name(); + } + + @Override + public String nodeName() { + return nodeName; + } + } + + @Override + public Tools5eIndex index() { + return index; + } + + @Override + public Tools5eSources getSources() { + return null; + } +} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellSchool.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellSchool.java index b69125a80..ee70f4120 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellSchool.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/SpellSchool.java @@ -4,19 +4,41 @@ public interface SpellSchool { String name(); - record CustomSpellSchool(String name) implements SpellSchool { + String code(); + + record CustomSpellSchool(String code, String name) implements SpellSchool { + @Override + public String code() { + return code; + } + + @Override + public String name() { + return name; + } } enum SchoolEnum implements SpellSchool { - Abjuration, - Conjuration, - Divination, - Enchantment, - Evocation, - Illusion, - Necromancy, - Transmutation, - None + Abjuration("A"), + Conjuration("C"), + Divination("D"), + Enchantment("E"), + Evocation("V"), + Illusion("I"), + Necromancy("N"), + Transmutation("T"), + Psychic("P"), + None("_"); + + private final String code; + + SchoolEnum(String abbreviation) { + this.code = abbreviation; + } + + public String code() { + return code; + } } static SpellSchool fromEncodedValue(String v) { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java deleted file mode 100644 index 6fac93fbd..000000000 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eHomebrewIndex.java +++ /dev/null @@ -1,282 +0,0 @@ -package dev.ebullient.convert.tools.dnd5e; - -import static dev.ebullient.convert.StringUtil.isPresent; - -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Consumer; - -import com.fasterxml.jackson.databind.JsonNode; - -import dev.ebullient.convert.config.CompendiumConfig; -import dev.ebullient.convert.config.TtrpgConfig; -import dev.ebullient.convert.io.Msg; -import dev.ebullient.convert.io.Tui; -import dev.ebullient.convert.tools.JsonNodeReader; -import dev.ebullient.convert.tools.dnd5e.PsionicType.CustomPsionicType; -import dev.ebullient.convert.tools.dnd5e.SkillOrAbility.CustomSkillOrAbility; -import dev.ebullient.convert.tools.dnd5e.SpellSchool.CustomSpellSchool; - -public class Tools5eHomebrewIndex implements JsonSource { - - private final Map homebrewMetaTypes = new HashMap<>(); - private final Tools5eIndex index; - - Tools5eHomebrewIndex(Tools5eIndex index) { - this.index = index; - } - - public void importBrew(Consumer processHomebrewTree) { - for (HomebrewMetaTypes homebrew : homebrewMetaTypes.values()) { - processHomebrewTree.accept(homebrew); - } - } - - public HomebrewMetaTypes getHomebrewMetaTypes(Tools5eSources sources) { - return homebrewMetaTypes.get(sources.primarySource()); - } - - public HomebrewMetaTypes getHomebrewMetaTypes(String source) { - return homebrewMetaTypes.get(source); - } - - public SkillOrAbility findHomebrewSkillOrAbility(String key, Tools5eSources sources) { - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); - if (meta != null) { - return meta.getSkillType(key); - } - return null; - } - - public SpellSchool findHomebrewSpellSchool(String abbreviation, Tools5eSources sources) { - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); - if (meta != null) { - return meta.getSpellSchool(abbreviation); - } - return null; - } - - public ItemType findHomebrewType(String fragment, Tools5eSources sources) { - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); - if (meta != null) { - JsonNode homebrewNode = meta.getItemProperty(fragment); - if (homebrewNode != null) { - return ItemType.fromNode(homebrewNode); - } - } - return null; - } - - public ItemMastery findHomebrewMastery(String fragment, Tools5eSources sources) { - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); - if (meta != null) { - JsonNode homebrewNode = meta.getItemMastery(fragment); - if (homebrewNode != null) { - return ItemMastery.fromNode(homebrewNode); - } - } - return null; - } - - public ItemProperty findHomebrewProperty(String fragment, Tools5eSources sources) { - HomebrewMetaTypes meta = homebrewMetaTypes.get(sources.primarySource()); - if (meta != null) { - JsonNode homebrewNode = meta.getItemProperty(fragment); - if (homebrewNode != null) { - return ItemProperty.fromNode(homebrewNode); - } - } - return null; - } - - public void clear() { - homebrewMetaTypes.clear(); - } - - public boolean addHomebrewSourcesIfPresent(String filename, JsonNode node) { - JsonNode sources = SourceField._meta.getFieldFrom(node, HomebrewFields.sources); - if (sources == null || sources.size() == 0) { - return false; - } - // TODO include homebrew date - String json = HomebrewFields.json.getTextOrNull(sources.get(0)); - if (json == null) { - tui().errorf("Source does not define json id: %s", sources.get(0)); - return false; - } - TtrpgConfig.includeAdditionalSource(json); - - HomebrewMetaTypes metaTypes = new HomebrewMetaTypes(json, filename, node); - for (JsonNode source : iterableElements(sources)) { - String fullName = HomebrewFields.full.getTextOrEmpty(source); - String abbreviation = HomebrewFields.abbreviation.getTextOrEmpty(source); - json = HomebrewFields.json.getTextOrEmpty(source); - if (fullName == null) { - tui().warnf(Msg.BREW, "Homebrew source %s missing full name: %s", json, fullName); - } - // add homebrew to known sources - if (TtrpgConfig.addHomebrewSource(fullName, json, abbreviation)) { - // one homebrew file may include multiple sources, the same mapping applies to - // all - HomebrewMetaTypes old = homebrewMetaTypes.put(json, metaTypes); - if (old != null) { - tui().errorf(Msg.BREW, "Shared homebrew id: %s and %s", old.filename, metaTypes.filename); - } - } else { - tui().errorf(Msg.BREW, "Skipping homebrew id %s from %s; duplicate source id", json, metaTypes.filename); - } - } - - JsonNode featureTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.optionalFeatureTypes); - JsonNode spellSchools = SourceField._meta.getFieldFrom(node, HomebrewFields.spellSchools); - JsonNode psionicTypes = SourceField._meta.getFieldFrom(node, HomebrewFields.psionicTypes); - JsonNode skillTypes = HomebrewFields.skill.getFrom(node); - if (featureTypes != null || spellSchools != null || psionicTypes != null || skillTypes != null) { - for (Entry entry : iterableFields(featureTypes)) { - metaTypes.setOptionalFeatureType(entry.getKey(), entry.getValue().asText()); - } - // ignoring short names for spell schools and psionic types - for (Entry entry : iterableFields(spellSchools)) { - metaTypes.setSpellSchool(entry.getKey(), - new CustomSpellSchool(HomebrewFields.full.getTextOrEmpty(entry.getValue()))); - } - for (Entry entry : iterableFields(psionicTypes)) { - metaTypes.setPsionicType(entry.getKey(), - tui().readJsonValue(entry.getValue(), CustomPsionicType.class)); - } - for (JsonNode skill : iterableElements(skillTypes)) { - String skillName = SourceField.name.getTextOrEmpty(skill); - if (skillName == null) { - tui().warnf(Msg.BREW, "Homebrew skill type missing name: %s", skill); - continue; - } - metaTypes.setSkillType(skillName, skill); - } - } - Tools5eSources.addFonts(SourceField._meta.getFrom(node), HomebrewFields.fonts); - return true; - } - - static class HomebrewMetaTypes { - final String jsonKey; - final String filename; - final JsonNode homebrewNode; - // name, long name - final Map optionalFeatureTypes = new HashMap<>(); - final Map psionicTypes = new HashMap<>(); - final Map skillOrAbility = new HashMap<>(); - final Map spellSchoolTypes = new HashMap<>(); - final Map itemTypes = new HashMap<>(); - final Map itemProperties = new HashMap<>(); - final Map itemMastery = new HashMap<>(); - - HomebrewMetaTypes(String jsonKey, String filename, JsonNode homebrewNode) { - this.jsonKey = jsonKey; - this.filename = filename; - this.homebrewNode = homebrewNode; - } - - public String getOptionalFeatureType(String key) { - return optionalFeatureTypes.get(key.toLowerCase()); - } - - public void setOptionalFeatureType(String key, String value) { - optionalFeatureTypes.put(key.toLowerCase(), value); - } - - public PsionicType getPsionicType(String key) { - return psionicTypes.get(key.toLowerCase()); - } - - public void setPsionicType(String key, PsionicType value) { - psionicTypes.put(key.toLowerCase(), value); - } - - public SkillOrAbility getSkillType(String key) { - return skillOrAbility.get(key.toLowerCase()); - } - - public void setSkillType(String key, JsonNode skill) { - skillOrAbility.put(key.toLowerCase(), new CustomSkillOrAbility(skill)); - } - - public SpellSchool getSpellSchool(String key) { - return spellSchoolTypes.get(key.toLowerCase()); - } - - public void setSpellSchool(String key, CustomSpellSchool value) { - spellSchoolTypes.put(key.toLowerCase(), value); - } - - public JsonNode getItemType(String abbreviation) { - return itemTypes.get(abbreviation); - } - - public JsonNode getItemProperty(String abbreviation) { - return itemProperties.get(abbreviation); - } - - public JsonNode getItemMastery(String name) { - return itemMastery.get(name); - } - - public void addElement(Tools5eIndexType type, String key, JsonNode value) { - String name = SourceField.name.getTextOrEmpty(value); - String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(value); - switch (type) { - case itemMastery -> { - if (isPresent(name)) { - itemMastery.put(name, value); - } else { - Tui.instance().errorf(Msg.BREW, "Missing name in %s", key); - } - } - case itemProperty -> { - if (isPresent(abbreviation)) { - itemProperties.put(abbreviation, value); - } else { - Tui.instance().errorf(Msg.BREW, "Missing abbreviation in %s", key); - } - } - case itemType -> { - if (isPresent(abbreviation)) { - itemTypes.put(abbreviation, value); - } else { - Tui.instance().errorf(Msg.BREW, "Missing abbreviation in %s", key); - } - } - default -> { - } // no-op - } - } - } - - enum HomebrewFields implements JsonNodeReader { - abbreviation, - fonts, - full, - json, - optionalFeatureTypes, - psionicTypes, - skill, - sources, - spellSchools, - spellDistanceUnits - } - - @Override - public CompendiumConfig cfg() { - return index.config; - } - - @Override - public Tools5eIndex index() { - return index; - } - - @Override - public Tools5eSources getSources() { - return null; - } -} diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 1afc6a3a2..65762e3a6 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -30,14 +30,14 @@ import dev.ebullient.convert.qute.SourceAndPage; import dev.ebullient.convert.tools.MarkdownConverter; import dev.ebullient.convert.tools.ToolsIndex; +import dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewFields; +import dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes; import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField; import dev.ebullient.convert.tools.dnd5e.Json2QuteRace.RaceFields; import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; import dev.ebullient.convert.tools.dnd5e.SkillOrAbility.CustomSkillOrAbility; import dev.ebullient.convert.tools.dnd5e.SpellSchool.CustomSpellSchool; -import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewFields; -import dev.ebullient.convert.tools.dnd5e.Tools5eHomebrewIndex.HomebrewMetaTypes; public class Tools5eIndex implements JsonSource, ToolsIndex { private static Tools5eIndex instance; @@ -68,13 +68,12 @@ public static Tools5eIndex getInstance() { private final Map subraceMap = new HashMap<>(); private final Map nameToLink = new HashMap<>(); - private final Map> spellClassIndex = new HashMap<>(); - private final Set srdKeys = new HashSet<>(); final Tools5eJsonSourceCopier copier = new Tools5eJsonSourceCopier(this); final OptionalFeatureIndex optFeatureIndex = new OptionalFeatureIndex(this); - final Tools5eHomebrewIndex homebrewIndex = new Tools5eHomebrewIndex(this); + final HomebrewIndex homebrewIndex = new HomebrewIndex(this); + final SpellIndex spellIndex = new SpellIndex(this); // index state volatile HomebrewMetaTypes homebrew = null; @@ -217,6 +216,7 @@ void addToSubraceIndex(Tools5eIndexType type, JsonNode node) { void addMagicVariantToIndex(Tools5eIndexType type, JsonNode node) { MagicVariant.populateGenericVariant(node); + addToIndex(type, node); } @@ -238,12 +238,13 @@ void addToIndex(Tools5eIndexType type, JsonNode node) { // if homebrew is set, then we're reading a homebrew file TtrpgValue.isHomebrew.setIn(node, homebrew != null); if (homebrew != null) { - homebrew.addElement(type, key, node); + homebrew.addCrossReference(type, key, node); } switch (type) { case optfeature -> { // add while we're ingesting (homebrew or not) + // will create/register an optionalFeatureType node optFeatureIndex.addOptionalFeature(key, node, homebrew); } case subclass -> { @@ -346,8 +347,8 @@ public void prepare() { // Post-creation of sources.. switch (type) { - case classtype, subclass -> optFeatureIndex.amendSources(key, jsonSource, homebrewIndex); - case optfeature -> optFeatureIndex.amendSources(key, jsonSource, homebrewIndex); + case classtype, subclass -> optFeatureIndex.amendSources(key, jsonSource); + case optfeature -> optFeatureIndex.amendSources(key, jsonSource); case classfeature -> { String classKey = Tools5eIndexType.classtype.fromChildKey(key); JsonNode classNode = nodeIndex.get(classKey); @@ -378,7 +379,8 @@ public void prepare() { } // end for each entry for (JsonNode variant : variants) { - nodeIndex.put(TtrpgValue.indexKey.getTextOrThrow(variant), variant); + String variantKey = TtrpgValue.indexKey.getTextOrThrow(variant); + nodeIndex.put(variantKey, variant); } variants.clear(); @@ -424,10 +426,23 @@ public void prepare() { tui().progressf("Removing dependent and dangling resources"); filteredIndex.keySet().removeIf(k -> otherwiseExcluded(k)); - // Populate classes and subclasses with related (included) - // features + // Use the OptionalFeature index to remove unused optional features + optFeatureIndex.removeUnusedOptionalFeatures( + (k) -> filteredIndex.containsKey(k), + (k) -> { + Tools5eSources sources = Tools5eSources.findSources(k); + if (sources.filterRuleApplied()) { + return; // keep because a rule says so (we already logged these) + } + logThis.accept(Msg.FEATURETYPE, "(drop) " + k); + filteredIndex.remove(k); + }); + + // Bubble-up: enabled subclasses, class features, and subclass features + // add themselves to their parents for (var entry : filteredIndex.entrySet()) { String entryKey = entry.getKey(); + var type = Tools5eIndexType.getTypeFromKey(entryKey); if (type == Tools5eIndexType.subclass || type == Tools5eIndexType.classfeature @@ -448,9 +463,41 @@ public void prepare() { JsonNode parent = getOriginNoFallback(parentKey); ArrayNode target = targetField.ensureArrayIn(parent); target.add(entryKey); + targetField.setIn(parent, target); + } else if (type == Tools5eIndexType.spell) { + // Create a spell entry for included spell + spellIndex.addSpell(entryKey, entry.getValue()); } } + // One last pass through to remove more orphans + filteredIndex.entrySet().removeIf(e -> { + String key = e.getKey(); + Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); + // These are unreachable; they have no features or subclasses + // Have no explicit aliases, and no way to detect what could or + // should be aliased to them. Often |xphb|phb or |phb|xphb variants + if (type == Tools5eIndexType.classtype) { + JsonNode subclasses = ClassFields.subclassKeys.getFrom(e.getValue()); + JsonNode features = ClassFields.featureKeys.getFrom(e.getValue()); + if (isEmpty(subclasses) && isEmpty(features)) { + // UNLIKELY + tui().logf(Msg.CLASSES, "(drop | no features or subclasses) %s", key); + return true; + } + } else if (type == Tools5eIndexType.subclass) { + JsonNode features = ClassFields.featureKeys.getFrom(e.getValue()); + if (isEmpty(features)) { + // These are abandoned |xphb|phb or |phb|xphb mixed classes that + // are naturally skipped when resolving aliases above. + // Remove them so they don't also mess with spells + tui().logf(Msg.CLASSES, "(drop | no subclass features) %s", key); + return true; + } + } + return false; + }); + // Deities have their own glorious reprint mess, which we only need to deal with // when we aren't hoarding all the things. if (config.reprintBehavior() != ReprintBehavior.all) { @@ -467,6 +514,10 @@ public void prepare() { filteredIndex.remove(k); }); } + + // And finally, create an index of classes/subclasses/feats for spells + // based on included sources & avaiable spells. + spellIndex.buildSpellIndex(filteredIndex.values()); } private void defineSubraces() { @@ -644,7 +695,6 @@ private boolean otherwiseExcluded(String key) { Tools5eIndexType.subclass, Msg.CLASSES) || removeIfParentExcluded(key, type, Tools5eIndexType.classtype, Msg.CLASSES); - case optfeature, optionalFeatureTypes -> removeUnusedOptionalFeatures(type, key); case subclass -> !sources.includedByConfig() || removeIfParentExcluded(key, type, Tools5eIndexType.classtype, Msg.CLASSES); case subrace -> !sources.includedByConfig() @@ -667,35 +717,11 @@ private boolean removeIfParentExcluded(String key, Tools5eIndexType type, Tools5 } boolean filterIncluded = filteredIndex.containsKey(parentKey); if (!filterIncluded) { - tui().debugf(msg, "(drop) %43s :: %s", parentKey, key); + tui().logf(msg, "(drop) %43s :: %s", parentKey, key); } return !filterIncluded; } - private boolean removeUnusedOptionalFeatures(Tools5eIndexType type, String key) { - OptionalFeatureType oft = optFeatureIndex.get(type, key); - Tools5eSources oftSources = oft.getSources(); - - // the feature type sources are amended by consuming classes/subclasses - boolean included = oft.inUse() && oftSources.includedByConfig(); - var msgType = Msg.FEATURETYPE; - - if (included && type == Tools5eIndexType.optfeature) { - msgType = Msg.FEATURE; - // If an optional feature (rather than a type), - // and the optional feature source is different from the parent source, - // then we need to see if the feature source is included - Tools5eSources ofSources = Tools5eSources.findSources(key); - if (!ofSources.primarySource().equals(oftSources.primarySource())) { - included = ofSources.includedByConfig(); - } - } - if (!included) { - tui().debugf(msgType, "(drop) %43s :: %s", oft.getKey(), key); - } - return !included; - } - public boolean notPrepared() { return filteredIndex == null; } @@ -790,59 +816,67 @@ public JsonNode getNode(String finalKey) { return filteredIndex.get(finalKey); } - public JsonNode getHomebrewNode(Tools5eIndexType type, String finalKey, String currentSource) { - HomebrewMetaTypes meta = homebrewIndex.getHomebrewMetaTypes(currentSource); - if (meta == null) { - return null; - } - String adaptKey = finalKey.replace(type.defaultSourceString().toLowerCase(), currentSource.toLowerCase()); - return filteredIndex.get(adaptKey); + public Collection getHomebrewMetaTypes(Tools5eSources activeSources) { + return homebrewIndex.getHomebrewMetaTypes(activeSources); } - public ItemProperty findItemProperty(String key, Tools5eSources activeSources) { + public ItemProperty findItemProperty(String key, Tools5eSources sources) { if (key == null || key.isEmpty()) { return null; } - JsonNode propertyNode = findTypePropertyNode(key); // check alias & phb/xphb + // now a mix of with and without sources + if (!Tools5eIndexType.itemProperty.isKey(key)) { + key = Tools5eIndexType.itemProperty.fromTagReference(key); + } + JsonNode propertyNode = findTypePropertyNode(Tools5eIndexType.itemProperty, key, sources); // check alias & phb/xphb if (propertyNode != null) { return ItemProperty.fromNode(propertyNode); } - // try homebrew property - return homebrewIndex.findHomebrewProperty(key, activeSources); + // try homebrew (normalize from key) + String[] parts = key.split("\\|"); + return homebrewIndex.findHomebrewProperty(parts[1], sources); } - public ItemType findItemType(String key, Tools5eSources activeSources) { + public ItemType findItemType(String key, Tools5eSources sources) { if (key == null || key.isEmpty()) { return null; } - JsonNode typeNode = findTypePropertyNode(key); // check alias & phb/xphb + // now a mix of with and without sources + if (!Tools5eIndexType.itemType.isKey(key)) { + key = Tools5eIndexType.itemType.fromTagReference(key); + } + JsonNode typeNode = findTypePropertyNode(Tools5eIndexType.itemType, key, sources); // check alias & phb/xphb if (typeNode != null) { return ItemType.fromNode(typeNode); } - // try homebrew property - return homebrewIndex.findHomebrewType(key, activeSources); + // try homebrew (normalize from key) + String[] parts = key.split("\\|"); + return homebrewIndex.findHomebrewType(parts[1], sources); } - public ItemMastery findItemMastery(String key, Tools5eSources activeSources) { - if (key == null || key.isEmpty()) { + public ItemMastery findItemMastery(String tagReference, Tools5eSources sources) { + if (tagReference == null || tagReference.isEmpty()) { return null; } - JsonNode masteryNode = getNode(getAliasOrDefault(key)); + // This is always a tag: name|source + String key = Tools5eIndexType.itemMastery.fromTagReference(tagReference); + JsonNode masteryNode = getOriginNoFallback(getAliasOrDefault(key)); if (masteryNode != null) { return ItemMastery.fromNode(masteryNode); } - // try homebrew property - return homebrewIndex.findHomebrewMastery(key, activeSources); + // try homebrew (normalize from key) + String[] parts = key.split("\\|"); + return homebrewIndex.findHomebrewMastery(parts[1], sources); } - private JsonNode findTypePropertyNode(String key) { + private JsonNode findTypePropertyNode(Tools5eIndexType type, String key, Tools5eSources sources) { String aliasKey = getAliasOrDefault(key); - JsonNode node = getNode(aliasKey); + JsonNode node = getOriginNoFallback(aliasKey); if (node == null && aliasKey.endsWith("phb")) { aliasKey = aliasKey.contains("|xphb") ? aliasKey.replace("|xphb", "|phb") : aliasKey.replace("|phb", "|xphb"); - node = getNode(aliasKey); + node = getOriginNoFallback(aliasKey); if (node != null) { addAlias(key, aliasKey); } @@ -850,10 +884,6 @@ private JsonNode findTypePropertyNode(String key) { return node; } - public HomebrewMetaTypes getHomebrewMetaTypes(Tools5eSources sources) { - return homebrewIndex.getHomebrewMetaTypes(sources); - } - public SkillOrAbility findSkillOrAbility(String key, Tools5eSources sources) { if (key == null || key.isEmpty()) { return null; @@ -869,17 +899,17 @@ public SkillOrAbility findSkillOrAbility(String key, Tools5eSources sources) { return skill; } - public SpellSchool findSpellSchool(String abbreviation, Tools5eSources sources) { - if (abbreviation == null || abbreviation.isEmpty()) { - return null; + public SpellSchool findSpellSchool(String code, Tools5eSources sources) { + if (code == null || code.isEmpty()) { + return SpellSchool.SchoolEnum.None; } - SpellSchool school = SpellSchool.fromEncodedValue(abbreviation); + SpellSchool school = SpellSchool.fromEncodedValue(code); if (school == null) { - school = homebrewIndex.findHomebrewSpellSchool(abbreviation, sources); + school = homebrewIndex.findHomebrewSpellSchool(code, sources); } if (school == null) { - tui().warnf(Msg.UNKNOWN, "Unknown spell school %s in %s", abbreviation, sources); - return new CustomSpellSchool(abbreviation); + tui().warnf(Msg.UNKNOWN, "Unknown spell school %s in %s", code, sources); + return new CustomSpellSchool(code, code); } return school; } @@ -940,7 +970,7 @@ public JsonNode getOrigin(String finalKey) { } } if (result == null) { - tui().log(new Exception("No element found for " + finalKey), false); + tui().logf(Msg.UNRESOLVED, "No element found for %s", finalKey); } } return result; @@ -1046,19 +1076,15 @@ public Set> includedEntries() { return filteredIndex.entrySet(); } - public Collection classesForSpell(String spellKey) { - return spellClassIndex.get(spellKey); - } - public OptionalFeatureType getOptionalFeatureType(JsonNode optfeatureNode) { return optFeatureIndex.get(optfeatureNode); } - public OptionalFeatureType getOptionalFeatureType(String ft, String source) { - if (ft == null) { + public OptionalFeatureType getOptionalFeatureType(String featureType) { + if (featureType == null) { return null; } - return optFeatureIndex.get(ft, source, homebrewIndex); + return optFeatureIndex.get(featureType); } @Override @@ -1133,6 +1159,10 @@ public Tools5eSources getSources() { return null; } + public SpellIndex getSpellIndex() { + return spellIndex; + } + public void cleanup() { if (instance == this) { instance = null; @@ -1150,7 +1180,7 @@ public void cleanup() { subraceMap.clear(); nameToLink.clear(); - spellClassIndex.clear(); + spellIndex.clear(); srdKeys.clear(); optFeatureIndex.clear(); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java index 275c8e308..4197015e0 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java @@ -90,6 +90,7 @@ public enum Tools5eIndexType implements IndexType, JsonNodeReader { note, // qute data type reference, // made up syntheticGroup, // qute data type + spellIndex, // made up ; final String templateName; @@ -273,7 +274,7 @@ public String fromTagReference(String crossRef) { this.name(), parts[0].trim(), parts[1].trim(), - parts.length > 2 ? parts[2] : defaultSourceString()) + valueOrDefault(parts, 2, defaultSourceString())) .toLowerCase(); } case classfeature -> { @@ -286,17 +287,48 @@ public String fromTagReference(String crossRef) { Tui.instance().errorf("Badly formed Class Feature key (not enough segments): %s", crossRef); yield null; } - String classSource = valueOrDefault(parts[2], "phb"); - String featureSource = parts.length > 4 ? parts[4] : classSource; - yield getClassFeatureKey( - parts[0], featureSource, - parts[1], classSource, - parts[3]); + String classSource = valueOrDefault(parts, 2, Tools5eIndexType.classtype.defaultSourceString()); + String featureSource = valueOrDefault(parts, 4, classSource); + yield "%s|%s|%s|%s|%s|%s".formatted(this.name(), + parts[0], + parts[1], + classSource, + parts[3], + featureSource) + .toLowerCase(); } - case itemMastery, itemProperty, itemType -> { - // utils.js: itemType.unpackUid, itemProperty.unpackUid - String source = parts.length > 1 ? parts[1] : defaultSourceString(); - yield "%s|%s|%s".formatted(this.name(), parts[0], source).toLowerCase(); + case itemProperty -> { + String source = valueOrDefault(parts, 1, ItemProperty.defaultItemSource(parts[0])); + yield "%s|%s|%s".formatted( + this.name(), + parts[0], + source).toLowerCase(); + } + case itemType -> { + String source = valueOrDefault(parts, 1, ItemType.defaultItemSource(parts[0])); + yield "%s|%s|%s".formatted( + this.name(), + parts[0], + source).toLowerCase(); + } + case subclass -> { + // Homebrew and reprint tags + // {@subclass Artillerist|Artificer|TCE|TCE} + // 0 subclassShortName, + // 1 IndexFields.className.getTextOrEmpty(x), + // 2 classSource || "phb", + // 3 subClassSource || "phb" + if (parts.length < 2) { + Tui.instance().errorf("Badly formed Subclass key (not enough segments): %s", crossRef); + yield null; + } + String scName = parts[0]; + String className = parts[1]; + String classSource = valueOrDefault(parts, 2, "phb"); + String subClassSource = valueOrDefault(parts, 3, "phb"); + yield getSubclassKey( + className, classSource, + scName, subClassSource); } case subclassFeature -> { // 0 name, @@ -311,15 +343,25 @@ yield getClassFeatureKey( yield null; } String classSource = valueOrDefault(parts[2], "phb"); - String subClassSource = valueOrDefault(parts[4], "phb"); - String featureSource = parts.length > 6 ? parts[6] : subClassSource; - yield getSubclassFeatureKey( - parts[0], featureSource, - parts[1], classSource, - parts[3], subClassSource, - parts[5]); + String scSource = valueOrDefault(parts[4], "phb"); + String featureSource = parts.length > 6 ? parts[6] : scSource; + yield "%s|%s|%s|%s|%s|%s|%s|%s".formatted( + Tools5eIndexType.subclassFeature, + parts[0], + parts[1], + classSource, + parts[3], + scSource, + parts[5], + featureSource) + .toLowerCase(); + } + default -> { + // 0 name, + // 1 source + yield createKey(parts[0], + parts.length > 1 ? parts[1] : defaultSourceString()); } - default -> "%s|%s".formatted(this.name(), crossRef).toLowerCase(); }; } @@ -334,11 +376,13 @@ public String toTagReference(JsonNode entry) { name, IndexFields.deck.getTextOrEmpty(entry), source); - // {@class Fighter|phb|Samurai|Samurai|xge} - case subclass -> Tools5eIndexType.getSubclassTextReference( + // {@subclass Artillerist|Artificer|TCE|TCE} + case subclass -> "%s|%s|%s|%s|%s".formatted( + name, IndexFields.className.getTextOrEmpty(entry), IndexFields.classSource.getTextOrEmpty(entry), - name, source, linkText); + source, + linkText); // {@subclassFeature Blessed Strikes|Cleric|PHB|Twilight|TCE|8|TCE} case subclassFeature -> "%s|%s|%s|%s|%s|%s|%s|%s".formatted( name, @@ -391,10 +435,8 @@ public String decoratedName(String name, JsonNode entry) { } public static String getSubclassKey(String className, String classSource, String subclassName, String subclassSource) { - if (classSource == null || classSource.isEmpty()) { - // phb remains in the subclass text reference (match allowed sources) - classSource = "phb"; - } + classSource = valueOrDefault(classSource, Tools5eIndexType.classtype.defaultSourceString()); + subclassSource = valueOrDefault(subclassSource, Tools5eIndexType.subclass.defaultSourceString()); return "%s|%s|%s|%s|%s".formatted( Tools5eIndexType.subclass, subclassName, @@ -404,47 +446,6 @@ public static String getSubclassKey(String className, String classSource, String .toLowerCase(); } - public static String getSubclassTextReference(String className, String classSource, String subclassName, - String subclassSource, String text) { - if (classSource == null || classSource.isEmpty()) { - // phb remains in the subclass text reference (match allowed sources) - classSource = "phb"; - } - // {@class Fighter|phb|Samurai|Samurai|xge} - return "%s|%s|%s|%s|%s".formatted( - className, - classSource, - valueOrDefault(text, subclassName), - subclassName, - subclassSource); - } - - public static String getClassFeatureKey(String name, String featureSource, String className, String classSource, - String level) { - return "%s|%s|%s|%s|%s|%s".formatted( - Tools5eIndexType.classfeature, - name, - className, - classSource, - level, - featureSource) - .toLowerCase(); - } - - public static String getSubclassFeatureKey(String name, String featureSource, String className, String classSource, - String scShortName, String scSource, String level) { - return "%s|%s|%s|%s|%s|%s|%s|%s".formatted( - Tools5eIndexType.subclassFeature, - name, - className, - classSource, - scShortName, - scSource, - level, - featureSource) - .toLowerCase(); - } - public String fromChildKey(String key) { if (key == null || key.isEmpty()) { return null; @@ -478,6 +479,7 @@ public boolean multiNode() { itemMastery, sense, skill, + spellIndex, status, syntheticGroup -> true; @@ -524,6 +526,7 @@ public boolean useQuteNote() { optionalFeatureTypes, sense, skill, + spellIndex, status, table, tableGroup, @@ -564,7 +567,8 @@ public String getRelativePath() { case legendaryGroup -> "bestiary/legendary-group"; case magicvariant -> "items"; case monster -> "bestiary"; - case optfeature, optionalFeatureTypes -> "optional-features"; + case optfeature -> "optional-features"; + case optionalFeatureTypes, spellIndex -> "lists"; case race, subrace -> "races"; case subclass, classtype -> "classes"; case table, tableGroup -> "tables"; @@ -647,7 +651,6 @@ boolean isDependentType() { return switch (this) { case card, classfeature, - optfeature, optionalFeatureTypes, subclass, subclassFeature, @@ -688,4 +691,8 @@ public void withArrayFrom(JsonNode node, String field, BiConsumer callback.accept(this, x)); } } + + boolean isKey(String crossRef) { + return crossRef != null && crossRef.startsWith(name()); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java index 336bd3d3e..24a0369a7 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java @@ -78,6 +78,13 @@ public Tools5eMarkdownConverter writeFiles(List types) { } } + if (types.contains(Tools5eIndexType.spell) || types.contains(Tools5eIndexType.spellIndex)) { + // We're doing this one a different way: + // Too many different variations of spell list + var spellIndexParent = new Json2QuteSpellIndex(index); + queue.noteCompendium.addAll(spellIndexParent.buildNotes()); + } + writer.writeFiles(index.compendiumFilePath(), queue.baseCompendium); writer.writeFiles(index.rulesFilePath(), queue.baseRules); @@ -190,7 +197,7 @@ private void writeQuteNoteFiles(Tools5eIndexType nodeType, String key, JsonNode case optionalFeatureTypes -> { OptionalFeatureType oft = index.getOptionalFeatureType(node); if (oft == null) { - index.tui().errorf("Unable to find optional feature type for %s", key); + index.tui().errorf(Msg.UNRESOLVED, "Unable to find optional feature type for %s", key); return; } QuteNote converted = new Json2QuteOptionalFeatureType(index, node, oft).buildNote(); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java index c9e60ca1a..374b8f45e 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static dev.ebullient.convert.StringUtil.isPresent; + import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -57,6 +59,18 @@ private static boolean isFreeRules2024(String key, JsonNode jsonElement) { || freeRulesKeys.contains(key); } + public static boolean has2024Content() { + // return true if any of the 2024 core sources are enabled + return List.of("XPHB", "XDMG", "XMM", "srd52", "freerules2024") + .stream().anyMatch(TtrpgConfig.getConfig()::sourceIncluded); + } + + public static boolean has2014Content() { + // return true if any of the 2024 core sources are enabled + return List.of("PHB", "DMG", "MM", "srd", "basicRules") + .stream().anyMatch(TtrpgConfig.getConfig()::sourceIncluded); + } + public static boolean includedByConfig(String key) { Tools5eSources sources = findSources(key); return sources != null && sources.includedByConfig(); @@ -204,9 +218,18 @@ private Tools5eSources(Tools5eIndexType type, String key, JsonNode jsonElement) this.srd = SourceAttributes.srd.coerceBooleanOrDefault(jsonElement, false); this.srd52 = SourceAttributes.srd52.coerceBooleanOrDefault(jsonElement, false); this.edition = SourceAttributes.edition.getTextOrEmpty(jsonElement); + addBrewSource(TtrpgValue.homebrewSource, jsonElement); + addBrewSource(TtrpgValue.homebrewBaseSource, jsonElement); testSourceRules(); } + private void addBrewSource(JsonNodeReader field, JsonNode jsonElement) { + String source = field.getTextOrNull(jsonElement); + if (isPresent(source)) { + this.sources.add(source); + } + } + public boolean isSrdOrFreeRules() { return srd || basicRules || srd52 || freeRules2024; } @@ -456,6 +479,17 @@ public void amendSources(Tools5eSources otherSources) { testSourceRules(); } + public void amendSources(Set brewSources) { + this.sources.addAll(brewSources); + testSourceRules(); + } + + public void amendHomebrewSources(JsonNode homebrewElement) { + addBrewSource(TtrpgValue.homebrewBaseSource, homebrewElement); + addBrewSource(TtrpgValue.homebrewSource, homebrewElement); + testSourceRules(); + } + @Override public boolean includedBy(Set sources) { return super.includedBy(sources) || diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java index aecdaa9e7..53508a03b 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java @@ -10,6 +10,7 @@ import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.NamedText; +import dev.ebullient.convert.qute.QuteUtil; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.Tools5eIndexType; import dev.ebullient.convert.tools.dnd5e.Tools5eSources; @@ -357,7 +358,7 @@ Collection spellcastingToTraits() { */ @TemplateData @RegisterForReflection - public static class Spellcasting { + public static class Spellcasting implements QuteUtil { /** Name: "Spellcasting" or "Innate Spellcasting" */ public String name; /** Formatted text that should be printed before the list of spells */ @@ -446,25 +447,6 @@ void appendList(List text, String title, List spells) { maybeAddBlankLine(text); text.add(String.format("**%s**: %s", title, String.join(", ", spells))); } - - void maybeAddBlankLine(List text) { - if (text.size() > 0 && !text.get(text.size() - 1).isBlank()) { - text.add(""); - } - } - - String levelToString(String level) { - switch (level) { - case "1": - return "1st"; - case "2": - return "2nd"; - case "3": - return "3rd"; - default: - return level + "th"; - } - } } /** diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java index 0182b3325..d9e6bf6c2 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java @@ -1,7 +1,7 @@ package dev.ebullient.convert.tools.dnd5e.qute; import java.util.List; -import java.util.stream.Stream; +import java.util.stream.Collectors; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.Tags; @@ -32,11 +32,13 @@ public class QuteSpell extends Tools5eQuteBase { public final String duration; /** String: rendered list of links to classes that can use this spell. May be incomplete or empty. */ public final String classes; + /** List of links to resources (classes, subclasses, feats, etc.) that have access to this spell */ + public final List references; public QuteSpell(Tools5eSources sources, String name, String source, String level, String school, boolean ritual, String time, String range, String components, String duration, - String classes, List images, String text, Tags tags) { + List references, List images, String text, Tags tags) { super(sources, name, source, images, text, tags); this.level = level; @@ -46,14 +48,18 @@ public QuteSpell(Tools5eSources sources, String name, String source, String leve this.range = range; this.components = components; this.duration = duration; - this.classes = classes; + this.references = references; + this.classes = references.stream() + .filter(s -> s.contains("class")) + .collect(Collectors.joining("; ")); } - /** List of class names that can use this spell. May be incomplete or empty. */ + /** List of class names (not links) that can use this spell. */ public List getClassList() { - return classes == null || classes.isEmpty() + return references == null || references.isEmpty() ? List.of() - : Stream.of(classes.split(",\\s*")) + : references.stream() + .filter(s -> s.contains("class")) .map(s -> s.replaceAll("\\[(.*?)\\].*", "$1")) .toList(); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java index 7179a8e19..f7984f493 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java @@ -10,6 +10,7 @@ import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.qute.QuteBase; import dev.ebullient.convert.tools.CompendiumSources; +import dev.ebullient.convert.tools.JsonTextConverter.SourceField; import dev.ebullient.convert.tools.Tags; import dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields; import dev.ebullient.convert.tools.dnd5e.Tools5eIndexType; @@ -109,6 +110,7 @@ public static String fixFileName(String name, Tools5eSources sources) { Tools5eFields.className.getTextOrEmpty(node), Tools5eFields.classSource.getTextOrEmpty(node), primarySource); + case optionalFeatureTypes -> getOptionalFeatureTypeResource(name); default -> fixFileName(name, primarySource, type); }; } @@ -121,6 +123,9 @@ public static String fixFileName(String name, String source, Tools5eIndexType ty || type == Tools5eIndexType.tableGroup) { return Tui.slugify(name); // file name is based on chapter, etc. } + if (type == Tools5eIndexType.optionalFeatureTypes) { + return getOptionalFeatureTypeResource(name); + } return Tui.slugify(name.replaceAll(" \\(\\*\\)", "-gv") + sourceIfNotDefault(source, type)); } @@ -178,6 +183,32 @@ public static String getDeityResourceName(String name, String source, String pan return pantheon + "-" + name + suffix; } + public static String getOptionalFeatureTypeResource(String name) { + return Tui.slugify("list-optfeaturetype-" + name); + } + + public static String getClassSpellList(String className) { + return "list-spells-%s-%s".formatted( + Tools5eIndexType.classtype.getRelativePath(), + className.toLowerCase()); + } + + public static String getClassSpellList(JsonNode classNode) { + return "list-spells-%s-%s".formatted( + Tools5eIndexType.classtype.getRelativePath(), + SourceField.name.getTextOrEmpty(classNode).toLowerCase()); + } + + public static String getSpellList(String name, Tools5eSources sources) { + Tools5eIndexType type = sources.getType(); + JsonNode node = sources.findNode(); + if (type == Tools5eIndexType.classtype) { + return getClassSpellList(node); + } + final String fileResource = fixFileName(name, sources); + return "list-spells-%s-%s".formatted(type.getRelativePath(), fileResource); + } + public Tools5eQuteBase withTargetFile(String filename) { this.filename = filename; return this; diff --git a/src/main/resources/convertData.json b/src/main/resources/convertData.json index 30dfa0883..5da094713 100644 --- a/src/main/resources/convertData.json +++ b/src/main/resources/convertData.json @@ -752,7 +752,7 @@ "vehicles.json" ], "indexes": { - "spell-source": "generated/gendata-spell-source-lookup.json" + "spell-source": "spells/sources.json" } }, "configPf2e": { diff --git a/src/test/java/dev/ebullient/convert/TestUtils.java b/src/test/java/dev/ebullient/convert/TestUtils.java index 2ad6edfba..a65fd5dd5 100644 --- a/src/test/java/dev/ebullient/convert/TestUtils.java +++ b/src/test/java/dev/ebullient/convert/TestUtils.java @@ -231,6 +231,11 @@ public static void commonTests(Path p, String l, List errors) { errors.add(String.format("Found invalid dice roll in %s: %s", p, l)); } } + // Alarm is a basic spell. It should always be linked. If it isn't, + // a reference has gone awry somewhere along the way + if (p.toString().contains("list-spells-") && l.contains(" Alarm")) { + errors.add(String.format("Missing link to Alarm spell in %s: %s", p, l)); + } } /** diff --git a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java index 106f83a26..8991b4c00 100644 --- a/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java +++ b/src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java @@ -95,6 +95,14 @@ void testLiveData_defaultSrd(QuarkusMainLauncher launcher) { assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { + List errors = new ArrayList<>(); + content.forEach(l -> { + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); + TestUtils.commonTests(p, l, errors); + }); + return errors; + }); } } @@ -114,6 +122,14 @@ void testLiveData_2014Srd(QuarkusMainLauncher launcher) { assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { + List errors = new ArrayList<>(); + content.forEach(l -> { + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); + TestUtils.commonTests(p, l, errors); + }); + return errors; + }); } } @@ -131,9 +147,18 @@ void testLiveData_2024Srd(QuarkusMainLauncher launcher) { TestUtils.TEST_RESOURCES.resolve("5e/sources-subset.json").toString(), TestUtils.TEST_RESOURCES.resolve("5e/images-remote.json").toString(), TestUtils.PATH_5E_TOOLS_DATA.toString()); + assertThat(result.exitCode()) .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) .isEqualTo(0); + TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { + List errors = new ArrayList<>(); + content.forEach(l -> { + TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors); + TestUtils.commonTests(p, l, errors); + }); + return errors; + }); } } @@ -234,7 +259,8 @@ void testLiveData_5eHomebrew(QuarkusMainLauncher launcher) { assertThat(testOutput.resolve("compendium/books/hamunds-herbalism-handbook")).isDirectory(); assertThat(testOutput.resolve("compendium/books/plane-shift-amonkhet")).isDirectory(); - assertThat(testOutput.resolve("compendium/backgrounds/cook-variant-dndwiki-bestbackgrounds.md")).isRegularFile(); + assertThat(testOutput.resolve("compendium/backgrounds/cook-variant-dndwiki-bestbackgrounds.md")) + .isRegularFile(); assertThat(testOutput.resolve("compendium/classes/alchemist-dynamo-engineer-vss.md")).isRegularFile(); TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> { @@ -265,11 +291,14 @@ void testLiveData_5eUA(QuarkusMainLauncher launcher) { TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana - Quick Characters.json").toString(), TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana - Traps Revisited.json").toString(), TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana - When Armies Clash.json").toString(), - TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2022 - Character Origins.json").toString(), + TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2022 - Character Origins.json") + .toString(), TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2022 - Expert Classes.json").toString(), - TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2022 - The Cleric and Revised Species.json") + TestUtils.PATH_5E_UA + .resolve("collection/Unearthed Arcana 2022 - The Cleric and Revised Species.json") + .toString(), + TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2023 - Bastions and Cantrips.json") .toString(), - TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2023 - Bastions and Cantrips.json").toString(), TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2023 - Druid & Paladin.json").toString(), TestUtils.PATH_5E_UA.resolve("collection/Unearthed Arcana 2023 - Player's Handbook Playtest 5.json") .toString(), diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java index 7bb44e97e..1c58cfd89 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java @@ -587,7 +587,7 @@ public void testSpellList(Path outputPath) { MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui); index.markdownConverter(writer) - .writeFiles(Tools5eIndexType.spell); + .writeFiles(List.of(Tools5eIndexType.spell, Tools5eIndexType.spellIndex)); TestUtils.assertDirectoryContents(spellDir, tui); } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java index f4614a761..5a472af71 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java @@ -197,7 +197,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); commonTests.assert_Present("subclass|thief|rogue|phb|phb"); - commonTests.assert_Present("subclass|thief|rogue|xphb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); commonTests.assert_Present("subrace|genasi (air)|genasi|eepc|eepc"); commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java index 88da1889b..1495615b3 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java @@ -192,7 +192,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); commonTests.assert_Present("subclass|thief|rogue|phb|phb"); - commonTests.assert_Present("subclass|thief|rogue|xphb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); diff --git a/src/test/resources/5e/sources-homebrew.json b/src/test/resources/5e/sources-homebrew.json index c51002bca..6796f017b 100644 --- a/src/test/resources/5e/sources-homebrew.json +++ b/src/test/resources/5e/sources-homebrew.json @@ -10,6 +10,7 @@ "src/test/resources/5e/psion.json", "src/test/resources/5e/ermis-bg.json", "sources/5e-homebrew/adventure/Anthony Joyce; The Blood Hunter Adventure.json", + "sources/5e-homebrew/adventure/Arcanum Worlds; Odyssey of the Dragonlords.json", "sources/5e-homebrew/adventure/JVC Parry; Call from the Deep.json", "sources/5e-homebrew/adventure/Kobold Press; Book of Lairs.json", "sources/5e-homebrew/background/D&D Wiki; Featured Quality Backgrounds.json", @@ -28,18 +29,20 @@ "sources/5e-homebrew/collection/Keith Baker; Exploring Eberron.json", "sources/5e-homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json", "sources/5e-homebrew/collection/Kobold Press; Deep Magic.json", + "sources/5e-homebrew/collection/Loot Tavern; Heliana's Guide To Monster Hunting.json", "sources/5e-homebrew/collection/MCDM Productions; The Talent and Psionics Open Playtest Round 2.json", "sources/5e-homebrew/collection/Mage Hand Press; Valda's Spire of Secrets.json", "sources/5e-homebrew/creature/Dragonix; Monster Manual Expanded III.json", "sources/5e-homebrew/creature/Kobold Press; Creature Codex.json", - "sources/5e-homebrew/creature/Kobold Press; Tome of Beasts.json", "sources/5e-homebrew/creature/Kobold Press; Tome of Beasts 2.json", + "sources/5e-homebrew/creature/Kobold Press; Tome of Beasts.json", "sources/5e-homebrew/creature/MCDM Productions; Flee, Mortals! preview.json", "sources/5e-homebrew/creature/MCDM Productions; Flee, Mortals!.json", "sources/5e-homebrew/creature/Nerzugal Role-Playing; Nerzugal's Extended Bestiary.json", "sources/5e-homebrew/deity/Frog God Games; The Lost Lands.json", - "sources/5e-homebrew/collection/Loot Tavern; Heliana's Guide To Monster Hunting.json", - "sources/5e-homebrew/race/Middle Finger of Vecna; Archon.json" + "sources/5e-homebrew/optionalfeature/laserllama; Laserllama's Exploit Compendium.json", + "sources/5e-homebrew/race/Middle Finger of Vecna; Archon.json", + "sources/5e-homebrew/subclass/LaserLlama; Druid Circles.json" ] }, "from": [ From 2f6078208ca33b3ad99caeffe494fd624ddfc7e2 Mon Sep 17 00:00:00 2001 From: GitHub <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:41:53 +0000 Subject: [PATCH 096/119] =?UTF-8?q?=F0=9F=A4=96=20update=20generated=20con?= =?UTF-8?q?tent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/templates/dnd5e/QuteSpell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/templates/dnd5e/QuteSpell.md b/docs/templates/dnd5e/QuteSpell.md index ce574bf2c..12e2b1623 100644 --- a/docs/templates/dnd5e/QuteSpell.md +++ b/docs/templates/dnd5e/QuteSpell.md @@ -15,7 +15,7 @@ List of source books using abbreviated name. Fantasy statblocks uses this list f ### classList -List of resource names (not links) that can use this spell. +List of class names (not links) that can use this spell. ### classes From 995ad165fcf2ab085b1bc482ff20840bc8ce4c63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:41:28 +0000 Subject: [PATCH 097/119] Bump graalvm/setup-graalvm from 1.2.6 to 1.2.7 Bumps [graalvm/setup-graalvm](https://github.com/graalvm/setup-graalvm) from 1.2.6 to 1.2.7. - [Release notes](https://github.com/graalvm/setup-graalvm/releases) - [Commits](https://github.com/graalvm/setup-graalvm/compare/4a200f28cd70d1940b5e33bd00830b7dc71a7e2b...c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466) --- updated-dependencies: - dependency-name: graalvm/setup-graalvm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 8d9d07c99..8a9bdba53 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -120,7 +120,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 + - uses: graalvm/setup-graalvm@c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466 # v1.2.7 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.NATIVE_JAVA_VERSION }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a444e10f4..463b90ab9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -98,7 +98,7 @@ jobs: Data-Pf2eTools enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 + - uses: graalvm/setup-graalvm@c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466 # v1.2.7 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.NATIVE_JAVA_VERSION }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index beec9b66d..e7b761dcf 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -122,7 +122,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@4a200f28cd70d1940b5e33bd00830b7dc71a7e2b # v1.2.6 + - uses: graalvm/setup-graalvm@c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466 # v1.2.7 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.NATIVE_JAVA_VERSION }} From d1ed72584c7f46088ab24ea93f7bb9699f5d02c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:49:53 +0000 Subject: [PATCH 098/119] Bump org.assertj:assertj-core from 3.27.2 to 3.27.3 Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.2 to 3.27.3. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.2...assertj-build-3.27.3) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e6cf68124..5a5ef23e1 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ io.quarkus.platform 3.17.6 - 3.27.2 + 3.27.3 3.4.0 3.0.7 76.1 From d1120712000130464c997e78adf91def0ab8db2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:49:59 +0000 Subject: [PATCH 099/119] Bump quarkus.platform.version from 3.17.6 to 3.17.7 Bumps `quarkus.platform.version` from 3.17.6 to 3.17.7. Updates `io.quarkus.platform:quarkus-bom` from 3.17.6 to 3.17.7 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.6...3.17.7) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.17.6 to 3.17.7 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.6...3.17.7) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5a5ef23e1..eb4241fdf 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.17.6 + 3.17.7 3.27.3 3.4.0 From 2e876e97f28bf95a5546782ad9e7d16a1b9b891a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:20:47 +0000 Subject: [PATCH 100/119] Bump graalvm/setup-graalvm from 1.2.7 to 1.2.8 Bumps [graalvm/setup-graalvm](https://github.com/graalvm/setup-graalvm) from 1.2.7 to 1.2.8. - [Release notes](https://github.com/graalvm/setup-graalvm/releases) - [Commits](https://github.com/graalvm/setup-graalvm/compare/c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466...aafbedb8d382ed0ca6167d3a051415f20c859274) --- updated-dependencies: - dependency-name: graalvm/setup-graalvm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pf2e-tools-data.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/tools-data.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index 8a9bdba53..b31068800 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -120,7 +120,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466 # v1.2.7 + - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.NATIVE_JAVA_VERSION }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 463b90ab9..c944d67d0 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -98,7 +98,7 @@ jobs: Data-Pf2eTools enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466 # v1.2.7 + - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.NATIVE_JAVA_VERSION }} diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index e7b761dcf..87189cf71 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -122,7 +122,7 @@ jobs: fail-on-cache-miss: true enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@c09e29bb115a83bd4b7c7e99bb46e2e8a1c50466 # v1.2.7 + - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 with: distribution: ${{ env.GRAALVM_DIST }} java-version: ${{ env.NATIVE_JAVA_VERSION }} From 5e3c7d8d737a37742b686f82e4d162a1e57523d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:25:41 +0000 Subject: [PATCH 101/119] Bump quarkus.platform.version from 3.17.7 to 3.17.8 Bumps `quarkus.platform.version` from 3.17.7 to 3.17.8. Updates `io.quarkus.platform:quarkus-bom` from 3.17.7 to 3.17.8 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.7...3.17.8) Updates `io.quarkus.platform:quarkus-maven-plugin` from 3.17.7 to 3.17.8 - [Commits](https://github.com/quarkusio/quarkus-platform/compare/3.17.7...3.17.8) --- updated-dependencies: - dependency-name: io.quarkus.platform:quarkus-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.quarkus.platform:quarkus-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eb4241fdf..788c0bc63 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ quarkus-bom io.quarkus.platform - 3.17.7 + 3.17.8 3.27.3 3.4.0 From 72a121de52728017625cd305317605a2bf70d263 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Fri, 24 Jan 2025 20:57:41 -0500 Subject: [PATCH 102/119] =?UTF-8?q?=F0=9F=93=9D=203.x=20changelog=20+=20fi?= =?UTF-8?q?xed=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 68 ++++++++++--------- docs/configuration.md | 23 +++++-- .../convert/config/ExportDocsTest.java | 4 +- .../resources/5e/sources-book-adventure.json | 2 +- src/test/resources/5e/sources-homebrew.json | 2 +- 5 files changed, 58 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 567e6f924..d2644b860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,39 +7,45 @@ **Note:** Entries marked with "🔥" indicate crucial or breaking changes that might affect your current setup. -> [!NOTE] -> ***If you generated content with an earlier verson of the CLI (1.x, 2.0.x, 2.1.x)***, you can use [a templater script](https://github.com/ebullient/ttrpg-convert-cli/blob/main/migration/ttrpg-cli-renameFiles-5e-2.1.0.md) to **rename files in your vault before merging** with freshly generated content. View the contents of the template before running it, and adjust parameters at the top to match your Vault. -> -> To run the template: Use 'Templater: Open Insert Template Modal' with an existing note or 'Templater: Create new note from Template' to create a new note, and choose the migration template from the list. - ## 🔥✨ 3.0.0: Moving the things -- The `-s` option is no more. All sources must be specified in config files. -- There have been some changes to how dice strings are rendered. -- `from` and `full-source` have been merged. The [`sources` key](./docs/configuration.md#specify-content-with-the-sources-key) now defines all types of source: `reference` for reference-only, `book`/`adventure` for complete source text, and `homebrew` for homebrew. - - ```json - { - "sources": { - "adventure": [ - ... - ], - "book": [ - ... - ], - "homebrew": [ - ... - ], - "reference": [ - ... - ] - }, - } - ``` - -- The [Source Map](./docs/sourceMap.md) for 5e sources now indicates if the source is a `book` or `adventure`. -- The Players Handbook directory has changed from `players-handbook` to `players-handbook-2014` -- **Reprint behavior** has always been knd of obscure, but it really matters now. The CLI will always default to one-note-per-thing, perferring the most recent version of said thing. This means that the 2024 PHB content will be preferred if you have that source available. Use [`include` and `exclude` configuration](docs/configuration.md#refine-content-choices) to tweak that behavior. +Support for the 2024 ruleset caused a lot of ripples. There is nothing small about this release. + +- *Specifying Sources* + - Removed `-s` option. Sources must be specified in config files. + - The [Source Map](./docs/sourceMap.md) for 5e sources now includes `book` or `adventure` status + - Combined `from` and `full-source` into unified `sources` key with four types: + + ```json + { + "sources": { + "reference": [...], // Reference-only content + "book": [...], // Complete book content + "adventure": [...], // Complete adventure content + "homebrew": [...] // Custom content + } + } + ``` + + - *Reprint handling*: CLI will default to the most recent version available for your selected sources. + - Use `include`/`exclude` config to override. + - For SRD/Basic rules for 2014 content, use: "srd", "basicrules" + - For SRD/Basic rules for 2024 content, use: "srd52", "freerules2024" +- *Generated content* + - Changed Players Handbook directory to `players-handbook-2014` + - Dice roller uses the `|text` flag to make text more consistent + and readable (dice roll occurs on hover) + - Added a `lists` folder in the compendium: + - Lists of optional features + - Lists of spells by class, school, subclass, background, feat... +- **5e Template updates** + - All templates: + - Added `books`: Abbreviated source book list + - Added `sourcesWithFootnote`: Primary source with additional sources as footnotes + - Added Bastion template (2024 rules) + - Spells: Added `references` list linking to related content + +Note: Path changes may affect existing content links. All path updates support dual-edition content structure. ## 🔖 ✨ 2.3.14: Improvements to Pathfinder rendering diff --git a/docs/configuration.md b/docs/configuration.md index 25b94048c..8325668ee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -316,13 +316,24 @@ The CLI `--index` option compiles two lists of data keys: ### Excluding content matching an `excludePattern` -This option allows you to exclude data entries based on matching patterns. +This option allows you to exclude data entries based on regular expression matching patterns. -```json -"excludePattern": [ - "race|.*|dmg" -] -``` +Note: A pipe (`|`) is a special character in regular expressions, and must be escaped. + +- JSON + + ```json + "excludePattern": [ + "race\\|.*\\|dmg" + ] + ``` + +- YAML + + ```yaml + excludePattern: + - race\|.*\|dmg + ``` ### Excluding specific content with `exclude` diff --git a/src/test/java/dev/ebullient/convert/config/ExportDocsTest.java b/src/test/java/dev/ebullient/convert/config/ExportDocsTest.java index 664ccd9dd..c01f7e7b9 100644 --- a/src/test/java/dev/ebullient/convert/config/ExportDocsTest.java +++ b/src/test/java/dev/ebullient/convert/config/ExportDocsTest.java @@ -135,7 +135,7 @@ public void exportExample() throws Exception { tools5Config.paths.compendium = "/compendium/"; tools5Config.paths.rules = "/compendium/rules/"; - tools5Config.excludePattern.add("race|.*|dmg"); + tools5Config.excludePattern.add("race\\|.*\\|dmg"); tools5Config.exclude.addAll(List.of( "monster|expert|dc", "monster|expert|sdw", @@ -167,7 +167,7 @@ public void exportExample() throws Exception { pf2eConfig.include.add("ability|buck|b1"); pf2eConfig.exclude.add("background|insurgent|apg"); - pf2eConfig.excludePattern.add("background|.*|lowg"); + pf2eConfig.excludePattern.add("background\\|.*\\|lowg"); pf2eConfig.template.put("ability", "../path/to/ability2md.txt"); pf2eConfig.tagPrefix = "ttrpg-cli"; diff --git a/src/test/resources/5e/sources-book-adventure.json b/src/test/resources/5e/sources-book-adventure.json index 561706b0f..011e053b1 100644 --- a/src/test/resources/5e/sources-book-adventure.json +++ b/src/test/resources/5e/sources-book-adventure.json @@ -28,6 +28,6 @@ "monster|expert|slw" ], "excludePattern": [ - "race|.*|dmg" + "race\\|.*\\|dmg" ] } diff --git a/src/test/resources/5e/sources-homebrew.json b/src/test/resources/5e/sources-homebrew.json index 6796f017b..ad71744fb 100644 --- a/src/test/resources/5e/sources-homebrew.json +++ b/src/test/resources/5e/sources-homebrew.json @@ -60,7 +60,7 @@ "monster|expert|slw" ], "excludePattern": [ - "race|.*|dmg" + "race\\|.*\\|dmg" ], "template": { "background": "examples/templates/tools5e/images-background2md.txt", From 8ec7ae1f796cc47df5f23dcce5930b5239992020 Mon Sep 17 00:00:00 2001 From: GitHub <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 02:00:02 +0000 Subject: [PATCH 103/119] =?UTF-8?q?=F0=9F=94=96=203.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/project.yml | 4 ++-- README-WINDOWS.md | 10 +++++----- docs/alternateRun.md | 6 +++--- examples/config/config.5e.json | 2 +- examples/config/config.5e.yaml | 2 +- examples/config/config.pf2e.json | 2 +- examples/config/config.pf2e.yaml | 2 +- pom.xml | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/project.yml b/.github/project.yml index 3f1320211..d8db61e7e 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -1,7 +1,7 @@ name: TTRPG Convert CLI release: - current-version: 2.3.18 - next-version: 2.3.18 + current-version: 3.0.0 + next-version: 3.0.0 snapshot-version: 299-SNAPSHOT build: artifact: ttrpg-convert-cli diff --git a/README-WINDOWS.md b/README-WINDOWS.md index 6e5db622d..29f836591 100644 --- a/README-WINDOWS.md +++ b/README-WINDOWS.md @@ -25,8 +25,8 @@ 1. From the [latest release][1], download the following files: - - `ttrpg-convert-cli-2.3.18-windows-x86_64.zip` - - `ttrpg-convert-cli-2.3.18-examples.zip` + - `ttrpg-convert-cli-3.0.0-windows-x86_64.zip` + - `ttrpg-convert-cli-3.0.0-examples.zip` 2. Unzip the downloaded files into a place you'll remember. For example, `Downloads`. 3. Navigate to the `bin` directory inside the unzipped files. It might be nested within another folder. You should see a `ttrpg-convert` EXE file in the folder - see the screenshot below. @@ -85,7 +85,7 @@ On Windows, the command output will look like this, with weird characters at the ```shell [ Γ£à OK] Finished reading config. -ΓÅ▒∩╕Å Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.18-windows-x86_64\ttrpg-convert-cli-2.3.18-windows-x86_64\bin\5etools-mirror-2.github.io\data +ΓÅ▒∩╕Å Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.0-windows-x86_64\ttrpg-convert-cli-3.0.0-windows-x86_64\bin\5etools-mirror-2.github.io\data [ Γ£à OK] Finished reading data. ``` @@ -100,7 +100,7 @@ You should then start seeing the emoji correctly: ```shell [ ✅ OK] Finished reading config. -⏱️ Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.18-windows-x86_64\ttrpg-convert-cli-2.3.18-windows-x86_64\bin\5etools-mirror-2.github.io\data +⏱️ Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.0-windows-x86_64\ttrpg-convert-cli-3.0.0-windows-x86_64\bin\5etools-mirror-2.github.io\data [ ✅ OK] Finished reading data. ``` @@ -132,7 +132,7 @@ Type in `dir` and press **Enter**. You should see output similar to this: ```shell Directory: - C:\Users\Kelly\Downloads\ttrpg-convert-cli-2.3.18-windows-x86_64\ttrpg-convert-cli-2.3.18-windows-x86_64\bin + C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.0-windows-x86_64\ttrpg-convert-cli-3.0.0-windows-x86_64\bin Mode LastWriteTime Length Name diff --git a/docs/alternateRun.md b/docs/alternateRun.md index cc05e3b7d..34b1c4f8e 100644 --- a/docs/alternateRun.md +++ b/docs/alternateRun.md @@ -24,7 +24,7 @@ JBang is a tool designed to simplify Java application execution. By eliminating 2. Install the pre-built release of ttrpg-convert-cli: ```shell - jbang app install --name ttrpg-convert --force --fresh https://github.com/ebullient/ttrpg-convert-cli/releases/download/2.3.18/ttrpg-convert-cli-2.3.18-runner.jar + jbang app install --name ttrpg-convert --force --fresh https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.0.0/ttrpg-convert-cli-3.0.0-runner.jar ``` 🚧 If you want the latest [_unreleased snapshot_][]: @@ -127,13 +127,13 @@ To run the CLI, you will need to have **Java 17** installed on your system. 2. Download the CLI as a jar - - Latest release: [ttrpg-convert-cli-2.3.18-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/2.3.18/ttrpg-convert-cli-2.3.18-runner.jar) + - Latest release: [ttrpg-convert-cli-3.0.0-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.0.0/ttrpg-convert-cli-3.0.0-runner.jar) - 🚧 [_unreleased snapshot_][]: [ttrpg-convert-cli-299-SNAPSHOT-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/299-SNAPSHOT/ttrpg-convert-cli-299-SNAPSHOT-runner.jar) 3. Verify the install by running the command: ```shell - java -jar ttrpg-convert-cli-2.3.18-runner.jar --help + java -jar ttrpg-convert-cli-3.0.0-runner.jar --help ``` 🚧 If you are using the [_unreleased snapshot_][], use the following command: diff --git a/examples/config/config.5e.json b/examples/config/config.5e.json index 1ed84ade5..ed6ad7f0b 100644 --- a/examples/config/config.5e.json +++ b/examples/config/config.5e.json @@ -30,7 +30,7 @@ "monster|expert|slw" ], "excludePattern" : [ - "race|.*|dmg" + "race\\|.*\\|dmg" ], "reprintBehavior" : "newest", "template" : { diff --git a/examples/config/config.5e.yaml b/examples/config/config.5e.yaml index 4df1575ad..b7f7cadf6 100644 --- a/examples/config/config.5e.yaml +++ b/examples/config/config.5e.yaml @@ -21,7 +21,7 @@ exclude: - "monster|expert|sdw" - "monster|expert|slw" excludePattern: -- "race|.*|dmg" +- "race\\|.*\\|dmg" reprintBehavior: "newest" template: background: "examples/templates/tools5e/images-background2md.txt" diff --git a/examples/config/config.pf2e.json b/examples/config/config.pf2e.json index 0f283e98b..f6a82e75e 100644 --- a/examples/config/config.pf2e.json +++ b/examples/config/config.pf2e.json @@ -20,7 +20,7 @@ "background|insurgent|apg" ], "excludePattern" : [ - "background|.*|lowg" + "background\\|.*\\|lowg" ], "reprintBehavior" : "newest", "template" : { diff --git a/examples/config/config.pf2e.yaml b/examples/config/config.pf2e.yaml index e1684b4a3..162b86a09 100644 --- a/examples/config/config.pf2e.yaml +++ b/examples/config/config.pf2e.yaml @@ -14,7 +14,7 @@ include: exclude: - "background|insurgent|apg" excludePattern: -- "background|.*|lowg" +- "background\\|.*\\|lowg" reprintBehavior: "newest" template: ability: "../path/to/ability2md.txt" diff --git a/pom.xml b/pom.xml index 788c0bc63..022956467 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ https://github.com/ebullient/ttrpg-convert-cli/issues - 299-SNAPSHOT + 3.0.0 3.4.0 3.13.0 From e46f604f2d7c282ab9ca19b5fdcb3c7767791531 Mon Sep 17 00:00:00 2001 From: GitHub <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 02:07:47 +0000 Subject: [PATCH 104/119] =?UTF-8?q?=F0=9F=94=A7=20Prepare=20for=20next=20r?= =?UTF-8?q?elease?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 022956467..788c0bc63 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ https://github.com/ebullient/ttrpg-convert-cli/issues - 3.0.0 + 299-SNAPSHOT 3.4.0 3.13.0 From ba9d433b705d36918117da8275bec7210526358f Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Sat, 25 Jan 2025 07:33:02 -0500 Subject: [PATCH 105/119] =?UTF-8?q?=F0=9F=91=B7=20update=20snapshot=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/project.yml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/project.yml b/.github/project.yml index d8db61e7e..342651cbf 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -2,7 +2,7 @@ name: TTRPG Convert CLI release: current-version: 3.0.0 next-version: 3.0.0 - snapshot-version: 299-SNAPSHOT + snapshot-version: 399-SNAPSHOT build: artifact: ttrpg-convert-cli jitpack: diff --git a/pom.xml b/pom.xml index 788c0bc63..001a723df 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ https://github.com/ebullient/ttrpg-convert-cli/issues - 299-SNAPSHOT + 399-SNAPSHOT 3.4.0 3.13.0 From 270383348ab0536a26223a8060f704edfaa196ef Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Sat, 25 Jan 2025 11:32:18 -0500 Subject: [PATCH 106/119] =?UTF-8?q?=F0=9F=93=9D=20Additional=20detail=20in?= =?UTF-8?q?=203.x=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2644b860..b9c99d81d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,20 @@ Support for the 2024 ruleset caused a lot of ripples. There is nothing small abo - All templates: - Added `books`: Abbreviated source book list - Added `sourcesWithFootnote`: Primary source with additional sources as footnotes - - Added Bastion template (2024 rules) + - Image display (see [image examples](examples/templates/tools5e)) + - hasImages - true if any images are present + - hasMoreImages - true if more than one image is present + - showAllImages - rendered wikilinks for all images. If there is more than one, they will be shown as a gallery. + - showMoreImages - rendered wikilinks for all but the first image. If there is more than one, they will be shown as a gallery. + - showPortraitImage - rendered wikilink for the first image with the '#right' anchor tag to float it to the right side. + - Added [Bastion template](docs/templates/dnd5e/QuteBastion/) (2024 rules) + - Class updates: + - Unified class progression tables + - [Hit Point Die](docs/templates/dnd5e/QuteClass/HitPointDie.md) + - [Starting Equipment](docs/templates/dnd5e/QuteClass/StartingEquipment.md) + - Improved [Multiclassing](docs/templates/dnd5e/QuteClass/Multiclassing.md) + - isClassic/classic: true if 2014 class + - isSidekick/sidekick: true if sidekick class - Spells: Added `references` list linking to related content Note: Path changes may affect existing content links. All path updates support dual-edition content structure. From c7e1693880a6cef537abe938b8ee3e10b03444ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:32:41 +0000 Subject: [PATCH 107/119] Bump github/codeql-action from 3.28.1 to 3.28.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.1 to 3.28.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b6a472f63d85b9c78a3ac5e89422239fc15e9b3c...f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ab7c11a01..8fe82a2b9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,6 +74,6 @@ jobs: ./mvnw -B -ntp verify -DskipFormat - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3105a43ca..c955b980b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1 + uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5 with: sarif_file: results.sarif From 92fad3796355812fa671cd2653bccaa45b89204c Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 29 Jan 2025 19:45:09 -0500 Subject: [PATCH 108/119] =?UTF-8?q?=F0=9F=91=B7=20Update=20tools-data.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tools-data.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml index 87189cf71..3c251cad7 100644 --- a/.github/workflows/tools-data.yml +++ b/.github/workflows/tools-data.yml @@ -4,6 +4,12 @@ on: - cron: "7 9 * * */5" workflow_dispatch: + inputs: + build: + description: "Build after creating cache" + default: true + required: true + type: boolean env: JAVA_VERSION: 17 @@ -60,15 +66,16 @@ jobs: gh repo clone TheGiddyLimit/unearthed-arcana sources/5e-unearthed-arcana -- --depth=1 gh repo clone TheGiddyLimit/homebrew sources/5e-homebrew -- --depth=1 - # Remove image contents. We just need the files to exist (linking) - find sources -type f -type f \ - \( -iname \*.jpg -o -iname \*.png -o -iname \*.webp \) \ - | while read FILE; do echo > "$FILE"; done + # Remove image (and other non-json) contents. + # Mostly relevant for images. We only need the files to exist (linking) + + find sources -type f ! -name '*.json' ! -path '*.git*' | while read FILE; do echo > "$FILE"; done ls -al sources test-with-data: + if: ${{ inputs.build }} name: Test with data needs: cache-setup runs-on: ubuntu-latest From 213e1f4b0b83f496c0756a79b5bfea6f242053a1 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Sat, 25 Jan 2025 17:26:55 -0500 Subject: [PATCH 109/119] =?UTF-8?q?=F0=9F=93=9D=202024=20support=20cleanup?= =?UTF-8?q?;=20update=20CLI=20--help?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 -- README.md | 1 - docs/configuration.md | 2 +- .../ebullient/convert/RpgDataConvertCli.java | 25 +++++++++++-------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 6f26bce46..e5d7ced16 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -18,8 +18,6 @@ body: > > - 🚜 [**Review the changelog**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥). > - 🔮 Check out [**Conventions**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/README.md#conventions) and [**Recommendations**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/README.md#recommendations-for-using-the-cli). - > - 🔥 Support for the 5e 2024 ruleset is [in progress](https://github.com/ebullient/ttrpg-convert-cli/discussions/586). - > Messages you may see related to XPHB, XMM, XDMG, or HP formulas are all related to this change - type: textarea id: the-problem diff --git a/README.md b/README.md index 27858b315..11acd6f08 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ I use [Obsidian](https://obsidian.md) to keep track of my campaign notes. This p > > - 🚜 [**Review the changelog**](CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥). > - 🔮 Check out [**Conventions**](#conventions) and [**Recommendations**](#recommendations-for-using-the-cli). -> - 🔥 Support for the 5e 2024 ruleset is [in progress](https://github.com/ebullient/ttrpg-convert-cli/discussions/586). ## Using the Command Line diff --git a/docs/configuration.md b/docs/configuration.md index 8325668ee..db5a26398 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -106,7 +106,7 @@ Here's a more comprehensive `config.json` file. "rules": "/compendium/rules/" }, "excludePattern": [ - "race|.*|dmg" + "race\\|.*\\|dmg" ], "exclude": [ "monster|expert|dc", diff --git a/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java b/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java index 2150098f7..8529354c8 100644 --- a/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java +++ b/src/main/java/dev/ebullient/convert/RpgDataConvertCli.java @@ -55,17 +55,20 @@ "Here is a brief example (JSON). See the project README.md for details.", "", "{", - " \"from\" : [", - " \"PHB\",", - " \"DMG\",", - " \"SCAG\",", - " ]", - " \"exclude\" : [", - " \"background|sage|phb\",", - " ]", - " \"excludePattern\" : [", - " \"race|.*|dmg\",", - " ]", + " \"sources\": {", + " \"adventure\": [", + " \"LMoP\"", + " ],", + " \"book\": [", + " \"PHB\"", + " ],", + " \"reference\": [", + " \"VGM\"", + " ]", + " },", + " \"paths\": {", + " \"rules\": \"/compendium/rules/\"", + " },", "}", "", }, mixinStandardHelpOptions = true, versionProvider = VersionProvider.class, showDefaultValues = true) From 6d3842aa9a6077af4710eb19569a61f9af9e47ed Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 29 Jan 2025 19:13:40 -0500 Subject: [PATCH 110/119] =?UTF-8?q?=F0=9F=90=9B=20filtering=20fixes:=20sub?= =?UTF-8?q?classes,=20subraces,=20deities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactored tests - fixed deity filters + logs - removed card & subrace from dependent types - cross-edition warning for subclass features --- .github/workflows/pull-request.yml | 1 + .../convert/tools/JsonNodeReader.java | 9 + .../convert/tools/dnd5e/Json2QuteClass.java | 65 ++- .../convert/tools/dnd5e/Json2QuteDeity.java | 52 +- .../convert/tools/dnd5e/JsonSource.java | 3 +- .../tools/dnd5e/JsonTextReplacement.java | 18 +- .../convert/tools/dnd5e/Tools5eIndex.java | 455 ++++++++++-------- .../convert/tools/dnd5e/Tools5eIndexType.java | 6 +- .../convert/tools/dnd5e/Tools5eSources.java | 23 +- .../convert/tools/dnd5e/CommonDataTests.java | 101 +--- .../tools/dnd5e/FilterAllNewestTest.java | 180 +++++-- .../convert/tools/dnd5e/FilterAllTest.java | 199 ++++++-- .../tools/dnd5e/FilterNoneEditionTest.java | 186 +++++-- .../convert/tools/dnd5e/FilterNoneTest.java | 189 ++++++-- .../tools/dnd5e/FilterSrd2014Test.java | 182 +++++-- .../tools/dnd5e/FilterSrd2024Test.java | 180 +++++-- .../tools/dnd5e/FilterSubset2014Test.java | 192 ++++++-- .../tools/dnd5e/FilterSubset2024Test.java | 195 ++++++-- .../tools/dnd5e/FilterSubsetMixedTest.java | 350 ++++++++++++++ src/test/resources/5e/sources.json | 6 +- 20 files changed, 1895 insertions(+), 697 deletions(-) create mode 100644 src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubsetMixedTest.java diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c944d67d0..8a66026a3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -59,6 +59,7 @@ jobs: - name: Build with Maven id: mvn-build run: | + mkdir -p sources ls -al sources ./mvnw -B -ntp -DskipFormat verify diff --git a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java index 10d9e6072..9877497e6 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java @@ -519,4 +519,13 @@ default void moveFrom(JsonNode source, JsonNode target) { ((ObjectNode) target).set(this.nodeName(), value); } } + + /** Destructive! */ + default void appendToArray(JsonNode target, String value) { + if (target == null) { + return; + } + ArrayNode array = ensureArrayIn(target).add(value); + setIn(target, array); + } } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java index f6f3a1ebb..a7521267c 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -102,7 +103,11 @@ protected QuteClass buildQuteResource() { public List buildSubclasses() { List quteSc = new ArrayList<>(); - for (String scKey : ClassFields.subclassKeys.getListOfStrings(rootNode, tui())) { + // List of subclasses may include duplicates + // See recovery for included subclass features that get abandoned + // over edition crossing + Set subclassKeys = index.findSubclasses(getSources().getKey()); + for (String scKey : subclassKeys) { JsonNode scNode = index.getNode(scKey); Tools5eSources scSources = Tools5eSources.findSources(scKey); String scName = scSources.getName(); @@ -128,6 +133,14 @@ public List buildSubclasses() { List images = new ArrayList<>(); List text = getFluff(scNode, Tools5eIndexType.subclassFluff, "##", images); + if (scSources.isClassic() && !getSources().isClassic()) { + // insert warning about mixed edition content + text.add(0, + "> This subclass is from a different game edition. You will need to do some adjustment to resolve differences."); + text.add(0, "> [!caution] Mixed edition content"); + } + + maybeAddBlankLine(text); text.add("## Class Features"); for (ClassFeature scf : scFeatures) { scf.appendText(this, text, scSources.primarySource()); @@ -748,10 +761,10 @@ public String itemSource() { // Unpack a subclass key static class SubclassKeyData implements KeyData { - final String scName; - final String className; - final String classSource; - final String scSource; + String scName; + String className; + String classSource; + String scSource; public SubclassKeyData(String key) { String[] parts = key.split("\\|"); @@ -789,13 +802,13 @@ public String itemSource() { // Unpack a subclass feature key static class SubclassFeatureKeyData implements KeyData { - final String scfName; - final String className; - final String classSource; - final String scName; - final String scSource; - final String level; - final String scfSource; + String scfName; + String className; + String classSource; + String scName; + String scSource; + String level; + String scfSource; public SubclassFeatureKeyData(String key) { String[] parts = key.split("\\|"); @@ -832,6 +845,32 @@ public String level() { public String itemSource() { return scfSource; } + + public String toKey() { + return String.join("|", + Tools5eIndexType.subclassFeature.name(), + scfName, + className, classSource, + scName, scSource, + level, scfSource) + .toLowerCase(); + } + + public String toSubclassKey() { + return String.join("|", + Tools5eIndexType.subclass.name(), + scName, + className, classSource, + scSource) + .toLowerCase(); + } + + public String toClassKey() { + return String.join("|", + Tools5eIndexType.classtype.name(), + className, classSource) + .toLowerCase(); + } } static class LevelProgression { @@ -961,7 +1000,6 @@ enum ClassFields implements JsonNodeReader { count, defaultEquipment("default"), // default is a reserved word faces, - featureKeys, from, full, gainSubclassFeature, @@ -988,7 +1026,6 @@ enum ClassFields implements JsonNodeReader { startingProficiencies, subclassFeature, subclassFeatures, - subclassKeys, subclassShortName, subclassSource, subclassTableGroups, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java index 579ba3729..21ac580ed 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java @@ -12,7 +12,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import dev.ebullient.convert.config.ReprintBehavior; import dev.ebullient.convert.config.TtrpgConfig; +import dev.ebullient.convert.io.Msg; +import dev.ebullient.convert.io.Tui; import dev.ebullient.convert.qute.ImageRef; import dev.ebullient.convert.tools.JsonNodeReader; import dev.ebullient.convert.tools.Tags; @@ -82,18 +85,23 @@ ImageRef getSymbolImage() { return null; } - public static Iterable findDeitiesToRemove(List allDeities) { + public static Iterable findDeities(List allDeities) { + var config = TtrpgConfig.getConfig(); + if (config.reprintBehavior() == ReprintBehavior.all) { + return allDeities.stream() + .filter(t -> Tools5eSources.includedByConfig(t.key)) + .peek(t -> Tui.instance().logf(Msg.DEITY, " ---- %s", t.key)) + .map(t -> t.key) + .toList(); + } + final Comparator byDate = Comparator .comparing(k -> TtrpgConfig.sourcePublicationDate(k)); Function deityKey = n -> { - String reprintAlias = DeityField.reprintAlias.getTextOrNull(n.node); - if (reprintAlias == null) { - String pantheon = DeityField.pantheon.getTextOrEmpty(n.node); - String name = SourceField.name.getTextOrEmpty(n.node); - return name + "-" + pantheon; - } - return reprintAlias; + String name = DeityField.reprintAlias.getTextOrDefault(n.node, SourceField.name.getTextOrEmpty(n.node)); + String pantheon = DeityField.pantheon.getTextOrEmpty(n.node); + return (name + "-" + pantheon).toLowerCase(); }; // Group by source @@ -106,26 +114,38 @@ public static Iterable findDeitiesToRemove(List allDeities) { .toList(); Map keepers = new HashMap<>(); - List keysToRemove = new ArrayList<>(); // Iterate over groups of deities in order of publication. // Keep the first deity of each name, add others to the remove pile. for (String book : sourcesByDate) { List deities = deityBySource.remove(book); if (keepers.isEmpty()) { // most recent bucket. Keep all. - deities.forEach(t -> keepers.put(deityKey.apply(t), t)); + deities.forEach(tuple -> { + String key = deityKey.apply(tuple); + if (Tools5eSources.includedByConfig(tuple.key)) { + Tui.instance().logf(Msg.DEITY, " ---- %60s :: %s", tuple.key, key); + keepers.put(key, tuple); + } else { + Tui.instance().logf(Msg.DEITY, "(drop) %s", tuple.key); + } + }); continue; } - for (Tuple deity : deities) { - String key = deityKey.apply(deity); - if (keepers.containsKey(key)) { - keysToRemove.add(deity.key); + for (Tuple tuple : deities) { + String key = deityKey.apply(tuple); + if (Tools5eSources.includedByConfig(tuple.key)) { + if (keepers.containsKey(key)) { + Tui.instance().logf(Msg.DEITY, "(drop | superseded) %47s => %s", tuple.key, key); + } else { + keepers.put(key, tuple); + Tui.instance().logf(Msg.DEITY, " ---- %60s :: %s", tuple.key, key); + } } else { - keepers.put(key, deity); + Tui.instance().logf(Msg.DEITY, "(drop) %s", tuple.key); } } } - return keysToRemove; + return keepers.entrySet().stream().map(e -> e.getValue().key).toList(); } enum DeityField implements JsonNodeReader { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java index b7cc610a8..b03115721 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java @@ -148,7 +148,7 @@ default void appendObjectToText(List text, JsonNode node, String heading // entriesOtherSource handled here. if (!source.isEmpty() && !cfg().sourceIncluded(source)) { - if (!cfg().sourceIncluded(getSources())) { + if (!getSources().includedByConfig()) { return; } } @@ -1252,7 +1252,6 @@ enum Tools5eFields implements JsonNodeReader { by, className, classSource, - classFeatureKeys, // ELH: keys for related class/subclass features condition, // speed, ac count, cr, diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index ea4647ff7..03883491b 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -880,10 +880,10 @@ default String linkifySubclassFeature(String match) { String subclassKey = Tools5eIndexType.subclass.fromChildKey(featureKey); // look up alias for subclass so link is correct, but don't follow reprints - // "subclass|redemption|paladin|phb|" : "subclass|oath of - // redemption|paladin|phb|", - // "subclass|twilight|cleric|phb|tce" : "subclass|twilight - // domain|cleric|phb|tce" + // "subclass|redemption|paladin|phb|" + // : "subclass|oath of redemption|paladin|phb|", + // "subclass|twilight|cleric|phb|tce" + // : "subclass|twilight domain|cleric|phb|tce" subclassKey = index().getAliasOrDefault(subclassKey, false); JsonNode subclassNode = index().getNode(subclassKey); @@ -897,13 +897,13 @@ default String linkifySubclassFeature(String match) { return linkText; } // Examine new subclass node's features, to see if there is a match - // e.g. for "subclassfeature|primal companion|ranger|phb|beast - // master|phb|3|tce", - // consider "subclassfeature|primal companion|ranger|xphb|beast - // master|xphb|3|xphb" + // e.g. for + // "subclassfeature|primal companion|ranger|phb|beast master|phb|3|tce", + // consider + // "subclassfeature|primal companion|ranger|xphb|beast master|xphb|3|xphb" String test = featureKey.replaceAll(subclassFeatureMask, "$1-$2"); boolean found = false; - for (String fkey : Tools5eFields.classFeatureKeys.getListOfStrings(subclassNode, tui())) { + for (String fkey : index().findClassFeatures(subclassKey)) { String compare = fkey.replaceAll(subclassFeatureMask, "$1-$2"); if (test.equals(compare)) { featureKey = fkey; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java index 65762e3a6..1e5341aca 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -17,7 +18,6 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -32,7 +32,7 @@ import dev.ebullient.convert.tools.ToolsIndex; import dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewFields; import dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes; -import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields; +import dev.ebullient.convert.tools.dnd5e.Json2QuteClass.SubclassFeatureKeyData; import dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField; import dev.ebullient.convert.tools.dnd5e.Json2QuteRace.RaceFields; import dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType; @@ -57,17 +57,21 @@ public static Tools5eIndex getInstance() { final CompendiumConfig config; // Initialization - private final Map nodeIndex = new HashMap<>(); - private final Map> subraceIndex = new HashMap<>(); - private final Map> tableIndex = new HashMap<>(); - + private final Map nodeIndex = new TreeMap<>(); // --index private Map filteredIndex = null; - private final Map aliases = new HashMap<>(); - private final Map reprints = new HashMap<>(); - private final Map subraceMap = new HashMap<>(); + private final Map> subraceIndex = new HashMap<>(); // --index + private final Map> tableIndex = new HashMap<>(); + + private final Map aliases = new TreeMap<>(); // --index + private final Map reprints = new TreeMap<>(); // --index + private final Map subraceMap = new TreeMap<>(); // --index private final Map nameToLink = new HashMap<>(); + // Class feature, Subclass, and Subclass Feature nonsense + private final Map> classFeatures = new TreeMap<>(); // --index + private final Map> subclassMap = new TreeMap<>(); // --index + private final Set srdKeys = new HashSet<>(); final Tools5eJsonSourceCopier copier = new Tools5eJsonSourceCopier(this); @@ -255,6 +259,7 @@ void addToIndex(Tools5eIndexType type, JsonNode node) { Tools5eFields.shortName.getTextOrEmpty(node), SourceField.source.getTextOrEmpty(node)); addAlias(lookupKey, key); + classFeatures.put(key, new HashSet<>()); } case table, tableGroup -> { SourceAndPage sp = new SourceAndPage(node); @@ -272,6 +277,12 @@ void addToIndex(Tools5eIndexType type, JsonNode node) { // adventures can be subdivided from books. Don't map source/id for those TtrpgConfig.sourceToIdMapping(source, id); } + String parentSource = Tools5eFields.parentSource.getTextOrNull(node); + if (parentSource != null && TtrpgConfig.getConfig().sourceIncluded(source)) { + // include the parent source if you include an adventure (related rules) + tui().debugf(Msg.SOURCE, "including %s due to %s", parentSource, source); + TtrpgConfig.includeAdditionalSource(parentSource); + } } case itemGroup -> { addAlias(key.replace("itemgroup|", "item|"), key); @@ -300,11 +311,10 @@ public void prepare() { tui().debugf("Preparing index using configuration:\n%s", Tui.jsonStringify(config)); - tui().progressf("Adding subraces (2014)"); // Add subraces to index defineSubraces(); - tui().progressf("Resolving copies and link sources"); + tui().progressf("Resolving copies and linking sources"); // Find remaining/included base items List baseItems = nodeIndex.values().stream() @@ -312,81 +322,57 @@ public void prepare() { .filter(n -> !ItemField.packContents.existsIn(n)) .toList(); - // Bring in parent adventures (before sources are created) - nodeIndex.values().stream() - .filter(n -> Tools5eFields.parentSource.existsIn(n)) - .forEach(n -> { - String source = SourceField.source.getTextOrEmpty(n); - String parentSource = Tools5eFields.parentSource.getTextOrNull(n); - if (TtrpgConfig.getConfig().sourceIncluded(source)) { - // include the parent source if you include an adventure (related rules) - tui().debugf(Msg.SOURCE, "including %s due to %s", parentSource, source); - TtrpgConfig.includeAdditionalSource(parentSource); - } - }); - - List variants = new ArrayList<>(); + List keys = new ArrayList<>(nodeIndex.keySet()); + List deities = new ArrayList<>(); // For each node: handle copies, link sources - for (Entry entry : nodeIndex.entrySet()) { - String key = entry.getKey(); - JsonNode jsonSource = entry.getValue(); + for (String key : keys) { + JsonNode jsonSource = nodeIndex.get(key); // check for / manage copies first. Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); jsonSource = copier.handleCopy(type, jsonSource); + nodeIndex.put(key, jsonSource); // update value with resolved/copied node // Pre-creation of sources.. - if (type == Tools5eIndexType.adventureData || type == Tools5eIndexType.bookData) { - // changes name and things used when constructing sources - linkSources(type, jsonSource); + switch (type) { + case adventureData, bookData -> linkSources(type, jsonSource); + default -> { + } } Tools5eSources.constructSources(key, jsonSource); - entry.setValue(jsonSource); // update with resolved copy + + if (type == Tools5eIndexType.deity) { + deities.add(new Tuple(key, jsonSource)); + continue; // deal with these later. + } + + // Reprints follow specialized variants, so we need to find the variants + // now (and will filter them out based on rules later...) + if (type.hasVariants()) { + List variants = findVariants(key, jsonSource, baseItems); + for (JsonNode variant : variants) { + String variantKey = TtrpgValue.indexKey.getTextOrThrow(variant); + Tools5eSources.constructSources(variantKey, variant); + JsonNode old = nodeIndex.put(variantKey, variant); + if (old != null && !old.equals(variant)) { + tui().errorf("Duplicate key: %s%nold: %s%nnew: %s", variantKey, old, variant); + } + } + } // Post-creation of sources.. switch (type) { case classtype, subclass -> optFeatureIndex.amendSources(key, jsonSource); case optfeature -> optFeatureIndex.amendSources(key, jsonSource); - case classfeature -> { - String classKey = Tools5eIndexType.classtype.fromChildKey(key); - JsonNode classNode = nodeIndex.get(classKey); - if (classNode != null) { - JsonNode featureKeys = Tools5eFields.classFeatureKeys.ensureArrayIn(classNode).add(key); - Tools5eFields.classFeatureKeys.setIn(jsonSource, featureKeys); - } - } - case subclassFeature -> { - // don't follow reprints, just go from shortname to subclass name - String scKey = Tools5eIndexType.subclass.fromChildKey(key); - scKey = getAliasOrDefault(scKey, false); - JsonNode scNode = nodeIndex.get(scKey); - if (scNode != null) { - JsonNode featureKeys = Tools5eFields.classFeatureKeys.ensureArrayIn(scNode).add(key); - Tools5eFields.classFeatureKeys.setIn(jsonSource, featureKeys); - } - } default -> { } } - - // Reprints do follow specialized variants, so we need to find the variants - // now (and will filter them out based on rules later...) - if (type.hasVariants()) { - variants.addAll(findVariants(key, jsonSource, baseItems)); - } } // end for each entry - for (JsonNode variant : variants) { - String variantKey = TtrpgValue.indexKey.getTextOrThrow(variant); - nodeIndex.put(variantKey, variant); - } - variants.clear(); - - filteredIndex = new HashMap<>(nodeIndex.size()); - tui().progressf("Applying source filters"); + filteredIndex = new HashMap<>(nodeIndex.size()); BiConsumer logThis = (msgType, msg) -> { if (msgType == Msg.TARGET) { @@ -396,37 +382,78 @@ public void prepare() { } }; - // Apply include/exclude rules & source filters + // Let's create a list of interesting keys + List interestingKeys = new ArrayList<>(nodeIndex.size()); for (var e : nodeIndex.entrySet()) { String key = e.getKey(); + JsonNode jsonSource = e.getValue(); Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); - // construct source if missing (which it may be for a variant) - Tools5eSources sources = Tools5eSources.constructSources(key, e.getValue()); + if (false + // Fluff types can continue to live only in the origin/nodeIndex + || type.isFluffType() + // Checking for reprints has aliasing knock-ons. + || isReprinted(key, jsonSource) + // While Deities are interesting, their handling is unique and done later + || type == Tools5eIndexType.deity + // Subclasses are also handled backwards (filled in by subclass features) + || type == Tools5eIndexType.subclass) { + // Theses are uninteresting. + } else { + interestingKeys.add(key); + } + } + + // Apply include/exclude rules & source filters + // to add included elements to the filter index + for (String key : interestingKeys) { + JsonNode jsonSource = getOriginNoFallback(key); + Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); + Tools5eSources sources = Tools5eSources.findSources(key); + if (sources == null) { + // This is programmer error. + tui().logf(Msg.SOURCE, "No sources found for %s", key); + continue; + } Msg msgType = sources.filterRuleApplied() ? Msg.TARGET : Msg.FILTER; - if (type.isFluffType()) { - // no-op - } else if (type.isDependentType() && msgType != Msg.TARGET) { - // keep dependent types unless there is a specific rule - filteredIndex.put(key, e.getValue()); + if (type.isDependentType()) { + // dependent types: don't keep if parent is excluded/missing + if (processDependentType(key)) { + logThis.accept(msgType, " ---- " + key); + filteredIndex.put(key, jsonSource); + } else { + logThis.accept(msgType, "(drop) " + key); + } } else if (sources.includedByConfig()) { - filteredIndex.put(key, e.getValue()); - logThis.accept(msgType, "------ " + key); - } else if (type.isOutputType()) { + // key is included (by a specific rule, or because the source is included) + filteredIndex.put(key, jsonSource); + logThis.accept(msgType, " ---- " + key); + + if (type == Tools5eIndexType.spell) { + // Create a spell entry for included spell + spellIndex.addSpell(key, jsonSource); + } + } else { + // source is not included, item is dropped logThis.accept(msgType, "(drop) " + key); } } - // Remove reprints based on included sources (and reprint behavior) - // If someone includes MM, but not MPMM, you want the MM version - tui().progressf("Resolving reprints"); - filteredIndex.entrySet().removeIf(e -> isReprinted(e.getKey(), e.getValue())); - - // Follow inclusion of certain types to remove additional related elements - tui().progressf("Removing dependent and dangling resources"); - filteredIndex.keySet().removeIf(k -> otherwiseExcluded(k)); + // classFeatures contains both features and subclass features + for (var entry : classFeatures.entrySet()) { + String scKey = entry.getKey(); + if (scKey.startsWith("subclass")) { + if (entry.getValue().isEmpty()) { + // no features associated with this subclass + logThis.accept(Msg.CLASSES, "(drop | no subclass features) " + scKey); + } else { + logThis.accept(Msg.CLASSES, " ---- " + scKey); + filteredIndex.put(scKey, nodeIndex.get(scKey)); + } + } + } - // Use the OptionalFeature index to remove unused optional features + // Remove unused optional features from the optional feature index optFeatureIndex.removeUnusedOptionalFeatures( (k) -> filteredIndex.containsKey(k), (k) -> { @@ -438,82 +465,13 @@ public void prepare() { filteredIndex.remove(k); }); - // Bubble-up: enabled subclasses, class features, and subclass features - // add themselves to their parents - for (var entry : filteredIndex.entrySet()) { - String entryKey = entry.getKey(); - - var type = Tools5eIndexType.getTypeFromKey(entryKey); - if (type == Tools5eIndexType.subclass - || type == Tools5eIndexType.classfeature - || type == Tools5eIndexType.subclassFeature) { - var parentType = switch (type) { - case subclass -> Tools5eIndexType.classtype; - case classfeature -> Tools5eIndexType.classtype; - case subclassFeature -> Tools5eIndexType.subclass; - default -> null; - }; - ClassFields targetField = switch (type) { - case subclass -> ClassFields.subclassKeys; - case classfeature -> ClassFields.featureKeys; - case subclassFeature -> ClassFields.featureKeys; - default -> null; - }; - String parentKey = getAliasOrDefault(parentType.fromChildKey(entryKey)); - JsonNode parent = getOriginNoFallback(parentKey); - ArrayNode target = targetField.ensureArrayIn(parent); - target.add(entryKey); - targetField.setIn(parent, target); - } else if (type == Tools5eIndexType.spell) { - // Create a spell entry for included spell - spellIndex.addSpell(entryKey, entry.getValue()); - } - } - - // One last pass through to remove more orphans - filteredIndex.entrySet().removeIf(e -> { - String key = e.getKey(); - Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); - // These are unreachable; they have no features or subclasses - // Have no explicit aliases, and no way to detect what could or - // should be aliased to them. Often |xphb|phb or |phb|xphb variants - if (type == Tools5eIndexType.classtype) { - JsonNode subclasses = ClassFields.subclassKeys.getFrom(e.getValue()); - JsonNode features = ClassFields.featureKeys.getFrom(e.getValue()); - if (isEmpty(subclasses) && isEmpty(features)) { - // UNLIKELY - tui().logf(Msg.CLASSES, "(drop | no features or subclasses) %s", key); - return true; - } - } else if (type == Tools5eIndexType.subclass) { - JsonNode features = ClassFields.featureKeys.getFrom(e.getValue()); - if (isEmpty(features)) { - // These are abandoned |xphb|phb or |phb|xphb mixed classes that - // are naturally skipped when resolving aliases above. - // Remove them so they don't also mess with spells - tui().logf(Msg.CLASSES, "(drop | no subclass features) %s", key); - return true; - } - } - return false; - }); - // Deities have their own glorious reprint mess, which we only need to deal with // when we aren't hoarding all the things. - if (config.reprintBehavior() != ReprintBehavior.all) { - tui().progressf("Dealing with deities"); - - List allDeities = filteredIndex.entrySet().stream() - .filter(e -> Tools5eIndexType.getTypeFromKey(e.getKey()) == Tools5eIndexType.deity) - .map(e -> new Tuple(e.getKey(), e.getValue())) - .toList(); - - // Remove deities that should be removed (superceded) - Json2QuteDeity.findDeitiesToRemove(allDeities).forEach(k -> { - tui().logf(Msg.DEITY, "(drop | superseded) %s", k); - filteredIndex.remove(k); - }); - } + tui().progressf("Dealing with deities"); + // Find deities that have not been superceded by a reprint + Json2QuteDeity.findDeities(deities).forEach(k -> { + filteredIndex.put(k, nodeIndex.get(k)); + }); // And finally, create an index of classes/subclasses/feats for spells // based on included sources & avaiable spells. @@ -521,6 +479,7 @@ public void prepare() { } private void defineSubraces() { + tui().progressf("Adding subraces"); for (Entry> entry : subraceIndex.entrySet()) { String raceKey = entry.getKey(); JsonNode jsonSource = nodeIndex.get(raceKey); @@ -656,7 +615,7 @@ private boolean isReprinted(String finalKey, JsonNode jsonSource) { } // Otherwise, we have a "newer" reprint that should be used instead - tui().logf(Msg.REPRINT, "(drop | reprinted) %s ==> %s", finalKey, reprintKey); + tui().logf(Msg.REPRINT, "(--->| reprinted) %s ==> %s", finalKey, reprintKey); // 1) create an alias mapping the old key to the reprinted key reprints.put(finalKey, reprintKey); // 2) add the sources of the reprint to the sources of the original (for later linking) @@ -666,8 +625,8 @@ private boolean isReprinted(String finalKey, JsonNode jsonSource) { } } if (SourceField.isReprinted.booleanOrDefault(jsonSource, false)) { - tui().logf(Msg.REPRINT, "(drop | isReprint) %s", finalKey); - return true; // the reprint will be used instead of this one. + tui().logf(Msg.REPRINT, "(--->| isReprint) %s", finalKey); + return true; // this is a reprint, but we have no alias.. } return false; // keep } @@ -675,51 +634,109 @@ private boolean isReprinted(String finalKey, JsonNode jsonSource) { /** * Filter sub-resources based on the inclusion of the parent resource. * - * @return true if resource has a parent, and that parent is excluded + * @return true if resource should be kept (not used in a filter) */ - private boolean otherwiseExcluded(String key) { - // If a class is excluded, specific classfeatures, optional features, - // subclasses, and subclassfeatures should also be removed - // (unless a specific rule says otherwise). - Tools5eSources sources = Tools5eSources.findSources(key); - if (sources.filterRuleApplied()) { - return false; // keep because a rule says so (we already logged these) - } - + private boolean processDependentType(final String key) { Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key); - return switch (type) { - case card -> removeIfParentExcluded(key, type, Tools5eIndexType.deck, Msg.DECK); - case classfeature -> removeIfParentExcluded(key, type, - Tools5eIndexType.classtype, Msg.CLASSES); - case subclassFeature -> removeIfParentExcluded(key, type, - Tools5eIndexType.subclass, Msg.CLASSES) - || removeIfParentExcluded(key, type, - Tools5eIndexType.classtype, Msg.CLASSES); - case subclass -> !sources.includedByConfig() - || removeIfParentExcluded(key, type, Tools5eIndexType.classtype, Msg.CLASSES); - case subrace -> !sources.includedByConfig() - || removeIfParentExcluded(key, type, Tools5eIndexType.race, Msg.RACES); - default -> false; // does not have a parent - }; - } + switch (type) { + case optionalFeatureTypes -> { + // optionalFeatureTypes are always included + return true; + } + case classfeature -> { + // classfeature is reliably tied to the class + // classfeature|ability score improvement|barbarian|phb|8|phb + // classfeature|ability score improvement|barbarian|xphb|12|xphb + String classKey = Tools5eIndexType.classtype.fromChildKey(key); + boolean reprinted = reprints.containsKey(classKey); + if (!reprinted && Tools5eSources.includedByConfig(classKey)) { + // Only keep the class feature if the parent class is not a reprint. + classFeatures.computeIfAbsent(classKey, k -> new HashSet<>()).add(key); + return true; // keep it + } + } + case subclassFeature -> { + // This is where things go sideways + // For example, these two versions of a subclass feature exists: + // subclassfeature|zealous presence|barbarian|phb|zealot|xge|10|xge + // subclassfeature|zealous presence|barbarian|xphb|zealot|xphb|10|xphb + // usually reachable through the matching subclass + // subclass|path of the zealot|barbarian|phb|xge + // subclass|path of the zealot|barbarian|xphb|xphb + // which relies on reprint behavior to resolve, if xphb is around + // subclass|path of the zealot|barbarian|phb|xge -> subclass|path of the zealot|barbarian|xphb|xphb + // subclass|path of the zealot|barbarian|xphb|xge -> subclass|path of the zealot|barbarian|xphb|xphb + String scfKey = key; + SubclassFeatureKeyData keyData = new SubclassFeatureKeyData(key); + + // does the subclass exist or is it a reprint + String scKey = getSubclassKey(keyData.toSubclassKey()); + boolean scIncluded = Tools5eSources.includedByConfig(scKey); + boolean scReprint = reprints.containsKey(scKey); + + if (scReprint) { + // the subclass (including its features) was reprinted. + return false; // remove it + } + + // does the parent class exist or is it a reprint + String classKey = keyData.toClassKey(); + boolean classIncluded = Tools5eSources.includedByConfig(classKey); + String classReprint = reprints.get(classKey); + + tui().debugf(Msg.CLASSES, "%s\n\t(%5s) %s -> %s\n\t(%5s) %s -> %s", key, + classReprint, classKey, classReprint, + scReprint, scKey, reprints.get(scKey)); + + if (classReprint != null) { + // This is the most common case: PHB -> XPHB + // the reprint behavior will handle this + Tools5eSources altSources = Tools5eSources.findSources(classReprint); + classIncluded = altSources != null && altSources.includedByConfig(); + if (!classIncluded) { + return false; // remove it, can't fix it + } + + // We found the class reprint. + // The reprinted class is the new resource anchor for generated notes + // Change the class source for the subclass feature + keyData.classSource = altSources.primarySource(); + + // is there a subclass key with this new class source? + var altScKey = getSubclassKey(keyData.toSubclassKey()); + boolean altScPresent = Tools5eSources.includedByConfig(altScKey); + if (altScPresent) { + // This is the sometimes-covered case: + // subclass|path of wild magic|barbarian|xphb|tce + // reset all the things to hit the happy path below + tui().debugf("subclassFeature subclass: %s -> %s", scKey, altScKey); + scfKey = keyData.toKey(); + scKey = altScKey; + classKey = classReprint; + scIncluded = altScPresent; + } else { + // There are times this case is not covered, especially in homebrew, for example: + // subclassfeature|adamantine hide|druid|phb|forged|exploringeberron|10|exploringeberron + // this subclassfeature is included, but there is no mapping to an xphb version of the subclass. + // + // The reset classKey will force the issue. If the subclass is also present/included, + // then it will be added to the adjusted class. + tui().debugf("subclassFeature oddball: %s -> %s", scKey, altScKey); + } + } - private boolean removeIfParentExcluded(String key, Tools5eIndexType type, Tools5eIndexType parentType, Msg msg) { - String parentKey = parentType.fromChildKey(key); - Tools5eSources parentSources = Tools5eSources.findSources(parentKey); - if (parentSources == null) { - // allow for corrections (aliases), not reprints - parentKey = getAliasOrDefault(parentKey, false); - parentSources = Tools5eSources.findSources(parentKey); - if (parentSources == null) { - tui().warnf(Msg.UNRESOLVED, "%35s :: unresolved parent of [%s]", parentKey, key); - return true; // has a parent, it is missing (dangling resource) + if (classIncluded && scIncluded) { + // keep the subclass feature if both the class and subclass are included + subclassMap.computeIfAbsent(classKey, k -> new HashSet<>()).add(scKey); + classFeatures.computeIfAbsent(scKey, k -> new HashSet<>()).add(scfKey); + return true; + } + } + default -> { + // no-op } } - boolean filterIncluded = filteredIndex.containsKey(parentKey); - if (!filterIncluded) { - tui().logf(msg, "(drop) %43s :: %s", parentKey, key); - } - return !filterIncluded; + return false; // remove it! } public boolean notPrepared() { @@ -765,6 +782,11 @@ void addAlias(String key, String alias) { } } + public String getSubclassKey(String targetKey) { + // short name to long name without following reprints. + return getAliasOrDefault(targetKey, false); + } + public List getAliasesFor(String targetKey) { return aliases.entrySet().stream() .filter(e -> e.getValue().equals(targetKey)) @@ -914,6 +936,14 @@ public SpellSchool findSpellSchool(String code, Tools5eSources sources) { return school; } + public Set findSubclasses(String classKey) { + return subclassMap.getOrDefault(classKey, Set.of()); + } + + public Set findClassFeatures(String classOrSubclassKey) { + return classFeatures.getOrDefault(classOrSubclassKey, Set.of()); + } + public JsonNode findTable(SourceAndPage sourceAndPage, String rowData) { List tables = tableIndex.get(sourceAndPage); if (tables != null) { @@ -1035,11 +1065,10 @@ public String linkifyByName(Tools5eIndexType type, String name) { public boolean customContentIncluded() { // The biggest hack of all time (not really). // I have some custom content for types/property/mastery that - // should be included, but only if: - // 1. No content is included (srdOnly) - // 2. Some combination of basic rules and/or phb/dmg is included - return srdOnly() || - config.sourcesIncluded(List.of( + // should be included, but only if some combination of + // basic/free rules, srd, phb or dmg is included + return config.noSources() + || config.sourcesIncluded(List.of( "srd", "basicRules", "phb", "dmg", "srd52", "freerules2024", "xphb", "xdmg")); } @@ -1092,13 +1121,15 @@ public void writeFullIndex(Path outputFile) throws IOException { if (notPrepared()) { throw new IllegalStateException("Index must be prepared before writing indexes"); } - Map allKeys = new TreeMap<>(); - allKeys.put("keys", new TreeSet<>(nodeIndex.keySet())); - allKeys.put("mapping", new TreeMap<>(aliases)); - allKeys.put("reprints", new TreeMap<>(reprints)); - allKeys.put("srdKeys", new TreeSet<>(srdKeys)); - allKeys.put("subraceMap", new TreeMap<>(subraceMap)); + Map allKeys = new LinkedHashMap<>(); + allKeys.put("keys", nodeIndex.keySet()); + allKeys.put("mapping", aliases); + allKeys.put("reprints", reprints); + allKeys.put("subraceMap", subraceMap); + allKeys.put("subclassMap", subclassMap); + allKeys.put("classFeatures", classFeatures); allKeys.put("optionalFeatures", optFeatureIndex.getMap()); + allKeys.put("srdKeys", srdKeys); tui().writeJsonFile(outputFile, allKeys); } diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java index 4197015e0..d692cdbdb 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java @@ -649,12 +649,10 @@ boolean isDependentType() { // These types are not directly filtered. // Special rules are applied after the parent item is filtered return switch (this) { - case card, - classfeature, + case classfeature, optionalFeatureTypes, subclass, - subclassFeature, - subrace -> + subclassFeature -> true; default -> false; }; diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java index 374b8f45e..32d89d20d 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java @@ -284,6 +284,19 @@ private boolean testSourceRules(CompendiumConfig config, Optional rules || (config.sourceIncluded("freerules2024") && this.freeRules2024); } + @Override + public boolean includedBy(Set sources) { + CompendiumConfig config = TtrpgConfig.getConfig(); + if (config.noSources()) { + return this.srd || this.basicRules || this.srd52 || this.freeRules2024; + } + return super.includedBy(sources) || + (this.basicRules && sources.contains("basicrules")) || + (this.srd && sources.contains("srd")) || + (this.srd52 && sources.contains("srd52")) || + (this.freeRules2024 && sources.contains("freerules2024")); + } + @Override public Tools5eIndexType getType() { return type; @@ -490,16 +503,6 @@ public void amendHomebrewSources(JsonNode homebrewElement) { testSourceRules(); } - @Override - public boolean includedBy(Set sources) { - return super.includedBy(sources) || - (this.basicRules && sources.contains("basicrules")) || - (this.srd && sources.contains("srd")) || - (this.srd52 && sources.contains("srd52")) || - (this.freeRules2024 && sources.contains("freerules2024")) || - (TtrpgConfig.getConfig().noSources() && (this.srd || this.basicRules)); - } - public boolean contains(Tools5eSources sources) { Collection sourcesList = sources.getSources(); return this.sources.stream().anyMatch(sourcesList::contains); diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java index 1c58cfd89..046c3ed50 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java @@ -10,6 +10,8 @@ import java.util.List; import java.util.stream.Stream; +import com.fasterxml.jackson.databind.JsonNode; + import dev.ebullient.convert.TestUtils; import dev.ebullient.convert.config.CompendiumConfig; import dev.ebullient.convert.config.CompendiumConfig.Configurator; @@ -33,6 +35,7 @@ public class CommonDataTests { public final Tools5eIndex index; public final TestInput variant; + public final CompendiumConfig config; enum TestInput { all, @@ -43,10 +46,11 @@ enum TestInput { srd2024, subset2014, subset2024, + subsetMixed, ; } - public CommonDataTests(TestInput variant, Path toolsData) throws Exception { + public CommonDataTests(TestInput variant, String config, Path toolsData) throws Exception { this.toolsData = toolsData; dataPresent = toolsData.toFile().exists(); @@ -68,97 +72,22 @@ public CommonDataTests(TestInput variant, Path toolsData) throws Exception { if (dataPresent) { templates.setCustomTemplates(TtrpgConfig.getConfig()); - var additional = new ArrayList<>(List.of("adventures.json", "books.json")); - switch (variant) { - case none -> { - // do nothing. SRD content.. newest of all editions (so 2024) - } - case noneEdition -> { - // no content specified (just SRD) - // Do not follow reprints across editions - var o = Tui.MAPPER.createObjectNode() - .put("reprintBehavior", "edition"); - configurator.readConfigIfPresent(o); - } - case srd2014 -> { - // only 2014 - var o = Tui.MAPPER.createObjectNode() - .set("sources", Tui.MAPPER.createObjectNode() - .set("reference", Tui.MAPPER.createArrayNode() - .add("srd").add("basicrules"))); - configurator.readConfigIfPresent(o); - } - case srd2024 -> { - // only 2024 - var o = Tui.MAPPER.createObjectNode() - .set("sources", Tui.MAPPER.createObjectNode() - .set("reference", Tui.MAPPER.createArrayNode() - .add("srd52").add("freerules2024"))); - configurator.readConfigIfPresent(o); - } - case subset2014 -> { - var o = Tui.MAPPER.createObjectNode() - .set("sources", Tui.MAPPER.createObjectNode() - .set("reference", Tui.MAPPER.createArrayNode() - .add("mm").add("tce").add("xge"))); - configurator.readConfigIfPresent(o); - - additional.addAll(List.of( - "adventure/adventure-lmop.json", - "book/book-dmg.json", - "book/book-mm.json", - "book/book-phb.json")); - } - case subset2024 -> { - var o = Tui.MAPPER.createObjectNode() - .set("sources", Tui.MAPPER.createObjectNode() - .set("reference", Tui.MAPPER.createArrayNode() - .add("mpmm"))); - configurator.readConfigIfPresent(o); - - additional.addAll(List.of( - "adventure/adventure-dsotdq.json", - "book/book-tdcsr.json", - "book/book-xphb.json", - "book/book-xdmg.json")); - } - case allNewest -> { - // default behavior: newest only - configurator.addSources(List.of("*")); - additional.addAll(List.of( - "adventure/adventure-wdh.json", - "adventure/adventure-pota.json", - "book/book-vgm.json", - "book/book-phb.json", "book/book-xphb.json", - "book/book-dmg.json", "book/book-xdmg.json")); - } - case all -> { - configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("paths.json")); - // add book/adventure (beyond reference material) - configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve("5e/sources.json")); - configurator.addSources(List.of("*")); - - additional.addAll(List.of( - "adventure/adventure-wdh.json", "adventure/adventure-pota.json", - "book/book-vgm.json", - "book/book-phb.json", "book/book-xphb.json", - "book/book-dmg.json", "book/book-xdmg.json")); - - // Literally all. Ignore reprints - var o = Tui.MAPPER.createObjectNode() - .put("reprintBehavior", "all"); - configurator.readConfigIfPresent(o); - } - } + JsonNode configNode = Tui.MAPPER.readTree(config); + configurator.readConfigIfPresent(configNode); - for (String x : additional) { - tui.readFile(toolsData.resolve(x), TtrpgConfig.getFixes(x), index::importTree); - } + // var additional = List.of( + // "adventures.json", + // "books.json"); + // for (String x : additional) { + // tui.readFile(toolsData.resolve(x), TtrpgConfig.getFixes(x), index::importTree); + // } tui.readToolsDir(toolsData, index::importTree); + index.resolveSources(toolsData); index.prepare(); } + this.config = TtrpgConfig.getConfig(); } public void afterEach() throws Exception { diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java index 59fe8c6ba..483a7b5bf 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.nio.file.Path; @@ -22,8 +24,19 @@ public class FilterAllNewestTest { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + String config = """ + { + "sources": { + "reference": [ + "*" + ] + }, + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -43,43 +56,71 @@ public void testKeyIndex() throws Exception { // All sources, but things that have been reprinted will be replaced by the newest version // e.g. PHB elements should be missing/replaced by XPHB equivalents if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.sourceIncluded("srd")).isTrue(); + assertThat(config.sourceIncluded("basicrules")).isTrue(); + assertThat(config.sourceIncluded("srd52")).isTrue(); + assertThat(config.sourceIncluded("freerules2024")).isTrue(); + + assertThat(config.sourceIncluded("DMG")).isTrue(); + assertThat(config.sourceIncluded("PHB")).isTrue(); + + assertThat(config.sourceIncluded("XDMG")).isTrue(); + assertThat(config.sourceIncluded("XPHB")).isTrue(); + commonTests.assert_MISSING("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); commonTests.assert_MISSING("action|cast a spell|phb"); commonTests.assert_MISSING("action|disengage|phb"); commonTests.assert_Present("action|disengage|xphb"); + + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_Present("feat|dueling|xphb"); + commonTests.assert_MISSING("feat|grappler|phb"); + commonTests.assert_Present("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_Present("feat|moderately armored|xphb"); + + commonTests.assert_Present("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_Present("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); - commonTests.assert_Present("classtype|artificer|tce"); - commonTests.assert_MISSING("classtype|bard|phb"); - commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_Present("deity|auril|faerûnian|scag"); commonTests.assert_Present("deity|auril|forgotten realms|phb"); commonTests.assert_Present("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); - commonTests.assert_Present("deity|the mockery|eberron|erlw"); - commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|phb"); + commonTests.assert_Present("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_Present("deity|gruumsh|dawn war|dmg"); // different pantheon + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); // different pantheon + commonTests.assert_Present("deity|gruumsh|nonhuman|phb"); // superseded + commonTests.assert_MISSING("deity|gruumsh|orc|scag"); // superseded + commonTests.assert_Present("deity|gruumsh|orc|vgm"); // keep this one commonTests.assert_Present("deity|the traveler|eberron|erlw"); commonTests.assert_MISSING("deity|the traveler|eberron|phb"); commonTests.assert_MISSING("deity|the traveler|exandria|egw"); commonTests.assert_Present("deity|the traveler|exandria|tdcsr"); + commonTests.assert_MISSING("disease|cackle fever|dmg"); commonTests.assert_Present("disease|cackle fever|xdmg"); - commonTests.assert_MISSING("feat|alert|phb"); - commonTests.assert_Present("feat|alert|xphb"); - commonTests.assert_Present("feat|dueling|xphb"); - commonTests.assert_MISSING("feat|grappler|phb"); - commonTests.assert_Present("feat|grappler|xphb"); - commonTests.assert_MISSING("feat|mobile|phb"); - commonTests.assert_MISSING("feat|moderately armored|phb"); - commonTests.assert_Present("feat|moderately armored|xphb"); + commonTests.assert_Present("hazard|quicksand pit|xdmg"); commonTests.assert_MISSING("hazard|quicksand|dmg"); commonTests.assert_MISSING("hazard|razorvine|dmg"); commonTests.assert_Present("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); commonTests.assert_Present("itemgroup|arcane focus|xphb"); commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); @@ -92,14 +133,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("itemgroup|musical instrument|xphb"); commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); commonTests.assert_Present("itemproperty|2h|xphb"); commonTests.assert_MISSING("itemproperty|bf|dmg"); commonTests.assert_Present("itemproperty|bf|xdmg"); + commonTests.assert_Present("itemproperty|er|tdcsr"); + commonTests.assert_Present("itemproperty|s|phb"); + commonTests.assert_MISSING("itemtype|$c|phb"); commonTests.assert_Present("itemtype|$c|xphb"); commonTests.assert_MISSING("itemtype|$g|dmg"); commonTests.assert_Present("itemtype|$g|xdmg"); + commonTests.assert_MISSING("itemtype|sc|dmg"); + commonTests.assert_Present("itemtype|sc|xphb"); + commonTests.assert_Present("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); commonTests.assert_Present("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_Present("item|+2 wraps of unarmed power|xdmg"); @@ -125,9 +174,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("item|ball bearings|xphb"); commonTests.assert_Present("item|ball bearing|phb"); commonTests.assert_MISSING("item|chain (10 feet)|phb"); - commonTests.assert_MISSING("item|chain mail|phb"); - commonTests.assert_Present("item|chain mail|xphb"); commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_Present("monster|abjurer wizard|mpmm"); commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_Present("monster|alkilith|mpmm"); @@ -150,22 +198,15 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|derro savant|oota"); commonTests.assert_Present("monster|sibriex|mpmm"); commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); commonTests.assert_Present("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); commonTests.assert_Present("optfeature|ambush|xphb"); commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); commonTests.assert_Present("optfeature|investment of the chain master|xphb"); - commonTests.assert_MISSING("race|bugbear|erlw"); - commonTests.assert_Present("race|bugbear|mpmm"); - commonTests.assert_MISSING("race|bugbear|vgm"); - commonTests.assert_MISSING("race|human|phb"); - commonTests.assert_Present("race|human|xphb"); - commonTests.assert_MISSING("race|tiefling|phb"); - commonTests.assert_Present("race|tiefling|xphb"); - commonTests.assert_Present("race|warforged|erlw"); - commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); - commonTests.assert_Present("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); commonTests.assert_Present("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); @@ -175,10 +216,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("reward|boon of fate|dmg"); commonTests.assert_MISSING("reward|boon of fortitude|dmg"); commonTests.assert_Present("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); commonTests.assert_Present("spell|acid splash|xphb"); commonTests.assert_Present("spell|aganazzar's scorcher|xge"); @@ -189,18 +233,10 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("spell|illusory script|phb"); commonTests.assert_Present("spell|illusory script|xphb"); commonTests.assert_Present("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); - commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); - commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_MISSING("subrace|human|human|phb|phb"); - commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); - commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_MISSING("trap|collapsing roof|dmg"); commonTests.assert_Present("trap|collapsing roof|xdmg"); commonTests.assert_MISSING("trap|falling net|dmg"); @@ -212,13 +248,73 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|rolling sphere|dmg"); commonTests.assert_Present("trap|rolling stone|xdmg"); - commonTests.assert_Present("variantrule|facing|dmg"); - commonTests.assert_MISSING("variantrule|falling|xge"); - commonTests.assert_Present("variantrule|familiars|mm"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); - commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|dmg"); commonTests.assert_Present("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_Present("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_MISSING("classtype|barbarian|phb"); + commonTests.assert_Present("classtype|barbarian|xphb"); + + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_Present("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_Present("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_Present("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_Present("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_Present("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_MISSING("classtype|rogue|phb"); + commonTests.assert_Present("classtype|rogue|xphb"); + + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_MISSING("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_Present("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_MISSING("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_MISSING("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_MISSING("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_Present("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_MISSING("race|tiefling|phb"); + commonTests.assert_Present("race|tiefling|xphb"); + commonTests.assert_Present("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_Present("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_Present("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); } } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java index 5a472af71..3e026fc81 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java @@ -27,8 +27,42 @@ public class FilterAllTest { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + // extra escaping for regex as we're reading from string + String config = """ + { + "reprintBehavior": "all", + "sources": { + "book": [ + "XGE" + ], + "adventure": [ + "OotA" + ], + "reference": [ + "*" + ] + }, + "include": [ + "race|changeling|mpmm" + ], + "exclude": [ + "monster|expert|dc", + "monster|expert|sdw", + "monster|expert|slw" + ], + "excludePattern": [ + "race\\\\|.*\\\\|dmg" + ], + "paths": { + "rules": "rules/", + "compendium": "" + }, + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -48,43 +82,71 @@ public void testKeyIndex() throws Exception { // All sources, but reprints will be followed. // PHB elements should be missing/replaced by XPHB equivalents (e.g.) if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.sourceIncluded("srd")).isTrue(); + assertThat(config.sourceIncluded("basicrules")).isTrue(); + assertThat(config.sourceIncluded("srd52")).isTrue(); + assertThat(config.sourceIncluded("freerules2024")).isTrue(); + + assertThat(config.sourceIncluded("DMG")).isTrue(); + assertThat(config.sourceIncluded("PHB")).isTrue(); + + assertThat(config.sourceIncluded("XDMG")).isTrue(); + assertThat(config.sourceIncluded("XPHB")).isTrue(); + commonTests.assert_Present("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); commonTests.assert_Present("action|cast a spell|phb"); commonTests.assert_Present("action|disengage|phb"); commonTests.assert_Present("action|disengage|xphb"); + + commonTests.assert_Present("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_Present("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_Present("feat|grappler|xphb"); + commonTests.assert_Present("feat|mobile|phb"); + commonTests.assert_Present("feat|moderately armored|phb"); + commonTests.assert_Present("feat|moderately armored|xphb"); + + commonTests.assert_Present("variantrule|facing|dmg"); + commonTests.assert_Present("variantrule|falling|xge"); + commonTests.assert_Present("variantrule|familiars|mm"); + commonTests.assert_Present("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_Present("background|baldur's gate acolyte|bgdia"); - commonTests.assert_Present("classtype|artificer|tce"); - commonTests.assert_Present("classtype|bard|phb"); - commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_Present("condition|blinded|phb"); commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_Present("deity|auril|faerûnian|scag"); commonTests.assert_Present("deity|auril|forgotten realms|phb"); commonTests.assert_Present("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_Present("deity|chemosh|dragonlance|phb"); - commonTests.assert_Present("deity|the mockery|eberron|erlw"); - commonTests.assert_Present("deity|the mockery|eberron|phb"); + commonTests.assert_Present("deity|ehlonna|greyhawk|phb"); + commonTests.assert_Present("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_Present("deity|gruumsh|dawn war|dmg"); + commonTests.assert_Present("deity|gruumsh|exandria|egw"); + commonTests.assert_Present("deity|gruumsh|nonhuman|phb"); + commonTests.assert_Present("deity|gruumsh|orc|scag"); + commonTests.assert_Present("deity|gruumsh|orc|vgm"); commonTests.assert_Present("deity|the traveler|eberron|erlw"); commonTests.assert_Present("deity|the traveler|eberron|phb"); commonTests.assert_Present("deity|the traveler|exandria|egw"); commonTests.assert_Present("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); commonTests.assert_Present("disease|cackle fever|xdmg"); - commonTests.assert_Present("feat|alert|phb"); - commonTests.assert_Present("feat|alert|xphb"); - commonTests.assert_Present("feat|dueling|xphb"); - commonTests.assert_Present("feat|grappler|phb"); - commonTests.assert_Present("feat|grappler|xphb"); - commonTests.assert_Present("feat|mobile|phb"); - commonTests.assert_Present("feat|moderately armored|phb"); - commonTests.assert_Present("feat|moderately armored|xphb"); + commonTests.assert_Present("hazard|quicksand pit|xdmg"); commonTests.assert_Present("hazard|quicksand|dmg"); commonTests.assert_Present("hazard|razorvine|dmg"); commonTests.assert_Present("hazard|razorvine|xdmg"); + commonTests.assert_Present("itemgroup|arcane focus|phb"); commonTests.assert_Present("itemgroup|arcane focus|xphb"); commonTests.assert_Present("itemgroup|carpet of flying|dmg"); @@ -97,14 +159,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("itemgroup|musical instrument|xphb"); commonTests.assert_Present("itemgroup|spell scroll|dmg"); commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_Present("itemproperty|2h|phb"); commonTests.assert_Present("itemproperty|2h|xphb"); commonTests.assert_Present("itemproperty|bf|dmg"); commonTests.assert_Present("itemproperty|bf|xdmg"); + commonTests.assert_Present("itemproperty|er|tdcsr"); + commonTests.assert_Present("itemproperty|s|phb"); + commonTests.assert_Present("itemtype|$c|phb"); commonTests.assert_Present("itemtype|$c|xphb"); commonTests.assert_Present("itemtype|$g|dmg"); commonTests.assert_Present("itemtype|$g|xdmg"); + commonTests.assert_Present("itemtype|sc|dmg"); + commonTests.assert_Present("itemtype|sc|xphb"); + commonTests.assert_Present("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_Present("item|+1 rod of the pact keeper|dmg"); commonTests.assert_Present("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_Present("item|+2 wraps of unarmed power|xdmg"); @@ -130,9 +200,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("item|ball bearings|xphb"); commonTests.assert_Present("item|ball bearing|phb"); commonTests.assert_Present("item|chain (10 feet)|phb"); - commonTests.assert_Present("item|chain mail|phb"); - commonTests.assert_Present("item|chain mail|xphb"); commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_Present("monster|abjurer wizard|mpmm"); commonTests.assert_Present("monster|abjurer|vgm"); commonTests.assert_Present("monster|alkilith|mpmm"); @@ -155,22 +224,15 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("monster|derro savant|oota"); commonTests.assert_Present("monster|sibriex|mpmm"); commonTests.assert_Present("monster|sibriex|mtf"); + commonTests.assert_Present("object|trebuchet|dmg"); commonTests.assert_Present("object|trebuchet|xdmg"); + commonTests.assert_Present("optfeature|ambush|tce"); commonTests.assert_Present("optfeature|ambush|xphb"); commonTests.assert_Present("optfeature|investment of the chain master|tce"); commonTests.assert_Present("optfeature|investment of the chain master|xphb"); - commonTests.assert_Present("race|bugbear|erlw"); - commonTests.assert_Present("race|bugbear|mpmm"); - commonTests.assert_Present("race|bugbear|vgm"); - commonTests.assert_Present("race|human|phb"); - commonTests.assert_Present("race|human|xphb"); - commonTests.assert_Present("race|tiefling|phb"); - commonTests.assert_Present("race|tiefling|xphb"); - commonTests.assert_Present("race|warforged|erlw"); - commonTests.assert_Present("race|yuan-ti pureblood|vgm"); - commonTests.assert_Present("race|yuan-ti|mpmm"); + commonTests.assert_Present("reward|blessing of weapon enhancement|dmg"); commonTests.assert_Present("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_Present("reward|blessing of wound closure|dmg"); @@ -180,10 +242,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("reward|boon of fate|dmg"); commonTests.assert_Present("reward|boon of fortitude|dmg"); commonTests.assert_Present("reward|boon of high magic|dmg"); + commonTests.assert_Present("sense|blindsight|phb"); commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_Present("skill|athletics|phb"); commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_Present("spell|acid splash|phb"); commonTests.assert_Present("spell|acid splash|xphb"); commonTests.assert_Present("spell|aganazzar's scorcher|xge"); @@ -194,18 +259,14 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("spell|illusory script|phb"); commonTests.assert_Present("spell|illusory script|xphb"); commonTests.assert_Present("spell|wrath of nature|xge"); + commonTests.assert_Present("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_Present("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_Present("subrace|human|human|phb|phb"); - commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_Present("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); - commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); commonTests.assert_Present("trap|collapsing roof|xdmg"); commonTests.assert_Present("trap|falling net|dmg"); @@ -217,13 +278,73 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("trap|poisoned darts|xdmg"); commonTests.assert_Present("trap|rolling sphere|dmg"); commonTests.assert_Present("trap|rolling stone|xdmg"); - commonTests.assert_Present("variantrule|facing|dmg"); - commonTests.assert_Present("variantrule|falling|xge"); - commonTests.assert_Present("variantrule|familiars|mm"); - commonTests.assert_Present("variantrule|simultaneous effects|xge"); - commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); commonTests.assert_Present("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_Present("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_Present("classtype|barbarian|phb"); + commonTests.assert_Present("classtype|barbarian|xphb"); + + commonTests.assert_Present("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_Present("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_Present("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_Present("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_Present("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_Present("classtype|rogue|phb"); + commonTests.assert_Present("classtype|rogue|xphb"); + + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_Present("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_Present("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_Present("race|bugbear|erlw"); + commonTests.assert_Present("race|bugbear|mpmm"); + commonTests.assert_Present("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); + commonTests.assert_Present("race|tiefling|xphb"); + commonTests.assert_Present("race|warforged|erlw"); + commonTests.assert_Present("race|yuan-ti pureblood|vgm"); + commonTests.assert_Present("race|yuan-ti|mpmm"); + + commonTests.assert_Present("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_Present("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_Present("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_Present("subrace|vampire (ixalan)|vampire|psz|psx"); } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java index 1495615b3..63c35f19a 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneEditionTest.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.nio.file.Path; @@ -22,8 +24,15 @@ public class FilterNoneEditionTest { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + String config = """ + { + "reprintBehavior": "edition", + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -40,45 +49,79 @@ public void cleanup() throws Exception { public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); + // NONE: resouces from 2024 freerules, 5.1 SRD, and 2014 basic rules + // without following reprints across editions. + // (2014 content will remain, alongside 2024 content) + if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.noSources()).isTrue(); + + assertThat(config.sourceIncluded("srd")).isFalse(); + assertThat(config.sourceIncluded("basicrules")).isFalse(); + assertThat(config.sourceIncluded("srd52")).isFalse(); + assertThat(config.sourceIncluded("freerules2024")).isFalse(); + + assertThat(config.sourceIncluded("DMG")).isFalse(); + assertThat(config.sourceIncluded("PHB")).isFalse(); + + assertThat(config.sourceIncluded("XDMG")).isFalse(); + assertThat(config.sourceIncluded("XPHB")).isFalse(); + commonTests.assert_MISSING("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); commonTests.assert_MISSING("action|cast a spell|phb"); commonTests.assert_MISSING("action|disengage|phb"); commonTests.assert_Present("action|disengage|xphb"); + + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); - commonTests.assert_MISSING("classtype|artificer|tce"); - commonTests.assert_Present("classtype|bard|phb"); - commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); commonTests.assert_Present("condition|blinded|xphb"); commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); - commonTests.assert_Present("deity|auril|forgotten realms|phb"); + commonTests.assert_Present("deity|auril|forgotten realms|phb"); // part of basic rules/srd commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); - commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); - commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_MISSING("deity|gruumsh|dawn war|dmg"); + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); + commonTests.assert_MISSING("deity|gruumsh|nonhuman|phb"); + commonTests.assert_MISSING("deity|gruumsh|orc|scag"); + commonTests.assert_MISSING("deity|gruumsh|orc|vgm"); commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); commonTests.assert_MISSING("deity|the traveler|eberron|phb"); commonTests.assert_MISSING("deity|the traveler|exandria|egw"); commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); // part of basic rules commonTests.assert_MISSING("disease|cackle fever|xdmg"); - commonTests.assert_MISSING("feat|alert|phb"); - commonTests.assert_Present("feat|alert|xphb"); - commonTests.assert_MISSING("feat|dueling|xphb"); - commonTests.assert_Present("feat|grappler|phb"); - commonTests.assert_MISSING("feat|grappler|xphb"); - commonTests.assert_MISSING("feat|mobile|phb"); - commonTests.assert_MISSING("feat|moderately armored|phb"); - commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); commonTests.assert_MISSING("hazard|quicksand|dmg"); commonTests.assert_MISSING("hazard|razorvine|dmg"); commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); @@ -91,14 +134,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("itemgroup|musical instrument|xphb"); commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); commonTests.assert_Present("itemproperty|2h|xphb"); commonTests.assert_MISSING("itemproperty|bf|dmg"); commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemproperty|er|tdcsr"); + commonTests.assert_Present("itemproperty|s|phb"); + commonTests.assert_MISSING("itemtype|$c|phb"); commonTests.assert_Present("itemtype|$c|xphb"); commonTests.assert_MISSING("itemtype|$g|dmg"); commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("itemtype|sc|dmg"); + commonTests.assert_Present("itemtype|sc|xphb"); + commonTests.assert_MISSING("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); @@ -124,9 +175,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("item|ball bearings|xphb"); commonTests.assert_Present("item|ball bearing|phb"); commonTests.assert_MISSING("item|chain (10 feet)|phb"); - commonTests.assert_Present("item|chain mail|phb"); - commonTests.assert_Present("item|chain mail|xphb"); commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); @@ -149,23 +199,16 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|derro savant|oota"); commonTests.assert_MISSING("monster|sibriex|mpmm"); commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); commonTests.assert_MISSING("optfeature|ambush|xphb"); commonTests.assert_Present("optfeature|dueling|phb"); commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); - commonTests.assert_MISSING("race|bugbear|erlw"); - commonTests.assert_MISSING("race|bugbear|mpmm"); - commonTests.assert_MISSING("race|bugbear|vgm"); - commonTests.assert_Present("race|human|phb"); - commonTests.assert_Present("race|human|xphb"); - commonTests.assert_Present("race|tiefling|phb"); // in srd - commonTests.assert_MISSING("race|tiefling|xphb"); - commonTests.assert_MISSING("race|warforged|erlw"); - commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); - commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); @@ -175,10 +218,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("reward|boon of fate|dmg"); commonTests.assert_MISSING("reward|boon of fortitude|dmg"); commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); commonTests.assert_Present("spell|acid splash|xphb"); commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); @@ -189,36 +235,88 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("spell|illusory script|phb"); commonTests.assert_Present("spell|illusory script|xphb"); commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); - commonTests.assert_Present("subclass|thief|rogue|phb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); - commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_Present("subrace|human|human|phb|phb"); - commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); // srd - commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); commonTests.assert_MISSING("trap|collapsing roof|xdmg"); commonTests.assert_Present("trap|falling net|dmg"); commonTests.assert_MISSING("trap|falling net|xdmg"); commonTests.assert_Present("trap|pits|dmg"); commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|poison needle trap|xge"); commonTests.assert_Present("trap|poison needle|dmg"); - commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_Present("trap|rolling sphere|dmg"); commonTests.assert_MISSING("trap|rolling stone|xdmg"); - commonTests.assert_MISSING("variantrule|facing|dmg"); - commonTests.assert_MISSING("variantrule|falling|xge"); - commonTests.assert_MISSING("variantrule|familiars|mm"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); - commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_MISSING("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_Present("classtype|barbarian|phb"); + commonTests.assert_Present("classtype|barbarian|xphb"); + + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_MISSING("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_MISSING("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_MISSING("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_MISSING("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_Present("classtype|rogue|phb"); + commonTests.assert_Present("classtype|rogue|xphb"); + + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_Present("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_Present("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); // in srd + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); // srd + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); } } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java index c13a72d81..b6f11145f 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.nio.file.Path; @@ -22,8 +24,14 @@ public class FilterNoneTest { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + String config = """ + { + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -40,45 +48,78 @@ public void cleanup() throws Exception { public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); + // NONE: _newest_ across 2024 freerules, 5.1 SRD, and 2014 basic rules + // (2014 basic rules / srd content unless replaced by 2024 free rules content) + if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.noSources()).isTrue(); + + assertThat(config.sourceIncluded("srd")).isFalse(); + assertThat(config.sourceIncluded("basicrules")).isFalse(); + assertThat(config.sourceIncluded("srd52")).isFalse(); + assertThat(config.sourceIncluded("freerules2024")).isFalse(); + + assertThat(config.sourceIncluded("DMG")).isFalse(); + assertThat(config.sourceIncluded("PHB")).isFalse(); + + assertThat(config.sourceIncluded("XDMG")).isFalse(); + assertThat(config.sourceIncluded("XPHB")).isFalse(); + commonTests.assert_MISSING("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); commonTests.assert_MISSING("action|cast a spell|phb"); commonTests.assert_MISSING("action|disengage|phb"); commonTests.assert_Present("action|disengage|xphb"); + + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); - commonTests.assert_MISSING("classtype|artificer|tce"); - commonTests.assert_MISSING("classtype|bard|phb"); - commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); commonTests.assert_Present("condition|blinded|xphb"); commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); commonTests.assert_Present("deity|auril|forgotten realms|phb"); commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); - commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); - commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_MISSING("deity|gruumsh|dawn war|dmg"); + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); + commonTests.assert_MISSING("deity|gruumsh|nonhuman|phb"); + commonTests.assert_MISSING("deity|gruumsh|orc|scag"); + commonTests.assert_MISSING("deity|gruumsh|orc|vgm"); commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); commonTests.assert_MISSING("deity|the traveler|eberron|phb"); commonTests.assert_MISSING("deity|the traveler|exandria|egw"); commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); // part of basic rules commonTests.assert_MISSING("disease|cackle fever|xdmg"); - commonTests.assert_MISSING("feat|alert|phb"); - commonTests.assert_Present("feat|alert|xphb"); - commonTests.assert_MISSING("feat|dueling|xphb"); - commonTests.assert_Present("feat|grappler|phb"); - commonTests.assert_MISSING("feat|grappler|xphb"); - commonTests.assert_MISSING("feat|mobile|phb"); - commonTests.assert_MISSING("feat|moderately armored|phb"); - commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); commonTests.assert_MISSING("hazard|quicksand|dmg"); commonTests.assert_MISSING("hazard|razorvine|dmg"); commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); @@ -91,14 +132,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("itemgroup|musical instrument|xphb"); commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); commonTests.assert_Present("itemproperty|2h|xphb"); commonTests.assert_MISSING("itemproperty|bf|dmg"); commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemproperty|er|tdcsr"); + commonTests.assert_Present("itemproperty|s|phb"); + commonTests.assert_MISSING("itemtype|$c|phb"); commonTests.assert_Present("itemtype|$c|xphb"); commonTests.assert_MISSING("itemtype|$g|dmg"); commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("itemtype|sc|dmg"); + commonTests.assert_Present("itemtype|sc|xphb"); + commonTests.assert_MISSING("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); @@ -124,9 +173,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("item|ball bearings|xphb"); commonTests.assert_Present("item|ball bearing|phb"); commonTests.assert_MISSING("item|chain (10 feet)|phb"); - commonTests.assert_MISSING("item|chain mail|phb"); - commonTests.assert_Present("item|chain mail|xphb"); commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); @@ -149,23 +197,16 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|derro savant|oota"); commonTests.assert_MISSING("monster|sibriex|mpmm"); commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); commonTests.assert_MISSING("optfeature|ambush|xphb"); commonTests.assert_Present("optfeature|dueling|phb"); commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); - commonTests.assert_MISSING("race|bugbear|erlw"); - commonTests.assert_MISSING("race|bugbear|mpmm"); - commonTests.assert_MISSING("race|bugbear|vgm"); - commonTests.assert_MISSING("race|human|phb"); - commonTests.assert_Present("race|human|xphb"); - commonTests.assert_Present("race|tiefling|phb"); // in srd - commonTests.assert_MISSING("race|tiefling|xphb"); - commonTests.assert_MISSING("race|warforged|erlw"); - commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); - commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); @@ -175,10 +216,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("reward|boon of fate|dmg"); commonTests.assert_MISSING("reward|boon of fortitude|dmg"); commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); commonTests.assert_Present("spell|acid splash|xphb"); commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); @@ -189,36 +233,95 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("spell|illusory script|phb"); commonTests.assert_Present("spell|illusory script|xphb"); commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); - commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); - commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_MISSING("subrace|human|human|phb|phb"); - commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); // srd - commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); commonTests.assert_MISSING("trap|collapsing roof|xdmg"); commonTests.assert_Present("trap|falling net|dmg"); commonTests.assert_MISSING("trap|falling net|xdmg"); commonTests.assert_Present("trap|pits|dmg"); commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|poison needle trap|xge"); commonTests.assert_Present("trap|poison needle|dmg"); - commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_Present("trap|rolling sphere|dmg"); commonTests.assert_MISSING("trap|rolling stone|xdmg"); - commonTests.assert_MISSING("variantrule|facing|dmg"); - commonTests.assert_MISSING("variantrule|falling|xge"); - commonTests.assert_MISSING("variantrule|familiars|mm"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); - commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + // 2024 free rules define barbarian, bard, cleric, druid, fighter, monk, + // paladin, ranger, rogue, sorcerer, warlock + // 2014 basic rules define barbarian, bard, cleric, druid, fighter, monk, + // paladin, ranger, rogue, sorcerer, warlock + // 5.1 SRD defines: barbarian, bard, cleric, druid, fighter, monk, + // paladin, ranger, rogue, sorcerer, warlock. + + commonTests.assert_MISSING("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_MISSING("classtype|barbarian|phb"); + commonTests.assert_Present("classtype|barbarian|xphb"); + + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_MISSING("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_MISSING("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_MISSING("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_MISSING("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_MISSING("classtype|rogue|phb"); + commonTests.assert_Present("classtype|rogue|xphb"); + + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_MISSING("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_Present("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_MISSING("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_MISSING("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_MISSING("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); // in srd + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); // srd + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); } } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java index ec8fe990b..c0a7234e5 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.nio.file.Path; @@ -22,8 +24,17 @@ public class FilterSrd2014Test { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + String config = """ + { + "sources": { + "reference": ["srd", "basicrules"] + }, + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -41,43 +52,71 @@ public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.sourceIncluded("srd")).isTrue(); + assertThat(config.sourceIncluded("basicrules")).isTrue(); + assertThat(config.sourceIncluded("srd52")).isFalse(); + assertThat(config.sourceIncluded("freerules2024")).isFalse(); + + assertThat(config.sourceIncluded("DMG")).isFalse(); + assertThat(config.sourceIncluded("PHB")).isFalse(); + + assertThat(config.sourceIncluded("XDMG")).isFalse(); + assertThat(config.sourceIncluded("XPHB")).isFalse(); + commonTests.assert_Present("action|attack|phb"); commonTests.assert_MISSING("action|attack|xphb"); commonTests.assert_Present("action|cast a spell|phb"); commonTests.assert_Present("action|disengage|phb"); commonTests.assert_MISSING("action|disengage|xphb"); + + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_MISSING("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("background|sage|phb"); commonTests.assert_MISSING("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); - commonTests.assert_MISSING("classtype|artificer|tce"); - commonTests.assert_Present("classtype|bard|phb"); - commonTests.assert_MISSING("classtype|bard|xphb"); + commonTests.assert_Present("condition|blinded|phb"); commonTests.assert_MISSING("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); commonTests.assert_Present("deity|auril|forgotten realms|phb"); commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); - commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); - commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_MISSING("deity|gruumsh|dawn war|dmg"); + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); + commonTests.assert_MISSING("deity|gruumsh|nonhuman|phb"); + commonTests.assert_MISSING("deity|gruumsh|orc|scag"); + commonTests.assert_MISSING("deity|gruumsh|orc|vgm"); commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); commonTests.assert_MISSING("deity|the traveler|eberron|phb"); commonTests.assert_MISSING("deity|the traveler|exandria|egw"); commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); - commonTests.assert_Present("disease|cackle fever|dmg"); + + commonTests.assert_Present("disease|cackle fever|dmg"); // srd/basicrules commonTests.assert_MISSING("disease|cackle fever|xdmg"); - commonTests.assert_MISSING("feat|alert|phb"); - commonTests.assert_MISSING("feat|alert|xphb"); - commonTests.assert_MISSING("feat|dueling|xphb"); - commonTests.assert_Present("feat|grappler|phb"); - commonTests.assert_MISSING("feat|grappler|xphb"); - commonTests.assert_MISSING("feat|mobile|phb"); - commonTests.assert_MISSING("feat|moderately armored|phb"); - commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); commonTests.assert_MISSING("hazard|quicksand|dmg"); commonTests.assert_MISSING("hazard|razorvine|dmg"); commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); @@ -90,14 +129,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("itemgroup|musical instrument|xphb"); commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); commonTests.assert_MISSING("itemgroup|spell scroll|xdmg"); + commonTests.assert_Present("itemproperty|2h|phb"); commonTests.assert_MISSING("itemproperty|2h|xphb"); commonTests.assert_MISSING("itemproperty|bf|dmg"); commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemproperty|er|tdcsr"); + commonTests.assert_Present("itemproperty|s|phb"); + commonTests.assert_Present("itemtype|$c|phb"); commonTests.assert_MISSING("itemtype|$c|xphb"); commonTests.assert_MISSING("itemtype|$g|dmg"); commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("itemtype|sc|dmg"); + commonTests.assert_MISSING("itemtype|sc|xphb"); + commonTests.assert_MISSING("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); @@ -123,9 +170,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("item|ball bearings|xphb"); commonTests.assert_Present("item|ball bearing|phb"); commonTests.assert_Present("item|chain (10 feet)|phb"); - commonTests.assert_Present("item|chain mail|phb"); - commonTests.assert_MISSING("item|chain mail|xphb"); commonTests.assert_MISSING("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); @@ -148,22 +194,15 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|derro savant|oota"); commonTests.assert_MISSING("monster|sibriex|mpmm"); commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); commonTests.assert_MISSING("optfeature|ambush|xphb"); commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); - commonTests.assert_MISSING("race|bugbear|erlw"); - commonTests.assert_MISSING("race|bugbear|mpmm"); - commonTests.assert_MISSING("race|bugbear|vgm"); - commonTests.assert_Present("race|human|phb"); - commonTests.assert_MISSING("race|human|xphb"); - commonTests.assert_Present("race|tiefling|phb"); - commonTests.assert_MISSING("race|tiefling|xphb"); - commonTests.assert_MISSING("race|warforged|erlw"); - commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); - commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); @@ -173,10 +212,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("reward|boon of fate|dmg"); commonTests.assert_MISSING("reward|boon of fortitude|dmg"); commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_Present("sense|blindsight|phb"); commonTests.assert_MISSING("sense|blindsight|xphb"); + commonTests.assert_Present("skill|athletics|phb"); commonTests.assert_MISSING("skill|athletics|xphb"); + commonTests.assert_Present("spell|acid splash|phb"); commonTests.assert_MISSING("spell|acid splash|xphb"); commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); @@ -187,36 +229,88 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("spell|illusory script|phb"); commonTests.assert_MISSING("spell|illusory script|xphb"); commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_Present("status|surprised|phb"); commonTests.assert_MISSING("status|surprised|xphb"); - commonTests.assert_Present("subclass|thief|rogue|phb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_Present("subrace|human|human|phb|phb"); - commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); - commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); commonTests.assert_MISSING("trap|collapsing roof|xdmg"); commonTests.assert_Present("trap|falling net|dmg"); commonTests.assert_MISSING("trap|falling net|xdmg"); commonTests.assert_Present("trap|pits|dmg"); commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|poison needle trap|xge"); commonTests.assert_Present("trap|poison needle|dmg"); - commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_Present("trap|rolling sphere|dmg"); commonTests.assert_MISSING("trap|rolling stone|xdmg"); - commonTests.assert_MISSING("variantrule|facing|dmg"); - commonTests.assert_MISSING("variantrule|falling|xge"); - commonTests.assert_MISSING("variantrule|familiars|mm"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_MISSING("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_Present("classtype|barbarian|phb"); + commonTests.assert_MISSING("classtype|barbarian|xphb"); + + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_MISSING("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_MISSING("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_MISSING("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_MISSING("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_Present("classtype|rogue|phb"); + commonTests.assert_MISSING("classtype|rogue|xphb"); + + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_Present("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_MISSING("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_MISSING("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_MISSING("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_MISSING("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_MISSING("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); } } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java index e7f763f55..f99871c95 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.nio.file.Path; @@ -22,8 +24,17 @@ public class FilterSrd2024Test { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + String config = """ + { + "sources": { + "reference": ["srd52", "freerules2024"] + }, + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -41,43 +52,71 @@ public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.sourceIncluded("srd")).isFalse(); + assertThat(config.sourceIncluded("basicrules")).isFalse(); + assertThat(config.sourceIncluded("srd52")).isTrue(); + assertThat(config.sourceIncluded("freerules2024")).isTrue(); + + assertThat(config.sourceIncluded("DMG")).isFalse(); + assertThat(config.sourceIncluded("PHB")).isFalse(); + + assertThat(config.sourceIncluded("XDMG")).isFalse(); + assertThat(config.sourceIncluded("XPHB")).isFalse(); + commonTests.assert_MISSING("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); commonTests.assert_MISSING("action|cast a spell|phb"); commonTests.assert_MISSING("action|disengage|phb"); commonTests.assert_Present("action|disengage|xphb"); + + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_MISSING("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); - commonTests.assert_MISSING("classtype|artificer|tce"); - commonTests.assert_MISSING("classtype|bard|phb"); - commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); commonTests.assert_MISSING("deity|auril|forgotten realms|phb"); commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); - commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); - commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_MISSING("deity|gruumsh|dawn war|dmg"); + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); + commonTests.assert_MISSING("deity|gruumsh|nonhuman|phb"); + commonTests.assert_MISSING("deity|gruumsh|orc|scag"); + commonTests.assert_MISSING("deity|gruumsh|orc|vgm"); commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); commonTests.assert_MISSING("deity|the traveler|eberron|phb"); commonTests.assert_MISSING("deity|the traveler|exandria|egw"); commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_MISSING("disease|cackle fever|dmg"); commonTests.assert_MISSING("disease|cackle fever|xdmg"); - commonTests.assert_MISSING("feat|alert|phb"); - commonTests.assert_Present("feat|alert|xphb"); - commonTests.assert_MISSING("feat|dueling|xphb"); - commonTests.assert_MISSING("feat|grappler|phb"); - commonTests.assert_MISSING("feat|grappler|xphb"); - commonTests.assert_MISSING("feat|mobile|phb"); - commonTests.assert_MISSING("feat|moderately armored|phb"); - commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); commonTests.assert_MISSING("hazard|quicksand|dmg"); commonTests.assert_MISSING("hazard|razorvine|dmg"); commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); @@ -90,14 +129,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("itemgroup|musical instrument|xphb"); commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); commonTests.assert_Present("itemproperty|2h|xphb"); commonTests.assert_MISSING("itemproperty|bf|dmg"); commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemproperty|er|tdcsr"); + commonTests.assert_MISSING("itemproperty|s|phb"); + commonTests.assert_MISSING("itemtype|$c|phb"); commonTests.assert_Present("itemtype|$c|xphb"); commonTests.assert_MISSING("itemtype|$g|dmg"); commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_MISSING("itemtype|sc|dmg"); + commonTests.assert_Present("itemtype|sc|xphb"); + commonTests.assert_MISSING("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); @@ -123,9 +170,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("item|ball bearings|xphb"); commonTests.assert_MISSING("item|ball bearing|phb"); commonTests.assert_MISSING("item|chain (10 feet)|phb"); - commonTests.assert_MISSING("item|chain mail|phb"); - commonTests.assert_Present("item|chain mail|xphb"); commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); @@ -148,22 +194,15 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|derro savant|oota"); commonTests.assert_MISSING("monster|sibriex|mpmm"); commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); commonTests.assert_MISSING("optfeature|ambush|xphb"); commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); - commonTests.assert_MISSING("race|bugbear|erlw"); - commonTests.assert_MISSING("race|bugbear|mpmm"); - commonTests.assert_MISSING("race|bugbear|vgm"); - commonTests.assert_MISSING("race|human|phb"); - commonTests.assert_Present("race|human|xphb"); - commonTests.assert_MISSING("race|tiefling|phb"); - commonTests.assert_MISSING("race|tiefling|xphb"); - commonTests.assert_MISSING("race|warforged|erlw"); - commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); - commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); @@ -173,10 +212,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("reward|boon of fate|dmg"); commonTests.assert_MISSING("reward|boon of fortitude|dmg"); commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); commonTests.assert_Present("spell|acid splash|xphb"); commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); @@ -187,36 +229,88 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("spell|illusory script|phb"); commonTests.assert_Present("spell|illusory script|xphb"); commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); - commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); - commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_MISSING("subrace|human|human|phb|phb"); - commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); - commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_MISSING("trap|collapsing roof|dmg"); commonTests.assert_MISSING("trap|collapsing roof|xdmg"); commonTests.assert_MISSING("trap|falling net|dmg"); commonTests.assert_MISSING("trap|falling net|xdmg"); commonTests.assert_MISSING("trap|pits|dmg"); commonTests.assert_MISSING("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|poison needle trap|xge"); commonTests.assert_MISSING("trap|poison needle|dmg"); - commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|rolling sphere|dmg"); commonTests.assert_MISSING("trap|rolling stone|xdmg"); - commonTests.assert_MISSING("variantrule|facing|dmg"); - commonTests.assert_MISSING("variantrule|falling|xge"); - commonTests.assert_MISSING("variantrule|familiars|mm"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); - commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|dmg"); commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_MISSING("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_MISSING("classtype|barbarian|phb"); + commonTests.assert_Present("classtype|barbarian|xphb"); + + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_MISSING("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_MISSING("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_MISSING("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_MISSING("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_MISSING("classtype|rogue|phb"); + commonTests.assert_Present("classtype|rogue|xphb"); + + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_MISSING("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_Present("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_MISSING("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_MISSING("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_MISSING("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_MISSING("race|tiefling|phb"); + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); } } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java index 440a86aa8..d0453f34c 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.nio.file.Path; @@ -22,8 +24,19 @@ public class FilterSubset2014Test { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + String config = """ + { + "sources": { + "adventure": ["lmop"], + "book": ["phb", "dmg", "mm"], + "reference": ["tce", "xge"] + }, + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -41,43 +54,81 @@ public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.sourceIncluded("srd")).isFalse(); + assertThat(config.sourceIncluded("basicrules")).isFalse(); + assertThat(config.sourceIncluded("srd52")).isFalse(); + assertThat(config.sourceIncluded("freerules2024")).isFalse(); + + assertThat(config.sourceIncluded("DMG")).isTrue(); + assertThat(config.sourceIncluded("PHB")).isTrue(); + + assertThat(config.sourceIncluded("XDMG")).isFalse(); + assertThat(config.sourceIncluded("XPHB")).isFalse(); + + assertThat(config.sourceIncluded("MM")).isTrue(); + assertThat(config.sourceIncluded("MPMM")).isFalse(); + assertThat(config.sourceIncluded("SCAG")).isFalse(); + assertThat(config.sourceIncluded("TCE")).isTrue(); + assertThat(config.sourceIncluded("XGE")).isTrue(); + + assertThat(config.sourceIncluded("PaBTSO")).isFalse(); + assertThat(config.sourceIncluded("OotA")).isFalse(); + assertThat(config.sourceIncluded("LMOP")).isTrue(); + commonTests.assert_Present("action|attack|phb"); commonTests.assert_MISSING("action|attack|xphb"); commonTests.assert_Present("action|cast a spell|phb"); commonTests.assert_Present("action|disengage|phb"); commonTests.assert_MISSING("action|disengage|xphb"); + + commonTests.assert_Present("feat|alert|phb"); + commonTests.assert_MISSING("feat|alert|xphb"); + commonTests.assert_MISSING("feat|dueling|xphb"); + commonTests.assert_Present("feat|grappler|phb"); + commonTests.assert_MISSING("feat|grappler|xphb"); + commonTests.assert_Present("feat|mobile|phb"); + commonTests.assert_Present("feat|moderately armored|phb"); + commonTests.assert_MISSING("feat|moderately armored|xphb"); + + commonTests.assert_Present("variantrule|facing|dmg"); + commonTests.assert_Present("variantrule|falling|xge"); + commonTests.assert_Present("variantrule|familiars|mm"); + commonTests.assert_Present("variantrule|simultaneous effects|xge"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("background|sage|phb"); commonTests.assert_MISSING("background|sage|xphb"); commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); - commonTests.assert_Present("classtype|artificer|tce"); - commonTests.assert_Present("classtype|bard|phb"); - commonTests.assert_MISSING("classtype|bard|xphb"); + commonTests.assert_Present("condition|blinded|phb"); commonTests.assert_MISSING("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); commonTests.assert_Present("deity|auril|forgotten realms|phb"); commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_Present("deity|chemosh|dragonlance|phb"); - commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); - commonTests.assert_Present("deity|the mockery|eberron|phb"); + commonTests.assert_Present("deity|ehlonna|greyhawk|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_Present("deity|gruumsh|dawn war|dmg"); + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); + commonTests.assert_Present("deity|gruumsh|nonhuman|phb"); + commonTests.assert_MISSING("deity|gruumsh|orc|scag"); + commonTests.assert_MISSING("deity|gruumsh|orc|vgm"); commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); commonTests.assert_Present("deity|the traveler|eberron|phb"); commonTests.assert_MISSING("deity|the traveler|exandria|egw"); commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + commonTests.assert_Present("disease|cackle fever|dmg"); commonTests.assert_MISSING("disease|cackle fever|xdmg"); - commonTests.assert_Present("feat|alert|phb"); - commonTests.assert_MISSING("feat|alert|xphb"); - commonTests.assert_MISSING("feat|dueling|xphb"); - commonTests.assert_Present("feat|grappler|phb"); - commonTests.assert_MISSING("feat|grappler|xphb"); - commonTests.assert_Present("feat|mobile|phb"); - commonTests.assert_Present("feat|moderately armored|phb"); - commonTests.assert_MISSING("feat|moderately armored|xphb"); + commonTests.assert_MISSING("hazard|quicksand pit|xdmg"); commonTests.assert_Present("hazard|quicksand|dmg"); commonTests.assert_Present("hazard|razorvine|dmg"); commonTests.assert_MISSING("hazard|razorvine|xdmg"); + commonTests.assert_Present("itemgroup|arcane focus|phb"); commonTests.assert_MISSING("itemgroup|arcane focus|xphb"); commonTests.assert_Present("itemgroup|carpet of flying|dmg"); @@ -90,14 +141,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("itemgroup|musical instrument|xphb"); commonTests.assert_Present("itemgroup|spell scroll|dmg"); commonTests.assert_MISSING("itemgroup|spell scroll|xdmg"); + commonTests.assert_Present("itemproperty|2h|phb"); commonTests.assert_MISSING("itemproperty|2h|xphb"); commonTests.assert_Present("itemproperty|bf|dmg"); commonTests.assert_MISSING("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemproperty|er|tdcsr"); + commonTests.assert_Present("itemproperty|s|phb"); + commonTests.assert_Present("itemtype|$c|phb"); commonTests.assert_MISSING("itemtype|$c|xphb"); commonTests.assert_Present("itemtype|$g|dmg"); commonTests.assert_MISSING("itemtype|$g|xdmg"); + commonTests.assert_Present("itemtype|sc|dmg"); + commonTests.assert_MISSING("itemtype|sc|xphb"); + commonTests.assert_Present("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_Present("item|+1 rod of the pact keeper|dmg"); commonTests.assert_MISSING("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_MISSING("item|+2 wraps of unarmed power|xdmg"); @@ -123,9 +182,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("item|ball bearings|xphb"); commonTests.assert_Present("item|ball bearing|phb"); commonTests.assert_Present("item|chain (10 feet)|phb"); - commonTests.assert_Present("item|chain mail|phb"); - commonTests.assert_MISSING("item|chain mail|xphb"); commonTests.assert_MISSING("item|chain|xphb"); + commonTests.assert_MISSING("monster|abjurer wizard|mpmm"); commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_MISSING("monster|alkilith|mpmm"); @@ -148,22 +206,15 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|derro savant|oota"); commonTests.assert_MISSING("monster|sibriex|mpmm"); commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_Present("object|trebuchet|dmg"); commonTests.assert_MISSING("object|trebuchet|xdmg"); + commonTests.assert_Present("optfeature|ambush|tce"); commonTests.assert_MISSING("optfeature|ambush|xphb"); commonTests.assert_Present("optfeature|investment of the chain master|tce"); commonTests.assert_MISSING("optfeature|investment of the chain master|xphb"); - commonTests.assert_MISSING("race|bugbear|erlw"); - commonTests.assert_MISSING("race|bugbear|mpmm"); - commonTests.assert_MISSING("race|bugbear|vgm"); - commonTests.assert_Present("race|human|phb"); - commonTests.assert_MISSING("race|human|xphb"); - commonTests.assert_Present("race|tiefling|phb"); - commonTests.assert_MISSING("race|tiefling|xphb"); - commonTests.assert_MISSING("race|warforged|erlw"); - commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); - commonTests.assert_MISSING("race|yuan-ti|mpmm"); + commonTests.assert_Present("reward|blessing of weapon enhancement|dmg"); commonTests.assert_MISSING("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_Present("reward|blessing of wound closure|dmg"); @@ -173,10 +224,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("reward|boon of fate|dmg"); commonTests.assert_Present("reward|boon of fortitude|dmg"); commonTests.assert_Present("reward|boon of high magic|dmg"); + commonTests.assert_Present("sense|blindsight|phb"); commonTests.assert_MISSING("sense|blindsight|xphb"); + commonTests.assert_Present("skill|athletics|phb"); commonTests.assert_MISSING("skill|athletics|xphb"); + commonTests.assert_Present("spell|acid splash|phb"); commonTests.assert_MISSING("spell|acid splash|xphb"); commonTests.assert_Present("spell|aganazzar's scorcher|xge"); @@ -187,36 +241,88 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("spell|illusory script|phb"); commonTests.assert_MISSING("spell|illusory script|xphb"); commonTests.assert_Present("spell|wrath of nature|xge"); + commonTests.assert_Present("status|surprised|phb"); commonTests.assert_MISSING("status|surprised|xphb"); - commonTests.assert_Present("subclass|thief|rogue|phb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_Present("subrace|human|human|phb|phb"); - commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); - commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_Present("trap|collapsing roof|dmg"); commonTests.assert_MISSING("trap|collapsing roof|xdmg"); commonTests.assert_Present("trap|falling net|dmg"); commonTests.assert_MISSING("trap|falling net|xdmg"); commonTests.assert_Present("trap|pits|dmg"); commonTests.assert_Present("trap|poison darts|dmg"); + commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_Present("trap|poison needle trap|xge"); commonTests.assert_Present("trap|poison needle|dmg"); - commonTests.assert_MISSING("trap|poisoned darts|xdmg"); commonTests.assert_Present("trap|rolling sphere|dmg"); commonTests.assert_MISSING("trap|rolling stone|xdmg"); - commonTests.assert_Present("variantrule|facing|dmg"); - commonTests.assert_Present("variantrule|falling|xge"); - commonTests.assert_Present("variantrule|familiars|mm"); - commonTests.assert_Present("variantrule|simultaneous effects|xge"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xphb"); + commonTests.assert_Present("vehicle|apparatus of kwalish|dmg"); commonTests.assert_MISSING("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_Present("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_Present("classtype|barbarian|phb"); + commonTests.assert_MISSING("classtype|barbarian|xphb"); + + commonTests.assert_Present("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_Present("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_Present("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_Present("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_Present("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_Present("classtype|rogue|phb"); + commonTests.assert_MISSING("classtype|rogue|xphb"); + + commonTests.assert_Present("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_Present("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_MISSING("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_MISSING("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_MISSING("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_MISSING("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_MISSING("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_Present("race|human|phb"); + commonTests.assert_MISSING("race|human|xphb"); + commonTests.assert_Present("race|tiefling|phb"); + commonTests.assert_MISSING("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_MISSING("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_MISSING("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_Present("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_Present("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); } } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java index 937b215e4..9b0768e64 100644 --- a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java @@ -1,5 +1,7 @@ package dev.ebullient.convert.tools.dnd5e; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.nio.file.Path; @@ -22,8 +24,19 @@ public class FilterSubset2024Test { @BeforeAll public static void setupDir() throws Exception { outputPath.toFile().mkdirs(); - // This uses test/resources/sources.json to constrain sources - commonTests = new CommonDataTests(testInput, TestUtils.PATH_5E_TOOLS_DATA); + String config = """ + { + "sources": { + "adventure": ["PaBTSO", "dsotdq"], + "book": ["tdcsr", "xdmg", "xphb"], + "reference": ["srd52", "freerules2024", "mpmm"] + }, + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); } @AfterAll @@ -41,41 +54,82 @@ public void testKeyIndex() throws Exception { commonTests.testKeyIndex(outputPath); if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.sourceIncluded("srd")).isFalse(); + assertThat(config.sourceIncluded("basicrules")).isFalse(); + assertThat(config.sourceIncluded("srd52")).isTrue(); + assertThat(config.sourceIncluded("freerules2024")).isTrue(); + + assertThat(config.sourceIncluded("DMG")).isFalse(); + assertThat(config.sourceIncluded("PHB")).isFalse(); + + assertThat(config.sourceIncluded("XDMG")).isTrue(); + assertThat(config.sourceIncluded("XPHB")).isTrue(); + + assertThat(config.sourceIncluded("MM")).isFalse(); + assertThat(config.sourceIncluded("MPMM")).isTrue(); + assertThat(config.sourceIncluded("SCAG")).isFalse(); + assertThat(config.sourceIncluded("TCE")).isFalse(); + assertThat(config.sourceIncluded("XGE")).isFalse(); + assertThat(config.sourceIncluded("tdcsr")).isTrue(); + + assertThat(config.sourceIncluded("PaBTSO")).isTrue(); + assertThat(config.sourceIncluded("dsotdq")).isTrue(); + assertThat(config.sourceIncluded("OotA")).isFalse(); + assertThat(config.sourceIncluded("LMOP")).isFalse(); + commonTests.assert_MISSING("action|attack|phb"); commonTests.assert_Present("action|attack|xphb"); commonTests.assert_MISSING("action|cast a spell|phb"); commonTests.assert_MISSING("action|disengage|phb"); commonTests.assert_Present("action|disengage|xphb"); + + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_Present("feat|dueling|xphb"); + commonTests.assert_MISSING("feat|grappler|phb"); + commonTests.assert_Present("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_Present("feat|moderately armored|xphb"); + + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_MISSING("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("background|sage|phb"); commonTests.assert_Present("background|sage|xphb"); - commonTests.assert_MISSING("classtype|bard|phb"); - commonTests.assert_Present("classtype|bard|xphb"); + commonTests.assert_MISSING("condition|blinded|phb"); commonTests.assert_Present("condition|blinded|xphb"); + commonTests.assert_MISSING("deity|auril|faerûnian|scag"); commonTests.assert_MISSING("deity|auril|forgotten realms|phb"); commonTests.assert_Present("deity|chemosh|dragonlance|dsotdq"); commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); - commonTests.assert_MISSING("deity|the mockery|eberron|erlw"); - commonTests.assert_MISSING("deity|the mockery|eberron|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|phb"); + commonTests.assert_Present("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_MISSING("deity|gruumsh|dawn war|dmg"); + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); + commonTests.assert_MISSING("deity|gruumsh|nonhuman|phb"); + commonTests.assert_MISSING("deity|gruumsh|orc|scag"); + commonTests.assert_MISSING("deity|gruumsh|orc|vgm"); commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); commonTests.assert_MISSING("deity|the traveler|eberron|phb"); commonTests.assert_MISSING("deity|the traveler|exandria|egw"); commonTests.assert_Present("deity|the traveler|exandria|tdcsr"); + commonTests.assert_MISSING("disease|cackle fever|dmg"); commonTests.assert_Present("disease|cackle fever|xdmg"); - commonTests.assert_MISSING("feat|alert|phb"); - commonTests.assert_Present("feat|alert|xphb"); - commonTests.assert_Present("feat|dueling|xphb"); - commonTests.assert_MISSING("feat|grappler|phb"); - commonTests.assert_Present("feat|grappler|xphb"); - commonTests.assert_MISSING("feat|mobile|phb"); - commonTests.assert_MISSING("feat|moderately armored|phb"); - commonTests.assert_Present("feat|moderately armored|xphb"); + commonTests.assert_Present("hazard|quicksand pit|xdmg"); commonTests.assert_MISSING("hazard|quicksand|dmg"); commonTests.assert_MISSING("hazard|razorvine|dmg"); commonTests.assert_Present("hazard|razorvine|xdmg"); + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); commonTests.assert_Present("itemgroup|arcane focus|xphb"); commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); @@ -88,14 +142,22 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("itemgroup|musical instrument|xphb"); commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + commonTests.assert_MISSING("itemproperty|2h|phb"); commonTests.assert_Present("itemproperty|2h|xphb"); commonTests.assert_MISSING("itemproperty|bf|dmg"); commonTests.assert_Present("itemproperty|bf|xdmg"); + commonTests.assert_Present("itemproperty|er|tdcsr"); + commonTests.assert_MISSING("itemproperty|s|phb"); + commonTests.assert_MISSING("itemtype|$c|phb"); commonTests.assert_Present("itemtype|$c|xphb"); commonTests.assert_MISSING("itemtype|$g|dmg"); commonTests.assert_Present("itemtype|$g|xdmg"); + commonTests.assert_MISSING("itemtype|sc|dmg"); + commonTests.assert_Present("itemtype|sc|xphb"); + commonTests.assert_MISSING("itemtypeadditionalentries|gs|phb|xge"); + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); commonTests.assert_Present("item|+1 rod of the pact keeper|xdmg"); commonTests.assert_Present("item|+2 wraps of unarmed power|xdmg"); @@ -121,9 +183,8 @@ public void testKeyIndex() throws Exception { commonTests.assert_Present("item|ball bearings|xphb"); commonTests.assert_MISSING("item|ball bearing|phb"); commonTests.assert_MISSING("item|chain (10 feet)|phb"); - commonTests.assert_MISSING("item|chain mail|phb"); - commonTests.assert_Present("item|chain mail|xphb"); commonTests.assert_Present("item|chain|xphb"); + commonTests.assert_Present("monster|abjurer wizard|mpmm"); commonTests.assert_MISSING("monster|abjurer|vgm"); commonTests.assert_Present("monster|alkilith|mpmm"); @@ -132,7 +193,7 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|ape|mm"); commonTests.assert_Present("monster|ape|xphb"); commonTests.assert_MISSING("monster|ash zombie|lmop"); - commonTests.assert_MISSING("monster|ash zombie|pabtso"); + commonTests.assert_Present("monster|ash zombie|pabtso"); commonTests.assert_MISSING("monster|awakened shrub|mm"); commonTests.assert_MISSING("monster|awakened shrub|xmm"); commonTests.assert_MISSING("monster|beast of the land|tce"); @@ -146,22 +207,15 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("monster|derro savant|oota"); commonTests.assert_Present("monster|sibriex|mpmm"); commonTests.assert_MISSING("monster|sibriex|mtf"); + commonTests.assert_MISSING("object|trebuchet|dmg"); commonTests.assert_Present("object|trebuchet|xdmg"); + commonTests.assert_MISSING("optfeature|ambush|tce"); commonTests.assert_Present("optfeature|ambush|xphb"); commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); commonTests.assert_Present("optfeature|investment of the chain master|xphb"); - commonTests.assert_MISSING("race|bugbear|erlw"); - commonTests.assert_Present("race|bugbear|mpmm"); - commonTests.assert_MISSING("race|bugbear|vgm"); - commonTests.assert_MISSING("race|human|phb"); - commonTests.assert_Present("race|human|xphb"); - commonTests.assert_MISSING("race|tiefling|phb"); - commonTests.assert_Present("race|tiefling|xphb"); - commonTests.assert_MISSING("race|warforged|erlw"); - commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); - commonTests.assert_Present("race|yuan-ti|mpmm"); + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); commonTests.assert_Present("reward|blessing of weapon enhancement|xdmg"); commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); @@ -171,10 +225,13 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("reward|boon of fate|dmg"); commonTests.assert_MISSING("reward|boon of fortitude|dmg"); commonTests.assert_MISSING("reward|boon of high magic|dmg"); + commonTests.assert_MISSING("sense|blindsight|phb"); commonTests.assert_Present("sense|blindsight|xphb"); + commonTests.assert_MISSING("skill|athletics|phb"); commonTests.assert_Present("skill|athletics|xphb"); + commonTests.assert_MISSING("spell|acid splash|phb"); commonTests.assert_Present("spell|acid splash|xphb"); commonTests.assert_MISSING("spell|aganazzar's scorcher|xge"); @@ -185,36 +242,88 @@ public void testKeyIndex() throws Exception { commonTests.assert_MISSING("spell|illusory script|phb"); commonTests.assert_Present("spell|illusory script|xphb"); commonTests.assert_MISSING("spell|wrath of nature|xge"); + commonTests.assert_MISSING("status|surprised|phb"); commonTests.assert_Present("status|surprised|xphb"); - commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); - commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); - commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); - commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); - commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); - commonTests.assert_MISSING("subrace|human|human|phb|phb"); - commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); - commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); - commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); - commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + commonTests.assert_MISSING("trap|collapsing roof|dmg"); commonTests.assert_Present("trap|collapsing roof|xdmg"); commonTests.assert_MISSING("trap|falling net|dmg"); commonTests.assert_Present("trap|falling net|xdmg"); commonTests.assert_MISSING("trap|pits|dmg"); commonTests.assert_MISSING("trap|poison darts|dmg"); + commonTests.assert_Present("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|poison needle trap|xge"); commonTests.assert_MISSING("trap|poison needle|dmg"); - commonTests.assert_Present("trap|poisoned darts|xdmg"); commonTests.assert_MISSING("trap|rolling sphere|dmg"); commonTests.assert_Present("trap|rolling stone|xdmg"); - commonTests.assert_MISSING("variantrule|facing|dmg"); - commonTests.assert_MISSING("variantrule|falling|xge"); - commonTests.assert_MISSING("variantrule|familiars|mm"); - commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); - commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + commonTests.assert_MISSING("vehicle|apparatus of kwalish|dmg"); commonTests.assert_Present("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_MISSING("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_MISSING("classtype|barbarian|phb"); + commonTests.assert_Present("classtype|barbarian|xphb"); + + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_MISSING("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_MISSING("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_MISSING("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_MISSING("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_MISSING("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_MISSING("classtype|rogue|phb"); + commonTests.assert_Present("classtype|rogue|xphb"); + + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_MISSING("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_Present("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_MISSING("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_MISSING("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_MISSING("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + // Races and subraces + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_Present("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_MISSING("race|tiefling|phb"); + commonTests.assert_Present("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_Present("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); } } } diff --git a/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubsetMixedTest.java b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubsetMixedTest.java new file mode 100644 index 000000000..1dd02e7ad --- /dev/null +++ b/src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubsetMixedTest.java @@ -0,0 +1,350 @@ +package dev.ebullient.convert.tools.dnd5e; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import dev.ebullient.convert.TestUtils; +import dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest + +public class FilterSubsetMixedTest { + static CommonDataTests commonTests; + static final TestInput testInput = TestInput.subsetMixed; + static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name()); + + @BeforeAll + public static void setupDir() throws Exception { + outputPath.toFile().mkdirs(); + String config = """ + { + "sources": { + "book": [ + "XDMG", + "XPHB" + ], + "adventure": [ + "PaBTSO", + "OotA" + ], + "reference": [ + "MM", + "MPMM", + "SCAG", + "TCE", + "XGE" + ], + "homebrew": [ + "sources/5e-homebrew/collection/Keith Baker; Exploring Eberron.json", + "sources/5e-homebrew/class/Matthew Mercer; Blood Hunter (2022).json" + ] + }, + "include": [ + "subclass|death domain|cleric|phb|dmg" + ], + "images": { + "copyInternal": false + } + } + """.stripIndent(); + commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA); + } + + @AfterAll + public static void done() throws IOException { + commonTests.afterAll(outputPath); + } + + @AfterEach + public void cleanup() throws Exception { + commonTests.afterEach(); + } + + @Test + public void testKeyIndex() throws Exception { + commonTests.testKeyIndex(outputPath); + + if (commonTests.dataPresent) { + var config = commonTests.config; + + assertThat(config.sourceIncluded("srd")).isFalse(); + assertThat(config.sourceIncluded("basicrules")).isFalse(); + assertThat(config.sourceIncluded("srd52")).isFalse(); + assertThat(config.sourceIncluded("freerules2024")).isFalse(); + + assertThat(config.sourceIncluded("DMG")).isFalse(); + assertThat(config.sourceIncluded("PHB")).isFalse(); + + assertThat(config.sourceIncluded("XDMG")).isTrue(); + assertThat(config.sourceIncluded("XPHB")).isTrue(); + + assertThat(config.sourceIncluded("MM")).isTrue(); + assertThat(config.sourceIncluded("MPMM")).isTrue(); + assertThat(config.sourceIncluded("SCAG")).isTrue(); + assertThat(config.sourceIncluded("TCE")).isTrue(); + assertThat(config.sourceIncluded("XGE")).isTrue(); + + assertThat(config.sourceIncluded("PaBTSO")).isTrue(); + assertThat(config.sourceIncluded("OotA")).isTrue(); + assertThat(config.sourceIncluded("LMOP")).isFalse(); + + commonTests.assert_MISSING("action|attack|phb"); + commonTests.assert_Present("action|attack|xphb"); + commonTests.assert_MISSING("action|cast a spell|phb"); + commonTests.assert_MISSING("action|disengage|phb"); + commonTests.assert_Present("action|disengage|xphb"); + + commonTests.assert_MISSING("feat|alert|phb"); + commonTests.assert_Present("feat|alert|xphb"); + commonTests.assert_Present("feat|dueling|xphb"); + commonTests.assert_MISSING("feat|grappler|phb"); + commonTests.assert_Present("feat|grappler|xphb"); + commonTests.assert_MISSING("feat|mobile|phb"); + commonTests.assert_MISSING("feat|moderately armored|phb"); + commonTests.assert_Present("feat|moderately armored|xphb"); + + commonTests.assert_MISSING("variantrule|facing|dmg"); + commonTests.assert_MISSING("variantrule|falling|xge"); + commonTests.assert_Present("variantrule|familiars|mm"); + commonTests.assert_MISSING("variantrule|simultaneous effects|xge"); + commonTests.assert_Present("variantrule|simultaneous effects|xphb"); + + commonTests.assert_MISSING("background|sage|phb"); + commonTests.assert_Present("background|sage|xphb"); + commonTests.assert_MISSING("background|baldur's gate acolyte|bgdia"); + + commonTests.assert_MISSING("condition|blinded|phb"); + commonTests.assert_Present("condition|blinded|xphb"); + + commonTests.assert_Present("deity|auril|faerûnian|scag"); + commonTests.assert_MISSING("deity|auril|forgotten realms|phb"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|dsotdq"); + commonTests.assert_MISSING("deity|chemosh|dragonlance|phb"); + commonTests.assert_MISSING("deity|ehlonna|greyhawk|phb"); + commonTests.assert_Present("deity|ehlonna|greyhawk|xdmg"); + commonTests.assert_MISSING("deity|gruumsh|dawn war|dmg"); + commonTests.assert_MISSING("deity|gruumsh|exandria|egw"); + commonTests.assert_MISSING("deity|gruumsh|nonhuman|phb"); + commonTests.assert_Present("deity|gruumsh|orc|scag"); + commonTests.assert_MISSING("deity|gruumsh|orc|vgm"); + commonTests.assert_MISSING("deity|the traveler|eberron|erlw"); + commonTests.assert_MISSING("deity|the traveler|eberron|phb"); + commonTests.assert_MISSING("deity|the traveler|exandria|egw"); + commonTests.assert_MISSING("deity|the traveler|exandria|tdcsr"); + + commonTests.assert_MISSING("disease|cackle fever|dmg"); + commonTests.assert_Present("disease|cackle fever|xdmg"); + + commonTests.assert_Present("hazard|quicksand pit|xdmg"); + commonTests.assert_MISSING("hazard|quicksand|dmg"); + commonTests.assert_MISSING("hazard|razorvine|dmg"); + commonTests.assert_Present("hazard|razorvine|xdmg"); + + commonTests.assert_MISSING("itemgroup|arcane focus|phb"); + commonTests.assert_Present("itemgroup|arcane focus|xphb"); + commonTests.assert_MISSING("itemgroup|carpet of flying|dmg"); + commonTests.assert_Present("itemgroup|carpet of flying|xdmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|dmg"); + commonTests.assert_MISSING("itemgroup|ioun stone|llk"); + commonTests.assert_Present("itemgroup|ioun stone|xdmg"); + commonTests.assert_MISSING("itemgroup|musical instrument|phb"); + commonTests.assert_MISSING("itemgroup|musical instrument|scag"); + commonTests.assert_Present("itemgroup|musical instrument|xphb"); + commonTests.assert_MISSING("itemgroup|spell scroll|dmg"); + commonTests.assert_Present("itemgroup|spell scroll|xdmg"); + + commonTests.assert_MISSING("itemproperty|2h|phb"); + commonTests.assert_Present("itemproperty|2h|xphb"); + commonTests.assert_MISSING("itemproperty|bf|dmg"); + commonTests.assert_Present("itemproperty|bf|xdmg"); + commonTests.assert_MISSING("itemproperty|er|tdcsr"); + commonTests.assert_MISSING("itemproperty|s|phb"); + + commonTests.assert_MISSING("itemtype|$c|phb"); + commonTests.assert_Present("itemtype|$c|xphb"); + commonTests.assert_MISSING("itemtype|$g|dmg"); + commonTests.assert_Present("itemtype|$g|xdmg"); + commonTests.assert_MISSING("itemtype|sc|dmg"); + commonTests.assert_Present("itemtype|sc|xphb"); + commonTests.assert_Present("itemtypeadditionalentries|gs|phb|xge"); + + commonTests.assert_MISSING("item|+1 rod of the pact keeper|dmg"); + commonTests.assert_Present("item|+1 rod of the pact keeper|xdmg"); + commonTests.assert_Present("item|+2 wraps of unarmed power|xdmg"); + commonTests.assert_MISSING("item|+2 wraps of unarmed prowess|bmt"); + commonTests.assert_MISSING("item|acid (vial)|phb"); + commonTests.assert_Present("item|acid absorbing tattoo|tce"); + commonTests.assert_Present("item|acid|xphb"); + commonTests.assert_MISSING("item|alchemist's doom|scc"); + commonTests.assert_MISSING("item|alchemist's fire (flask)|phb"); + commonTests.assert_Present("item|alchemist's fire|xphb"); + commonTests.assert_MISSING("item|alchemist's supplies|phb"); + commonTests.assert_Present("item|alchemist's supplies|xphb"); + commonTests.assert_MISSING("item|amulet of health|dmg"); + commonTests.assert_Present("item|amulet of health|xdmg"); + commonTests.assert_MISSING("item|amulet of proof against detection and location|dmg"); + commonTests.assert_Present("item|amulet of proof against detection and location|xdmg"); + commonTests.assert_MISSING("item|armor of invulnerability|dmg"); + commonTests.assert_Present("item|armor of invulnerability|xdmg"); + commonTests.assert_MISSING("item|automatic pistol|dmg"); + commonTests.assert_MISSING("item|automatic rifle|dmg"); + commonTests.assert_Present("item|automatic rifle|xdmg"); + commonTests.assert_MISSING("item|ball bearings (bag of 1,000)|phb"); + commonTests.assert_Present("item|ball bearings|xphb"); + commonTests.assert_MISSING("item|ball bearing|phb"); + commonTests.assert_MISSING("item|chain (10 feet)|phb"); + commonTests.assert_Present("item|chain|xphb"); + + commonTests.assert_Present("monster|abjurer wizard|mpmm"); + commonTests.assert_MISSING("monster|abjurer|vgm"); + commonTests.assert_Present("monster|alkilith|mpmm"); + commonTests.assert_MISSING("monster|alkilith|mtf"); + commonTests.assert_MISSING("monster|animated object (huge)|phb"); + commonTests.assert_MISSING("monster|ape|mm"); + commonTests.assert_Present("monster|ape|xphb"); + commonTests.assert_MISSING("monster|ash zombie|lmop"); + commonTests.assert_Present("monster|ash zombie|pabtso"); + commonTests.assert_Present("monster|awakened shrub|mm"); + commonTests.assert_MISSING("monster|awakened shrub|xmm"); + commonTests.assert_MISSING("monster|beast of the land|tce"); + commonTests.assert_Present("monster|beast of the land|xphb"); + commonTests.assert_MISSING("monster|bestial spirit (air)|tce"); + commonTests.assert_Present("monster|bestial spirit (air)|xphb"); + commonTests.assert_MISSING("monster|cat|mm"); + commonTests.assert_Present("monster|cat|xphb"); + commonTests.assert_Present("monster|derro savant|mpmm"); // supersedes both mtf & oota + commonTests.assert_MISSING("monster|derro savant|mtf"); + commonTests.assert_MISSING("monster|derro savant|oota"); + commonTests.assert_Present("monster|sibriex|mpmm"); + commonTests.assert_MISSING("monster|sibriex|mtf"); + + commonTests.assert_MISSING("object|trebuchet|dmg"); + commonTests.assert_Present("object|trebuchet|xdmg"); + + commonTests.assert_MISSING("optfeature|ambush|tce"); + commonTests.assert_Present("optfeature|ambush|xphb"); + commonTests.assert_MISSING("optfeature|investment of the chain master|tce"); + commonTests.assert_Present("optfeature|investment of the chain master|xphb"); + + commonTests.assert_MISSING("reward|blessing of weapon enhancement|dmg"); + commonTests.assert_Present("reward|blessing of weapon enhancement|xdmg"); + commonTests.assert_MISSING("reward|blessing of wound closure|dmg"); + commonTests.assert_Present("reward|blessing of wound closure|xdmg"); + commonTests.assert_MISSING("reward|boon of combat prowess|dmg"); + commonTests.assert_MISSING("reward|boon of dimensional travel|dmg"); + commonTests.assert_MISSING("reward|boon of fate|dmg"); + commonTests.assert_MISSING("reward|boon of fortitude|dmg"); + commonTests.assert_MISSING("reward|boon of high magic|dmg"); + + commonTests.assert_MISSING("sense|blindsight|phb"); + commonTests.assert_Present("sense|blindsight|xphb"); + + commonTests.assert_MISSING("skill|athletics|phb"); + commonTests.assert_Present("skill|athletics|xphb"); + + commonTests.assert_MISSING("spell|acid splash|phb"); + commonTests.assert_Present("spell|acid splash|xphb"); + commonTests.assert_Present("spell|aganazzar's scorcher|xge"); + commonTests.assert_MISSING("spell|blade barrier|phb"); + commonTests.assert_Present("spell|blade barrier|xphb"); + commonTests.assert_MISSING("spell|feeblemind|phb"); + commonTests.assert_Present("spell|illusory dragon|xge"); + commonTests.assert_MISSING("spell|illusory script|phb"); + commonTests.assert_Present("spell|illusory script|xphb"); + commonTests.assert_Present("spell|wrath of nature|xge"); + + commonTests.assert_MISSING("status|surprised|phb"); + commonTests.assert_Present("status|surprised|xphb"); + + commonTests.assert_MISSING("trap|collapsing roof|dmg"); + commonTests.assert_Present("trap|collapsing roof|xdmg"); + commonTests.assert_MISSING("trap|falling net|dmg"); + commonTests.assert_Present("trap|falling net|xdmg"); + commonTests.assert_MISSING("trap|pits|dmg"); + commonTests.assert_MISSING("trap|poison darts|dmg"); + commonTests.assert_Present("trap|poisoned darts|xdmg"); + commonTests.assert_Present("trap|poison needle trap|xge"); + commonTests.assert_MISSING("trap|poison needle|dmg"); + commonTests.assert_MISSING("trap|rolling sphere|dmg"); + commonTests.assert_Present("trap|rolling stone|xdmg"); + + commonTests.assert_MISSING("vehicle|apparatus of kwalish|dmg"); + commonTests.assert_Present("vehicle|apparatus of kwalish|xdmg"); + + // Classes, subclasses, class features, and subclass features + + commonTests.assert_Present("classtype|artificer|tce"); + + // "Path of Wild Magic|Barbarian||Wild Magic|TCE|3", + // "Bolstering Magic|Barbarian||Wild Magic|TCE|6", + // "Unstable Backlash|Barbarian||Wild Magic|TCE|10", + // "Controlled Surge|Barbarian||Wild Magic|TCE|14", + + commonTests.assert_MISSING("classtype|barbarian|phb"); + commonTests.assert_Present("classtype|barbarian|xphb"); + + commonTests.assert_MISSING("subclass|path of wild magic|barbarian|phb|tce"); + commonTests.assert_Present("subclass|path of wild magic|barbarian|xphb|tce"); + + commonTests.assert_Present("subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce"); + commonTests.assert_Present("subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce"); + commonTests.assert_Present("subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce"); + commonTests.assert_Present("subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce"); + commonTests.assert_Present("subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce"); + + // "Thief|Rogue||Thief||3", + // "Supreme Sneak|Rogue||Thief||9", + // "Use Magic Device|Rogue||Thief||13", + // "Thief's Reflexes|Rogue||Thief||17" + + commonTests.assert_MISSING("classtype|rogue|phb"); + commonTests.assert_Present("classtype|rogue|xphb"); + + commonTests.assert_MISSING("subclass|thief|rogue|phb|phb"); + commonTests.assert_MISSING("subclass|thief|rogue|xphb|phb"); + commonTests.assert_Present("subclass|thief|rogue|xphb|xphb"); + + commonTests.assert_MISSING("subclassfeature|thief|rogue|phb|thief|phb|3|phb"); + commonTests.assert_Present("subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb"); + commonTests.assert_MISSING("subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb"); + commonTests.assert_Present("subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb"); + commonTests.assert_MISSING("subclassfeature|use magic device|rogue|phb|thief|phb|13|phb"); + commonTests.assert_Present("subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb"); + commonTests.assert_MISSING("subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb"); + commonTests.assert_Present("subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb"); + + commonTests.assert_MISSING("race|bugbear|erlw"); + commonTests.assert_Present("race|bugbear|mpmm"); + commonTests.assert_MISSING("race|bugbear|vgm"); + commonTests.assert_MISSING("race|human|phb"); + commonTests.assert_Present("race|human|xphb"); + commonTests.assert_MISSING("race|tiefling|phb"); + commonTests.assert_Present("race|tiefling|xphb"); + commonTests.assert_MISSING("race|warforged|erlw"); + commonTests.assert_MISSING("race|yuan-ti pureblood|vgm"); + commonTests.assert_Present("race|yuan-ti|mpmm"); + + commonTests.assert_MISSING("subrace|genasi (air)|genasi|eepc|eepc"); + commonTests.assert_Present("subrace|genasi (air)|genasi|mpmm|mpmm"); + commonTests.assert_MISSING("subrace|human|human|phb|phb"); + commonTests.assert_MISSING("subrace|luma (sable)|luma|hwcs|hwcs"); + commonTests.assert_MISSING("subrace|tiefling (zariel)|tiefling|phb|mtf"); + commonTests.assert_MISSING("subrace|tiefling|tiefling|phb|phb"); + commonTests.assert_MISSING("subrace|vampire (ixalan)|vampire|psz|psx"); + } + } + + @Test + public void testClassList() { + commonTests.testClassList(outputPath); + } +} diff --git a/src/test/resources/5e/sources.json b/src/test/resources/5e/sources.json index a0057755a..b417fd635 100644 --- a/src/test/resources/5e/sources.json +++ b/src/test/resources/5e/sources.json @@ -14,9 +14,9 @@ "race|changeling|mpmm" ], "exclude": [ - "monster\\|expert\\|dc", - "monster\\|expert\\|sdw", - "monster\\|expert\\|slw" + "monster|expert|dc", + "monster|expert|sdw", + "monster|expert|slw" ], "excludePattern": [ "race\\|.*\\|dmg" From 22228ab69749c66bf02210232577ed5638b3d1f4 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 29 Jan 2025 20:51:05 -0500 Subject: [PATCH 111/119] =?UTF-8?q?=F0=9F=90=9B=20Backslashes=20in=20table?= =?UTF-8?q?s.=20Resolves=20#622?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/ebullient/convert/tools/JsonTextConverter.java | 8 ++++---- .../convert/tools/dnd5e/JsonTextReplacement.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java index e3d5bcb7f..989853479 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java @@ -237,9 +237,9 @@ default String diceFormula(String diceRoll) { default String diceFormula(String diceRoll, String displayText, boolean average) { // needs to be escaped: \\ to escape the \\ so it is preserved in the output - String noform = parseState().inMarkdownTable() ? "\\\\|noform" : "|noform"; - String avg = parseState().inMarkdownTable() ? "\\\\|avg" : "|avg"; - String dtxt = parseState().inMarkdownTable() ? "\\\\|text(" : "|text("; + String noform = parseState().inMarkdownTable() ? "\\|noform" : "|noform"; + String avg = parseState().inMarkdownTable() ? "\\|avg" : "|avg"; + String dtxt = parseState().inMarkdownTable() ? "\\|text(" : "|text("; String textValue = displayText == null ? "" : displayText.replace("`", ""); // Only a dice formula in the roll part. May also have display text. @@ -256,7 +256,7 @@ default String codeString(String text, DiceFormulaState formulaState) { // reduce dice strings.. when parsing tags, we can't see leadng average default String simplifyFormattedDiceText(String text) { DiceFormulaState formulaState = parseState().diceFormulaState(); - String dtxt = parseState().inMarkdownTable() ? "\\\\|text(" : "|text("; + String dtxt = parseState().inMarkdownTable() ? "\\|text(" : "|text("; // 26 (`dice:1d20+8|noform|text(+8)`) --> `dice:1d20+8|noform|text(26)` (`+8`) text = textAverageRoll.matcher(text).replaceAll((match) -> { diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java index 03883491b..4eeb44292 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java @@ -474,7 +474,7 @@ default String replaceSkillOrAbility(MatchResult match) { DiceRoller roller = cfg().useDiceRoller(); boolean notSuppressed = roller == DiceRoller.enabledUsingFS && !parseState().inTrait(); if (!abilityCheck && (roller == DiceRoller.enabled || notSuppressed)) { - String dtxt = parseState().inMarkdownTable() ? "\\\\|text(" : "|text("; + String dtxt = parseState().inMarkdownTable() ? "\\|text(" : "|text("; mod = "`dice: d20" + mod + dtxt + mod + ")`"; } From cdfb7dd8b741f0fb7fe318ea728b37dba97785ad Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Wed, 29 Jan 2025 22:06:13 -0500 Subject: [PATCH 112/119] =?UTF-8?q?=F0=9F=90=9B=20Fix=20nested=20headers?= =?UTF-8?q?=20in=20composed=20notes.=20Resolves=20#621?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/dev/ebullient/convert/tools/JsonTextConverter.java | 2 +- .../dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java index 989853479..964ecc3ca 100644 --- a/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java +++ b/src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java @@ -411,7 +411,7 @@ default boolean prependField(JsonNode entry, JsonNodeReader field, List } default boolean prependField(String name, List inner) { - if (name != null) { + if (isPresent(name)) { name = replaceText(name.trim()); if (inner.isEmpty()) { inner.add(name); diff --git a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java index bbb45a102..777e7a0a0 100644 --- a/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java +++ b/src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java @@ -94,7 +94,7 @@ private void appendElement(JsonNode entry, List text, Tags tags) { } else if (entry.has("table")) { appendTable(name, entry, text); } else { - appendToText(text, entry, "###"); + appendToText(text, entry, null); } if (type == Tools5eIndexType.itemType && abbreviation != null) { From 821694a723ab5cd950d8d84d6934b8a9db9b41b8 Mon Sep 17 00:00:00 2001 From: GitHub <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:18:40 +0000 Subject: [PATCH 113/119] =?UTF-8?q?=F0=9F=94=96=203.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/project.yml | 4 ++-- README-WINDOWS.md | 10 +++++----- docs/alternateRun.md | 6 +++--- pom.xml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/project.yml b/.github/project.yml index 342651cbf..64c49f712 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -1,7 +1,7 @@ name: TTRPG Convert CLI release: - current-version: 3.0.0 - next-version: 3.0.0 + current-version: 3.0.1 + next-version: 3.0.1 snapshot-version: 399-SNAPSHOT build: artifact: ttrpg-convert-cli diff --git a/README-WINDOWS.md b/README-WINDOWS.md index 29f836591..1f3388628 100644 --- a/README-WINDOWS.md +++ b/README-WINDOWS.md @@ -25,8 +25,8 @@ 1. From the [latest release][1], download the following files: - - `ttrpg-convert-cli-3.0.0-windows-x86_64.zip` - - `ttrpg-convert-cli-3.0.0-examples.zip` + - `ttrpg-convert-cli-3.0.1-windows-x86_64.zip` + - `ttrpg-convert-cli-3.0.1-examples.zip` 2. Unzip the downloaded files into a place you'll remember. For example, `Downloads`. 3. Navigate to the `bin` directory inside the unzipped files. It might be nested within another folder. You should see a `ttrpg-convert` EXE file in the folder - see the screenshot below. @@ -85,7 +85,7 @@ On Windows, the command output will look like this, with weird characters at the ```shell [ Γ£à OK] Finished reading config. -ΓÅ▒∩╕Å Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.0-windows-x86_64\ttrpg-convert-cli-3.0.0-windows-x86_64\bin\5etools-mirror-2.github.io\data +ΓÅ▒∩╕Å Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.1-windows-x86_64\ttrpg-convert-cli-3.0.1-windows-x86_64\bin\5etools-mirror-2.github.io\data [ Γ£à OK] Finished reading data. ``` @@ -100,7 +100,7 @@ You should then start seeing the emoji correctly: ```shell [ ✅ OK] Finished reading config. -⏱️ Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.0-windows-x86_64\ttrpg-convert-cli-3.0.0-windows-x86_64\bin\5etools-mirror-2.github.io\data +⏱️ Reading C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.1-windows-x86_64\ttrpg-convert-cli-3.0.1-windows-x86_64\bin\5etools-mirror-2.github.io\data [ ✅ OK] Finished reading data. ``` @@ -132,7 +132,7 @@ Type in `dir` and press **Enter**. You should see output similar to this: ```shell Directory: - C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.0-windows-x86_64\ttrpg-convert-cli-3.0.0-windows-x86_64\bin + C:\Users\Kelly\Downloads\ttrpg-convert-cli-3.0.1-windows-x86_64\ttrpg-convert-cli-3.0.1-windows-x86_64\bin Mode LastWriteTime Length Name diff --git a/docs/alternateRun.md b/docs/alternateRun.md index 34b1c4f8e..404e1d56c 100644 --- a/docs/alternateRun.md +++ b/docs/alternateRun.md @@ -24,7 +24,7 @@ JBang is a tool designed to simplify Java application execution. By eliminating 2. Install the pre-built release of ttrpg-convert-cli: ```shell - jbang app install --name ttrpg-convert --force --fresh https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.0.0/ttrpg-convert-cli-3.0.0-runner.jar + jbang app install --name ttrpg-convert --force --fresh https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.0.1/ttrpg-convert-cli-3.0.1-runner.jar ``` 🚧 If you want the latest [_unreleased snapshot_][]: @@ -127,13 +127,13 @@ To run the CLI, you will need to have **Java 17** installed on your system. 2. Download the CLI as a jar - - Latest release: [ttrpg-convert-cli-3.0.0-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.0.0/ttrpg-convert-cli-3.0.0-runner.jar) + - Latest release: [ttrpg-convert-cli-3.0.1-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.0.1/ttrpg-convert-cli-3.0.1-runner.jar) - 🚧 [_unreleased snapshot_][]: [ttrpg-convert-cli-299-SNAPSHOT-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/299-SNAPSHOT/ttrpg-convert-cli-299-SNAPSHOT-runner.jar) 3. Verify the install by running the command: ```shell - java -jar ttrpg-convert-cli-3.0.0-runner.jar --help + java -jar ttrpg-convert-cli-3.0.1-runner.jar --help ``` 🚧 If you are using the [_unreleased snapshot_][], use the following command: diff --git a/pom.xml b/pom.xml index 001a723df..a2233ca8d 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ https://github.com/ebullient/ttrpg-convert-cli/issues - 399-SNAPSHOT + 3.0.1 3.4.0 3.13.0 From 8c662a2e6860ca6c0a50c0d654effda2d7681e8d Mon Sep 17 00:00:00 2001 From: GitHub <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:26:21 +0000 Subject: [PATCH 114/119] =?UTF-8?q?=F0=9F=94=A7=20Prepare=20for=20next=20r?= =?UTF-8?q?elease?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a2233ca8d..001a723df 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ https://github.com/ebullient/ttrpg-convert-cli/issues - 3.0.1 + 399-SNAPSHOT 3.4.0 3.13.0 From 09685075aeddbaee3ee73a68657d319492e661d1 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 30 Jan 2025 14:22:04 -0500 Subject: [PATCH 115/119] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=F0=9F=91=B7=20=20?= =?UTF-8?q?replace=20some=20workflows=20w/=20reusable=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/maven.yml | 120 +++++++++++++--- .github/workflows/pf2e-data-cache.yml | 69 +++++++++ .github/workflows/pf2e-tools-data.yml | 128 +++++------------ .github/workflows/pull-request.yml | 67 ++++----- .github/workflows/tools-data.yml | 170 ----------------------- .github/workflows/tools5e-data-cache.yml | 167 ++++++++++++++++++++++ .github/workflows/tools5e-data.yml | 97 +++++++++++++ 7 files changed, 492 insertions(+), 326 deletions(-) create mode 100644 .github/workflows/pf2e-data-cache.yml delete mode 100644 .github/workflows/tools-data.yml create mode 100644 .github/workflows/tools5e-data-cache.yml create mode 100644 .github/workflows/tools5e-data.yml diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index f4c6d94ef..c3c3093b3 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,4 +1,4 @@ -name: Java CI with Maven +name: Main Maven build on: workflow_dispatch: @@ -15,49 +15,131 @@ on: env: JAVA_VERSION: 17 - GRAALVM_DIST: graalvm-community JAVA_DISTRO: temurin + NATIVE_JAVA_VERSION: 23 + GRAALVM_DIST: graalvm-community GH_BOT_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com" GH_BOT_NAME: "GitHub Action" permissions: read-all jobs: - main-root: + main_root: runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' && github.repository == 'ebullient/ttrpg-convert-cli' + outputs: + is_main: ${{ steps.is_main_root.outputs.is_main }} steps: - name: Echo a message - id: is-main-root - run: echo "This is the main branch of 'ebullient/ttrpg-convert-cli'" + id: is_main_root + if: github.ref == 'refs/heads/main' && github.repository == 'ebullient/ttrpg-convert-cli' + run: | + echo "This is the main branch of 'ebullient/ttrpg-convert-cli'" + echo "is_main=true" >> $GITHUB_OUTPUT metadata: uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@main + tools5e_cache: + uses: ./.github/workflows/tools5e-data-cache.yml + + pf2e_cache: + uses: ./.github/workflows/pf2e-data-cache.yml + build: - uses: ebullient/workflows/.github/workflows/java-snap-build.yml@main - needs: [metadata] + runs-on: ubuntu-latest + needs: [main_root, metadata, tools5e_cache, pf2e_cache] permissions: contents: write actions: write - with: - artifact: ${{ needs.metadata.outputs.artifact }} - snapshot: ${{ needs.metadata.outputs.snapshot }} - push: "README.md README-WINDOWS.md docs" - secrets: inherit + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + fetch-tags: false + + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: tools5e-cache-load + with: + path: sources + key: ${{ needs.tools5e_cache.outputs.cache_key }} + restore-keys: | + Data-5etools- + Data-5etools + + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: pf2e-cache-load + with: + path: sources/Pf2eTools + key: Data-Pf2eTools- + restore-keys: | + Data-Pf2eTools- + Data-Pf2eTools + + - name: Build with Maven + uses: ebullient/workflows/.github/actions/maven-build@main + with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} + java-version: ${{ env.JAVA_VERSION }} + java-distribution: ${{ env.JAVA_DISTRO }} + + - name: Push changes to files + if: needs.main_root.outputs.is_main + uses: ebullient/workflows/.github/actions/push-changes@main + with: + files: "README.md README-WINDOWS.md docs" native-build: - needs: [metadata, build] - uses: ebullient/workflows/.github/workflows/java-release-native-binaries.yml@main + needs: [metadata, tools5e_cache, pf2e_cache, build] + name: Build ${{ matrix.os }} binary + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 3 + matrix: + os: [macos-13, macos-latest, windows-latest, ubuntu-latest] permissions: contents: read actions: write - with: - version: ${{ needs.metadata.outputs.snapshot }} - secrets: inherit + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: tools5e-cache-load + with: + path: sources + key: ${{ needs.tools5e_cache.outputs.cache_key }} + restore-keys: | + Data-5etools- + Data-5etools + enableCrossOsArchive: true + + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: pf2e-cache-load + with: + path: sources/Pf2eTools + key: Data-Pf2eTools- + restore-keys: | + Data-Pf2eTools- + Data-Pf2eTools + enableCrossOsArchive: true + + - name: Native build with Maven + uses: ebullient/workflows/.github/actions/native-build@main + with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} + native-java-version: ${{ env.NATIVE_JAVA_VERSION }} + distribution: ${{ env.GRAALVM_DIST }} + matrix-os: ${{ matrix.os }} snap-release: - needs: [main-root, metadata, build, native-build] + needs: [main_root, metadata, build, native-build] + if: needs.main_root.outputs.is_main uses: ebullient/workflows/.github/workflows/java-snapshot.yml@main permissions: contents: write diff --git a/.github/workflows/pf2e-data-cache.yml b/.github/workflows/pf2e-data-cache.yml new file mode 100644 index 000000000..75e090f99 --- /dev/null +++ b/.github/workflows/pf2e-data-cache.yml @@ -0,0 +1,69 @@ +name: Pf2eTools Data Cache +on: + schedule: + - cron: "7 9 * * */5" + + workflow_call: + outputs: + cache_key: + description: "Cache key for current version" + value: ${{ jobs.pf2e_cache.outputs.cache_key }} + workflow_dispatch: + +permissions: read-all + +jobs: + pf2e_cache: + runs-on: ubuntu-latest + outputs: + cache_key: ${{ steps.test_data_key.outputs.cache_key }} + concurrency: + group: pf2e-data + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - name: Pf2e Tools release cache key + id: test_data_key + run: | + LATEST_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/Pf2eToolsOrg/Pf2eTools/releases/latest | jq -r .tag_name) + echo $LATEST_VERSION + + echo "🔹 Use $LATEST_VERSION" + echo "tools_version=${LATEST_VERSION}" >> $GITHUB_OUTPUT + echo "cache_key=Data-Pf2eTools-${LATEST_VERSION}" >> $GITHUB_OUTPUT + + - name: Check Cache Data + id: test_data_check + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: sources + key: ${{ steps.test_data_key.outputs.cache_key }} + lookup-only: true + enableCrossOsArchive: true + + - name: Download Test Data + id: test-data-download + if: steps.test_data_check.outputs.cache-hit != 'true' + env: + LATEST_VERSION: ${{ steps.test_data_key.outputs.tools_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "🔹 Download $LATEST_VERSION" + + # sparse clone of src + mkdir -p sources/Pf2eTools + git clone --quiet --depth=1 -b ${LATEST_VERSION} -c advice.detachedHead=false --no-checkout https://github.com/Pf2eToolsOrg/Pf2eTools.git sources/Pf2eTools + pushd sources/Pf2eTools + git sparse-checkout init + git sparse-checkout set data img + git checkout ${LATEST_VERSION} + rm -rf .git + find img -type f | while read FILE; do echo > "$FILE"; done + popd + + ls -al sources/Pf2eTools + du -sh sources/Pf2eTools + diff --git a/.github/workflows/pf2e-tools-data.yml b/.github/workflows/pf2e-tools-data.yml index b31068800..e23d94d20 100644 --- a/.github/workflows/pf2e-tools-data.yml +++ b/.github/workflows/pf2e-tools-data.yml @@ -1,7 +1,6 @@ -name: Pf2e Tools Data +name: Pf2e Tools Test on: schedule: - # At 09:07 on Saturday (because why not) - cron: "7 9 * * */5" workflow_dispatch: @@ -16,96 +15,40 @@ env: permissions: read-all jobs: - cache-setup: - runs-on: ubuntu-latest - outputs: - cache_key: ${{ steps.test-data-key.outputs.cache_key }} - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - - name: Pf2e Tools release cache key - id: test-data-key - run: | - LATEST_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/Pf2eToolsOrg/Pf2eTools/releases/latest | jq -r .tag_name) - echo $LATEST_VERSION - - echo "🔹 Use $LATEST_VERSION" - echo "tools_version=${LATEST_VERSION}" >> $GITHUB_OUTPUT - echo "cache_key=Data-Pf2eTools-${LATEST_VERSION}" >> $GITHUB_OUTPUT - - - name: Check Cache Data - id: test-data-check - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: sources - key: ${{ steps.test-data-key.outputs.cache_key }} - lookup-only: true - enableCrossOsArchive: true - - - name: Download Test Data - id: test-data-download - if: steps.test-data-check.outputs.cache-hit != 'true' - env: - LATEST_VERSION: ${{ steps.test-data-key.outputs.tools_version }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p sources + metadata: + uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@main - echo "🔹 Download $LATEST_VERSION" + pf2e_cache: + uses: ./.github/workflows/pf2e-data-cache.yml - gh repo clone Pf2eToolsOrg/Pf2eTools sources/Pf2eTools -- --depth=1 -c advice.detachedHead=false -b $LATEST_VERSION - - # Remove image contents. We just need the files to exist (linking) - find sources -type f -type f \ - \( -iname \*.jpg -o -iname \*.png -o -iname \*.webp \) \ - | while read FILE; do echo > "$FILE"; done - - ls -al sources - - test-with-data: - - name: Test with data - needs: cache-setup + build: runs-on: ubuntu-latest + needs: [pf2e_cache, metadata] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: cache + id: pf2e-cache-load with: path: sources - key: ${{ needs.cache-setup.outputs.cache_key }} + key: ${{ needs.pf2e_cache.outputs.cache_key }} + restore-keys: | + Pf2eTools- + Pf2eTools fail-on-cache-miss: true - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 + - name: Build with Maven + uses: ebullient/workflows/.github/actions/maven-build@main with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} java-version: ${{ env.JAVA_VERSION }} - distribution: ${{ env.JAVA_DISTRO }} - cache: maven - - - name: Build with Maven - id: mvn-build - run: | - ls -al sources - ./mvnw -B -ntp -DskipFormat verify + java-distribution: ${{ env.JAVA_DISTRO }} - native-test-with-data: - - name: Test on ${{ matrix.os }} - needs: [cache-setup, test-with-data] - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - max-parallel: 1 - matrix: - os: [windows-latest, macos-latest, ubuntu-latest] + native-build: + runs-on: ubuntu-latest + needs: [pf2e_cache, metadata, build] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -113,37 +56,30 @@ jobs: fetch-depth: 1 - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: cache + id: pf2e-cache-load with: path: sources - key: ${{ needs.cache-setup.outputs.cache_key }} + key: ${{ needs.pf2e_cache.outputs.cache_key }} + restore-keys: | + Pf2eTools- + Pf2eTools fail-on-cache-miss: true - enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 + - name: Native build with Maven + uses: ebullient/workflows/.github/actions/native-build@main with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} + native-java-version: ${{ env.NATIVE_JAVA_VERSION }} distribution: ${{ env.GRAALVM_DIST }} - java-version: ${{ env.NATIVE_JAVA_VERSION }} - github-token: ${{ secrets.GITHUB_TOKEN }} - cache: 'maven' - - - if: runner.os == 'Windows' - name: clean before native build - shell: cmd - run: | - ./mvnw -B -ntp -DskipTests -DskipFormat clean - - - name: Build, run, and test in native mode - id: mvn-native-build - run: | - ./mvnw -B -ntp -Dnative -DskipFormat verify + matrix-os: ubuntu-latest report-native-build: name: Report errors runs-on: ubuntu-latest if: ${{ failure() }} - needs: [test-with-data, native-test-with-data] + needs: [build, native-build] permissions: contents: read issues: write diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8a66026a3..22c7b913c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,5 +1,4 @@ name: PR Maven Build - on: pull_request: paths: @@ -24,24 +23,30 @@ jobs: metadata: uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@main + tools5e_cache: + uses: ./.github/workflows/tools5e-data-cache.yml + + pf2e_cache: + uses: ./.github/workflows/pf2e-data-cache.yml + build: runs-on: ubuntu-latest - needs: [metadata] + needs: [tools5e_cache, metadata] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: tools5e-cache + id: tools5e-cache-load with: path: sources - key: Data-5etools- + key: ${{ needs.tools5e_cache.outputs.cache_key }} restore-keys: | Data-5etools- Data-5etools - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: pf2e-cache + id: pf2e-cache-load with: path: sources/Pf2eTools key: Data-Pf2eTools- @@ -49,30 +54,17 @@ jobs: Data-Pf2eTools- Data-Pf2eTools - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 + - name: Build with Maven + uses: ebullient/workflows/.github/actions/maven-build@main with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} java-version: ${{ env.JAVA_VERSION }} - distribution: ${{ env.JAVA_DISTRO }} - cache: maven - - - name: Build with Maven - id: mvn-build - run: | - mkdir -p sources - ls -al sources - ./mvnw -B -ntp -DskipFormat verify + java-distribution: ${{ env.JAVA_DISTRO }} native-build: - - name: Test on ${{ matrix.os }} - needs: [metadata] - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - max-parallel: 1 - matrix: - os: [windows-latest, macos-latest, ubuntu-latest] + runs-on: ubuntu-latest + needs: [tools5e_cache, metadata, build] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -80,36 +72,29 @@ jobs: fetch-depth: 1 - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: tools5e-cache + id: tools5e-cache-load with: path: sources - key: Data-5etools- + key: ${{ needs.tools5e_cache.outputs.cache_key }} restore-keys: | Data-5etools- Data-5etools - enableCrossOsArchive: true - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: cache + id: pf2e-cache-load with: path: sources/Pf2eTools key: Data-Pf2eTools- restore-keys: | Data-Pf2eTools- Data-Pf2eTools - enableCrossOsArchive: true - - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 + - name: Native build with Maven + uses: ebullient/workflows/.github/actions/native-build@main with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} + native-java-version: ${{ env.NATIVE_JAVA_VERSION }} distribution: ${{ env.GRAALVM_DIST }} - java-version: ${{ env.NATIVE_JAVA_VERSION }} - github-token: ${{ secrets.GITHUB_TOKEN }} - cache: 'maven' - - - name: Build and run in native mode - id: mvn-native-build - env: - MAVEN_OPTS: "-Xmx1g" - run: | - ./mvnw -B -ntp -Dnative -DskipTests -DskipFormat verify + matrix-os: ubuntu-latest diff --git a/.github/workflows/tools-data.yml b/.github/workflows/tools-data.yml deleted file mode 100644 index 3c251cad7..000000000 --- a/.github/workflows/tools-data.yml +++ /dev/null @@ -1,170 +0,0 @@ -name: Test Tools Data -on: - schedule: - - cron: "7 9 * * */5" - - workflow_dispatch: - inputs: - build: - description: "Build after creating cache" - default: true - required: true - type: boolean - -env: - JAVA_VERSION: 17 - JAVA_DISTRO: temurin - NATIVE_JAVA_VERSION: 23 - GRAALVM_DIST: graalvm-community - FAIL_ISSUE: 140 - -permissions: read-all - -jobs: - cache-setup: - runs-on: ubuntu-latest - outputs: - cache_key: ${{ steps.test-data-key.outputs.cache_key }} - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - - name: Tools release cache key - id: test-data-key - run: | - LATEST_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/5etools-mirror-3/5etools-src/releases/latest | jq -r .tag_name) - echo $LATEST_VERSION - - echo "🔹 Use $LATEST_VERSION" - echo "tools_version=${LATEST_VERSION}" >> $GITHUB_OUTPUT - echo "cache_key=Data-5etools-${LATEST_VERSION}" >> $GITHUB_OUTPUT - - - name: Check Cache Data - id: test-data-check - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - path: sources - key: ${{ steps.test-data-key.outputs.cache_key }} - lookup-only: true - enableCrossOsArchive: true - - - name: Download Test Data - id: test-data-download - if: steps.test-data-check.outputs.cache-hit != 'true' - env: - LATEST_VERSION: ${{ steps.test-data-key.outputs.tools_version }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p sources - - echo "🔹 Download $LATEST_VERSION" - - gh repo clone 5etools-mirror-3/5etools-src sources/5etools-src -- --depth=1 -c advice.detachedHead=false -b $LATEST_VERSION - gh repo clone 5etools-mirror-3/5etools-img sources/5etools-img -- --depth=1 -c advice.detachedHead=false -b $LATEST_VERSION - gh repo clone TheGiddyLimit/unearthed-arcana sources/5e-unearthed-arcana -- --depth=1 - gh repo clone TheGiddyLimit/homebrew sources/5e-homebrew -- --depth=1 - - # Remove image (and other non-json) contents. - # Mostly relevant for images. We only need the files to exist (linking) - - find sources -type f ! -name '*.json' ! -path '*.git*' | while read FILE; do echo > "$FILE"; done - - ls -al sources - - test-with-data: - - if: ${{ inputs.build }} - name: Test with data - needs: cache-setup - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: cache - with: - path: sources - key: ${{ needs.cache-setup.outputs.cache_key }} - fail-on-cache-miss: true - - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 - with: - java-version: ${{ env.JAVA_VERSION }} - distribution: ${{ env.JAVA_DISTRO }} - cache: maven - - - name: Build with Maven - id: mvn-build - run: | - ls -al sources - ./mvnw -B -ntp -DskipFormat verify - - native-test-with-data: - - name: Test on ${{ matrix.os }} - needs: [cache-setup, test-with-data] - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - max-parallel: 1 - matrix: - os: [windows-latest, macos-latest, ubuntu-latest] - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - id: cache - with: - path: sources - key: ${{ needs.cache-setup.outputs.cache_key }} - fail-on-cache-miss: true - enableCrossOsArchive: true - - - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 - with: - distribution: ${{ env.GRAALVM_DIST }} - java-version: ${{ env.NATIVE_JAVA_VERSION }} - github-token: ${{ secrets.GITHUB_TOKEN }} - cache: 'maven' - - - if: runner.os == 'Windows' - name: clean before native build - shell: cmd - run: | - ./mvnw -B -ntp -DskipTests -DskipFormat clean - - - name: Build, run, and test in native mode - id: mvn-native-build - run: | - ./mvnw -B -ntp -Dnative -DskipFormat verify - - report-native-build: - - name: Report errors - runs-on: ubuntu-latest - if: ${{ failure() }} - needs: [test-with-data, native-test-with-data] - permissions: - contents: read - issues: write - - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - - id: gh-issue - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh issue comment ${{ env.FAIL_ISSUE }} --body "[Maven build failed: ${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" - gh issue reopen ${{ env.FAIL_ISSUE }} diff --git a/.github/workflows/tools5e-data-cache.yml b/.github/workflows/tools5e-data-cache.yml new file mode 100644 index 000000000..6f9050dc4 --- /dev/null +++ b/.github/workflows/tools5e-data-cache.yml @@ -0,0 +1,167 @@ +name: 5e Tools Data Cache +on: + schedule: + - cron: "7 9 * * */5" + + workflow_call: + outputs: + cache_key: + description: "Cache key for current version" + value: ${{ jobs.tools5e_cache.outputs.cache_key }} + workflow_dispatch: + +permissions: read-all + +jobs: + tools5e_cache: + runs-on: ubuntu-latest + outputs: + cache_key: ${{ steps.test_data_key.outputs.cache_key }} + concurrency: + group: 5etools-data + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - name: Tools release cache key + id: test_data_key + run: | + LATEST_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/5etools-mirror-3/5etools-src/releases/latest | jq -r .tag_name) + echo $LATEST_VERSION + + echo "🔹 Use $LATEST_VERSION" + echo "tools_version=${LATEST_VERSION}" >> $GITHUB_OUTPUT + echo "cache_key=Data-5etools-${LATEST_VERSION}" >> $GITHUB_OUTPUT + + - name: Check Cache Data + id: test_data_check + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: sources + key: ${{ steps.test_data_key.outputs.cache_key }} + lookup-only: true + enableCrossOsArchive: true + + - name: Download Test Data + id: test-data-download + if: steps.test_data_check.outputs.cache-hit != 'true' + env: + LATEST_VERSION: ${{ steps.test_data_key.outputs.tools_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "🔹 Download $LATEST_VERSION" + + # sparse clone of src + mkdir -p sources/5etools-src + git clone --quiet --depth=1 -b ${LATEST_VERSION} -c advice.detachedHead=false --no-checkout https://github.com/5etools-mirror-3/5etools-src.git sources/5etools-src + pushd sources/5etools-src + git sparse-checkout init + git sparse-checkout set data + git checkout ${LATEST_VERSION} + rm -rf .git + rm *.png + rm *.html + rm *.zip + popd + + mkdir -p sources/5etools-img + gh release download ${LATEST_VERSION} -D sources/5etools-img -R 5etools-mirror-3/5etools-img + pushd sources/5etools-img + zip -FF img-${LATEST_VERSION}.zip --out fixed.zip + unzip -q fixed.zip -d . + du -sh . + rm *.z* + # replace all images w/ empty files. We don't care about image content for test purposes + find img -type f | while read FILE; do echo > "$FILE"; done + du -sh . + popd + + # Don't grab all of homebrew. Too big + mkdir -p sources/5e-homebrew/adventure + mkdir -p sources/5e-homebrew/background + mkdir -p sources/5e-homebrew/book + mkdir -p sources/5e-homebrew/class + mkdir -p sources/5e-homebrew/collection + mkdir -p sources/5e-homebrew/creature + mkdir -p sources/5e-homebrew/deity + mkdir -p sources/5e-homebrew/optionalfeature + mkdir -p sources/5e-homebrew/race + mkdir -p sources/5e-homebrew/subclass + + paths=( + "collection/MCDM Productions; Strongholds and Followers.json" + "adventure/Anthony Joyce; The Blood Hunter Adventure.json" + "adventure/Arcanum Worlds; Odyssey of the Dragonlords.json" + "adventure/JVC Parry; Call from the Deep.json" + "adventure/Kobold Press; Book of Lairs.json" + "background/D&D Wiki; Featured Quality Backgrounds.json" + "book/Ghostfire Gaming; Grim Hollow Campaign Guide.json" + "book/Ghostfire Gaming; Stibbles Codex of Companions.json" + "book/MCDM Productions; Arcadia Issue 3.json" + "class/D&D Wiki; Swashbuckler.json" + "class/Foxfire94; Vampire.json" + "class/KibblesTasty; Inventor.json" + "class/LaserLlama; Alternate Barbarian.json" + "class/Matthew Mercer; Blood Hunter (2022).json" + "class/badooga; Badooga's Psion.json" + "collection/Arcana Games; Arkadia.json" + "collection/Ghostfire Gaming; Grim Hollow - The Monster Grimoire.json" + "collection/Jasmine Yang; Hamund's Herbalism Handbook.json" + "collection/Keith Baker; Exploring Eberron.json" + "collection/Kobold Press; Deep Magic 14 Elemental Magic.json" + "collection/Kobold Press; Deep Magic.json" + "collection/Loot Tavern; Heliana's Guide To Monster Hunting.json" + "collection/MCDM Productions; The Talent and Psionics Open Playtest Round 2.json" + "collection/Mage Hand Press; Valda's Spire of Secrets.json" + "creature/Dragonix; Monster Manual Expanded III.json" + "creature/Kobold Press; Creature Codex.json" + "creature/Kobold Press; Tome of Beasts 2.json" + "creature/Kobold Press; Tome of Beasts.json" + "creature/MCDM Productions; Flee, Mortals! preview.json" + "creature/MCDM Productions; Flee, Mortals!.json" + "creature/Nerzugal Role-Playing; Nerzugal's Extended Bestiary.json" + "deity/Frog God Games; The Lost Lands.json" + "optionalfeature/laserllama; Laserllama's Exploit Compendium.json" + "race/Middle Finger of Vecna; Archon.json" + "subclass/LaserLlama; Druid Circles.json" + ) + + for i in "${paths[@]}"; do + echo "$i" + url=${i// /%20} + echo "$url" + curl -s -S -L -o "sources/5e-homebrew/$i" "https://raw.githubusercontent.com/TheGiddyLimit/homebrew/refs/heads/master/${url}" + done + + # Don't grab all of unearthed arcana + mkdir -p sources/5e-unearthed-arcana/collection + + paths=( + "collection/Unearthed Arcana - Downtime.json" + "collection/Unearthed Arcana - Encounter Building.json" + "collection/Unearthed Arcana - Into the Wild.json" + "collection/Unearthed Arcana - Quick Characters.json" + "collection/Unearthed Arcana - Traps Revisited.json" + "collection/Unearthed Arcana - When Armies Clash.json" + "collection/Unearthed Arcana 2022 - Character Origins.json" + "collection/Unearthed Arcana 2022 - Expert Classes.json" + "collection/Unearthed Arcana 2022 - The Cleric and Revised Species.json" + "collection/Unearthed Arcana 2023 - Bastions and Cantrips.json" + "collection/Unearthed Arcana 2023 - Druid & Paladin.json" + "collection/Unearthed Arcana 2023 - Player's Handbook Playtest 5.json" + "collection/Unearthed Arcana 2023 - Player's Handbook Playtest 6.json" + "collection/Unearthed Arcana 2023 - Player's Handbook Playtest 7.json" + ) + + for i in "${paths[@]}"; do + echo "$i" + url=${i// /%20} + echo "$url" + curl -s -S -L -o "sources/5e-unearthed-arcana/$i" "https://raw.githubusercontent.com/TheGiddyLimit/unearthed-arcana/refs/heads/master/${url}" + done + + ls -al sources + du -sh sources + diff --git a/.github/workflows/tools5e-data.yml b/.github/workflows/tools5e-data.yml new file mode 100644 index 000000000..58453da83 --- /dev/null +++ b/.github/workflows/tools5e-data.yml @@ -0,0 +1,97 @@ +name: 5e Tools Test +on: + schedule: + - cron: "7 9 * * */5" + + workflow_dispatch: + +env: + JAVA_VERSION: 17 + JAVA_DISTRO: temurin + NATIVE_JAVA_VERSION: 23 + GRAALVM_DIST: graalvm-community + FAIL_ISSUE: 140 + +permissions: read-all + +jobs: + metadata: + uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@main + + tools5e_cache: + uses: ./.github/workflows/tools5e-data-cache.yml + + build: + runs-on: ubuntu-latest + needs: [tools5e_cache, metadata] + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: tools5e-cache-load + with: + path: sources + key: ${{ needs.tools5e_cache.outputs.cache_key }} + restore-keys: | + Data-5etools- + Data-5etools + fail-on-cache-miss: true + + - name: Build with Maven + uses: ebullient/workflows/.github/actions/maven-build@main + with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} + java-version: ${{ env.JAVA_VERSION }} + java-distribution: ${{ env.JAVA_DISTRO }} + + native-build: + runs-on: ubuntu-latest + needs: [tools5e_cache, metadata, build] + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: tools5e-cache-load + with: + path: sources + key: ${{ needs.tools5e_cache.outputs.cache_key }} + restore-keys: | + Data-5etools- + Data-5etools + fail-on-cache-miss: true + + - name: Native build with Maven + uses: ebullient/workflows/.github/actions/native-build@main + with: + artifact: ${{ needs.metadata.outputs.artifact }} + version: ${{ needs.metadata.outputs.snapshot }} + native-java-version: ${{ env.NATIVE_JAVA_VERSION }} + distribution: ${{ env.GRAALVM_DIST }} + matrix-os: ubuntu-latest + + report-native-build: + + name: Report errors + runs-on: ubuntu-latest + if: ${{ failure() }} + needs: [build, native-build] + permissions: + contents: read + issues: write + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - id: gh-issue + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh issue comment ${{ env.FAIL_ISSUE }} --body "[Maven build failed: ${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" + gh issue reopen ${{ env.FAIL_ISSUE }} From dd1b7ff497c6996b158602fa3ee1ec2d8a48fc60 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 30 Jan 2025 16:01:53 -0500 Subject: [PATCH 116/119] =?UTF-8?q?=F0=9F=91=B7=20tools=20data=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/maven.yml | 4 ++-- .github/workflows/tools5e-data-cache.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c3c3093b3..14e104b03 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -71,7 +71,7 @@ jobs: id: pf2e-cache-load with: path: sources/Pf2eTools - key: Data-Pf2eTools- + key: ${{ needs.pf2e_cache.outputs.cache_key }} restore-keys: | Data-Pf2eTools- Data-Pf2eTools @@ -122,7 +122,7 @@ jobs: id: pf2e-cache-load with: path: sources/Pf2eTools - key: Data-Pf2eTools- + key: ${{ needs.pf2e_cache.outputs.cache_key }} restore-keys: | Data-Pf2eTools- Data-Pf2eTools diff --git a/.github/workflows/tools5e-data-cache.yml b/.github/workflows/tools5e-data-cache.yml index 6f9050dc4..7f666c435 100644 --- a/.github/workflows/tools5e-data-cache.yml +++ b/.github/workflows/tools5e-data-cache.yml @@ -70,11 +70,12 @@ jobs: gh release download ${LATEST_VERSION} -D sources/5etools-img -R 5etools-mirror-3/5etools-img pushd sources/5etools-img zip -FF img-${LATEST_VERSION}.zip --out fixed.zip - unzip -q fixed.zip -d . - du -sh . + unzip -q fixed.zip rm *.z* + du -sh . # replace all images w/ empty files. We don't care about image content for test purposes find img -type f | while read FILE; do echo > "$FILE"; done + mv img/* . du -sh . popd From 8b6fcf86b25e346b8f90744910838b5d5a3c2692 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 30 Jan 2025 21:14:54 -0500 Subject: [PATCH 117/119] =?UTF-8?q?=F0=9F=91=B7=20update=20maven=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mvn/wrapper/MavenWrapperDownloader.java | 142 ------- .mvn/wrapper/maven-wrapper.properties | 5 +- mvnw | 459 ++++++++++------------- mvnw.cmd | 337 ++++++++--------- 4 files changed, 353 insertions(+), 590 deletions(-) delete mode 100644 .mvn/wrapper/MavenWrapperDownloader.java diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index 17083930c..000000000 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader -{ - private static final String WRAPPER_VERSION = "3.1.1"; - - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = - "https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/" + WRAPPER_VERSION - + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to use instead of the - * default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main( String args[] ) - { - System.out.println( "- Downloader started" ); - File baseDirectory = new File( args[0] ); - System.out.println( "- Using base directory: " + baseDirectory.getAbsolutePath() ); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File( baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH ); - String url = DEFAULT_DOWNLOAD_URL; - if ( mavenWrapperPropertyFile.exists() ) - { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try - { - mavenWrapperPropertyFileInputStream = new FileInputStream( mavenWrapperPropertyFile ); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load( mavenWrapperPropertyFileInputStream ); - url = mavenWrapperProperties.getProperty( PROPERTY_NAME_WRAPPER_URL, url ); - } - catch ( IOException e ) - { - System.out.println( "- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'" ); - } - finally - { - try - { - if ( mavenWrapperPropertyFileInputStream != null ) - { - mavenWrapperPropertyFileInputStream.close(); - } - } - catch ( IOException e ) - { - // Ignore ... - } - } - } - System.out.println( "- Downloading from: " + url ); - - File outputFile = new File( baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH ); - if ( !outputFile.getParentFile().exists() ) - { - if ( !outputFile.getParentFile().mkdirs() ) - { - System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() - + "'" ); - } - } - System.out.println( "- Downloading to: " + outputFile.getAbsolutePath() ); - try - { - downloadFileFromURL( url, outputFile ); - System.out.println( "Done" ); - System.exit( 0 ); - } - catch ( Throwable e ) - { - System.out.println( "- Error downloading" ); - e.printStackTrace(); - System.exit( 1 ); - } - } - - private static void downloadFileFromURL( String urlString, File destination ) - throws Exception - { - if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) - { - String username = System.getenv( "MVNW_USERNAME" ); - char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); - Authenticator.setDefault( new Authenticator() - { - @Override - protected PasswordAuthentication getPasswordAuthentication() - { - return new PasswordAuthentication( username, password ); - } - } ); - } - URL website = new URL( urlString ); - ReadableByteChannel rbc; - rbc = Channels.newChannel( website.openStream() ); - FileOutputStream fos = new FileOutputStream( destination ); - fos.getChannel().transferFrom( rbc, 0, Long.MAX_VALUE ); - fos.close(); - rbc.close(); - } - -} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 61a2ef150..84f60ef34 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -14,5 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.8/apache-maven-3.8.8-bin.zip diff --git a/mvnw b/mvnw index eaa3d308f..19529ddf8 100755 --- a/mvnw +++ b/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,298 +19,241 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`\\unset -f command; \\command -v java`" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index abb7c3242..b150b91ed 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,188 +1,149 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" From 1c488fa91ba678baa741e0bb0181c891d5257f8d Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 30 Jan 2025 21:29:20 -0500 Subject: [PATCH 118/119] =?UTF-8?q?=F0=9F=91=B7=20use=20concurrency=20grou?= =?UTF-8?q?p=20to=20reduce=20native=20builds=20after=20push=20to=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/maven.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 14e104b03..7768105b9 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -94,6 +94,9 @@ jobs: needs: [metadata, tools5e_cache, pf2e_cache, build] name: Build ${{ matrix.os }} binary runs-on: ${{ matrix.os }} + concurrency: + group: native-${{ matrix.os }}-${{ github.ref }} + cancel-in-progress: true strategy: fail-fast: false max-parallel: 3 From deb4e8f990962d9959258beee070068a81b8f6e7 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Thu, 30 Jan 2025 22:22:52 -0500 Subject: [PATCH 119/119] =?UTF-8?q?=F0=9F=90=9B=20fix=20tests=20for=20wind?= =?UTF-8?q?ows=20native=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/dev/ebullient/convert/CustomTemplatesTest.java | 6 +++--- src/test/java/dev/ebullient/convert/TestUtils.java | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java index ab6f1d98f..fb8e25296 100644 --- a/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java +++ b/src/test/java/dev/ebullient/convert/CustomTemplatesTest.java @@ -76,8 +76,8 @@ void testCommandHelp(LaunchResult result) { testOutput = rootTestOutput.resolve("help"); result.echoSystemOut(); assertThat(result.getOutput()) - .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) - .contains("Usage: ttrpg-convert"); + .withFailMessage("Usage statement not found in output. Output:%n%s", TestUtils.dump(result)) + .contains("Usage:"); } @Test @@ -86,7 +86,7 @@ void testCommandVersion(LaunchResult result) { testOutput = rootTestOutput.resolve("version"); result.echoSystemOut(); assertThat(result.getOutput()) - .withFailMessage("Command failed. Output:%n%s", TestUtils.dump(result)) + .withFailMessage("Version statement not found in output. Output:%n%s", TestUtils.dump(result)) .contains("ttrpg-convert version"); } diff --git a/src/test/java/dev/ebullient/convert/TestUtils.java b/src/test/java/dev/ebullient/convert/TestUtils.java index a65fd5dd5..1bf97ea44 100644 --- a/src/test/java/dev/ebullient/convert/TestUtils.java +++ b/src/test/java/dev/ebullient/convert/TestUtils.java @@ -354,7 +354,8 @@ static List checkDirectoryContents(Path directory, Tui tui, } public static String dump(LaunchResult result) { - return "\n" + result.getOutput() + "\nSystem err:\n" + result.getErrorOutput(); + return "\nSystem out:\n" + result.getOutput() + + "\nSystem err:\n" + result.getErrorOutput(); } public static void deleteDir(Path path) {