Compare commits

..

79 commits
main ... 1.9.13

Author SHA1 Message Date
WBHarry
3d98f16ac6 Raised version 2026-05-02 22:58:11 +02:00
WBHarry
98c8e1f498
Fixed so that reseting Uses upon finishing downtime uses the actor's rollData and not just the actor data (#1859) 2026-05-02 15:59:36 -04:00
WBHarry
2e524878b3 Raised version 2026-05-01 23:06:40 +02:00
WBHarry
161c8222dc Corrected a typo in Greater Earth Elemental and Huge Green Ooze 2026-05-01 20:58:56 +02:00
WBHarry
00a1d9766c Fixed SRD DireBat experience value 2026-05-01 17:47:47 +02:00
WBHarry
f7b82b7d7e Corrected contributing link in readme 2026-04-29 22:04:37 +02:00
WBHarry
3d7199e7e5 Corrected Glowing Rings damage 2026-04-27 16:07:55 +02:00
WBHarry
b3ab7ee3a7 Raised version 2026-04-26 11:27:32 +02:00
WBHarry
3157fd450f
[Fix] V13 - Companion Fixes (#1840)
* Fixed companion initial max stress. Cleaned up prepareData flow

* Clamped adversary resources
2026-04-26 11:20:20 +02:00
WBHarry
cfd9950aae Translation fixes 2026-04-25 20:33:12 +02:00
WBHarry
702c9e0b6a Fixed translations. Fixed actions without Effects causing errors in Homebrew ItemFeatures 2026-04-25 18:35:22 +02:00
WBHarry
607ee3b580 Raised version 2026-04-23 17:11:30 +02:00
WBHarry
931b84d16d Fixed so that resource reset on downtime can handle math expressions 2026-04-20 00:07:49 +02:00
WBHarry
2a79468ce1 Corrected use of Foundry's Reset translation 2026-04-15 19:00:21 +02:00
WBHarry
c4d314171b
Corrected SRD (#1797) 2026-04-13 20:46:53 +02:00
WBHarry
24073e4a16 Fixed trait counting in CharacterCreation. Fixed description layout and labels in CharacterCreation. Fixed drag/drop images from Compendium Browser 2026-04-12 13:59:59 +02:00
WBHarry
cc5bfbe27d
Fixed more missing translations (#1793) 2026-04-12 11:38:37 +02:00
WBHarry
f66088971d Improved beastform translation structure 2026-04-12 11:33:49 +02:00
WBHarry
882143c1bb Corrected updateActorsRangeDependentEffects when token is null 2026-04-12 11:22:25 +02:00
WBHarry
baa72ff461 Added saefety to updateActorsRangeDepenedentEffects 2026-04-12 11:06:44 +02:00
WBHarry
cf28e011f2 Raised version 2026-04-12 00:29:04 +02:00
WBHarry
623f62e4b9 Merge branch 'V13' of https://github.com/Foundryborne/daggerheart into V13 2026-04-11 23:13:54 +02:00
WBHarry
40109dbbe4 Fixed system.json 2026-04-11 23:13:21 +02:00
WBHarry
d12220c64f
Fixes (#1790)
* Fixes

* .
2026-04-11 22:55:41 +02:00
WBHarry
f4282429cd Fixed H4 elements in editors being hard to see in light mode 2026-04-11 22:51:46 +02:00
WBHarry
1f02795b5d Fixed Battlepoints menu being hard to see in light-mode 2026-04-11 22:46:56 +02:00
WBHarry
d7b37f8178 Fixed faulty deploy script 2026-04-03 20:17:20 +02:00
WBHarry
f8df53ed83 Updated github deploy manifest to be the latest on the V13 branch 2026-04-03 00:10:32 +02:00
WBHarry
8ee5db2832 Fixed our templates taking custom scene distance into account 2026-04-02 23:38:18 +02:00
WBHarry
bbc521ece0 Labrys axe, rain of blades fixes. Enriched Description improvement on armor and weapon 2026-04-02 22:30:49 +02:00
WBHarry
be8d7f6469 Fixed so that tokens with vision range set to Infinity doesn't make summon actions error out 2026-04-02 17:07:52 +02:00
WBHarry
3a4b66f487 Merge branch 'main' into release 2026-03-27 08:27:10 +01:00
WBHarry
cdf159b4a7 Merge branch 'main' into release 2026-03-23 01:13:44 +01:00
WBHarry
413a37483c Merge branch 'main' into release 2026-03-17 22:45:41 +01:00
WBHarry
c5e21d9d92 Merge branch 'main' into release 2026-03-16 01:32:48 +01:00
WBHarry
10c0b6b51e Merge branch 'main' into release 2026-03-15 11:44:45 +01:00
WBHarry
652a554c9a Merge branch 'main' into release 2026-03-13 01:29:02 +01:00
WBHarry
c0ed5fe697 Merge branch 'main' into release 2026-03-13 00:19:34 +01:00
WBHarry
ff65abe09b Merge branch 'main' into release 2026-03-07 01:33:55 +01:00
WBHarry
ec7a7b378d Merge branch 'main' into release 2026-02-26 12:47:44 +01:00
WBHarry
8e34356905 Merge branch 'main' into release 2026-02-12 22:30:17 +01:00
WBHarry
35bceac520 Merge branch 'main' into release 2026-02-09 12:43:51 +01:00
WBHarry
436acb0617 Merge branch 'main' into release 2026-02-04 00:24:56 +01:00
WBHarry
1d114633f5 Merge branch 'main' into release 2026-02-02 02:07:29 +01:00
WBHarry
1c70b46639 Merge branch 'main' into release 2026-02-01 01:20:57 +01:00
WBHarry
ae38245877 Merge branch 'main' into release 2026-01-28 12:57:18 +01:00
WBHarry
af5d3d4568 Merge branch 'main' into release 2026-01-25 17:07:47 +01:00
WBHarry
6b5c1ff965 Merge branch 'main' into release 2026-01-25 16:22:39 +01:00
WBHarry
42a22a49f0 Merge branch 'main' into release 2026-01-23 11:52:22 +01:00
WBHarry
da77c2a190 Merge branch 'main' into release 2026-01-17 01:42:52 +01:00
WBHarry
68decf0b57 Merge branch 'main' into release 2026-01-16 21:48:15 +01:00
WBHarry
4f0670cc35 Merge branch 'main' into release 2026-01-16 10:16:12 +01:00
WBHarry
b346ce6766 Merge branch 'main' into release 2026-01-16 10:06:17 +01:00
WBHarry
dddd0581f7 Merge branch 'main' into release 2026-01-15 10:16:38 +01:00
WBHarry
83329fac46 Merge branch 'main' into release 2026-01-10 00:23:09 +01:00
WBHarry
ee0b7b2792 Merge branch 'main' into release 2026-01-09 17:49:14 +01:00
WBHarry
4d062a6892 Merge branch 'main' into release 2025-12-31 04:52:34 +01:00
WBHarry
487c1fd9a2 Merge branch 'main' into release 2025-12-29 14:02:53 +01:00
WBHarry
3aa5cd806a Merge branch 'main' into release 2025-12-27 18:17:36 +01:00
WBHarry
2e93b79633 Merge branch 'main' into release 2025-12-24 03:06:02 +01:00
WBHarry
244dbd4902 Merge branch 'main' into release 2025-12-24 01:18:51 +01:00
WBHarry
c7aed6825a Merge branch 'main' into release 2025-12-13 23:06:23 +01:00
WBHarry
9cb5112b62 Merge branch 'main' into release 2025-12-08 02:35:06 +01:00
WBHarry
81b6f7fc51 Merge branch 'main' into release 2025-12-07 00:54:06 +01:00
WBHarry
828fffd552 Merge branch 'main' into release 2025-11-26 09:47:07 +01:00
WBHarry
fc5626ac47 Merge branch 'main' into release 2025-11-25 00:52:11 +01:00
WBHarry
2e62545aa7 Merge branch 'main' into release 2025-11-23 15:41:24 +01:00
WBHarry
b09c712dd5 Merging main 2025-11-20 11:48:58 +01:00
WBHarry
ca4336bd39 Merge branch 'main' into release 2025-11-17 16:55:14 +01:00
WBHarry
77ac11c522 Merge branch 'main' into release 2025-11-17 10:17:50 +01:00
WBHarry
50311679a5 Merge branch 'main' into release 2025-11-11 22:15:30 +01:00
WBHarry
3a7bcd1b0a Merge branch 'main' into release 2025-11-11 18:04:23 +01:00
WBHarry
511e4bd644 Merge branch 'main' into release 2025-11-11 16:23:35 +01:00
WBHarry
395820513b Merge branch 'main' into release 2025-11-11 16:06:03 +01:00
WBHarry
3566ea3fd3 Merge branch 'main' into release 2025-08-26 20:32:04 +02:00
WBHarry
29d502fb97 Merge branch 'main' into release 2025-08-24 21:11:38 +02:00
WBHarry
685a25d25a Merge branch 'main' into release 2025-08-22 01:47:03 +02:00
WBHarry
dd045b3df7 Merge branch 'main' into release 2025-08-19 20:58:05 +02:00
WBHarry
0aabcec340 Raised version 2025-08-19 18:56:30 +02:00
1068 changed files with 13165 additions and 22573 deletions

View file

@ -1,6 +1,3 @@
[*] [*]
indent_size = 4 indent_size = 4
indent_style = spaces indent_style = spaces
end_of_line = lf
[*.yml]
indent_size = 2

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
* text=auto eol=lf
*.json text eol=lf

View file

@ -1,42 +0,0 @@
name: Project CI
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Cache NPM Deps
id: cache-npm
uses: actions/cache@v3
with:
path: node_modules/
key: npm-${{ hashFiles('package-lock.json') }}
- name: Install NPM Deps
if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
run: npm ci
- name: Lint
run: npm run lint

View file

@ -35,9 +35,8 @@ jobs:
env: env:
version: ${{steps.get_version.outputs.version-without-v}} version: ${{steps.get_version.outputs.version-without-v}}
url: https://github.com/${{github.repository}} url: https://github.com/${{github.repository}}
manifest: https://raw.githubusercontent.com/${{github.repository}}/v14/system.json manifest: https://raw.githubusercontent.com/${{github.repository}}/V13/system.json
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
flags.hotReload: false
# Create a zip file with all files required by the module to add to the release # Create a zip file with all files required by the module to add to the release
- run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/ - run: zip -r ./system.zip system.json README.md LICENSE build/daggerheart.js build/tagify.css styles/daggerheart.css assets/ templates/ packs/ lang/

1
.gitignore vendored
View file

@ -6,4 +6,3 @@ Build
build build
foundry foundry
styles/daggerheart.css styles/daggerheart.css
styles/daggerheart.css.map

13
.prettierrc Normal file
View file

@ -0,0 +1,13 @@
{
"trailingComma": "none",
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 120,
"endOfLine": "lf",
"bracketSameLine": true
}

View file

@ -1,9 +1,78 @@
# Contributing to Daggerheart # Contributing to Foundryborne
Thank you for your interest in contributing to the Foundryborne project! Welcome! This is a community-driven project to bring [Daggerheart](https://www.daggerheart.com/) to [FoundryVTT](https://foundryvtt.com/) as a full system. We're excited to have you here and appreciate your interest in contributing.
To ensure that all contributions align with our project goals and architectural standards, we ask that you **do not submit outside contributions without first receiving feedback from the development team.** ---
If you have an idea or a fix you'd like to contribute, please start a discussion or open an issue first. We'd love to hear from you and collaborate on the best way to move forward! ## 🤝 How to Contribute
Thank you for your understanding and support. We welcome contributions of all kinds:
- Bug reports
- Feature suggestions
- Code contributions
- UI/UX mockups
- Documentation improvements
- Questions and discussions
Please be respectful and collaborative — were all here to build something great together.
### Community Translations
Please note that we are not accepting community translations in the main project. Instead, community translations should be published as a module.
---
## 🧭 General Guidelines
- **Use GitHub Issues** to report bugs or propose features
- **Start a Discussion** for larger ideas or questions
- **Open a Pull Request** once you've confirmed your work aligns with project direction
- **Keep things modular and maintainable** — if you're not sure how to structure something, ask!
- **Orient your code on existing examples**, and feel free to suggest a standard if it makes things clearer
---
## 🗂️ Project Structure
Please try to follow the general logic of the existing code when submitting PRs.
We encourage contributors to leave comments or open Discussions when proposing structural or organizational changes.
---
## 🧾 Issue & PR Best Practices
**For Issues:**
- Use clear, descriptive titles
- Provide a concise explanation of the problem or idea
- Include reproduction steps or example scenarios if it's a bug
- Add screenshots or logs if helpful
**For Pull Requests:**
- Use a clear title summarizing the change
- Provide a brief description of what your code does and why
- Link to any related Issues
- Keep PRs focused — smaller is better
---
## 🔖 Labels and Boards
We use GitHub labels to help organize contributions. If your issue or PR relates to a specific category, feel free to tag it. There is also a GitHub Project Board to help track active work and priorities.
---
## 📣 Communication
Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Discussions](https://github.com/Foundryborne/daggerheart/discussions). You're welcome to use any of these, though we may consolidate to one in the future.
---
## 🤗 Thank You!
Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring _Daggerheart_ to life in FoundryVTT through **Foundryborne**!
🐸🛠️

View file

@ -66,10 +66,6 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart
Looking to contribute to the project? Look no further, check out our [contributing guide](CONTRIBUTING.md), and keep the [Code of Conduct](coc.md) in mind when working on things. Looking to contribute to the project? Look no further, check out our [contributing guide](CONTRIBUTING.md), and keep the [Code of Conduct](coc.md) in mind when working on things.
## AI Policy
The Foundryborne Daggerheart system does not make use of AI (generative or otherwise) for any area of its implementation. We expect all contributors to follow this same policy when contributing with a pull request; contributions made using AI will be rejected outright.
## Disclaimer: ## Disclaimer:
**Daggerheart System** **Daggerheart System**

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" transform="translate(0,0)" style=""><path d="M418.813 30.625c-21.178 26.27-49.712 50.982-84.125 70.844-36.778 21.225-75.064 33.62-110.313 38.06a310.317 310.317 0 0 0 6.813 18.25c16.01.277 29.366-.434 36.406-1.5l9.47-1.53 8.436-1.28.22 10.186a307.48 307.48 0 0 1-1.095 18.72l56.625 8.843c.86-.095 1.713-.15 2.563-.157 11.188-.114 21.44 7.29 24.468 18.593.657 2.448.922 4.903.845 7.313 5.972-2.075 11.753-4.305 17.28-6.72l9.595-4.188 2.313 10.22a340.211 340.211 0 0 1 7.375 48.062C438.29 247.836 468.438 225.71 493 197.5c-3.22-36.73-16.154-78.04-39.125-117.813a290.509 290.509 0 0 0-2.22-3.78l-27.56 71.374c5.154.762 10.123 3.158 14.092 7.126 9.81 9.807 9.813 25.69 0 35.5-9.812 9.81-25.722 9.807-35.53 0-8.86-8.858-9.69-22.68-2.532-32.5l38.938-100.844a322.02 322.02 0 0 0-20.25-25.937zM51.842 118.72c-8.46 17.373-15.76 36.198-21.187 56.436-14.108 52.617-13.96 103.682-2.812 143.438 13.3-2.605 26.442-3.96 39.312-4.03 1.855-.012 3.688.02 5.53.06 20.857.48 40.98 4.332 59.97 11.5a355.064 355.064 0 0 1-1.656-34.218c0-27.8 3.135-54.377 9-78.937l2.47-10.407 9.655 4.562c29.467 13.98 66.194 23.424 106.28 25.22 5.136-20.05 8.19-39.78 9.408-58.75-35.198 4.83-75.387 2.766-116.407-8.22-38.363-10.272-72.314-26.78-99.562-46.656zm230.594 82.218c-1.535 10.452-3.615 21.03-6.218 31.687a312.754 312.754 0 0 0 46-3.97 24.98 24.98 0 0 1-1.532-21.748l-38.25-5.97zM105 201.375l4.156 18.22-21.594 4.905c8.75 5.174 13.353 15.703 10.594 26-3.32 12.394-16.045 19.758-28.437 16.438-12.394-3.32-19.76-16.075-16.44-28.47a23.235 23.235 0 0 1 3.126-6.874l-21.062 4.78-4.125-18.218 73.78-16.78zm388.594 22.813c-25.53 25.46-55.306 45.445-86.906 60.5.05 2.397.093 4.8.093 7.218 0 9.188-.354 18.232-1.03 27.125 16.635 1.33 32.045-1.7 45.344-9.374 25.925-14.962 40.608-45.694 42.5-85.47zm-338.844 3c-4.03 19.993-6.33 41.31-6.406 63.593l.125-.342c30.568 10.174 62.622 17.572 95.25 21.375l7.5.875.718 7.5 5.687 60.125-18.625 1.75-2.53-26.75a23.117 23.117 0 0 1-14.845.968c-12.393-3.32-19.76-16.042-16.438-28.436.285-1.06.647-2.08 1.063-3.063a496.627 496.627 0 0 1-57.406-14.53c2.69 49.62 16.154 94.04 36.094 126.656 22.366 36.588 52.13 57.78 83.968 57.78 31.838.003 61.602-21.19 83.97-57.78 19.536-31.96 32.846-75.244 35.905-123.656a499.132 499.132 0 0 1-48.25 11.656c1.914 4.57 2.415 9.78 1.033 14.938-3.322 12.394-16.045 19.758-28.438 16.437a23.01 23.01 0 0 1-2.125-.686l-2.5 26.47-18.594-1.752 5.688-60.125.72-7.5 7.498-.875c29.245-3.407 57.995-9.717 85.657-18.312v-1.594c0-21.573-2.27-42.23-6.064-61.75C351.132 242.653 313.092 250 272.312 250c-43.59 0-83.986-8.658-117.562-22.813zm-87.5 105.968c-10.87.102-21.995 1.22-33.375 3.313 12.695 31.62 33.117 53.07 59 60 16.9 4.523 34.896 2.536 52.813-5.25-4.382-13.89-7.874-28.606-10.344-43.97-21.115-9.623-43.934-14.32-68.094-14.094zm137.5 80.22h130.813c-40.082 44.594-92.623 42.844-130.813 0z" fill="#fff" fill-opacity="1"></path></g></svg>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,3 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="transparent" stroke="#18162e"/> <path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="transparent" stroke="#18162e"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 397 B

After

Width:  |  Height:  |  Size: 476 B

Before After
Before After

View file

@ -1,3 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="#18152E" stroke="#F3C267"/> <path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="#18152E" stroke="#F3C267"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 472 B

Before After
Before After

24
daggerheart.d.ts vendored
View file

@ -1,11 +1,8 @@
import '@client/global.mjs'; import '@client/global.mjs';
import '@common/global.mjs';
import '@common/primitives/global.mjs';
import Canvas from '@client/canvas/board.mjs'; import Canvas from '@client/canvas/board.mjs';
// Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such
// This declare global hopefully fixes that // This declare global hopefully fixes that
// Note: eslint is not aware of these, whatever is added here should go in the eslint's globals list
declare global { declare global {
/** /**
* A simple event framework used throughout Foundry Virtual Tabletop. * A simple event framework used throughout Foundry Virtual Tabletop.
@ -15,28 +12,9 @@ declare global {
class Hooks extends foundry.helpers.Hooks {} class Hooks extends foundry.helpers.Hooks {}
const fromUuid = foundry.utils.fromUuid; const fromUuid = foundry.utils.fromUuid;
const fromUuidSync = foundry.utils.fromUuidSync; const fromUuidSync = foundry.utils.fromUuidSync;
/**
* A representation of a color in hexadecimal format.
* This class provides methods for transformations and manipulations of colors.
*/
class Color extends foundry.utils.Color {}
/** /**
* The singleton game canvas * The singleton game canvas
*/ */
const canvas: Canvas; const canvas: Canvas;
const ActiveEffect: foundry.documents.ActiveEffect;
const Actor: foundry.documents.Actor;
const BaseScene: foundry.documents.BaseScene;
const ChatMessage: foundry.documents.ChatMessage;
const Combat: foundry.documents.Combat;
const Combatant: foundry.documents.Combatant;
const Item: foundry.documents.Item;
const Macro: foundry.documents.Macro;
const Scene: foundry.documents.Scene;
const TokenDocument: foundry.documents.TokenDocument;
const Collection: foundry.utils.Collection;
const FormDataExtended: foundry.applications.ux.FormDataExtended;
const TextEditor: foundry.applications.ux.TextEditor;
} }

View file

@ -9,7 +9,10 @@ import * as dice from './module/dice/_module.mjs';
import * as fields from './module/data/fields/_module.mjs'; import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs'; import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs';
import { import {
handlebarsRegistration, handlebarsRegistration,
runMigrations, runMigrations,
@ -18,6 +21,7 @@ import {
} from './module/systemRegistration/_module.mjs'; } from './module/systemRegistration/_module.mjs';
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs'; import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css'; import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
import TokenManager from './module/documents/tokenManager.mjs'; import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM; CONFIG.DH = SYSTEM;
@ -32,13 +36,6 @@ CONFIG.Dice.daggerheart = {
FateRoll: FateRoll FateRoll: FateRoll
}; };
CONFIG.RegionBehavior.dataModels = {
...CONFIG.RegionBehavior.dataModels,
...data.regionBehaviors
};
Object.assign(CONFIG.Dice.termTypes, dice.diceTypes);
CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config; CONFIG.Actor.dataModels = models.actors.config;
CONFIG.Actor.collection = collections.DhActorCollection; CONFIG.Actor.collection = collections.DhActorCollection;
@ -48,7 +45,6 @@ CONFIG.Item.dataModels = models.items.config;
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config; CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects };
CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.Combat.dataModels = { base: models.DhCombat }; CONFIG.Combat.dataModels = { base: models.DhCombat };
@ -60,13 +56,11 @@ CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs'; CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.regions.layerClass = placeables.DhRegionLayer; CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer; CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.Region.objectClass = placeables.DhRegion;
CONFIG.RollTable.documentClass = documents.DhRollTable; CONFIG.RollTable.documentClass = documents.DhRollTable;
CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs'; CONFIG.RollTable.resultTemplate = 'systems/daggerheart/templates/ui/chat/table-result.hbs';
@ -90,6 +84,7 @@ CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns; CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
CONFIG.ux.TokenManager = new TokenManager(); CONFIG.ux.TokenManager = new TokenManager();
CONFIG.debug.triggers = false; CONFIG.debug.triggers = false;
@ -196,11 +191,6 @@ Hooks.once('init', () => {
makeDefault: true, makeDefault: true,
label: sheetLabel('TYPES.Actor.environment') label: sheetLabel('TYPES.Actor.environment')
}); });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.NPC, {
types: ['npc'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.npc')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, { Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'], types: ['party'],
makeDefault: true, makeDefault: true,
@ -223,7 +213,6 @@ Hooks.once('init', () => {
SYSTEM.id, SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig, applications.sheetConfigs.ActiveEffectConfig,
{ {
types: ['base', 'beastform', 'horde'],
makeDefault: true, makeDefault: true,
label: sheetLabel('DOCUMENT.ActiveEffect') label: sheetLabel('DOCUMENT.ActiveEffect')
} }
@ -281,6 +270,7 @@ Hooks.on('setup', () => {
...damageThresholds, ...damageThresholds,
'proficiency', 'proficiency',
'evasion', 'evasion',
'armorScore',
'scars', 'scars',
'levelData.level.current' 'levelData.level.current'
] ]
@ -342,27 +332,75 @@ Hooks.on('renderHandlebarsApplication', (_, element) => {
enricherRenderSetup(element); enricherRenderSetup(element);
}); });
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => { Hooks.on('chatMessage', (_, message) => {
if (data.openForAllPlayers && data.partyId) { if (message.startsWith('/dr')) {
const party = game.actors.get(data.partyId); const result =
if (!party) return; message.trim().toLowerCase() === '/dr' ? { result: {} } : rollCommandToJSON(message.replace(/\/dr\s?/, ''));
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false;
}
const TagTeamDialog = game.system.api.applications.dialogs.TagTeamDialog; const { result: rollCommand, flavor } = result;
const dialog = foundry.applications.instances.get(`TagTeamDialog-${party.id}`) ?? new TagTeamDialog(party);
dialog.tabGroups.application = 'tagTeamRoll'; const reaction = rollCommand.reaction;
await dialog.render({ force: true }); const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true });
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage,
grantResources
});
return false;
} }
});
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, async data => { if (message.startsWith('/fr')) {
if (data.openForAllPlayers && data.partyId) { const result =
const party = game.actors.get(data.partyId); message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, ''));
if (!party) return;
const GroupRollDialog = game.system.api.applications.dialogs.GroupRollDialog; if (!result) {
const dialog = foundry.applications.instances.get(`GroupRollDialog-${party.id}`) ?? new GroupRollDialog(party); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
dialog.tabGroups.application = 'groupRoll'; return false;
await dialog.render({ force: true }); }
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
} }
}); });
@ -446,33 +484,3 @@ Hooks.on('canvasTearDown', canvas => {
Hooks.on('canvasReady', canas => { Hooks.on('canvasReady', canas => {
game.system.registeredTriggers.registerSceneTriggers(canvas.scene); game.system.registeredTriggers.registerSceneTriggers(canvas.scene);
}); });
/** Make the user to select a document type, instead of having a default doc type for them to accidentally keep */
Hooks.on('renderDialogV2', (_dialog, html) => {
if (!html.classList.contains('dialog')) return;
const cls = html.classList.contains('item-create')
? documents.DHItem.implementation
: html.classList.contains('actor-create')
? documents.DhpActor.implementation
: null;
if (!cls) return;
const form = html.querySelector('form');
const submit = html.querySelector('button[type=submit]');
const select = html.querySelector('select[name=type]');
const nameInput = html.querySelector('input[name=name]');
if (!form || !select || !submit || !nameInput) return;
nameInput.placeholder = cls.defaultName({});
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.selected = true;
select.required = true;
select.prepend(emptyOption);
submit.addEventListener('click', event => {
if (!form.reportValidity()) {
event.preventDefault();
event.stopPropagation();
}
});
});

View file

@ -1,101 +0,0 @@
import globals from 'globals';
import { defineConfig, globalIgnores } from 'eslint/config';
import tseslint from 'typescript-eslint';
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
/** @type {Partial<RulesConfig>} */
export const stylisticRules = {
'@stylistic/indent': [
'error',
4,
{
SwitchCase: 1
}
],
'@stylistic/max-len': ['error', {
code: 120,
ignoreComments: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true
}],
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: 'always' }],
'@stylistic/arrow-parens': ['error', 'as-needed'],
'@stylistic/quote-props': ['error', 'as-needed'],
'@stylistic/array-bracket-newline': ['error', 'consistent'],
'@stylistic/key-spacing': 'error',
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/space-in-parens': ['error', 'never'],
'@stylistic/space-infix-ops': 2,
'@stylistic/keyword-spacing': 2,
'@stylistic/semi-spacing': 2,
'@stylistic/no-multi-spaces': 2,
'@stylistic/no-extra-semi': 2,
'@stylistic/no-whitespace-before-property': 2,
'@stylistic/space-unary-ops': 2
};
export default defineConfig([
globalIgnores(['foundry/**/*', 'build/**/*']),
{
files: ['gulpfile.js', 'postcss.config.js'],
languageOptions: { globals: globals.node }
},
{
files: ['**/*.{js,mjs,cjs}'],
plugins: {
'@stylistic': stylistic
},
languageOptions: {
globals: {
...globals.browser,
CONFIG: 'readonly',
CONST: 'readonly',
// Global classes
Color: 'readonly',
Handlebars: 'readonly',
Hooks: 'readonly',
PIXI: 'readonly',
ProseMirror: 'readonly',
Roll: 'readonly',
// global namespaces
canvas: 'readonly',
foundry: 'readonly',
game: 'readonly',
ui: 'readonly',
// global functions
fromUuid: 'readonly',
fromUuidSync: 'readonly',
getDocumentClass: 'readonly',
_del: 'readonly',
_replace: 'readonly',
_loc: 'readonly',
// Documents
ActiveEffect: 'readonly',
Actor: 'readonly',
BaseScene: 'readonly',
ChatMessage: 'readonly',
Combat: 'readonly',
Combatant: 'readonly',
Item: 'readonly',
Macro: 'readonly',
Scene: 'readonly',
TokenDocument: 'readonly',
// Other
Collection: 'readonly',
FormDataExtended: 'readonly',
TextEditor: 'readonly'
}
},
rules: {
'no-undef': 'error',
// 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
...stylisticRules
}
},
{
files: ['**/*.ts'],
extends: [js.configs.recommended, tseslint.configs.recommended]
}
]);

View file

@ -1,15 +1,9 @@
// Less configuration // Less configuration
var gulp = require('gulp'); var gulp = require('gulp');
var less = require('gulp-less'); var less = require('gulp-less');
var sourcemaps = require('gulp-sourcemaps');
gulp.task('less', function (cb) { gulp.task('less', function (cb) {
gulp.src('styles/daggerheart.less') gulp.src('styles/daggerheart.less').pipe(less()).pipe(gulp.dest('styles'));
.pipe(sourcemaps.init())
.pipe(less())
.on('error', console.error.bind(console))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('styles'));
cb(); cb();
}); });

View file

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "es2022", "module": "ES6",
"target": "es2022", "target": "ES6",
"paths": { "paths": {
"@client/*": ["./foundry/client/*"], "@client/*": ["./foundry/client/*"],
"@common/*": ["./foundry/common/*"] "@common/*": ["./foundry/common/*"]

View file

@ -14,16 +14,13 @@
"beastform": "Beastform" "beastform": "Beastform"
}, },
"ActiveEffect": { "ActiveEffect": {
"base": "Standard", "beastform": "Beastform"
"beastform": "Beastform",
"horde": "Horde"
}, },
"Actor": { "Actor": {
"character": "Character", "character": "Character",
"companion": "Companion", "companion": "Companion",
"adversary": "Adversary", "adversary": "Adversary",
"environment": "Environment", "environment": "Environment",
"npc": "NPC",
"party": "Party" "party": "Party"
} }
}, },
@ -56,7 +53,6 @@
}, },
"damage": { "damage": {
"name": "Damage", "name": "Damage",
"critical": "Damage (Critical)",
"tooltip": "Direct damage without a roll." "tooltip": "Direct damage without a roll."
}, },
"effect": { "effect": {
@ -89,14 +85,7 @@
}, },
"Config": { "Config": {
"beastform": { "beastform": {
"exact": { "label": "Beastform Max Tier", "hint": "The Character's Tier is used if empty" }, "exact": { "label": "Beastform Max Tier", "hint": "The Character's Tier is used if empty" }
"modifications": {
"traitBonuses": {
"label": { "single": "Trait Bonus", "plural": "Trait Bonuses" },
"hint": "Pick bonuses you apply to freely chosen traits at the time of transforming",
"bonus": "Bonus Amount"
}
}
}, },
"countdown": { "countdown": {
"defaultOwnership": "Default Ownership", "defaultOwnership": "Default Ownership",
@ -110,17 +99,9 @@
"customFormula": "Custom Formula", "customFormula": "Custom Formula",
"formula": "Formula" "formula": "Formula"
}, },
"area": {
"sectionTitle": "Areas",
"shape": "Shape",
"size": "Size"
},
"displayInChat": "Display in chat", "displayInChat": "Display in chat",
"deleteTriggerTitle": "Delete Trigger", "deleteTriggerTitle": "Delete Trigger",
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?", "deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?"
"advantageState": "Advantage State",
"damageOnSave": "Damage on Save",
"useDefaultItemValues": "Use default Item values"
}, },
"RollField": { "RollField": {
"diceRolling": { "diceRolling": {
@ -132,11 +113,10 @@
} }
}, },
"Settings": { "Settings": {
"attackModifier": "Attack Modifier", "attackBonus": "Attack Bonus",
"attackName": "Attack Name", "attackName": "Attack Name",
"criticalThreshold": "Critical Threshold", "criticalThreshold": "Critical Threshold",
"includeBase": { "label": "Include Item Damage" }, "includeBase": { "label": "Include Item Damage" },
"groupAttack": { "label": "Group Attack" },
"multiplier": "Multiplier", "multiplier": "Multiplier",
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.", "saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
"resultBased": { "resultBased": {
@ -167,9 +147,7 @@
"Config": { "Config": {
"rangeDependence": { "rangeDependence": {
"title": "Range Dependence" "title": "Range Dependence"
}, }
"stacking": { "title": "Stacking" },
"targetDispositions": "Affected Dispositions"
}, },
"RangeDependance": { "RangeDependance": {
"hint": "Settings for an optional distance at which this effect should activate", "hint": "Settings for an optional distance at which this effect should activate",
@ -221,9 +199,6 @@
"headerTitle": "Adversary Reaction Roll" "headerTitle": "Adversary Reaction Roll"
} }
}, },
"Base": {
"CannotAddType": "Cannot add {itemType} items to {actorType} actors."
},
"Character": { "Character": {
"advantageSources": { "advantageSources": {
"label": "Advantage Sources", "label": "Advantage Sources",
@ -247,8 +222,6 @@
}, },
"defaultHopeDice": "Default Hope Dice", "defaultHopeDice": "Default Hope Dice",
"defaultFearDice": "Default Fear Dice", "defaultFearDice": "Default Fear Dice",
"defaultAdvantageDice": "Default Advantage Dice",
"defaultDisadvantageDice": "Default Disadvantage Dice",
"disadvantageSources": { "disadvantageSources": {
"label": "Disadvantage Sources", "label": "Disadvantage Sources",
"hint": "Add single words or short text as reminders and hints of what a character has disadvantage on." "hint": "Add single words or short text as reminders and hints of what a character has disadvantage on."
@ -333,27 +306,6 @@
} }
}, },
"newAdversary": "New Adversary" "newAdversary": "New Adversary"
},
"NPC": {
"FIELDS": {
"motives": { "label": "Motives" }
}
},
"Party": {
"Subtitle": {
"character": "{community} {ancestry} | {subclass} {class}",
"companion": "Companion of {partner}"
},
"RemoveConfirmation": {
"title": "Remove member {name}",
"text": "Are you sure you want to remove {name} from the party?"
},
"Thresholds": {
"minor": "MIN",
"major": "MAJ",
"severe": "SEV"
},
"triggerRestContent": "This will trigger a dialog to players make their downtime moves. Are you sure?"
} }
}, },
"APPLICATIONS": { "APPLICATIONS": {
@ -389,7 +341,7 @@
"selectSecondaryWeapon": "Select Secondary Weapon", "selectSecondaryWeapon": "Select Secondary Weapon",
"selectSubclass": "Select Subclass", "selectSubclass": "Select Subclass",
"setupSkipTitle": "Skipping Character Setup", "setupSkipTitle": "Skipping Character Setup",
"setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking button in the top-right. Are you sure you want to continue?", "setupSkipContent": "You are skipping the Character Setup by adding this manually. The character setup is the blinking arrows in the top-right. Are you sure you want to continue?",
"startingItems": "Starting Items", "startingItems": "Starting Items",
"story": "Story", "story": "Story",
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.", "storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
@ -412,11 +364,7 @@
"giveSpotlight": "Give The Spotlight", "giveSpotlight": "Give The Spotlight",
"requestingSpotlight": "Requesting The Spotlight", "requestingSpotlight": "Requesting The Spotlight",
"requestSpotlight": "Request The Spotlight", "requestSpotlight": "Request The Spotlight",
"openCountdowns": "Countdowns", "openCountdowns": "Countdowns"
"adversaryCategories": {
"friendly": "Friendly",
"adversaries": "Adversaries"
}
}, },
"CompendiumBrowserSettings": { "CompendiumBrowserSettings": {
"title": "Enable Compendiums", "title": "Enable Compendiums",
@ -497,15 +445,14 @@
}, },
"DaggerheartMenu": { "DaggerheartMenu": {
"title": "GM Tools", "title": "GM Tools",
"refreshFeatures": "Refresh Features", "refreshFeatures": "Refresh Features"
"fallingAndCollision": "Falling And Collision Damage"
}, },
"DeleteConfirmation": { "DeleteConfirmation": {
"title": "Delete {type} - {name}", "title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?" "text": "Are you sure you want to delete {name}?"
}, },
"DamageReduction": { "DamageReduction": {
"maxUseableArmor": "Useable Armor Slots", "armorMarks": "Armor Marks",
"armorWithStress": "Spend 1 stress to use an extra mark", "armorWithStress": "Spend 1 stress to use an extra mark",
"thresholdImmunities": "Threshold Immunities", "thresholdImmunities": "Threshold Immunities",
"stress": "Stress", "stress": "Stress",
@ -711,13 +658,19 @@
}, },
"PendingReactionsDialog": { "PendingReactionsDialog": {
"title": "Pending Reaction Rolls Found", "title": "Pending Reaction Rolls Found",
"unfinishedRolls": "Some Tokens have not finished their Reaction Rolls.", "unfinishedRolls": "Some Tokens still need to roll their Reaction Roll.",
"warning": "Unfinished reaction rolls will be considered as failed.", "confirmation": "Are you sure you want to continue ?",
"confirmation": "Are you sure you want to continue?" "warning": "Undone reaction rolls will be considered as failed"
}, },
"ReactionRoll": { "ReactionRoll": {
"title": "Reaction Roll: {trait}" "title": "Reaction Roll: {trait}"
}, },
"RerollDialog": {
"title": "Reroll",
"damageTitle": "Reroll Damage",
"deselectDiceNotification": "Deselect one of the selected dice first",
"acceptCurrentRolls": "Accept Current Rolls"
},
"ResourceDice": { "ResourceDice": {
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
@ -731,28 +684,15 @@
}, },
"TagTeamSelect": { "TagTeamSelect": {
"title": "Tag Team Roll", "title": "Tag Team Roll",
"FIELDS": {
"initiator": {
"memberId": { "label": "Initiating Character" },
"cost": { "label": "Hope Cost" }
}
},
"leaderTitle": "Initiating Character", "leaderTitle": "Initiating Character",
"membersTitle": "Participants", "membersTitle": "Participants",
"partyTeam": "Party Team", "partyTeam": "Party Team",
"hopeCost": "Hope Cost", "hopeCost": "Hope Cost",
"initiatingCharacter": "Initiating Character", "initiatingCharacter": "Initiating Character",
"selectParticipants": "Select the two participants",
"startTagTeamRoll": "Start Tag Team Roll",
"openDialogForAll": "Open Dialog For All",
"rollType": "Roll Type",
"makeYourRoll": "Make your roll",
"cancelTagTeamRoll": "Cancel Tag Team Roll",
"finishTagTeamRoll": "Finish Tag Team Roll",
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll", "linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
"damageNotRolled": "Damage not rolled in chat message yet", "damageNotRolled": "Damage not rolled in chat message yet",
"insufficientHope": "The initiating character doesn't have enough hope", "insufficientHope": "The initiating character doesn't have enough hope",
"createTagTeam": "Create Tag Team Roll", "createTagTeam": "Create TagTeam Roll",
"chatMessageRollTitle": "Roll", "chatMessageRollTitle": "Roll",
"cancelConfirmTitle": "Cancel Tag Team Roll", "cancelConfirmTitle": "Cancel Tag Team Roll",
"cancelConfirmText": "Are you sure you want to cancel the Tag Team Roll? This will close it for all other players too.", "cancelConfirmText": "Are you sure you want to cancel the Tag Team Roll? This will close it for all other players too.",
@ -761,26 +701,8 @@
"selectRoll": "Select which roll value to be used for the Tag Team" "selectRoll": "Select which roll value to be used for the Tag Team"
} }
}, },
"GroupRollSelect": {
"cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too.",
"cancelConfirmTitle": "Cancel Group Roll",
"initializationTitle": "Character Selection",
"finishGroupRoll": "Finish Group Roll",
"leader": "Leader",
"leaderRoll": "Leader Roll",
"members": "Members",
"openDialogForAll": "Open Dialog For All",
"removeRoll": "Remove Roll",
"resultsHint": "Results will appear when characters roll",
"selectLeaderHint": "Select one Character to be the leader",
"selectParticipantsHint": "Select one Character to be the leader",
"startGroupRoll": "Start Group Roll",
"title": "Group Roll"
},
"TokenConfig": { "TokenConfig": {
"actorSizeUsed": "Actor size is set, determining the dimensions", "actorSizeUsed": "Actor size is set, determining the dimensions"
"tokenSize": "Token Size",
"sizeCategory": "Size Category"
} }
}, },
"CLASS": { "CLASS": {
@ -790,15 +712,6 @@
} }
}, },
"CONFIG": { "CONFIG": {
"ActiveEffectDuration": {
"temporary": "Temporary",
"act": "Next Spotlight",
"scene": "Next Scene",
"shortRest": "Next Rest",
"longRest": "Next Long Rest",
"session": "Next Session",
"custom": "Custom"
},
"ActionAutomationChoices": { "ActionAutomationChoices": {
"never": "Never", "never": "Never",
"showDialog": "Show Dialog Only", "showDialog": "Show Dialog Only",
@ -871,11 +784,6 @@
"bruiser": "for each Bruiser adversary.", "bruiser": "for each Bruiser adversary.",
"solo": "for each Solo adversary." "solo": "for each Solo adversary."
}, },
"ArmorInteraction": {
"none": { "label": "Ignores Armor" },
"active": { "label": "Active w/ Armor" },
"inactive": { "label": "Inactive w/ Armor" }
},
"ArmorFeature": { "ArmorFeature": {
"burning": { "burning": {
"name": "Burning", "name": "Burning",
@ -1226,12 +1134,6 @@
"description": "" "description": ""
} }
}, },
"fallAndCollision": {
"veryClose": { "label": "Very Close", "chatTitle": "Fall Damage: Very Close" },
"close": { "label": "Close", "chatTitle": "Fall Damage: Close" },
"far": { "label": "Far", "chatTitle": "Fall Damage: Far" },
"collision": { "label": "Collision", "chatTitle": "Dangerous Collision" }
},
"FeatureForm": { "FeatureForm": {
"label": "Feature Form", "label": "Feature Form",
"passive": "Passive", "passive": "Passive",
@ -1342,11 +1244,6 @@
"selectType": "Select Action Type", "selectType": "Select Action Type",
"selectAction": "Action Selection" "selectAction": "Action Selection"
}, },
"TagTeamRollTypes": {
"trait": "Trait",
"ability": "Ability",
"damageAbility": "Damage Ability"
},
"TargetTypes": { "TargetTypes": {
"any": "Any", "any": "Any",
"friendly": "Friendly", "friendly": "Friendly",
@ -1359,8 +1256,8 @@
"cone": "Cone", "cone": "Cone",
"emanation": "Emanation", "emanation": "Emanation",
"inFront": "In Front", "inFront": "In Front",
"rectangle": "Rectangle", "rect": "Rectangle",
"line": "Line" "ray": "Ray"
}, },
"TokenSize": { "TokenSize": {
"tiny": "Tiny", "tiny": "Tiny",
@ -1975,17 +1872,6 @@
"name": "Healing Roll" "name": "Healing Roll"
} }
}, },
"ChangeTypes": {
"armor": {
"newArmorEffect": "Armor Effect",
"FIELDS": {
"interaction": {
"label": "Armor Interaction",
"hint": "Does the character wearing armor suppress this effect?"
}
}
}
},
"Duration": { "Duration": {
"passive": "Passive", "passive": "Passive",
"temporary": "Temporary" "temporary": "Temporary"
@ -2010,10 +1896,6 @@
} }
}, },
"GENERAL": { "GENERAL": {
"Ability": {
"single": "Ability",
"plural": "Abilities"
},
"Action": { "Action": {
"single": "Action", "single": "Action",
"plural": "Actions" "plural": "Actions"
@ -2399,7 +2281,6 @@
"duality": "Duality", "duality": "Duality",
"dualityDice": "Duality Dice", "dualityDice": "Duality Dice",
"dualityRoll": "Duality Roll", "dualityRoll": "Duality Roll",
"effect": "Effect",
"enabled": "Enabled", "enabled": "Enabled",
"evasion": "Evasion", "evasion": "Evasion",
"equipment": "Equipment", "equipment": "Equipment",
@ -2448,11 +2329,9 @@
"single": "Miss", "single": "Miss",
"plural": "Miss" "plural": "Miss"
}, },
"missingX": "Missing {x}",
"maxWithThing": "Max {thing}", "maxWithThing": "Max {thing}",
"missingDragDropThing": "Drop {thing} here", "missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass", "multiclass": "Multiclass",
"name": "Name",
"newCategory": "New Category", "newCategory": "New Category",
"newThing": "New {thing}", "newThing": "New {thing}",
"next": "Next", "next": "Next",
@ -2476,10 +2355,6 @@
"rerolled": "Rerolled", "rerolled": "Rerolled",
"rerollThing": "Reroll {thing}", "rerollThing": "Reroll {thing}",
"resource": "Resource", "resource": "Resource",
"result": {
"single": "Result",
"plural": "Results"
},
"roll": "Roll", "roll": "Roll",
"rollAll": "Roll All", "rollAll": "Roll All",
"rollDamage": "Roll Damage", "rollDamage": "Roll Damage",
@ -2539,9 +2414,6 @@
"recovery": { "label": "Recovery" }, "recovery": { "label": "Recovery" },
"type": { "label": "Type" }, "type": { "label": "Type" },
"value": { "label": "Value" } "value": { "label": "Value" }
},
"identifier": {
"label": "Identifier"
} }
}, },
"Ancestry": { "Ancestry": {
@ -2567,11 +2439,10 @@
"tokenImg": { "label": "Token Image" }, "tokenImg": { "label": "Token Image" },
"tokenRingImg": { "label": "Subject Texture" }, "tokenRingImg": { "label": "Subject Texture" },
"tokenSize": { "tokenSize": {
"placeholder": "Token Size", "placeholder": "Using character dimensions",
"disabledPlaceholder": "Token Size", "disabledPlaceholder": "Set by character size",
"height": { "label": "Height" }, "height": { "label": "Height" },
"width": { "label": "Width" }, "width": { "label": "Width" },
"depth": { "label": "Depth" },
"scale": { "label": "Token Scale" } "scale": { "label": "Token Scale" }
}, },
"evolved": { "evolved": {
@ -2657,7 +2528,8 @@
"MACROS": { "MACROS": {
"Spotlight": { "Spotlight": {
"errors": { "errors": {
"noTokenSelected": "A token on the canvas must either be selected or hovered to spotlight it" "noActiveCombat": "There is no active encounter",
"noCombatantSelected": "A combatant token must be either selected or hovered to spotlight it"
} }
} }
}, },
@ -2732,10 +2604,6 @@
"hint": "Automatically increase the GM's fear pool on a fear duality roll result." "hint": "Automatically increase the GM's fear pool on a fear duality roll result."
}, },
"FIELDS": { "FIELDS": {
"autoExpireActiveEffects": {
"label": "Auto Expire Active Effects",
"hint": "Active Effects with set durations will automatically be removed when their durations are up"
},
"damageReductionRulesDefault": { "damageReductionRulesDefault": {
"label": "Damage Reduction Rules Default", "label": "Damage Reduction Rules Default",
"hint": "Wether using armor and reductions has rules on by default" "hint": "Wether using armor and reductions has rules on by default"
@ -2833,15 +2701,6 @@
"hideObserverPermissionInChat": { "hideObserverPermissionInChat": {
"label": "Hide Chat Info From Players", "label": "Hide Chat Info From Players",
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden" "hint": "Information such as hit/miss on attack rolls against adversaries will be hidden"
},
"hidePartyStats": {
"label": "Hide Party Stats",
"hint": "Resources and stats in the party sheet's member list will be hidden to the following users, even if the user is part of the same party",
"choices": {
"never": "Never, always show",
"players": "Hide From Players",
"always": "Hide from Everyone"
}
} }
} }
}, },
@ -2914,10 +2773,6 @@
} }
}, },
"Keybindings": { "Keybindings": {
"partySheet": {
"name": "Toggle Party Sheet",
"hint": "Open or close the active party's sheet"
},
"spotlight": { "spotlight": {
"name": "Spotlight Combatant", "name": "Spotlight Combatant",
"hint": "Move the spotlight to a hovered or selected token that's present in an active encounter" "hint": "Move the spotlight to a hovered or selected token that's present in an active encounter"
@ -2962,7 +2817,6 @@
"system": "Dice Preset", "system": "Dice Preset",
"font": "Font", "font": "Font",
"critical": "Duality Critical Animation", "critical": "Duality Critical Animation",
"muted": "Muted",
"diceAppearance": "Dice Appearance", "diceAppearance": "Dice Appearance",
"animations": "Animations", "animations": "Animations",
"defaultAnimations": "Set Animations As Player Defaults", "defaultAnimations": "Set Animations As Player Defaults",
@ -3071,6 +2925,18 @@
"immunityTo": "Immunity: {immunities}" "immunityTo": "Immunity: {immunities}"
}, },
"featureTitle": "Class Feature", "featureTitle": "Class Feature",
"groupRoll": {
"title": "Group Roll",
"leader": "Leader",
"partyTeam": "Party Team",
"team": "Team",
"selectLeader": "Select a Leader",
"selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll",
"rerollContent": "Are you sure you want to reroll your {trait} roll?",
"rerollTooltip": "Reroll",
"wholePartySelected": "The whole party is selected"
},
"healingRoll": { "healingRoll": {
"title": "Heal - {damage}", "title": "Heal - {damage}",
"heal": "Heal", "heal": "Heal",
@ -3097,7 +2963,6 @@
} }
}, },
"ChatLog": { "ChatLog": {
"rerollActionRoll": "Reroll Action",
"rerollDamage": "Reroll Damage", "rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll" "assignTagRoll": "Assign as Tag Roll"
}, },
@ -3114,8 +2979,6 @@
}, },
"EffectsDisplay": { "EffectsDisplay": {
"removeThing": "[Right Click] Remove {thing}", "removeThing": "[Right Click] Remove {thing}",
"increaseStacks": "[Left Click] Increment Stacks",
"decreaseStacks": "[Right Click] Decrement Stacks",
"appliedBy": "Applied By: {by}" "appliedBy": "Applied By: {by}"
}, },
"ItemBrowser": { "ItemBrowser": {
@ -3231,6 +3094,7 @@
"subclassesAlreadyPresent": "You already have a class and multiclass subclass", "subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"gmMenuRefresh": "You refreshed all actions and resources {types}", "gmMenuRefresh": "You refreshed all actions and resources {types}",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
"gmRequired": "This action requires an online GM", "gmRequired": "This action requires an online GM",
"gmOnly": "This can only be accessed by the GM", "gmOnly": "This can only be accessed by the GM",
"noActorOwnership": "You do not have permissions for this character", "noActorOwnership": "You do not have permissions for this character",
@ -3239,12 +3103,7 @@
"tokenActorsMissing": "[{names}] missing Actors", "tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token", "knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}"
"noTokenTargeted": "No token is targeted",
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM"
},
"Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."
}, },
"Sidebar": { "Sidebar": {
"actorDirectory": { "actorDirectory": {
@ -3253,8 +3112,6 @@
"companion": "Level {level} - {partner}", "companion": "Level {level} - {partner}",
"companionNoPartner": "No Partner", "companionNoPartner": "No Partner",
"duplicateToNewTier": "Duplicate to New Tier", "duplicateToNewTier": "Duplicate to New Tier",
"activateParty": "Make Active Party",
"partyIsActive": "Active",
"createAdversary": "Create Adversary", "createAdversary": "Create Adversary",
"pickTierTitle": "Pick a new tier for this adversary" "pickTierTitle": "Pick a new tier for this adversary"
}, },
@ -3267,7 +3124,6 @@
"Tooltip": { "Tooltip": {
"disableEffect": "Disable Effect", "disableEffect": "Disable Effect",
"enableEffect": "Enable Effect", "enableEffect": "Enable Effect",
"edit": "Edit",
"openItemWorld": "Open Item World", "openItemWorld": "Open Item World",
"openActorWorld": "Open Actor World", "openActorWorld": "Open Actor World",
"sendToChat": "Send to Chat", "sendToChat": "Send to Chat",

View file

@ -154,8 +154,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.active = this.tabGroups[v.group] v.active = this.tabGroups[v.group]
? this.tabGroups[v.group] === v.id ? this.tabGroups[v.group] === v.id
: this.tabGroups.primary !== 'equipment' : this.tabGroups.primary !== 'equipment'
? v.active ? v.active
: false; : false;
v.cssClass = v.active ? 'active' : ''; v.cssClass = v.active ? 'active' : '';
switch (v.id) { switch (v.id) {
@ -211,9 +211,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.suggestedTraits = this.setup.class.system context.suggestedTraits = this.setup.class.system
? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => { ? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => {
const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey]; const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey];
return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`; return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`;
}) })
: []; : [];
context.traits = { context.traits = {
values: Object.keys(this.setup.traits).map(traitKey => { values: Object.keys(this.setup.traits).map(traitKey => {
@ -439,18 +439,15 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null } 'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
}; };
if (type === 'subclasses') { if (type === 'subclasses')
const classItem = this.setup.class;
const uuid = classItem?._stats.compendiumSource ?? classItem?.uuid;
presets.filter = { presets.filter = {
'system.linkedClass': { key: 'system.linkedClass', value: uuid } 'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
}; };
}
if (equipment.includes(type)) if (equipment.includes(type))
presets.filter = { presets.filter = {
'system.tier': { key: 'system.tier', value: 1 }, 'system.tier': { key: 'system.tier', value: 1 },
type: { key: 'type', value: type } 'type': { key: 'type', value: type }
}; };
ui.compendiumBrowser.open(presets); ui.compendiumBrowser.open(presets);
@ -562,7 +559,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
experiences: { experiences: {
...this.setup.experiences, ...this.setup.experiences,
...Object.keys(this.character.system.experiences).reduce((acc, key) => { ...Object.keys(this.character.system.experiences).reduce((acc, key) => {
acc[`${key}`] = _del; acc[`-=${key}`] = null;
return acc; return acc;
}, {}) }, {})
} }
@ -613,8 +610,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
[foundry.utils.randomID()]: {} [foundry.utils.randomID()]: {}
}; };
} else if (item.type === 'subclass' && event.target.closest('.subclass-card')) { } else if (item.type === 'subclass' && event.target.closest('.subclass-card')) {
const classSubclasses = await this.setup.class.system.fetchSubclasses(); if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) {
if (classSubclasses.every(subclass => subclass.uuid !== item.uuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return; return;
} }

View file

@ -50,7 +50,7 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
const excludedSourceData = this.browserSettings.excludedSources; const excludedSourceData = this.browserSettings.excludedSources;
const excludedPackData = this.browserSettings.excludedPacks; const excludedPackData = this.browserSettings.excludedPacks;
context.typePackCollections = game.packs.reduce((acc, pack) => { context.typePackCollections = game.packs.reduce((acc, pack) => {
const { type, label, packageType, packageName: basePackageName, name, id } = pack.metadata; const { type, label, packageType, packageName: basePackageName, id } = pack.metadata;
if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc; if (!CompendiumBrowserSettings.#browserPackTypes.includes(type)) return acc;
const isWorldPack = packageType === 'world'; const isWorldPack = packageType === 'world';
@ -68,15 +68,13 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
if (!acc[type].sources[packageName]) if (!acc[type].sources[packageName])
acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] }; acc[type].sources[packageName] = { label: sourceLabel, checked: sourceChecked, packs: [] };
const included = const checked = !excludedPackData[id] || !excludedPackData[id].excludedDocumentTypes.includes(type);
!excludedPackData[packageName] ||
!excludedPackData[packageName][name]?.excludedDocumentTypes.includes(type);
acc[type].sources[packageName].packs.push({ acc[type].sources[packageName].packs.push({
name, pack: id,
type, type,
label: id === game.system.id ? game.system.title : game.i18n.localize(label), label: id === game.system.id ? game.system.title : game.i18n.localize(label),
checked: included checked: checked
}); });
return acc; return acc;
@ -108,16 +106,16 @@ export default class CompendiumBrowserSettings extends HandlebarsApplicationMixi
toggleTypedPack(event) { toggleTypedPack(event) {
event.stopPropagation(); event.stopPropagation();
const { type, source, packName } = event.target.dataset; const { type, pack } = event.target.dataset;
const currentlyExcluded = this.browserSettings.excludedPacks[source]?.[packName] const currentlyExcluded = this.browserSettings.excludedPacks[pack]
? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.includes(type) ? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.includes(type)
: false; : false;
this.browserSettings.excludedPacks[source] ??= {}; if (!this.browserSettings.excludedPacks[pack])
this.browserSettings.excludedPacks[source][packName] ??= { excludedDocumentTypes: [] }; this.browserSettings.excludedPacks[pack] = { excludedDocumentTypes: [] };
this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes = currentlyExcluded this.browserSettings.excludedPacks[pack].excludedDocumentTypes = currentlyExcluded
? this.browserSettings.excludedPacks[source][packName].excludedDocumentTypes.filter(x => x !== type) ? this.browserSettings.excludedPacks[pack].excludedDocumentTypes.filter(x => x !== type)
: [...(this.browserSettings.excludedPacks[source][packName]?.excludedDocumentTypes ?? []), type]; : [...(this.browserSettings.excludedPacks[pack]?.excludedDocumentTypes ?? []), type];
this.render(); this.render();
} }

View file

@ -10,9 +10,10 @@ export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
export { default as ItemTransferDialog } from './itemTransfer.mjs'; export { default as ItemTransferDialog } from './itemTransfer.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs';
export { default as GroupRollDialog } from './groupRollDialog.mjs';
export { default as RiskItAllDialog } from './riskItAllDialog.mjs'; export { default as RiskItAllDialog } from './riskItAllDialog.mjs';
export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs'; export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs';

View file

@ -72,7 +72,7 @@ export default class ActionSelectionDialog extends HandlebarsApplicationMixin(Ap
static async #onChooseAction(event, button) { static async #onChooseAction(event, button) {
const { actionId } = button.dataset; const { actionId } = button.dataset;
this.#action = this.item.system.actionsList.find(a => a._id === actionId); this.#action = this.#item.system.actionsList.find(a => a._id === actionId);
Object.defineProperty(this.#event, 'shiftKey', { Object.defineProperty(this.#event, 'shiftKey', {
get() { get() {
return event.shiftKey; return event.shiftKey;

View file

@ -10,12 +10,6 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.selected = null; this.selected = null;
this.evolved = { form: null }; this.evolved = { form: null };
this.hybrid = { forms: {}, advantages: {}, features: {} }; this.hybrid = { forms: {}, advantages: {}, features: {} };
this.modifications = {
traitBonuses: configData.modifications.traitBonuses.map(x => ({
trait: null,
bonus: x.bonus
}))
};
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
} }
@ -34,7 +28,6 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
selectBeastform: this.selectBeastform, selectBeastform: this.selectBeastform,
toggleHybridFeature: this.toggleHybridFeature, toggleHybridFeature: this.toggleHybridFeature,
toggleHybridAdvantage: this.toggleHybridAdvantage, toggleHybridAdvantage: this.toggleHybridAdvantage,
toggleTraitBonus: this.toggleTraitBonus,
submitBeastform: this.submitBeastform submitBeastform: this.submitBeastform
}, },
form: { form: {
@ -55,7 +48,6 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' }, tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' },
beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' }, beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' },
advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' }, advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' },
modifications: { template: 'systems/daggerheart/templates/dialogs/beastform/modifications.hbs' },
footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' } footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' }
}; };
@ -154,9 +146,6 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
{} {}
); );
context.modifications = this.modifications;
context.traits = CONFIG.DH.ACTOR.abilities;
context.tier = beastformTiers[this.tabGroups.primary]; context.tier = beastformTiers[this.tabGroups.primary];
context.tierKey = this.tabGroups.primary; context.tierKey = this.tabGroups.primary;
@ -166,9 +155,6 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
} }
canSubmit() { canSubmit() {
const modificationsFinished = this.modifications.traitBonuses.every(x => x.trait);
if (!modificationsFinished) return false;
if (this.selected) { if (this.selected) {
switch (this.selected.system.beastformType) { switch (this.selected.system.beastformType) {
case 'normal': case 'normal':
@ -275,13 +261,6 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.render(); this.render();
} }
static toggleTraitBonus(_, button) {
const { index, trait } = button.dataset;
this.modifications.traitBonuses[index].trait =
this.modifications.traitBonuses[index].trait === trait ? null : trait;
this.render();
}
static async submitBeastform() { static async submitBeastform() {
await this.close({ submitted: true }); await this.close({ submitted: true });
} }
@ -313,23 +292,6 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
} }
} }
const beastformEffect = selected.effects.find(x => x.type === 'beastform');
for (const traitBonus of app.modifications.traitBonuses) {
const existingChange = beastformEffect.changes.find(
x => x.key === `system.traits.${traitBonus.trait}.value`
);
if (existingChange) {
existingChange.value = Number.parseInt(existingChange.value) + traitBonus.bonus;
} else {
beastformEffect.changes.push({
key: `system.traits.${traitBonus.trait}.value`,
mode: 2,
priority: null,
value: traitBonus.bonus
});
}
}
resolve({ resolve({
selected: selected, selected: selected,
evolved: { ...app.evolved, form: evolved }, evolved: { ...app.evolved, form: evolved },

View file

@ -77,8 +77,8 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
if (!this.data.optional.portrait.keep) { if (!this.data.optional.portrait.keep) {
foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor)); foundry.utils.setProperty(update, 'img', this.actor.schema.fields.img.initial(this.actor));
foundry.utils.setProperty(update, 'prototypeToken.texture', _replace({})); foundry.utils.setProperty(update, 'prototypeToken.==texture', {});
foundry.utils.setProperty(update, 'prototypeToken.ring', _replace({})); foundry.utils.setProperty(update, 'prototypeToken.==ring', {});
} }
if (this.data.optional.biography.keep) if (this.data.optional.biography.keep)
@ -89,7 +89,7 @@ export default class CharacterResetDialog extends HandlebarsApplicationMixin(App
const { system, ...rest } = update; const { system, ...rest } = update;
await this.actor.update({ await this.actor.update({
...rest, ...rest,
system: _replace(system ?? {}) '==system': system ?? {}
}); });
const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot']; const inventoryItemTypes = ['weapon', 'armor', 'consumable', 'loot'];

View file

@ -35,6 +35,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage, updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction, toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect, toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
@ -70,8 +71,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config; context.rollConfig = this.config;
context.hasRoll = !!this.config.roll; context.hasRoll = !!this.config.roll;
context.canRoll = true; context.canRoll = true;
context.selectedMessageMode = this.config.selectedMessageMode ?? game.settings.get('core', 'messageMode'); context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({ context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action, action,
label, label,
icon icon
@ -123,10 +124,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.advantage = this.config.roll?.advantage; context.advantage = this.config.roll?.advantage;
context.disadvantage = this.config.roll?.disadvantage; context.disadvantage = this.config.roll?.disadvantage;
context.diceOptions = CONFIG.DH.GENERAL.diceTypes; context.diceOptions = CONFIG.DH.GENERAL.diceTypes;
context.dieFaces = CONFIG.DH.GENERAL.dieFaces.reduce((acc, face) => {
acc[face] = `d${face}`;
return acc;
}, {});
context.isLite = this.config.roll?.lite; context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula; context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
@ -136,6 +133,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.reactionOverride = this.reactionOverride; context.reactionOverride = this.reactionOverride;
} }
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
context.activeTagTeamRoll = true;
context.tagTeamSelected = this.config.tagTeamSelected;
}
return context; return context;
} }
@ -146,17 +149,19 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
})); }));
} }
static updateRollConfiguration(_event, _, formData) { static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedMessageMode = rest.selectedMessageMode; this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) { if (this.config.costs) {
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs); this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);
} }
if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses); if (this.config.uses) this.config.uses = foundry.utils.mergeObject(this.config.uses, rest.uses);
if (rest.roll?.dice) { if (rest.roll?.dice) {
this.roll = foundry.utils.mergeObject(this.roll, rest.roll.dice); Object.entries(rest.roll.dice).forEach(([key, value]) => {
this.roll[key] = value;
});
} }
if (rest.hasOwnProperty('trait')) { if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait; this.config.roll.trait = rest.trait;
@ -175,15 +180,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.disadvantage = advantage === -1; this.disadvantage = advantage === -1;
this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage; this.config.roll.advantage = this.config.roll.advantage === advantage ? 0 : advantage;
if (this.config.roll.advantage === 0) return this.render();
const defaultFaces =
this.config.roll.advantage === 1
? this.config.data.rules.roll.defaultAdvantageDice
: this.config.data.rules.roll.defaultDisadvantageDice;
const faces = Number.parseInt(defaultFaces);
this.roll.advantageFaces = Number.isNaN(faces) ? this.roll.advantageFaces : faces;
this.render(); this.render();
} }
@ -196,14 +192,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
? this.config.costs.filter(x => x.extKey !== button.dataset.key) ? this.config.costs.filter(x => x.extKey !== button.dataset.key)
: [ : [
...this.config.costs, ...this.config.costs,
{ {
extKey: button.dataset.key, extKey: button.dataset.key,
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
value: 1, value: 1,
name: this.config.data?.system.experiences?.[button.dataset.key]?.name name: this.config.data?.system.experiences?.[button.dataset.key]?.name
} }
]; ];
this.render(); this.render();
} }
@ -213,12 +209,17 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.actionType = this.reactionOverride this.config.actionType = this.reactionOverride
? 'reaction' ? 'reaction'
: this.config.actionType === 'reaction' : this.config.actionType === 'reaction'
? 'action' ? 'action'
: this.config.actionType; : this.config.actionType;
this.render(); this.render();
} }
} }
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static toggleSelectedEffect(_event, button) { static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render(); this.render();

View file

@ -22,8 +22,6 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
}, },
actions: { actions: {
toggleSelectedEffect: this.toggleSelectedEffect, toggleSelectedEffect: this.toggleSelectedEffect,
updateGroupAttack: this.updateGroupAttack,
toggleCritical: this.toggleCritical,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -54,9 +52,8 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
context.hasHealing = this.config.hasHealing; context.hasHealing = this.config.hasHealing;
context.directDamage = this.config.directDamage; context.directDamage = this.config.directDamage;
context.selectedMessageMode = this.config.selectedMessageMode; context.selectedRollMode = this.config.selectedRollMode;
context.isCritical = this.config.isCritical; context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
action, action,
label, label,
icon icon
@ -65,45 +62,15 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length); context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects; context.selectedEffects = this.selectedEffects;
context.damageOptions = this.config.damageOptions;
context.rangeOptions = CONFIG.DH.GENERAL.groupAttackRange;
return context; return context;
} }
static updateRollConfiguration(_event, _, formData) { static updateRollConfiguration(_event, _, formData) {
const data = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, data.roll); foundry.utils.mergeObject(this.config.roll, rest.roll);
foundry.utils.mergeObject(this.config.modifiers, data.modifiers); foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
this.config.selectedMessageMode = data.selectedMessageMode; this.config.selectedRollMode = rest.selectedRollMode;
if (data.damageOptions) {
const numAttackers = data.damageOptions.groupAttack?.numAttackers;
if (typeof numAttackers !== 'number' || numAttackers % 1 !== 0) {
data.damageOptions.groupAttack.numAttackers = null;
}
foundry.utils.mergeObject(this.config.damageOptions, data.damageOptions);
}
this.render();
}
static updateGroupAttack() {
const targets = Array.from(game.user.targets);
if (targets.length === 0)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noTokenTargeted'));
const actorId = this.roll.data.parent.id;
const range = this.config.damageOptions.groupAttack.range;
const groupAttackTokens = game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(actorId, range);
this.config.damageOptions.groupAttack.numAttackers = groupAttackTokens.length;
this.render();
}
static toggleCritical() {
this.config.isCritical = !this.config.isCritical;
this.render(); this.render();
} }

View file

@ -1,4 +1,4 @@
import { damageKeyToNumber, getArmorSources, getDamageLabel } from '../../helpers/utils.mjs'; import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@ -10,7 +10,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject; this.reject = reject;
this.actor = actor; this.actor = actor;
this.damage = damage; this.damage = damage;
this.damageType = damageType; this.damageType = damageType;
this.rulesDefault = game.settings.get( this.rulesDefault = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
@ -21,21 +20,14 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.rulesDefault this.rulesDefault
); );
const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled); const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
const armor = orderedArmorSources.reduce((acc, { name, document }) => { const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value;
const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData; const maxArmorMarks = canApplyArmor ? availableArmor : 0;
acc.push({
name,
effect: document,
marks: [...Array(max).keys()].reduce((acc, _, index) => {
const spent = index < current;
acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent };
return acc;
}, {})
});
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false };
return acc; return acc;
}, []); }, {});
const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce( const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
(acc, _) => { (acc, _) => {
acc[foundry.utils.randomID()] = { selected: false }; acc[foundry.utils.randomID()] = { selected: false };
@ -129,42 +121,36 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
context.thresholdImmunities = context.thresholdImmunities =
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null; Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } = const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
this.getDamageInfo(); this.getDamageInfo();
context.armorScore = this.actor.system.armorScore.max; context.armorScore = this.actor.system.armorScore;
context.armorMarks = currentMarks; context.armorMarks = currentMarks;
context.basicMarksUsed =
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value;
const stressReductionStress = this.availableStressReductions const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0) ? stressReductions.reduce((acc, red) => acc + red.cost, 0)
: 0; : 0;
const stress = this.actor.system.resources.stress;
context.stress = context.stress =
selectedStressMarks.length > 0 || this.availableStressReductions selectedStressMarks.length > 0 || this.availableStressReductions
? { ? {
value: stress.value + selectedStressMarks.length + stressReductionStress, value:
max: stress.max this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress,
} max: this.actor.system.resources.stress.max
}
: null; : null;
context.maxArmorUsed = maxArmorUsed; const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
context.availableArmor = availableArmor;
context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length;
const armorSources = [];
for (const source of this.marks.armor) {
armorSources.push({
label: source.name,
uuid: source.effect.uuid,
marks: source.marks
});
}
context.marks = { context.marks = {
armor: armorSources, armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark;
return acc;
}, {}),
stress: this.marks.stress stress: this.marks.stress
}; };
context.usesStressArmor = Object.keys(context.marks.stress).length;
context.availableStressReductions = this.availableStressReductions; context.availableStressReductions = this.availableStressReductions;
context.damage = getDamageLabel(this.damage); context.damage = getDamageLabel(this.damage);
@ -181,31 +167,27 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
getDamageInfo = () => { getDamageInfo = () => {
const selectedArmorMarks = this.marks.armor.flatMap(x => Object.values(x.marks).filter(x => x.selected)); const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
const stressReductions = this.availableStressReductions const stressReductions = this.availableStressReductions
? Object.values(this.availableStressReductions).filter(red => red.selected) ? Object.values(this.availableStressReductions).filter(red => red.selected)
: []; : [];
const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.length; const currentMarks =
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
const maxArmorUsed = this.actor.system.rules.damageReduction.maxArmorMarked.value + selectedStressMarks.length;
const availableArmor =
maxArmorUsed -
this.marks.armor.reduce((acc, source) => {
acc += Object.values(source.marks).filter(x => x.selected).length;
return acc;
}, 0);
const armorMarkReduction = const armorMarkReduction =
selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark; selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0); let currentDamage = Math.max(
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0
);
if (this.reduceSeverity) { if (this.reduceSeverity) {
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0); currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
} }
if (this.thresholdImmunities[currentDamage]) currentDamage = 0; if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor }; return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
}; };
static toggleRules() { static toggleRules() {
@ -213,10 +195,13 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value; const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value;
this.marks = { this.marks = {
armor: this.marks.armor.map((mark, index) => { armor: Object.keys(this.marks.armor).reduce((acc, key, index) => {
const mark = this.marks.armor[key];
const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor; const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor;
return { ...mark, selected: keepSelectValue ? mark.selected : false }; acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false };
}),
return acc;
}, {}),
stress: this.marks.stress stress: this.marks.stress
}; };
@ -224,8 +209,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
static setMarks(_, target) { static setMarks(_, target) {
const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path); const currentMark = this.marks[target.dataset.type][target.dataset.key];
const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo(); const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo();
if (!currentMark.selected && currentDamage === 0) { if (!currentMark.selected && currentDamage === 0) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone')); ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone'));
@ -233,18 +218,12 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
if (this.rulesOn) { if (this.rulesOn) {
if (target.dataset.type === 'armor' && !currentMark.selected && !availableArmor) { if (!currentMark.selected && currentMarks === this.actor.system.armorScore) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks')); ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks'));
return; return;
} }
} }
const stressUsed = selectedStressMarks.length;
if (target.dataset.type === 'armor' && stressUsed) {
const updateResult = this.updateStressArmor(target.dataset.id, !currentMark.selected);
if (updateResult === false) return;
}
if (currentMark.selected) { if (currentMark.selected) {
const currentDamageLabel = getDamageLabel(currentDamage); const currentDamageLabel = getDamageLabel(currentDamage);
for (let reduction of stressReductions) { for (let reduction of stressReductions) {
@ -253,16 +232,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
} }
if (target.dataset.type === 'stress' && currentMark.armorMarkId) { if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) {
for (const source of this.marks.armor) { selectedStressMarks.forEach(mark => (mark.selected = false));
const match = Object.keys(source.marks).find(key => key === currentMark.armorMarkId);
if (match) {
source.marks[match].selected = false;
break;
}
}
currentMark.armorMarkId = null;
} }
} }
@ -270,25 +241,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.render(); this.render();
} }
updateStressArmor(armorMarkId, select) {
let stressMarkKey = null;
if (select) {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].selected && !this.marks.stress[key].armorMarkId
);
} else {
stressMarkKey = Object.keys(this.marks.stress).find(
key => this.marks.stress[key].armorMarkId === armorMarkId
);
if (!stressMarkKey)
stressMarkKey = Object.keys(this.marks.stress).find(key => this.marks.stress[key].selected);
}
if (!stressMarkKey) return false;
this.marks.stress[stressMarkKey].armorMarkId = select ? armorMarkId : null;
}
static useStressReduction(_, target) { static useStressReduction(_, target) {
const damageValue = Number(target.dataset.reduction); const damageValue = Number(target.dataset.reduction);
const stressReduction = this.availableStressReductions[damageValue]; const stressReduction = this.availableStressReductions[damageValue];
@ -327,18 +279,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
static async takeDamage() { static async takeDamage() {
const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo(); const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo();
const armorChanges = this.marks.armor.reduce((acc, source) => { const armorSpent = selectedArmorMarks.length + selectedStressMarks.length;
const amount = Object.values(source.marks).filter(x => x.selected).length; const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0);
if (amount) acc.push({ uuid: source.effect.uuid, amount });
return acc; this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent });
}, []);
const stressSpent =
selectedStressMarks.filter(x => x.armorMarkId).length +
stressReductions.reduce((acc, red) => acc + red.cost, 0);
this.resolve({ modifiedDamage: currentDamage, armorChanges, stressSpent });
await this.close(true); await this.close(true);
} }

View file

@ -57,7 +57,6 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar'); let returnMessage = game.i18n.localize('DAGGERHEART.UI.Chat.deathMove.avoidScar');
if (config.roll.fate.value <= this.actor.system.levelData.level.current) { if (config.roll.fate.value <= this.actor.system.levelData.level.current) {
const maxHope = this.actor.system.resources.hope.max + this.actor.system.scars;
const newScarAmount = this.actor.system.scars + 1; const newScarAmount = this.actor.system.scars + 1;
await this.actor.update({ await this.actor.update({
system: { system: {
@ -65,7 +64,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
} }
}); });
if (newScarAmount >= maxHope) { if (newScarAmount >= this.actor.system.resources.hope.max) {
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount }); return game.i18n.format('DAGGERHEART.UI.Chat.deathMove.journeysEnd', { scars: newScarAmount });
} }

View file

@ -1,4 +1,4 @@
import { expireActiveEffects, refreshIsAllowed } from '../../helpers/utils.mjs'; import { refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -203,7 +203,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
const msg = { const msg = {
user: game.user.id, user: game.user.id,
system: { system: {
moves: moves.map(move => ({ ...move, actions: Array.from(move.actions) })), moves: moves,
actor: this.actor.uuid actor: this.actor.uuid
}, },
speaker: cls.getSpeaker(), speaker: cls.getSpeaker(),
@ -259,16 +259,12 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
const resetValue = increasing const resetValue = increasing
? 0 ? 0
: feature.system.resource.max : feature.system.resource.max
? new Roll( ? new Roll(Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData())).evaluateSync().total
Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData()) : 0;
).evaluateSync().total
: 0;
await feature.update({ 'system.resource.value': resetValue }); await feature.update({ 'system.resource.value': resetValue });
} }
expireActiveEffects(this.actor, [this.shortRest ? 'shortRest' : 'longRest']);
this.close(); this.close();
} else { } else {
this.render(); this.render();

View file

@ -0,0 +1,204 @@
import autocomplete from 'autocompleter';
import { abilities } from '../../config/actorConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actors) {
super();
this.actors = actors;
this.actorLeader = {};
this.actorsMembers = [];
}
get title() {
return 'Group Roll';
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll'],
position: { width: 'auto', height: 'auto' },
window: {
title: 'DAGGERHEART.UI.Chat.groupRoll.title'
},
actions: {
roll: GroupRollDialog.#roll,
removeLeader: GroupRollDialog.#removeLeader,
removeMember: GroupRollDialog.#removeMember
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'group-roll',
template: 'systems/daggerheart/templates/dialogs/group-roll/group-roll.hbs'
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const leaderChoices = this.actors.filter(x => this.actorsMembers.every(member => member.actor?.id !== x.id));
const memberChoices = this.actors.filter(
x => this.actorLeader?.actor?.id !== x.id && this.actorsMembers.every(member => member.actor?.id !== x.id)
);
htmlElement.querySelectorAll('.leader-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(leaderChoices);
} else {
text = text.toLowerCase();
var suggestions = leaderChoices.filter(n => n.name.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (actor, search) {
const actorName = game.i18n.localize(actor.name);
const matchIndex = actorName.toLowerCase().indexOf(search);
const beforeText = actorName.slice(0, matchIndex);
const matchText = actorName.slice(matchIndex, matchIndex + search.length);
const after = actorName.slice(matchIndex + search.length, actorName.length);
const img = document.createElement('img');
img.src = actor.img;
const element = document.createElement('li');
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.appendChild(label);
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: actor => {
element.value = actor.uuid;
this.actorLeader = { actor: actor, trait: 'agility', difficulty: 0 };
this.render();
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
htmlElement.querySelectorAll('.team-push-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(memberChoices);
} else {
text = text.toLowerCase();
var suggestions = memberChoices.filter(n => n.name.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (actor, search) {
const actorName = game.i18n.localize(actor.name);
const matchIndex = actorName.toLowerCase().indexOf(search);
const beforeText = actorName.slice(0, matchIndex);
const matchText = actorName.slice(matchIndex, matchIndex + search.length);
const after = actorName.slice(matchIndex + search.length, actorName.length);
const img = document.createElement('img');
img.src = actor.img;
const element = document.createElement('li');
element.appendChild(img);
const label = document.createElement('span');
label.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
element.appendChild(label);
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: actor => {
element.value = actor.uuid;
this.actorsMembers.push({ actor: actor, trait: 'agility', difficulty: 0 });
this.render({ force: true });
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.leader = this.actorLeader;
context.members = this.actorsMembers;
context.traitList = abilities;
context.allSelected = this.actorsMembers.length + (this.actorLeader?.actor ? 1 : 0) === this.actors.length;
context.rollDisabled = context.members.length === 0 || !this.actorLeader?.actor;
return context;
}
static updateData(event, _, formData) {
const { actorLeader, actorsMembers } = foundry.utils.expandObject(formData.object);
this.actorLeader = foundry.utils.mergeObject(this.actorLeader, actorLeader);
this.actorsMembers = foundry.utils.mergeObject(this.actorsMembers, actorsMembers);
this.render(true);
}
static async #removeLeader(_, button) {
this.actorLeader = null;
this.render();
}
static async #removeMember(_, button) {
this.actorsMembers = this.actorsMembers.filter(m => m.actor.uuid !== button.dataset.memberUuid);
this.render();
}
static async #roll() {
const cls = getDocumentClass('ChatMessage');
const systemData = {
leader: this.actorLeader,
members: this.actorsMembers
};
const msg = {
type: 'groupRoll',
user: game.user.id,
speaker: cls.getSpeaker(),
title: game.i18n.localize('DAGGERHEART.UI.Chat.groupRoll.title'),
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ system: systemData }
)
};
cls.create(msg);
this.close();
}
}

View file

@ -1,500 +0,0 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(party) {
super({ id: `GroupRollDialog-${party.id}` });
this.party = party;
this.partyMembers = party.system.partyMembers
.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
.map(member => ({
...member.toObject(),
uuid: member.uuid,
id: member.id,
selected: true,
owned: member.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)
}));
this.leader = null;
this.openForAllPlayers = true;
this.tabGroups.application = Object.keys(party.system.groupRoll.participants).length
? 'groupRoll'
: 'initialization';
Hooks.on(socketEvent.Refresh, this.groupRollRefresh.bind());
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll-dialog'],
position: { width: 390, height: 'auto' },
window: {
icon: 'fa-solid fa-users'
},
actions: {
toggleSelectMember: this.#toggleSelectMember,
startGroupRoll: this.#startGroupRoll,
makeRoll: this.#makeRoll,
removeRoll: this.#removeRoll,
rerollDice: this.#rerollDice,
markSuccessful: this.#markSuccessful,
cancelRoll: this.#onCancelRoll,
finishRoll: this.#finishRoll
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
initialization: {
id: 'initialization',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/initialization.hbs'
},
main: {
id: 'main',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/main.hbs'
},
leader: {
id: 'leader',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs'
},
result: {
id: 'result',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/result.hbs'
},
footer: {
id: 'footer',
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/footer.hbs'
}
};
/** @inheritdoc */
static TABS = {
application: {
tabs: [{ id: 'initialization' }, { id: 'groupRoll' }]
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement
.querySelector('.main-character-field')
?.addEventListener('input', this.updateLeaderField.bind(this));
}
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
for (const memberKey of Object.keys(this.party.system.groupRoll.aidingCharacters)) {
parts[memberKey] = {
id: memberKey,
template: 'systems/daggerheart/templates/dialogs/groupRollDialog/parts/member.hbs'
};
}
return parts;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isGM = game.user.isGM;
context.isEditable =
game.user.isGM ||
this.party.system.partyMembers.some(actor => {
const selected = Boolean(this.party.system.groupRoll.participants[actor.id]);
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
});
context.fields = this.party.system.schema.fields.groupRoll.fields;
context.data = this.party.system.groupRoll;
context.traitOptions = CONFIG.DH.ACTOR.abilities;
context.members = {};
context.aidKeys = Object.keys(this.party.system.groupRoll.aidingCharacters);
context.allHaveRolled = Object.keys(context.data.participants).every(key => {
const data = context.data.participants[key];
return Boolean(data.rollData);
});
return context;
}
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
partContext.partId = partId;
partContext.leader = this.getRollCharacterData(this.party.system.groupRoll.leader);
switch (partId) {
case 'initialization':
partContext.groupRollFields = this.party.system.schema.fields.groupRoll.fields;
partContext.memberSelection = this.partyMembers;
const selectedMembers = partContext.memberSelection.filter(x => x.selected);
partContext.selectedLeader = this.leader;
partContext.selectedLeaderOptions = selectedMembers
.filter(actor => actor.owned)
.map(x => ({ value: x.id, label: x.name }));
partContext.selectedLeaderDisabled = !selectedMembers.length;
partContext.canStartGroupRoll = selectedMembers.length > 1 && this.leader?.memberId;
partContext.openForAllPlayers = this.openForAllPlayers;
break;
case 'result':
const leader = this.party.system.groupRoll.leader;
partContext.hasRolled =
leader?.rollData ||
Object.values(this.party.system.groupRoll?.aidingCharacters ?? {}).some(x => x.successful !== null);
const { modifierTotal, modifiers } = Object.values(this.party.system.groupRoll.aidingCharacters).reduce(
(acc, curr) => {
const modifier = curr.successful === true ? 1 : curr.successful === false ? -1 : null;
if (modifier) {
acc.modifierTotal += modifier;
acc.modifiers.push(modifier);
}
return acc;
},
{ modifierTotal: 0, modifiers: [] }
);
const leaderTotal = leader?.rollData ? leader.roll.total : null;
partContext.groupRoll = {
totalLabel: leader?.rollData
? game.i18n.format('DAGGERHEART.GENERAL.withThing', {
thing: leader.roll.totalLabel
})
: null,
totalDualityClass: leader?.roll?.isCritical ? 'critical' : leader?.roll?.withHope ? 'hope' : 'fear',
total: leaderTotal + modifierTotal,
leaderTotal: leaderTotal,
modifiers
};
break;
case 'footer':
partContext.canFinishRoll =
Boolean(this.party.system.groupRoll.leader?.rollData) &&
Object.values(this.party.system.groupRoll.aidingCharacters).every(x => x.successful !== null);
break;
}
if (Object.keys(this.party.system.groupRoll.aidingCharacters).includes(partId)) {
const characterData = this.party.system.groupRoll.aidingCharacters[partId];
partContext.members[partId] = this.getRollCharacterData(characterData, partId);
}
return partContext;
}
getRollCharacterData(data, partId) {
if (!data) return {};
const actor = game.actors.get(data.id);
const isLeader = data === this.party.system.groupRoll.leader;
const roll = data.roll;
const withTypeSuffix = !roll ? null : roll.isCritical ? 'criticalShort' : roll.withHope ? 'hope' : 'fear';
const thing = withTypeSuffix ? _loc(`DAGGERHEART.GENERAL.${withTypeSuffix}`) : null;
return {
...data,
type: isLeader ? 'leader' : 'aid',
basePath: isLeader ? 'system.groupRoll.leader' : `system.groupRoll.aidingCharacters.${data.id}`,
rollChoiceLabel: _loc(CONFIG.DH.ACTOR.abilities[data.rollChoice]?.label),
roll: data.roll,
isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: partId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
modifier: data.successful ? 1 : data.successful === false ? -1 : 0,
withLabelShort: thing ? _loc('DAGGERHEART.GENERAL.withThing', { thing }) : null
};
}
#getCharacterDataById(id) {
if (!id) return null;
const groupRoll = this.party.system.groupRoll;
if (id === 'leader' || id === groupRoll.leader?.id) {
return { data: groupRoll.leader, basePath: 'system.groupRoll.leader' };
} else if (id in groupRoll.aidingCharacters) {
return { data: groupRoll.aidingCharacters[id], basePath: `system.groupRoll.aidingCharacters.${id}` };
}
return null;
}
static async updateData(event, _, formData) {
const partyData = foundry.utils.expandObject(formData.object);
this.updatePartyData(partyData, this.getUpdatingParts(event.target));
}
async updatePartyData(update, updatingParts, options = { render: true }) {
if (!game.users.activeGM)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
const gmUpdate = async update => {
await this.party.update(update);
this.render({ parts: updatingParts });
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.GroupRoll, action: 'refresh', parts: updatingParts }
});
};
await emitGMUpdate(
GMUpdateEvent.UpdateDocument,
gmUpdate,
update,
this.party.uuid,
options.render ? { refreshType: RefreshType.GroupRoll, action: 'refresh', parts: updatingParts } : undefined
);
}
getUpdatingParts(target) {
const { initialization, leader, result, footer } = this.constructor.PARTS;
const isInitialization = this.tabGroups.application === initialization.id;
const updatingMember = target.closest('.member-roll-container.aid')?.dataset?.memberKey;
const updatingLeader = target.closest('.member-roll-container.leader');
return [
...(isInitialization ? [initialization.id] : []),
...(updatingMember ? [updatingMember] : []),
...(updatingLeader ? [leader.id] : []),
...(!isInitialization ? [result.id, footer.id] : [])
];
}
groupRollRefresh = ({ refreshType, action, parts }) => {
if (refreshType !== RefreshType.GroupRoll) return;
switch (action) {
case 'startGroupRoll':
this.tabGroups.application = 'groupRoll';
break;
case 'refresh':
this.render({ parts });
break;
case 'close':
this.close();
break;
}
};
async close(options = {}) {
/* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */
if (options.closeKey) return;
Hooks.off(socketEvent.Refresh, this.groupRollRefresh);
return super.close(options);
}
//#region Initialization
static #toggleSelectMember(_, button) {
const member = this.partyMembers.find(x => x.id === button.dataset.id);
member.selected = !member.selected;
if (this.leader?.memberId === member.id) {
this.leader = null;
}
this.render();
}
updateLeaderField(event) {
if (!this.leader) this.leader = {};
this.leader.memberId = event.target.value;
this.render();
}
static async #startGroupRoll() {
const leader = this.partyMembers.find(x => x.id === this.leader.memberId);
const aidingCharacters = this.partyMembers.reduce((acc, curr) => {
if (curr.selected && curr.id !== this.leader.memberId)
acc[curr.id] = { id: curr.id, name: curr.name, img: curr.img };
return acc;
}, {});
await this.party.update({
'system.groupRoll': _replace(
new game.system.api.data.GroupRollData({
...this.party.system.groupRoll.toObject(),
leader: { id: leader.id, name: leader.name, img: leader.img },
aidingCharacters
})
)
});
const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id };
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, hookData);
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GroupRollStart,
data: hookData
});
this.render();
}
//#endregion
/** @this GroupRollDialog */
static async #makeRoll(_event, button) {
const member = button.closest('[data-member-key]').dataset.memberKey;
const { data, basePath } = this.#getCharacterDataById(member);
const actor = game.actors.find(x => x.id === data.id);
if (!actor) return;
const result = await actor.rollTrait(data.rollChoice, {
skips: {
createMessage: true,
resources: true,
triggers: true
}
});
if (!result) return;
const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll;
this.updatePartyData(
{
[basePath]: { rollData, successful: null }
},
this.getUpdatingParts(button)
);
}
/** @this GroupRollDialog */
static async #removeRoll(_event, button) {
const member = button.closest('[data-member-key]').dataset.memberKey;
const { basePath } = this.#getCharacterDataById(member);
this.updatePartyData(
{
[basePath]: {
rollData: null,
rollChoice: null,
selected: false,
successful: null
}
},
this.getUpdatingParts(button)
);
}
/** @this GroupRollDialog */
static async #rerollDice(_, button) {
const { diceType } = button.dataset;
const { data, basePath } = this.#getCharacterDataById(button.dataset.member);
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2;
const newRoll = game.system.api.dice.DualityRoll.fromData(data.rollData);
const dice = newRoll.dice[dieIndex];
await dice.reroll(`/r1=${dice.total}`, {
liveRoll: {
roll: newRoll,
isReaction: true
}
});
const rollData = newRoll.toJSON();
this.updatePartyData(
{
[`${basePath}.rollData`]: rollData
},
this.getUpdatingParts(button)
);
}
static #markSuccessful(_event, button) {
const memberKey = button.closest('[data-member-key]').dataset.memberKey;
const previousValue = this.party.system.groupRoll.aidingCharacters[memberKey].successful;
const newValue = Boolean(button.dataset.success === 'true');
this.updatePartyData(
{
[`system.groupRoll.aidingCharacters.${memberKey}.successful`]:
previousValue === newValue ? null : newValue
},
this.getUpdatingParts(button)
);
}
static async #onCancelRoll(_event, _button, options = { confirm: true }) {
this.cancelRoll(options);
}
async cancelRoll(options = { confirm: true }) {
if (options.confirm) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.cancelConfirmTitle')
},
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.cancelConfirmText')
});
if (!confirmed) return;
}
await this.updatePartyData(
{
'system.groupRoll': {
leader: null,
aidingCharacters: _replace({})
}
},
[],
{ render: false }
);
this.close();
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.GroupRoll, action: 'close' }
});
}
static async #finishRoll() {
const totalRoll = this.party.system.groupRoll.leader.roll;
for (const character of Object.values(this.party.system.groupRoll.aidingCharacters)) {
totalRoll.terms.push(new foundry.dice.terms.OperatorTerm({ operator: character.successful ? '+' : '-' }));
totalRoll.terms.push(new foundry.dice.terms.NumericTerm({ number: 1 }));
}
await totalRoll._evaluate();
const systemData = totalRoll.options;
const actor = game.actors.get(this.party.system.groupRoll.leader.id);
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: 'dualityRoll',
user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.title'),
speaker: cls.getSpeaker({ actor }),
system: systemData,
rolls: [JSON.stringify(totalRoll)],
sound: null,
flags: { core: { RollTable: true } }
};
await cls.create(msgData);
const resourceMap = new ResourceUpdateMap(actor);
if (totalRoll.isCritical) {
resourceMap.addResources([
{ key: 'stress', value: -1, total: 1 },
{ key: 'hope', value: 1, total: 1 }
]);
} else if (totalRoll.withHope) {
resourceMap.addResources([{ key: 'hope', value: 1, total: 1 }]);
} else {
resourceMap.addResources([{ key: 'fear', value: 1, total: 1 }]);
}
resourceMap.updateResources();
/* Fin */
this.cancelRoll({ confirm: false });
}
}

View file

@ -38,15 +38,13 @@ export default class ItemTransferDialog extends HandlebarsApplicationMixin(Appli
originActor ??= item?.actor; originActor ??= item?.actor;
const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew; const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew;
const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null; const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null;
const max = item?.system.quantity ?? originActor.system.gold[currency] ?? 0;
return { return {
originActor, originActor,
targetActor, targetActor,
itemImage: item?.img, itemImage: item?.img,
currencyIcon: currencySetting?.icon, currencyIcon: currencySetting?.icon,
max, max: item?.system.quantity ?? originActor.system.gold[currency] ?? 0,
initial: targetActor.system.metadata.quantifiable?.includes(item.type) ? max : 1,
title: item?.name ?? currencySetting?.label title: item?.name ?? currencySetting?.label
}; };
} }

View file

@ -0,0 +1,290 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(message, options = {}) {
super(options);
this.message = message;
this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
const type = message.system.damage[typeKey];
acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
const part = type.parts[partKey];
acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
const dice = part.dice[diceKey];
const activeResults = dice.results.filter(x => x.active);
acc[diceKey] = {
dice: dice.dice,
selectedResults: activeResults.length,
maxSelected: activeResults.length,
results: activeResults.map(x => ({ ...x, selected: true }))
};
return acc;
}, {});
return acc;
}, {});
return acc;
}, {});
}
static DEFAULT_OPTIONS = {
id: 'reroll-dialog',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
window: {
icon: 'fa-solid fa-dice'
},
actions: {
toggleResult: RerollDamageDialog.#toggleResult,
selectRoll: RerollDamageDialog.#selectRoll,
doReroll: RerollDamageDialog.#doReroll,
save: RerollDamageDialog.#save
}
};
/** @override */
static PARTS = {
main: {
id: 'main',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/damage/main.hbs'
},
footer: {
id: 'footer',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
}
};
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.damageTitle');
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
element.addEventListener('change', this.toggleDice.bind(this));
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.damage = this.damage;
context.disabledReroll = !this.getRerollDice().length;
context.saveDisabled = !this.isSelectionDone();
return context;
}
static async #save() {
const update = {
'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
const type = this.damage[typeKey];
let typeTotal = 0;
const messageType = this.message.system.damage[typeKey];
const parts = Object.keys(type).map(partKey => {
const part = type[partKey];
const messagePart = messageType.parts[partKey];
let partTotal = messagePart.modifierTotal;
const dice = Object.keys(part).map(diceKey => {
const dice = part[diceKey];
const total = dice.results.reduce((acc, result) => {
if (result.active) acc += result.result;
return acc;
}, 0);
partTotal += total;
const messageDice = messagePart.dice[diceKey];
return {
...messageDice,
total: total,
results: dice.results.map(x => ({
...x,
hasRerolls: dice.results.length > 1
}))
};
});
typeTotal += partTotal;
return {
...messagePart,
total: partTotal,
dice: dice
};
});
acc[typeKey] = {
...messageType,
total: typeTotal,
parts: parts
};
return acc;
}, {})
};
await this.message.update(update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
await this.close();
}
getRerollDice() {
const rerollDice = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
Object.keys(dice.results).forEach(resultKey => {
const result = dice.results[resultKey];
if (result.toReroll) {
rerollDice.push({
...result,
dice: dice.dice,
type: typeKey,
part: partKey,
dice: diceKey,
result: resultKey
});
}
});
});
});
});
return rerollDice;
}
isSelectionDone() {
const diceFinishedData = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
diceFinishedData.push(selected === dice.maxSelected);
});
});
});
return diceFinishedData.every(x => x);
}
toggleDice(event) {
const target = event.target;
const { type, part, dice } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
toggleDice.toReroll = !allRerolled;
toggleDice.results.forEach(result => {
if (result.active) {
result.toReroll = !allRerolled;
}
});
this.render();
}
static #toggleResult(event) {
event.stopPropagation();
const target = event.target.closest('.to-reroll-result');
const { type, part, dice, result } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const toggleResult = toggleDice.results[result];
toggleResult.toReroll = !toggleResult.toReroll;
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allToReroll = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
toggleDice.toReroll = allToReroll;
this.render();
}
static async #selectRoll(_, button) {
const { type, part, dice, result } = button.dataset;
const diceVal = this.damage[type][part][dice];
const diceResult = diceVal.results[result];
if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
);
}
if (diceResult.active) {
diceVal.toReroll = false;
diceResult.toReroll = false;
}
diceVal.selectedResults += diceResult.active ? -1 : 1;
diceResult.active = !diceResult.active;
this.render();
}
static async #doReroll() {
const toReroll = this.getRerollDice().map(x => {
const { type, part, dice, result } = x;
const diceData = this.damage[type][part][dice].results[result];
return {
...diceData,
dice: this.damage[type][part][dice].dice,
typeKey: type,
partKey: part,
diceKey: dice,
resultsIndex: result
};
});
const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
dice: roll.dice,
options: { appearance: {} }
};
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
}
toReroll.forEach((data, index) => {
const { typeKey, partKey, diceKey, resultsIndex } = data;
const rerolledDice = roll.dice[index];
const dice = this.damage[typeKey][partKey][diceKey];
dice.toReroll = false;
dice.results[resultsIndex].active = false;
dice.results[resultsIndex].discarded = true;
dice.results[resultsIndex].toReroll = false;
dice.results.splice(dice.results.length, 0, {
...rerolledDice.results[0],
toReroll: false,
selected: true
});
});
this.render();
}
}

View file

@ -0,0 +1,279 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(message, options = {}) {
super(options);
this.message = message;
this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
const type = message.system.damage[typeKey];
acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
const part = type.parts[partKey];
acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
const dice = part.dice[diceKey];
const activeResults = dice.results.filter(x => x.active);
acc[diceKey] = {
dice: dice.dice,
selectedResults: activeResults.length,
maxSelected: activeResults.length,
results: activeResults.map(x => ({ ...x, selected: true }))
};
return acc;
}, {});
return acc;
}, {});
return acc;
}, {});
}
static DEFAULT_OPTIONS = {
id: 'reroll-dialog',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
window: {
icon: 'fa-solid fa-dice'
},
actions: {
toggleResult: RerollDialog.#toggleResult,
selectRoll: RerollDialog.#selectRoll,
doReroll: RerollDialog.#doReroll,
save: RerollDialog.#save
}
};
/** @override */
static PARTS = {
main: {
id: 'main',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/main.hbs'
},
footer: {
id: 'footer',
template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
}
};
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.title');
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
element.addEventListener('change', this.toggleDice.bind(this));
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.damage = this.damage;
context.disabledReroll = !this.getRerollDice().length;
context.saveDisabled = !this.isSelectionDone();
return context;
}
static async #save() {
const update = {
'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
const type = this.damage[typeKey];
let typeTotal = 0;
const messageType = this.message.system.damage[typeKey];
const parts = Object.keys(type).map(partKey => {
const part = type[partKey];
const messagePart = messageType.parts[partKey];
let partTotal = messagePart.modifierTotal;
const dice = Object.keys(part).map(diceKey => {
const dice = part[diceKey];
const total = dice.results.reduce((acc, result) => {
if (result.active) acc += result.result;
return acc;
}, 0);
partTotal += total;
const messageDice = messagePart.dice[diceKey];
return {
...messageDice,
total: total,
results: dice.results.map(x => ({
...x,
hasRerolls: dice.results.length > 1
}))
};
});
typeTotal += partTotal;
return {
...messagePart,
total: partTotal,
dice: dice
};
});
acc[typeKey] = {
...messageType,
total: typeTotal,
parts: parts
};
return acc;
}, {})
};
await this.message.update(update);
await this.close();
}
getRerollDice() {
const rerollDice = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
Object.keys(dice.results).forEach(resultKey => {
const result = dice.results[resultKey];
if (result.toReroll) {
rerollDice.push({
...result,
dice: dice.dice,
type: typeKey,
part: partKey,
dice: diceKey,
result: resultKey
});
}
});
});
});
});
return rerollDice;
}
isSelectionDone() {
const diceFinishedData = [];
Object.keys(this.damage).forEach(typeKey => {
const type = this.damage[typeKey];
Object.keys(type).forEach(partKey => {
const part = type[partKey];
Object.keys(part).forEach(diceKey => {
const dice = part[diceKey];
const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
diceFinishedData.push(selected === dice.maxSelected);
});
});
});
return diceFinishedData.every(x => x);
}
toggleDice(event) {
const target = event.target;
const { type, part, dice } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
toggleDice.toReroll = !allRerolled;
toggleDice.results.forEach(result => {
if (result.active) {
result.toReroll = !allRerolled;
}
});
this.render();
}
static #toggleResult(event) {
event.stopPropagation();
const target = event.target.closest('.to-reroll-result');
const { type, part, dice, result } = target.dataset;
const toggleDice = this.damage[type][part][dice];
const toggleResult = toggleDice.results[result];
toggleResult.toReroll = !toggleResult.toReroll;
const existingDiceRerolls = this.getRerollDice().filter(
x => x.type === type && x.part === part && x.dice === dice
);
const allToReroll = existingDiceRerolls.length === toggleDice.results.length;
toggleDice.toReroll = allToReroll;
this.render();
}
static async #selectRoll(_, button) {
const { type, part, dice, result } = button.dataset;
const diceVal = this.damage[type][part][dice];
const diceResult = diceVal.results[result];
if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
);
}
if (diceResult.active) {
diceVal.toReroll = false;
diceResult.toReroll = false;
}
diceVal.selectedResults += diceResult.active ? -1 : 1;
diceResult.active = !diceResult.active;
this.render();
}
static async #doReroll() {
const toReroll = this.getRerollDice().map(x => {
const { type, part, dice, result } = x;
const diceData = this.damage[type][part][dice].results[result];
return {
...diceData,
dice: this.damage[type][part][dice].dice,
typeKey: type,
partKey: part,
diceKey: dice,
resultsIndex: result
};
});
const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
dice: roll.dice,
options: { appearance: {} }
};
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
}
toReroll.forEach((data, index) => {
const { typeKey, partKey, diceKey, resultsIndex } = data;
const rerolledDice = roll.dice[index];
const dice = this.damage[typeKey][partKey][diceKey];
dice.toReroll = false;
dice.results[resultsIndex].active = false;
dice.results[resultsIndex].discarded = true;
dice.results[resultsIndex].toReroll = false;
dice.results.splice(dice.results.length, 0, {
...rerolledDice.results[0],
toReroll: false,
selected: true
});
});
this.render();
}
}

View file

@ -1,4 +1,4 @@
import { itemAbleRollParse, triggerChatRollFx } from '../../helpers/utils.mjs'; import { itemAbleRollParse } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@ -69,7 +69,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item); const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}${this.item.system.resource.dieFaces}`; const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate(); const roll = await new Roll(diceFormula).evaluate();
await triggerChatRollFx([roll]); if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false })); this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
this.resetUsed = true; this.resetUsed = true;

File diff suppressed because it is too large Load diff

View file

@ -50,11 +50,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
).showGenericStatusEffects; ).showGenericStatusEffects;
context.genericStatusEffects = useGeneric context.genericStatusEffects = useGeneric
? Object.keys(context.statusEffects).reduce((acc, key) => { ? Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key]; const effect = context.statusEffects[key];
if (!effect.systemEffect) acc[key] = effect; if (!effect.systemEffect) acc[key] = effect;
return acc; return acc;
}, {}) }, {})
: null; : null;
context.hasCompanion = this.actor.system.companion; context.hasCompanion = this.actor.system.companion;
@ -68,11 +68,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
const warning = const warning =
tokensWithoutActors.length === 1 tokensWithoutActors.length === 1
? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', { ? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', {
name: tokensWithoutActors[0].name name: tokensWithoutActors[0].name
}) })
: game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', { : game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', {
names: tokensWithoutActors.map(x => x.name).join(', ') names: tokensWithoutActors.map(x => x.name).join(', ')
}); });
const tokens = canvas.tokens.controlled const tokens = canvas.tokens.controlled
.filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type)) .filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
@ -122,16 +122,15 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
async toggleClowncar(actors) { async toggleClowncar(actors) {
const animationDuration = 500; const animationDuration = 500;
const scene = game.scenes.get(game.user.viewedScene); const activeTokens = actors.flatMap(member => member.getActiveTokens());
/* getDependentTokens returns already removed tokens with id = null. Need to filter that until it's potentially fixed from Foundry */
const activeTokens = actors.flatMap(member =>
member.getDependentTokens({ scenes: scene }).filter(x => x._id && !x._destroyed)
);
const { x: actorX, y: actorY } = this.document; const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) { if (activeTokens.length > 0) {
for (let token of activeTokens) { for (let token of activeTokens) {
await token.update({ x: actorX, y: actorY, alpha: 0 }, { animation: { duration: animationDuration } }); await token.document.update(
setTimeout(() => token.delete(), animationDuration); { x: actorX, y: actorY, alpha: 0 },
{ animation: { duration: animationDuration } }
);
setTimeout(() => token.document.delete(), animationDuration);
} }
} else { } else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene); const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
@ -141,16 +140,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
tokenData.push(data.toObject()); tokenData.push(data.toObject());
} }
const viewedLevel = game.scenes.get(game.user.viewedScene).levels.get(game.user.viewedLevel);
const elevation = this.actor.token?.elevation ?? viewedLevel.elevation.bottom;
const newTokens = await activeScene.createEmbeddedDocuments( const newTokens = await activeScene.createEmbeddedDocuments(
'Token', 'Token',
tokenData.map(tokenData => ({ tokenData.map(tokenData => ({
...tokenData, ...tokenData,
alpha: 0, alpha: 0,
level: viewedLevel,
elevation: elevation,
x: actorX, x: actorX,
y: actorY y: actorY
})) }))
@ -174,8 +168,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
nonZeroIndex === sideMiddle nonZeroIndex === sideMiddle
? 0 ? 0
: nonZeroIndex < sideMiddle : nonZeroIndex < sideMiddle
? -nonZeroIndex ? -nonZeroIndex
: nonZeroIndex - sideMiddle; : nonZeroIndex - sideMiddle;
return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient }; return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient };
} else if (index < side + inbetween) { } else if (index < side + inbetween) {
const inbetweenIndex = nonZeroIndex - side; const inbetweenIndex = nonZeroIndex - side;
@ -183,8 +177,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
inbetweenIndex === inbetweenMiddle inbetweenIndex === inbetweenMiddle
? 0 ? 0
: inbetweenIndex < inbetweenMiddle : inbetweenIndex < inbetweenMiddle
? -inbetweenIndex ? -inbetweenIndex
: inbetweenIndex - inbetweenMiddle; : inbetweenIndex - inbetweenMiddle;
return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance }; return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance };
} else if (index < 2 * side + inbetween) { } else if (index < 2 * side + inbetween) {
const sideIndex = nonZeroIndex - side - inbetween; const sideIndex = nonZeroIndex - side - inbetween;
@ -192,8 +186,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
sideIndex === sideMiddle sideIndex === sideMiddle
? 0 ? 0
: sideIndex < sideMiddle : sideIndex < sideMiddle
? sideIndex ? sideIndex
: -(sideIndex - sideMiddle); : -(sideIndex - sideMiddle);
return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient }; return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient };
} else { } else {
const inbetweenIndex = nonZeroIndex - 2 * side - inbetween; const inbetweenIndex = nonZeroIndex - 2 * side - inbetween;
@ -201,8 +195,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
inbetweenIndex === inbetweenMiddle inbetweenIndex === inbetweenMiddle
? 0 ? 0
: inbetweenIndex < inbetweenMiddle : inbetweenIndex < inbetweenMiddle
? inbetweenIndex ? inbetweenIndex
: -(inbetweenIndex - inbetweenMiddle); : -(inbetweenIndex - inbetweenMiddle);
return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance }; return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance };
} }
}) })

View file

@ -156,7 +156,6 @@ export default class DhCharacterLevelUp extends LevelUpBase {
if (multiclasses?.[0]) { if (multiclasses?.[0]) {
const data = multiclasses[0]; const data = multiclasses[0];
const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {}; const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {};
const subclasses = (await multiclass?.system?.fetchSubclasses()) ?? [];
context.multiclass = { context.multiclass = {
...data, ...data,
@ -176,12 +175,13 @@ export default class DhCharacterLevelUp extends LevelUpBase {
alreadySelected alreadySelected
}; };
}) ?? [], }) ?? [],
subclasses: subclasses.map(subclass => ({ subclasses:
...subclass, multiclass?.system?.subclasses.map(subclass => ({
uuid: subclass.uuid, ...subclass,
selected: data.secondaryData.subclass === subclass.uuid, uuid: subclass.uuid,
disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid selected: data.secondaryData.subclass === subclass.uuid,
})), disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid
})) ?? [],
compendium: 'classes', compendium: 'classes',
limit: 1 limit: 1
}; };
@ -210,9 +210,9 @@ export default class DhCharacterLevelUp extends LevelUpBase {
achievementExperiences = level.achievements.experiences achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => { ? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience); if (experience.name) acc.push(experience);
return acc; return acc;
}, []) }, [])
: []; : [];
} }
@ -315,15 +315,15 @@ export default class DhCharacterLevelUp extends LevelUpBase {
: null; : null;
advancement[choiceKey] = multiclassItem advancement[choiceKey] = multiclassItem
? { ? {
...multiclassItem.toObject(), ...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain domain: checkbox.secondaryData.domain
? game.i18n.localize( ? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label .label
) )
: null, : null,
subclass: subclass ? subclass.name : null subclass: subclass ? subclass.name : null
} }
: {}; : {};
break; break;
} }

View file

@ -67,7 +67,7 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
break; break;
case 'summary': case 'summary':
const levelKeys = Object.keys(this.levelup.levels); const levelKeys = Object.keys(this.levelup.levels);
const actorDamageDice = this.actor.system.attack.damage.parts.hitPoints.value.dice; const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice;
const actorRange = this.actor.system.attack.range; const actorRange = this.actor.system.attack.range;
let achievementExperiences = []; let achievementExperiences = [];
@ -77,9 +77,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
achievementExperiences = level.achievements.experiences achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => { ? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience); if (experience.name) acc.push(experience);
return acc; return acc;
}, []) }, [])
: []; : [];
} }
context.achievements = { context.achievements = {
@ -155,15 +155,15 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
vicious: { vicious: {
damage: advancement.vicious?.damage damage: advancement.vicious?.damage
? { ? {
old: actorDamageDice, old: actorDamageDice,
new: advancement.vicious.damage new: advancement.vicious.damage
} }
: null, : null,
range: advancement.vicious?.range range: advancement.vicious?.range
? { ? {
old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`), old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`),
new: game.i18n.localize(advancement.vicious.range.label) new: game.i18n.localize(advancement.vicious.range.label)
} }
: null : null
}, },
simple: advancement.simple ?? {} simple: advancement.simple ?? {}

View file

@ -135,6 +135,192 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections }; context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections };
context.showTabs = this.tabGroups.primary !== 'summary'; context.showTabs = this.tabGroups.primary !== 'summary';
break; break;
const actorArmor = this.actor.system.armor;
const levelKeys = Object.keys(this.levelup.levels);
let achivementProficiency = 0;
const achievementCards = [];
let achievementExperiences = [];
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
achivementProficiency += level.achievements.proficiency ?? 0;
const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null;
if (cards) {
for (var card of cards) {
const itemCard = await foundry.utils.fromUuid(card.uuid);
achievementCards.push(itemCard);
}
}
achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience);
return acc;
}, [])
: [];
}
context.achievements = {
proficiency: {
old: this.actor.system.proficiency,
new: this.actor.system.proficiency + achivementProficiency,
shown: achivementProficiency > 0
},
damageThresholds: {
major: {
old: this.actor.system.damageThresholds.major,
new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel
},
severe: {
old: this.actor.system.damageThresholds.severe,
new:
this.actor.system.damageThresholds.severe +
(actorArmor
? changedActorLevel - currentActorLevel
: (changedActorLevel - currentActorLevel) * 2)
},
unarmored: !actorArmor
},
domainCards: {
values: achievementCards,
shown: achievementCards.length > 0
},
experiences: {
values: achievementExperiences
}
};
const advancement = {};
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
for (var choiceKey of Object.keys(level.choices)) {
const choice = level.choices[choiceKey];
for (var checkbox of Object.values(choice)) {
switch (choiceKey) {
case 'proficiency':
case 'hitPoint':
case 'stress':
case 'evasion':
advancement[choiceKey] = advancement[choiceKey]
? advancement[choiceKey] + Number(checkbox.value)
: Number(checkbox.value);
break;
case 'trait':
if (!advancement[choiceKey]) advancement[choiceKey] = {};
for (var traitKey of checkbox.data) {
if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0;
advancement[choiceKey][traitKey] += 1;
}
break;
case 'domainCard':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
if (checkbox.data.length === 1) {
const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]);
advancement[choiceKey].push(choiceItem.toObject());
}
break;
case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
const data = checkbox.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(
x => x === data
);
return this.actor.system.experiences[experience]?.description ?? '';
});
advancement[choiceKey].push({ data: data, value: checkbox.value });
break;
case 'subclass':
if (checkbox.data[0]) {
const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
if (!advancement[choiceKey]) advancement[choiceKey] = [];
advancement[choiceKey].push({
...subclassItem.toObject(),
featureLabel: game.i18n.localize(
subclassFeatureLabels[Number(checkbox.secondaryData.featureState)]
)
});
}
break;
case 'multiclass':
const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]);
const subclass = multiclassItem
? await foundry.utils.fromUuid(checkbox.secondaryData.subclass)
: null;
advancement[choiceKey] = multiclassItem
? {
...multiclassItem.toObject(),
domain: checkbox.secondaryData.domain
? game.i18n.localize(
CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain]
.label
)
: null,
subclass: subclass ? subclass.name : null
}
: {};
break;
}
}
}
}
context.advancements = {
statistics: {
proficiency: {
old: context.achievements.proficiency.new,
new: context.achievements.proficiency.new + (advancement.proficiency ?? 0)
},
hitPoints: {
old: this.actor.system.resources.hitPoints.max,
new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0)
},
stress: {
old: this.actor.system.resources.stress.max,
new: this.actor.system.resources.stress.max + (advancement.stress ?? 0)
},
evasion: {
old: this.actor.system.evasion,
new: this.actor.system.evasion + (advancement.evasion ?? 0)
}
},
traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => {
if (advancement.trait?.[traitKey]) {
if (!acc) acc = {};
acc[traitKey] = {
label: game.i18n.localize(abilities[traitKey].label),
old: this.actor.system.traits[traitKey].value,
new: this.actor.system.traits[traitKey].value + advancement.trait[traitKey]
};
}
return acc;
}, null),
domainCards: advancement.domainCard ?? [],
experiences:
advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ??
[],
multiclass: advancement.multiclass,
subclass: advancement.subclass
};
context.advancements.statistics.proficiency.shown =
context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old;
context.advancements.statistics.hitPoints.shown =
context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old;
context.advancements.statistics.stress.shown =
context.advancements.statistics.stress.new > context.advancements.statistics.stress.old;
context.advancements.statistics.evasion.shown =
context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old;
context.advancements.statistics.shown =
context.advancements.statistics.proficiency.shown ||
context.advancements.statistics.hitPoints.shown ||
context.advancements.statistics.stress.shown ||
context.advancements.statistics.evasion.shown;
break;
} }
return context; return context;
@ -172,14 +358,14 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) { if (experienceIncreaseTagify) {
const allExperiences = { const allExperiences = {
...this.actor.system.experiences,
...Object.values(this.levelup.levels).reduce((acc, level) => { ...Object.values(this.levelup.levels).reduce((acc, level) => {
for (const key of Object.keys(level.achievements.experiences)) { for (const key of Object.keys(level.achievements.experiences)) {
acc[key] = level.achievements.experiences[key]; acc[key] = level.achievements.experiences[key];
} }
return acc; return acc;
}, {}), }, {})
...this.actor.system.experiences
}; };
tagifyElement( tagifyElement(
experienceIncreaseTagify, experienceIncreaseTagify,
@ -198,35 +384,37 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this._dragDrop.forEach(d => d.bind(htmlElement)); this._dragDrop.forEach(d => d.bind(htmlElement));
} }
tagifyUpdate = type => async (_, { option, removed }) => { tagifyUpdate =
const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( type =>
(acc, choiceKey) => { async (_, { option, removed }) => {
const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce(
Object.keys(choice).forEach(checkboxNr => { (acc, choiceKey) => {
const checkbox = choice[checkboxNr]; const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey];
if ( Object.keys(choice).forEach(checkboxNr => {
choiceKey === type && const checkbox = choice[checkboxNr];
(removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount) if (
) { choiceKey === type &&
acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount)
} ) {
}); acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`;
}
});
return acc; return acc;
}, },
null null
); );
if (!updatePath) { if (!updatePath) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft'));
return; return;
} }
const currentData = foundry.utils.getProperty(this.levelup, updatePath); const currentData = foundry.utils.getProperty(this.levelup, updatePath);
const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option];
await this.levelup.updateSource({ [updatePath]: updatedData }); await this.levelup.updateSource({ [updatePath]: updatedData });
this.render(); this.render();
}; };
static async updateForm(event, _, formData) { static async updateForm(event, _, formData) {
const { levelup } = foundry.utils.expandObject(formData.object); const { levelup } = foundry.utils.expandObject(formData.object);
@ -289,7 +477,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const secondaryData = Object.keys( const secondaryData = Object.keys(
foundry.utils.getProperty(this.levelup, `${target.dataset.path}.secondaryData`) foundry.utils.getProperty(this.levelup, `${target.dataset.path}.secondaryData`)
).reduce((acc, key) => { ).reduce((acc, key) => {
acc[key] = _del; acc[`-=${key}`] = null;
return acc; return acc;
}, {}); }, {});
await this.levelup.updateSource({ await this.levelup.updateSource({
@ -323,9 +511,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`); const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`);
if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) { if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) {
// Simple handling that doesn't cover potential Custom LevelTiers. // Simple handling that doesn't cover potential Custom LevelTiers.
update[`${basePath}.${button.dataset.option}`] = _del; update[`${basePath}.-=${button.dataset.option}`] = null;
} else { } else {
update[`${basePath}.${button.dataset.option}.${button.dataset.checkboxNr}`] = _del; update[`${basePath}.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = null;
} }
} else { } else {
if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) { if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) {
@ -405,10 +593,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const domainCards = this.levelup.levels[this.levelup.currentLevel].achievements.domainCards; const domainCards = this.levelup.levels[this.levelup.currentLevel].achievements.domainCards;
const illegalDomainCards = option.secondaryData.domain const illegalDomainCards = option.secondaryData.domain
? Object.keys(domainCards) ? Object.keys(domainCards)
.map(key => ({ ...domainCards[key], key })) .map(key => ({ ...domainCards[key], key }))
.filter( .filter(
x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain
) )
: []; : [];
illegalDomainCards.forEach(card => { illegalDomainCards.forEach(card => {
update[`levels.${this.levelup.currentLevel}.achievements.domainCards.${card.key}.uuid`] = null; update[`levels.${this.levelup.currentLevel}.achievements.domainCards.${card.key}.uuid`] = null;

View file

@ -62,15 +62,7 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
} }
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'Level') {
const level = await foundry.documents.Level.fromDropData(data);
if (level?.parent === this.document) return this._onSortLevel(event, level);
return;
}
const item = await foundry.utils.fromUuid(data.uuid); const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') { if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
let sceneUuid = data.uuid; let sceneUuid = data.uuid;
@ -120,6 +112,12 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
foundry.utils.fromUuidSync(x) foundry.utils.fromUuidSync(x)
); );
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
}
}
super._processSubmitData(event, form, submitData, options); super._processSubmitData(event, form, submitData, options);
} }
} }

View file

@ -122,7 +122,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
type: 'button', type: 'button',
action: 'reset', action: 'reset',
icon: 'fa-solid fa-arrow-rotate-left', icon: 'fa-solid fa-arrow-rotate-left',
label: game.i18n.localize('SETTINGS.UI.ACTIONS.Reset') label: game.i18n.localize('Reset')
}, },
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: game.i18n.localize('EDITOR.Save') } { type: 'submit', icon: 'fa-solid fa-floppy-disk', label: game.i18n.localize('EDITOR.Save') }
]; ];

View file

@ -1,5 +1,6 @@
import { DhHomebrew } from '../../data/settings/_module.mjs'; import { DhHomebrew } from '../../data/settings/_module.mjs';
import { Resource } from '../../data/settings/Homebrew.mjs'; import { Resource } from '../../data/settings/Homebrew.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -111,7 +112,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
switch (partId) { switch (partId) {
case 'domains': case 'domains':
const selectedDomain = this.settings.domains[this.selected.domain] ?? null; const selectedDomain = this.selected.domain ? this.settings.domains[this.selected.domain] : null;
const enrichedDescription = selectedDomain const enrichedDescription = selectedDomain
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description) ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description)
: null; : null;
@ -251,8 +252,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const configTitle = isDowntime const configTitle = isDowntime
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove') ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove')
: type === 'armorFeatures' : type === 'armorFeatures'
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature') ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature')
: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature'); : game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature');
const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure( const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure(
configTitle, configTitle,
@ -297,7 +298,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const isDowntime = ['shortRest', 'longRest'].includes(type); const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`; const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({ await this.settings.updateSource({
[`${path}.${id}`]: _del [`${path}.-=${id}`]: null
}); });
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
@ -321,7 +322,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const fields = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).schema.fields; const fields = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).schema.fields;
const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => { const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => {
acc[key] = _del; acc[`-=${key}`] = null;
return acc; return acc;
}, {}); }, {});
@ -381,7 +382,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
[`itemFeatures.${target.dataset.type}`]: Object.keys( [`itemFeatures.${target.dataset.type}`]: Object.keys(
this.settings.itemFeatures[target.dataset.type] this.settings.itemFeatures[target.dataset.type]
).reduce((acc, key) => { ).reduce((acc, key) => {
acc[key] = _del; acc[`-=${key}`] = null;
return acc; return acc;
}, {}) }, {})
@ -402,12 +403,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const domainName = button.form.elements.domainName.value; const domainName = button.form.elements.domainName.value;
if (!domainName) return; if (!domainName) return;
const newSlug = domainName.slugify(); const newSlug = slugify(domainName);
const existingDomains = [ const existingDomains = [
...Object.values(this.settings.domains), ...Object.values(this.settings.domains),
...Object.values(CONFIG.DH.DOMAIN.domains) ...Object.values(CONFIG.DH.DOMAIN.domains)
]; ];
if (existingDomains.find(x => x.id === newSlug)) { if (existingDomains.find(x => slugify(game.i18n.localize(x.label)) === newSlug)) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.domains.duplicateDomain'));
return; return;
} }
@ -454,12 +455,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
if (!confirmed) return; if (!confirmed) return;
await this.settings.updateSource({ await this.settings.updateSource({
[`domains.${this.selected.domain}`]: _del [`domains.-=${this.selected.domain}`]: null
}); });
const currentSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew); const currentSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
if (currentSettings.domains[this.selected.domain]) { if (currentSettings.domains[this.selected.domain]) {
await currentSettings.updateSource({ [`domains.${this.selected.domain}`]: _del }); await currentSettings.updateSource({ [`domains.-=${this.selected.domain}`]: null });
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, currentSettings); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, currentSettings);
} }
@ -506,7 +507,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
static async deleteAdversaryType(_, target) { static async deleteAdversaryType(_, target) {
const { key } = target.dataset; const { key } = target.dataset;
await this.settings.updateSource({ [`adversaryTypes.${key}`]: _del }); await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType; this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
this.render(); this.render();
@ -528,7 +529,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const identifier = button.form.elements.identifier.value; const identifier = button.form.elements.identifier.value;
if (!identifier) return; if (!identifier) return;
const sluggedIdentifier = identifier.slugify(); const sluggedIdentifier = slugify(identifier);
await this.settings.updateSource({ await this.settings.updateSource({
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier) [`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)
@ -562,7 +563,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
const { actorType, resourceKey } = target.dataset; const { actorType, resourceKey } = target.dataset;
await this.settings.updateSource({ await this.settings.updateSource({
[`resources.${actorType}.resources.${resourceKey}`]: _del [`resources.${actorType}.resources.-=${resourceKey}`]: null
}); });
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());

View file

@ -2,8 +2,8 @@ export { default as ActionConfig } from './action-config.mjs';
export { default as ActionSettingsConfig } from './action-settings-config.mjs'; export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs'; export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs';
export { default as NPCSettings } from './npc-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs';
export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';

View file

@ -1,4 +1,3 @@
import { getUnusedDamageTypes } from '../../helpers/utils.mjs';
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs'; import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api; const { ApplicationV2 } = foundry.applications.api;
@ -36,9 +35,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
editDoc: this.editDoc, editDoc: this.editDoc,
addTrigger: this.addTrigger, addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger, removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger, expandTrigger: this.expandTrigger
addBeastformTraitBonus: this.addBeastformTraitBonus,
removeBeastformTraitBonus: this.removeBeastformTraitBonus
}, },
form: { form: {
handler: this.updateForm, handler: this.updateForm,
@ -107,7 +104,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
} }
}; };
static CLEAN_ARRAYS = ['cost', 'effects', 'summon']; static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
_getTabs(tabs) { _getTabs(tabs) {
for (const v of Object.values(tabs)) { for (const v of Object.values(tabs)) {
@ -156,13 +153,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.openSection = this.openSection; context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH; context.config = CONFIG.DH;
if (this.action.damage) { if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length; context.hasBaseDamage = !!this.action.parent.attack;
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
}
context.costOptions = this.getCostOptions(); context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions(); context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this); context.disableOption = this.disableOption.bind(this);
@ -204,7 +196,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
}; };
} }
if (this.action.parent.metadata?.isInventoryItem) { if (this.action.parent.metadata?.isQuantifiable) {
options.quantity = { options.quantity = {
label: 'DAGGERHEART.GENERAL.itemQuantity', label: 'DAGGERHEART.GENERAL.itemQuantity',
group: 'Global' group: 'Global'
@ -264,9 +256,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
key = event.target.closest('[data-key]').dataset.key; key = event.target.closest('[data-key]').dataset.key;
if (!this.action[key]) return; if (!this.action[key]) return;
const value = key === 'areas' ? { name: this.action.item.name } : {}; data[key].push(this.action.defaultValues[key] ?? {});
data[key].push(this.action.defaultValues[key] ?? value);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
} }
@ -301,64 +291,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
static addDamage(_event) { static addDamage(_event) {
if (!this.action.damage.parts) return; if (!this.action.damage.parts) return;
const data = this.action.toObject(),
const choices = getUnusedDamageTypes(this.action._source.damage.parts); part = {};
const content = new foundry.data.fields.StringField({ if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
label: game.i18n.localize('Damage Type'), data.damage.parts.push(part);
choices, this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
required: true
}).toFormGroup(
{},
{
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}
).outerHTML;
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue();
part.applyTo = type;
if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial;
data.damage.parts[type] = part;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
};
const typeDialog = new foundry.applications.api.DialogV2({
buttons: [
foundry.utils.mergeObject(
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true
},
{ callback: callback }
)
],
content: content,
rejectClose: false,
modal: false,
window: {
title: game.i18n.localize('Add Damage')
},
position: { width: 300 }
});
typeDialog.render(true);
} }
static removeDamage(_event, button) { static removeDamage(_event, button) {
if (!this.action.damage.parts) return; if (!this.action.damage.parts) return;
const data = this.action.toObject(); const data = this.action.toObject(),
const key = button.dataset.key; index = button.dataset.index;
delete data.damage.parts[key]; data.damage.parts.splice(index, 1);
data.damage.parts[`${key}`] = _del;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
} }
@ -416,21 +360,6 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
} }
} }
static async addBeastformTraitBonus() {
const data = this.action.toObject();
data.beastform.modifications.traitBonuses = [
...data.beastform.modifications.traitBonuses,
this.action.schema.fields.beastform.fields.modifications.fields.traitBonuses.element.getInitialValue()
];
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async removeBeastformTraitBonus(_event, button) {
const data = this.action.toObject();
data.beastform.modifications.traitBonuses.splice(button.dataset.index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
updateSummonCount(event) { updateSummonCount(event) {
event.stopPropagation(); event.stopPropagation();
const wrapper = event.target.closest('.summon-count-wrapper'); const wrapper = event.target.closest('.summon-count-wrapper');

View file

@ -19,19 +19,17 @@ export default class DHActionConfig extends DHActionBaseConfig {
return context; return context;
} }
static async addEffect(event) { static async addEffect(_event) {
const { areaIndex } = event.target.dataset;
if (!this.action.effects) return; if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)();
const data = this.action.toObject(); const data = this.action.toObject();
const created = await this.action.item.createEmbeddedDocuments('ActiveEffect', [ const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
game.system.api.data.activeEffects.BaseEffect.getDefaultObject({ transfer: false }) render: false
]); });
data.effects.push({ _id: created._id });
if (areaIndex !== undefined) data.areas[areaIndex].effects.push(created[0]._id);
else data.effects.push({ _id: created[0]._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created[0]._id).sheet.render(true); this.action.item.effects.get(created._id).sheet.render(true);
} }
/** /**
@ -54,19 +52,9 @@ export default class DHActionConfig extends DHActionBaseConfig {
static removeEffect(event, button) { static removeEffect(event, button) {
if (!this.action.effects) return; if (!this.action.effects) return;
const index = button.dataset.index,
const { areaIndex, index } = button.dataset;
let effectId = null;
if (areaIndex !== undefined) {
effectId = this.action.areas[areaIndex].effects[index];
const data = this.action.toObject();
data.areas[areaIndex].effects.splice(index, 1);
this.constructor.updateForm.call(this, null, null, { object: foundry.utils.flattenObject(data) });
} else {
effectId = this.action.effects[index]._id; effectId = this.action.effects[index]._id;
this.constructor.removeElement.call(this, event, button); this.constructor.removeElement.bind(this)(event, button);
}
this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]); this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]);
} }

View file

@ -31,35 +31,21 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
} }
static async addEffect(_event) { static async addEffect(_event) {
const { areaIndex } = event.target.dataset;
if (!this.action.effects) return; if (!this.action.effects) return;
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject();
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject({ transfer: false });
const data = this.action.toObject(); const data = this.action.toObject();
this.sheetUpdate(data, effectData); this.sheetUpdate(data, effectData);
this.effects = [...this.effects, effectData]; this.effects = [...this.effects, effectData];
data.effects.push({ _id: effectData.id });
if (areaIndex !== undefined) data.areas[areaIndex].effects.push(effectData.id);
else data.effects.push({ _id: effectData.id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
} }
static removeEffect(event, button) { static removeEffect(event, button) {
if (!this.action.effects) return; if (!this.action.effects) return;
const { areaIndex, index } = button.dataset; const index = button.dataset.index,
let effectId = null;
if (areaIndex !== undefined) {
effectId = this.action.areas[areaIndex].effects[index];
const data = this.action.toObject();
data.areas[areaIndex].effects.splice(index, 1);
this.constructor.updateForm.call(this, null, null, { object: foundry.utils.flattenObject(data) });
} else {
effectId = this.action.effects[index]._id; effectId = this.action.effects[index]._id;
this.constructor.removeElement.call(this, event, button); this.constructor.removeElement.bind(this)(event, button);
}
this.sheetUpdate( this.sheetUpdate(
this.action.toObject(), this.action.toObject(),
this.effects.find(x => x.id === effectId), this.effects.find(x => x.id === effectId),
@ -69,7 +55,7 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig {
static async editEffect(event) { static async editEffect(event) {
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId; const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
const updatedEffect = await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting( const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(
this.getEffectDetails(id) this.getEffectDetails(id)
); );
if (!updatedEffect) return; if (!updatedEffect) return;

View file

@ -18,7 +18,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: { changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'],
scrollable: ['ol[data-changes]'] scrollable: ['ol[data-changes]']
}, },
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
@ -41,7 +40,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]} * @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
*/ */
static getChangeChoices() { static getChangeChoices() {
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty', 'DhNPC']; const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
const getAllLeaves = (root, group, parentPath = '') => { const getAllLeaves = (root, group, parentPath = '') => {
const leaves = []; const leaves = [];
@ -150,18 +149,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
minLength: 0 minLength: 0
}); });
}); });
htmlElement
.querySelector('.stacking-change-checkbox')
?.addEventListener('change', this.stackingChangeToggle.bind(this));
htmlElement
.querySelector('.armor-change-checkbox')
?.addEventListener('change', this.armorChangeToggle.bind(this));
htmlElement
.querySelector('.armor-damage-thresholds-checkbox')
?.addEventListener('change', this.armorDamageThresholdToggle.bind(this));
} }
async _prepareContext(options) { async _prepareContext(options) {
@ -175,7 +162,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
const partContext = await super._preparePartContext(partId, context); const partContext = await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
case 'details': case 'details':
partContext.isItemEffect = partContext.isItemEffect || this.options.isSetting;
const useGeneric = game.settings.get( const useGeneric = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance CONFIG.DH.SETTINGS.gameSettings.appearance
@ -187,166 +173,8 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
})); }));
} }
break; break;
case 'settings':
const groups = {
time: _loc('EFFECT.DURATION.UNITS.GROUPS.time'),
combat: _loc('EFFECT.DURATION.UNITS.GROUPS.combat')
};
partContext.durationUnits = CONST.ACTIVE_EFFECT_DURATION_UNITS.map(value => ({
value,
label: _loc(`EFFECT.DURATION.UNITS.${value}`),
group: CONST.ACTIVE_EFFECT_TIME_DURATION_UNITS.includes(value) ? groups.time : groups.combat
}));
break;
case 'changes':
const singleTypes = ['armor'];
const typedChanges = context.source.changes.reduce((acc, change, index) => {
if (singleTypes.includes(change.type)) {
acc[change.type] = { ...change, index };
}
return acc;
}, {});
partContext.changes = partContext.changes.filter(c => !!c);
partContext.typedChanges = typedChanges;
break;
} }
return partContext; return partContext;
} }
stackingChangeToggle(event) {
const stackingFields = this.document.system.schema.fields.stacking.fields;
const systemData = {
stacking: event.target.checked
? { value: stackingFields.value.initial, max: stackingFields.max.initial }
: null
};
return this.submit({ updateData: { system: systemData } });
}
armorChangeToggle(event) {
if (event.target.checked) {
this.addArmorChange();
} else {
this.removeTypedChange(event.target.dataset.index);
}
}
/* Could be generalised if needed later */
addArmorChange() {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system?.changes ?? {});
changes.push(game.system.api.data.activeEffects.changeTypes.armor.getInitialValue());
return this.submit({ updateData: { system: { changes } } });
}
removeTypedChange(indexString) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system.changes);
const index = Number(indexString);
changes.splice(index, 1);
return this.submit({ updateData: { system: { changes } } });
}
armorDamageThresholdToggle(event) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system?.changes ?? {});
const index = Number(event.target.dataset.index);
if (event.target.checked) {
changes[index].value.damageThresholds = { major: 0, severe: 0 };
} else {
changes[index].value.damageThresholds = null;
}
return this.submit({ updateData: { system: { changes } } });
}
/** @inheritdoc */
_renderChange(context) {
const { change, index, defaultPriority } = context;
if (!(change.type in CONFIG.DH.GENERAL.baseActiveEffectModes)) return null;
const changeTypesSchema = this.document.system.schema.fields.changes.element.types;
const fields = context.fields ?? (changeTypesSchema[change.type] ?? changeTypesSchema.add).fields;
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
Object.assign(
change,
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
}, {})
);
return (
game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.(
change,
index,
defaultPriority
) ??
foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/activeEffect/change.hbs',
{
change,
index,
defaultPriority,
fields,
types: Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, key) => {
r[key] = CONFIG.DH.GENERAL.baseActiveEffectModes[key].label;
return r;
}, {})
}
)
);
}
/** @inheritDoc */
_onChangeForm(_formConfig, event) {
if (foundry.utils.isElementInstanceOf(event.target, 'select') && event.target.name === 'system.duration.type') {
const durationSection = this.element.querySelector('.custom-duration-section');
if (event.target.value === 'custom') durationSection.classList.add('visible');
else durationSection.classList.remove('visible');
const durationDescription = this.element.querySelector('.duration-description');
if (event.target.value === 'temporary') durationDescription.classList.add('visible');
else durationDescription.classList.remove('visible');
}
}
/** @inheritDoc */
_processFormData(event, form, formData) {
const submitData = super._processFormData(event, form, formData);
if (submitData.start && !submitData.start.time) submitData.start.time = '0';
else if (!submitData) submitData.start = null;
return submitData;
}
/** @inheritDoc */
_processSubmitData(event, form, submitData, options) {
if (this.options.isSetting) {
// Settings should update source instead
this.document.updateSource(submitData);
this.render();
} else {
return super._processSubmitData(event, form, submitData, options);
}
}
/** Creates an active effect config for a setting */
static async configureSetting(effect, options = {}) {
const document = new CONFIG.ActiveEffect.documentClass({ ...foundry.utils.duplicate(effect), _id: effect.id });
return new Promise(resolve => {
const app = new this({ document, ...options, isSetting: true });
app.addEventListener(
'close',
() => {
const newEffect = app.document.toObject(true);
newEffect.id = newEffect._id;
delete newEffect._id;
resolve(newEffect);
},
{ once: true }
);
app.render({ force: true });
});
}
} }

View file

@ -95,7 +95,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
}); });
if (!confirmed) return; if (!confirmed) return;
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del }); await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
} }
async _onDragStart(event) { async _onDragStart(event) {
@ -110,7 +110,6 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
} }
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);

View file

@ -101,8 +101,8 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
if (relinkAchievementData.length > 0) { if (relinkAchievementData.length > 0) {
relinkAchievementData.forEach(data => { relinkAchievementData.forEach(data => {
updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.${data.experience}`] = updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.-=${data.experience}`] =
_del; null;
}); });
} else if (relinkSelectionData.length > 0) { } else if (relinkSelectionData.length > 0) {
relinkSelectionData.forEach(data => { relinkSelectionData.forEach(data => {
@ -137,7 +137,7 @@ export default class DHCharacterSettings extends DHBaseActorSettings {
await this.actor.update({ await this.actor.update({
...updates, ...updates,
[`system.experiences.${target.dataset.experience}`]: _del [`system.experiences.-=${target.dataset.experience}`]: null
}); });
} }
} }

View file

@ -117,6 +117,6 @@ export default class DHCompanionSettings extends DHBaseActorSettings {
}); });
if (!confirmed) return; if (!confirmed) return;
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del }); await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
} }
} }

View file

@ -68,9 +68,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
*/ */
static async #addCategory() { static async #addCategory() {
await this.actor.update({ await this.actor.update({
[`system.potentialAdversaries.${foundry.utils.randomID()}`]: { [`system.potentialAdversaries.${foundry.utils.randomID()}.label`]: game.i18n.localize(
label: game.i18n.localize('DAGGERHEART.ACTORS.Environment.newAdversary') 'DAGGERHEART.ACTORS.Environment.newAdversary'
} )
}); });
} }
@ -79,7 +79,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #removeCategory(_, target) { static async #removeCategory(_, target) {
await this.actor.update({ [`system.potentialAdversaries.${target.dataset.categoryId}`]: _del }); await this.actor.update({ [`system.potentialAdversaries.-=${target.dataset.categoryId}`]: null });
} }
/** /**
@ -121,7 +121,6 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
} }
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return; if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return;

View file

@ -1,85 +0,0 @@
import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
export default class DHNPCSettings extends DHBaseActorSettings {
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['npc-settings'],
position: { width: 455, height: 'auto' },
actions: {},
dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features' },
{ dragSelector: '.feature-item', dropSelector: null }
]
};
/**@override */
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
details: {
id: 'details',
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/details.hbs'
},
features: {
id: 'features',
template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs'
}
};
/** @override */
static TABS = {
primary: {
tabs: [{ id: 'details' }, { id: 'features' }],
initial: 'details',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
const featureForms = ['passive', 'action', 'reaction'];
context.features = context.document.system.features.sort((a, b) =>
a.system.featureForm !== b.system.featureForm
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
: a.sort - b.sort
);
return context;
}
/* -------------------------------------------- */
async _onDragStart(event) {
const featureItem = event.currentTarget.closest('.feature-item');
if (featureItem) {
const feature = this.actor.items.get(featureItem.id);
const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true };
event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
}
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
if (data.fromInternal && item.parent?.uuid === this.actor.uuid) {
return;
}
const itemData = item.toObject();
delete itemData._id;
await this.actor.createEmbeddedDocuments('Item', [itemData]);
}
}
}

View file

@ -0,0 +1,223 @@
import autocomplete from 'autocompleter';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(effect) {
super({});
this.effect = foundry.utils.deepClone(effect);
this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'],
tag: 'form',
position: {
width: 560
},
form: {
submitOnChange: false,
closeOnSubmit: false,
handler: SettingActiveEffectConfig.#onSubmit
},
actions: {
editImage: SettingActiveEffectConfig.#editImage,
addChange: SettingActiveEffectConfig.#addChange,
deleteChange: SettingActiveEffectConfig.#deleteChange
}
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
};
static TABS = {
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
/**@inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.source = this.effect;
context.fields = game.system.api.documents.DhActiveEffect.schema.fields;
context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields;
return context;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices;
htmlElement.querySelectorAll('.effect-change-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(changeChoices);
} else {
text = text.toLowerCase();
var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text));
update(suggestions);
}
},
render: function (item, search) {
const label = game.i18n.localize(item.label);
const matchIndex = label.toLowerCase().indexOf(search);
const beforeText = label.slice(0, matchIndex);
const matchText = label.slice(matchIndex, matchIndex + search.length);
const after = label.slice(matchIndex + search.length, label.length);
const element = document.createElement('li');
element.innerHTML =
`${beforeText}${matchText ? `<strong>${matchText}</strong>` : ''}${after}`.replaceAll(
' ',
'&nbsp;'
);
if (item.hint) {
element.dataset.tooltip = game.i18n.localize(item.hint);
}
return element;
},
renderGroup: function (label) {
const itemElement = document.createElement('div');
itemElement.textContent = game.i18n.localize(label);
return itemElement;
},
onSelect: function (item) {
element.value = `system.${item.value}`;
},
click: e => e.fetch(),
customize: function (_input, _inputRect, container) {
container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ;
},
minLength: 0
});
});
}
async _preparePartContext(partId, context) {
if (partId in context.tabs) context.tab = context.tabs[partId];
switch (partId) {
case 'details':
context.statuses = CONFIG.statusEffects.map(s => ({ value: s.id, label: game.i18n.localize(s.name) }));
context.isActorEffect = false;
context.isItemEffect = true;
const useGeneric = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects;
if (!useGeneric) {
context.statuses = [
...context.statuses,
Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
value: status.id,
label: game.i18n.localize(status.name)
}))
];
}
break;
case 'changes':
context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => {
modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`);
return modes;
}, {});
context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES;
break;
}
return context;
}
static async #onSubmit(_event, _form, formData) {
this.data = foundry.utils.expandObject(formData.object);
this.close();
}
/**
* Edit a Document image.
* @this {DocumentSheetV2}
* @type {ApplicationClickAction}
*/
static async #editImage(_event, target) {
if (target.nodeName !== 'IMG') {
throw new Error('The editImage action is available only for IMG elements.');
}
const attr = target.dataset.edit;
const current = foundry.utils.getProperty(this.effect, attr);
const fp = new FilePicker.implementation({
current,
type: 'image',
callback: path => (target.src = path),
position: {
top: this.position.top + 40,
left: this.position.left + 10
}
});
await fp.browse();
}
/**
* Add a new change to the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #addChange() {
const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const updatedChanges = Object.values(changes ?? {});
updatedChanges.push({});
this.effect = { ...rest, changes: updatedChanges };
this.render();
}
/**
* Delete a change from the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #deleteChange(event) {
const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
const updatedChanges = Object.values(submitData.changes);
const row = event.target.closest('li');
const index = Number(row.dataset.index) || 0;
updatedChanges.splice(index, 1);
this.effect = { ...submitData, changes: updatedChanges };
this.render();
}
static async configure(effect, options = {}) {
return new Promise(resolve => {
const app = new this(effect, options);
app.addEventListener('close', () => resolve(app.data), { once: true });
app.render({ force: true });
});
}
}

View file

@ -147,7 +147,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
const effectIndex = this.move.effects.findIndex(x => x.id === id); const effectIndex = this.move.effects.findIndex(x => x.id === id);
const effect = this.move.effects[effectIndex]; const effect = this.move.effects[effectIndex];
const updatedEffect = const updatedEffect =
await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect); await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
if (!updatedEffect) return; if (!updatedEffect) return;
await this.updateMove({ await this.updateMove({
@ -168,8 +168,8 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
updatedEffects = deleteEffect updatedEffects = deleteEffect
? currentEffects.filter(x => x.id !== effectData.id) ? currentEffects.filter(x => x.id !== effectData.id)
: existingEffectIndex === -1 : existingEffectIndex === -1
? [...currentEffects, effectData] ? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData); : currentEffects.with(existingEffectIndex, effectData);
await this.updateMove({ await this.updateMove({
[`${this.movePath}.effects`]: updatedEffects [`${this.movePath}.effects`]: updatedEffects
}); });
@ -206,7 +206,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
} }
}); });
} else { } else {
await this.updateMove({ [`${this.actionsPath}.${target.dataset.id}`]: _del }); await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
} }
this.render(); this.render();
@ -235,9 +235,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return this.hasEffects return this.hasEffects
? tabs ? tabs
: Object.keys(tabs).reduce((acc, key) => { : Object.keys(tabs).reduce((acc, key) => {
if (key !== 'effects') acc[key] = tabs[key]; if (key !== 'effects') acc[key] = tabs[key];
return acc; return acc;
}, {}); }, {});
} }
/** @override */ /** @override */

View file

@ -67,9 +67,9 @@ export default function DHTokenConfigMixin(Base) {
changes.height = tokenSize; changes.height = tokenSize;
} }
// const deletions = { actorId: _del }; const deletions = { '-=actorId': null, '-=actorLink': null };
// const mergeOptions = { inplace: false, performDeletions: true, actorLink: false }; const mergeOptions = { inplace: false, performDeletions: true };
this._preview.updateSource(changes); this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
if (this._preview?.object?.destroyed === false) { if (this._preview?.object?.destroyed === false) {
this._preview.object.initializeSources(); this._preview.object.initializeSources();

View file

@ -2,5 +2,4 @@ export { default as Adversary } from './adversary.mjs';
export { default as Character } from './character.mjs'; export { default as Character } from './character.mjs';
export { default as Companion } from './companion.mjs'; export { default as Companion } from './companion.mjs';
export { default as Environment } from './environment.mjs'; export { default as Environment } from './environment.mjs';
export { default as NPC } from './npc.mjs';
export { default as Party } from './party.mjs'; export { default as Party } from './party.mjs';

View file

@ -31,16 +31,6 @@ export default class AdversarySheet extends DHBaseActorSheet {
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null dropSelector: null
} }
],
contextMenus: [
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
}
] ]
}; };

View file

@ -1,9 +1,10 @@
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
import DhDeathMove from '../../dialogs/deathMove.mjs'; import DhDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -12,6 +13,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['character'], classes: ['character'],
position: { width: 850, height: 800 }, position: { width: 850, height: 800 },
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
actions: { actions: {
toggleVault: CharacterSheet.#toggleVault, toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute, rollAttribute: CharacterSheet.#rollAttribute,
@ -32,8 +35,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
cancelBeastform: CharacterSheet.#cancelBeastform, cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement, toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime, useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty, viewParty: CharacterSheet.#viewParty
toggleArmorMangement: CharacterSheet.#toggleArmorManagement
}, },
window: { window: {
resizable: true, resizable: true,
@ -57,22 +59,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
], ],
contextMenus: [ contextMenus: [
{
handler: CharacterSheet.#getCreationMainContextOptions,
selector: '.character-details [data-action="editDoc"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
},
{ {
handler: CharacterSheet.#getDomainCardContextOptions, handler: CharacterSheet.#getDomainCardContextOptions,
selector: '[data-item-uuid][data-type="domainCard"]', selector: '[data-item-uuid][data-type="domainCard"]',
@ -82,7 +68,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
}, },
{ {
handler: CharacterSheet.#getEquipmentContextOptions, handler: CharacterSheet.#getEquipamentContextOptions,
selector: '[data-item-uuid][data-type="armor"], [data-item-uuid][data-type="weapon"]', selector: '[data-item-uuid][data-type="armor"], [data-item-uuid][data-type="weapon"]',
options: { options: {
parentClassHooks: false, parentClassHooks: false,
@ -184,19 +170,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
return applicationOptions; return applicationOptions;
} }
/** @inheritdoc */
_toggleDisabled(disabled) {
// Overriden to only disable text inputs by default.
// Everything else is done by checking @root.editable in the sheet
const form = this.form;
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
input.disabled = disabled;
}
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
}
/** @inheritDoc */ /** @inheritDoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
@ -228,9 +201,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => { context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
acc[key] = { acc[key] = {
...this.document.system.traits[key], ...this.document.system.traits[key],
label: _loc(CONFIG.DH.ACTOR.abilities[key].label), name: game.i18n.localize(CONFIG.DH.ACTOR.abilities[key].name),
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)), verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x))
isSpellcasting: this.document.system.spellcastModifierTrait?.key === key
}; };
return acc; return acc;
@ -246,11 +218,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.resources.stress.emptyPips = context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
context.equippedItems = sortBy(
this.document.items.filter(i => i.system.equipped && (i.type === 'weapon' || i.usable)),
i => (i.type === 'weapon' ? (i.system.secondary ? 1 : 0) : 2)
);
context.beastformActive = this.document.effects.find(x => x.type === 'beastform'); context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
return context; return context;
@ -338,56 +305,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
/* Context Menu */ /* Context Menu */
/* -------------------------------------------- */ /* -------------------------------------------- */
static #getCreationMainContextOptions() {
/** Returns true if the item is managed by the level up wizard. Such items shouldn't allow things like manual removal */
function isItemWizardManaged(item) {
const actor = item?.actor;
if (!actor) return false;
// If levelup automation is off in general or for this character, all items are unmanaged
// This is disabled until we have proper granted feature removal, for now this feature is to correct errors
// const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
// if (!levelupAuto) return false;
// Core items aren't part of levelup data. TODO: add some way to flag a specific character as no auto leveling
const classPair = actor.system.class;
const coreItems = [actor.system.ancestry, actor.system.community, classPair?.value, classPair?.subclass];
if (coreItems.includes(item)) return true;
const levelups = Object.values(actor.system.levelData?.levelups) ?? [];
const uuid = item.uuid;
const sourceUuid = item._stats.compendiumSource; // on older characters this may be missing
return levelups.some(data => {
if (item.type === 'subclass') {
const selectedSubclasses = data.selections.map(s => s.secondaryData?.subclass).filter(s => !!s);
return sourceUuid
? selectedSubclasses.includes(sourceUuid)
: selectedSubclasses.length && item.system.isMulticlass;
}
const matchesCard = data.achievements.domainCards.some(i => i.itemUuid === uuid);
const matchesSelection = data.selections.some(s => s.itemUuid === uuid);
return matchesCard || matchesSelection;
});
}
return [
{
label: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
visible: target => {
const doc = getDocFromElementSync(target);
return doc?.isOwner && !isItemWizardManaged(doc);
},
onClick: async (event, target) => {
const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete();
else return doc.deleteDialog();
}
}
];
}
/** /**
* Get the set of ContextMenu options for DomainCards. * Get the set of ContextMenu options for DomainCards.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
@ -398,13 +315,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [ const options = [
{ {
label: 'toLoadout', name: 'toLoadout',
icon: 'fa-solid fa-arrow-up', icon: 'fa-solid fa-arrow-up',
visible: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault; return doc && doc.system.inVault;
}, },
onClick: async (_, target) => { callback: async target => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot; const actorLoadout = doc.actor.system.loadoutSlot;
if (actorLoadout.available) return doc.update({ 'system.inVault': false }); if (actorLoadout.available) return doc.update({ 'system.inVault': false });
@ -412,13 +329,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
}, },
{ {
label: 'recall', name: 'recall',
icon: 'fa-solid fa-bolt-lightning', icon: 'fa-solid fa-bolt-lightning',
visible: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && doc.system.inVault; return doc && doc.system.inVault;
}, },
onClick: async (event, target) => { callback: async (target, event) => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot; const actorLoadout = doc.actor.system.loadoutSlot;
if (!actorLoadout.available) { if (!actorLoadout.available) {
@ -451,17 +368,17 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
}, },
{ {
label: 'toVault', name: 'toVault',
icon: 'fa-solid fa-arrow-down', icon: 'fa-solid fa-arrow-down',
visible: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && !doc.system.inVault; return doc && !doc.system.inVault;
}, },
onClick: async (_, target) => (await getDocFromElement(target)).update({ 'system.inVault': true }) callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true })
} }
].map(option => ({ ].map(option => ({
...option, ...option,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`, name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>` icon: `<i class="${option.icon}"></i>`
})); }));
@ -474,29 +391,29 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @this {CharacterSheet} * @this {CharacterSheet}
* @protected * @protected
*/ */
static #getEquipmentContextOptions() { static #getEquipamentContextOptions() {
const options = [ const options = [
{ {
label: 'equip', name: 'equip',
icon: 'fa-solid fa-hands', icon: 'fa-solid fa-hands',
visible: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc.isOwner && doc && !doc.system.equipped; return doc && !doc.system.equipped;
}, },
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target) callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
}, },
{ {
label: 'unequip', name: 'unequip',
icon: 'fa-solid fa-hands', icon: 'fa-solid fa-hands',
visible: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc.isOwner && doc && doc.system.equipped; return doc && doc.system.equipped;
}, },
onClick: (event, target) => CharacterSheet.#toggleEquipItem.call(this, event, target) callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
} }
].map(option => ({ ].map(option => ({
...option, ...option,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`, name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>` icon: `<i class="${option.icon}"></i>`
})); }));
@ -722,12 +639,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
async updateArmorMarks(event) { async updateArmorMarks(event) {
const inputValue = Number(event.currentTarget.value); const armor = this.document.system.armor;
const { value, max } = this.document.system.armorScore; if (!armor) return;
const changeValue = Math.min(inputValue - value, max - value);
event.currentTarget.value = inputValue < 0 ? 0 : value + changeValue; const maxMarks = this.document.system.armorScore;
this.document.system.updateArmorValue({ value: changeValue }); const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks);
await armor.update({ 'system.marks.value': value });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -785,11 +702,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
filter: filter:
key === 'subclasses' key === 'subclasses'
? { ? {
'system.linkedClass.uuid': { 'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid', key: 'system.linkedClass.uuid',
value: this.document.system.class.value?._stats.compendiumSource value: this.document.system.class.value._stats.compendiumSource
} }
} }
: undefined, : undefined,
render: { render: {
noFolder: true noFolder: true
@ -803,16 +720,35 @@ export default class CharacterSheet extends DHBaseActorSheet {
* Rolls an attribute check based on the clicked button's dataset attribute. * Rolls an attribute check based on the clicked button's dataset attribute.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #rollAttribute(_event, button) { static async #rollAttribute(event, button) {
const result = await this.document.rollTrait(button.dataset.attribute); const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document),
roll: {
trait: button.dataset.attribute,
type: 'trait'
},
hasRoll: true,
actionType: 'action',
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
})
};
const result = await this.document.diceRoll(config);
if (!result) return; if (!result) return;
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources = const costResources =
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
{}; {};
result.resourceUpdates.addResources(costResources); config.resourceUpdates.addResources(costResources);
await result.resourceUpdates.updateResources(); await config.resourceUpdates.updateResources();
} }
//TODO: redo toggleEquipItem method //TODO: redo toggleEquipItem method
@ -887,13 +823,10 @@ export default class CharacterSheet extends DHBaseActorSheet {
* Toggles ArmorScore resource value. * Toggles ArmorScore resource value.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleArmor(_, button, _element) { static async #toggleArmor(_, button, element) {
const { value, max } = this.document.system.armorScore; const ArmorValue = Number.parseInt(button.dataset.value);
const inputValue = Number.parseInt(button.dataset.value); const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
const newValue = value >= inputValue ? inputValue - 1 : inputValue; await this.document.system.armor.update({ 'system.marks.value': newValue });
const changeValue = Math.min(newValue - value, max - value);
this.document.system.updateArmorValue({ value: changeValue });
} }
/** /**
@ -1019,99 +952,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
static async #toggleArmorManagement(_event, target) {
const existingTooltip = document.body.querySelector('.locked-tooltip .armor-management-container');
if (existingTooltip) {
game.tooltip.dismissLockedTooltips();
return;
}
const armorSources = getArmorSources(this.document)
.filter(s => !s.disabled)
.toReversed()
.map(({ name, document, data }) => ({
...data,
uuid: document.uuid,
name
}));
if (!armorSources.length) return;
const useResourcePips = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).useResourcePips;
const html = document.createElement('div');
html.innerHTML = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/armorManagement.hbs`,
{
sources: armorSources,
useResourcePips
}
);
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip dh-style',
direction: 'DOWN'
});
html.querySelectorAll('.armor-slot').forEach(element => {
element.addEventListener('click', CharacterSheet.armorSourcePipUpdate);
});
}
static async armorSourceInput(event) {
const effect = await foundry.utils.fromUuid(event.target.dataset.uuid);
const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
event.target.value = value;
const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress');
progressBar.value = value;
}
/** Update specific armor source */
static async armorSourcePipUpdate(event) {
const target = event.target.closest('.armor-slot');
const { uuid, value } = target.dataset;
const document = await foundry.utils.fromUuid(uuid);
let inputValue = Number.parseInt(value);
let decreasing = false;
let newCurrent = 0;
if (document.type === 'armor') {
decreasing = document.system.armor.current >= inputValue;
newCurrent = decreasing ? inputValue - 1 : inputValue;
await document.update({ 'system.armor.current': newCurrent });
} else if (document.system.armorData) {
const { current } = document.system.armorData;
decreasing = current >= inputValue;
newCurrent = decreasing ? inputValue - 1 : inputValue;
const newChanges = document.system.changes.map(change => ({
...change,
value: change.type === 'armor' ? { ...change.value, current: newCurrent } : change.value
}));
await document.update({ 'system.changes': newChanges });
} else {
return;
}
const container = target.closest('.slot-bar');
for (const armorSlot of container.querySelectorAll('.armor-slot i')) {
const index = Number.parseInt(armorSlot.dataset.index);
if (decreasing && index >= newCurrent) {
armorSlot.classList.remove('fa-shield');
armorSlot.classList.add('fa-shield-halved');
} else if (!decreasing && index < newCurrent) {
armorSlot.classList.add('fa-shield');
armorSlot.classList.remove('fa-shield-halved');
}
}
}
static async #toggleResourceManagement(event, button) { static async #toggleResourceManagement(event, button) {
event.stopPropagation(); event.stopPropagation();
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container'); const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
@ -1145,11 +985,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
); );
const target = button.closest('.resource-section'); const target = button.closest('.resource-section');
game.tooltip.dismissLockedTooltips(); game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, { game.tooltip.activate(target, {
html, html,
locked: true, locked: true,
cssClass: 'bordered-tooltip dh-style', cssClass: 'bordered-tooltip',
direction: 'DOWN', direction: 'DOWN',
noOffset: true noOffset: true
}); });

View file

@ -11,17 +11,7 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
toggleStress: DhCompanionSheet.#toggleStress, toggleStress: DhCompanionSheet.#toggleStress,
actionRoll: DhCompanionSheet.#actionRoll, actionRoll: DhCompanionSheet.#actionRoll,
levelManagement: DhCompanionSheet.#levelManagement levelManagement: DhCompanionSheet.#levelManagement
}, }
contextMenus: [
{
handler: DHBaseActorSheet.getBaseAttackContextOptions,
selector: '[data-item-uuid][data-type="attack"]',
options: {
parentClassHooks: false,
fixed: true
}
}
]
}; };
static PARTS = { static PARTS = {

View file

@ -1,136 +0,0 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
export default class NPCSheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['npc'],
position: { width: 660, height: 600 },
window: { resizable: true },
actions: {},
window: {
resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
]
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs',
scrollable: ['.feature-section']
},
notes: {
template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs'
}
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [{ id: 'notes' }, { id: 'features' }],
initial: 'notes',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
/** @inheritdoc */
_prepareTabs(group) {
const result = super._prepareTabs(group);
if (group === 'primary') {
result.features.empty = this.document.system.features.length === 0;
}
return result;
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'features':
await this._prepareFeaturesContext(context, options);
break;
case 'notes':
await this._prepareNotesContext(context, options);
break;
}
return context;
}
/**
* Prepare render context for the Header part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareHeaderContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
context.description = await TextEditor.implementation.enrichHTML(system.description, {
secrets: this.document.isOwner,
relativeTo: this.document
});
}
/**
* Prepare render context for the Features part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareFeaturesContext(context, _options) {
const featureForms = ['passive', 'action', 'reaction'];
context.features = this.document.system.features.sort((a, b) =>
a.system.featureForm !== b.system.featureForm
? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
: a.sort - b.sort
);
}
/**
* Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context
* @param {ApplicationRenderOptions} options
* @returns {Promise<void>}
* @protected
*/
async _prepareNotesContext(context, _options) {
const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
const paths = {
notes: 'notes'
};
for (const [key, path] of Object.entries(paths)) {
const value = foundry.utils.getProperty(system, path);
context[key] = {
field: system.schema.getField(path),
value,
enriched: await TextEditor.implementation.enrichHTML(value, {
secrets: this.document.isOwner,
relativeTo: this.document
})
};
}
}
}

View file

@ -1,9 +1,10 @@
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
import { getDocFromElement, sortBy } from '../../../helpers/utils.mjs'; import { getDocFromElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs'; import { ItemBrowser } from '../../ui/itemBrowser.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs'; import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
import DhpActor from '../../../documents/actor.mjs'; import DhpActor from '../../../documents/actor.mjs';
export default class Party extends DHBaseActorSheet { export default class Party extends DHBaseActorSheet {
@ -17,15 +18,15 @@ export default class Party extends DHBaseActorSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['party'], classes: ['party'],
position: { position: {
width: 600, width: 550,
height: 900 height: 900
}, },
window: { window: {
resizable: true resizable: true
}, },
actions: { actions: {
openDocument: Party.#openDocument,
deletePartyMember: Party.#deletePartyMember, deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope, toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints, toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress, toggleStress: Party.#toggleStress,
@ -34,7 +35,9 @@ export default class Party extends DHBaseActorSheet {
refeshActions: Party.#refeshActions, refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest, triggerRest: Party.#triggerRest,
tagTeamRoll: Party.#tagTeamRoll, tagTeamRoll: Party.#tagTeamRoll,
groupRoll: Party.#groupRoll groupRoll: Party.#groupRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
}, },
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }] dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
}; };
@ -43,10 +46,16 @@ export default class Party extends DHBaseActorSheet {
static PARTS = { static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
partyMembers: { partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs', resources: {
template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs',
scrollable: [''] scrollable: ['']
}, },
/* NOT YET IMPLEMENTED */
// projects: {
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
// scrollable: ['']
// },
inventory: { inventory: {
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs', template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
scrollable: ['.tab.inventory .items-section'] scrollable: ['.tab.inventory .items-section']
@ -57,13 +66,20 @@ export default class Party extends DHBaseActorSheet {
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'partyMembers' }, { id: 'inventory' }, { id: 'notes' }], tabs: [
{ id: 'partyMembers' },
{ id: 'resources' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
{ id: 'notes' }
],
initial: 'partyMembers', initial: 'partyMembers',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary', 'npc']; static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary'];
static DICE_ROLL_ACTOR_TYPES = ['character']; static DICE_ROLL_ACTOR_TYPES = ['character'];
async _onRender(context, options) { async _onRender(context, options) {
@ -76,22 +92,12 @@ export default class Party extends DHBaseActorSheet {
/* Prepare Context */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
context.showStats =
settings.hidePartyStats === 'never' || (settings.hidePartyStats === 'players' && game.user.isGM);
return context;
}
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header': case 'header':
await this._prepareHeaderContext(context, options); await this._prepareHeaderContext(context, options);
break; break;
case 'partyMembers':
await this._prepareMembersContext(context, options);
case 'notes': case 'notes':
await this._prepareNotesContext(context, options); await this._prepareNotesContext(context, options);
break; break;
@ -114,61 +120,6 @@ export default class Party extends DHBaseActorSheet {
secrets: this.document.isOwner, secrets: this.document.isOwner,
relativeTo: this.document relativeTo: this.document
}); });
context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator);
context.groupRollActive = Boolean(this.document.system.groupRoll.leader);
}
async _prepareMembersContext(context, _options) {
context.partyMembers = [];
const traits = ['agility', 'strength', 'finesse', 'instinct', 'presence', 'knowledge'];
for (const actor of this.document.system.partyMembers) {
const weapons = [];
if (actor.type === 'character') {
if (actor.system.usedUnarmed) {
weapons.push(actor.system.usedUnarmed);
}
const equipped = actor.items.filter(i => i.system.equipped && i.type === 'weapon');
weapons.push(...sortBy(equipped, i => (i.system.secondary ? 1 : 0)));
}
context.partyMembers.push({
uuid: actor.uuid,
img: actor.img,
name: actor.name,
subtitle: (() => {
if (!['character', 'companion'].includes(actor.type)) {
return game.i18n.format(`TYPES.Actor.${actor.type}`);
}
const { value: classItem, subclass } = actor.system.class ?? {};
const partner = actor.system.partner;
const ancestry = actor.system.ancestry;
const community = actor.system.community;
if (partner || (classItem && subclass && ancestry && community)) {
return game.i18n.format(`DAGGERHEART.ACTORS.Party.Subtitle.${actor.type}`, {
class: classItem?.name,
subclass: subclass?.name,
partner: partner?.name,
ancestry: ancestry?.name,
community: community?.name
});
}
})(),
type: actor.type,
resources: actor.system.resources,
armorScore: actor.system.armorScore,
damageThresholds: actor.system.damageThresholds,
evasion: actor.system.evasion,
difficulty: actor.system.difficulty,
traits: actor.system.traits
? traits.map(t => ({
label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`),
value: actor.system.traits[t].value
}))
: null,
weapons
});
}
} }
/** /**
@ -199,12 +150,6 @@ export default class Party extends DHBaseActorSheet {
} }
} }
static async #openDocument(_, target) {
const uuid = target.dataset.uuid;
const document = await foundry.utils.fromUuid(uuid);
document?.sheet?.render(true);
}
/** /**
* Toggles a hope resource value. * Toggles a hope resource value.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -245,14 +190,11 @@ export default class Party extends DHBaseActorSheet {
* Toggles a armor slot resource value. * Toggles a armor slot resource value.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleArmorSlot(_, target) { static async #toggleArmorSlot(_, target, element) {
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
const { value, max } = actor.system.armorScore; const armorValue = Number.parseInt(target.dataset.value);
const inputValue = Number.parseInt(target.dataset.value); const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
const newValue = value >= inputValue ? inputValue - 1 : inputValue; await armorItem.update({ 'system.marks.value': newValue });
const changeValue = Math.min(newValue - value, max - value);
await actor.system.updateArmorValue({ value: changeValue });
this.render(); this.render();
} }
@ -306,18 +248,24 @@ export default class Party extends DHBaseActorSheet {
static async downtimeMoveQuery({ actorId, downtimeType }) { static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId); const actor = await foundry.utils.fromUuid(actorId);
if (!actor || !actor?.isOwner) return; if (!actor || !actor?.isOwner) reject();
new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({ new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({
force: true force: true
}); });
} }
static async #tagTeamRoll() { static async #tagTeamRoll() {
new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true }); new game.system.api.applications.dialogs.TagTeamDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({
force: true
});
} }
static async #groupRoll(_params) { static async #groupRoll(_params) {
new game.system.api.applications.dialogs.GroupRollDialog(this.document).render({ force: true }); new GroupRollDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({ force: true });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -479,22 +427,43 @@ export default class Party extends DHBaseActorSheet {
} }
static async #deletePartyMember(event, target) { static async #deletePartyMember(event, target) {
const doc = await foundry.utils.fromUuid(target.closest('[data-uuid]')?.dataset.uuid); const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) { if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {
title: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.title', { title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.adversary'),
name: doc.name name: doc.name
}) })
}, },
content: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.text', { name: doc.name }) content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
}); });
if (!confirmed) return; if (!confirmed) return;
} }
const currentMembers = this.document.system.partyMembers.map(x => x.uuid); const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid); const newMemberdList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMembersList }); await this.document.update({ 'system.partyMembers': newMemberdList });
}
static async #deleteItem(event, target) {
const doc = await getDocFromElement(target.closest('.inventory-item'));
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Actor.party'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
this.document.deleteEmbeddedDocuments('Item', [doc.id]);
} }
} }

View file

@ -72,15 +72,20 @@ const typeSettingsMap = {
*/ */
export default function DHApplicationMixin(Base) { export default function DHApplicationMixin(Base) {
class DHSheetV2 extends HandlebarsApplicationMixin(Base) { class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/** /**
* @param {DHSheetV2Configuration} [options={}] * @param {DHSheetV2Configuration} [options={}]
*/ */
constructor(options = {}) { constructor(options = {}) {
super(options); super(options);
/**
* @type {foundry.applications.ux.DragDrop[]}
* @private
*/
this._dragDrop = this._createDragDropHandlers();
} }
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/** /**
* The default options for the sheet. * The default options for the sheet.
* @type {DHSheetV2Configuration} * @type {DHSheetV2Configuration}
@ -89,7 +94,7 @@ export default function DHApplicationMixin(Base) {
classes: ['daggerheart', 'sheet', 'dh-style'], classes: ['daggerheart', 'sheet', 'dh-style'],
actions: { actions: {
triggerContextMenu: DHSheetV2.#triggerContextMenu, triggerContextMenu: DHSheetV2.#triggerContextMenu,
createDoc: DHSheetV2.#onCreateDoc, createDoc: DHSheetV2.#createDoc,
editDoc: DHSheetV2.#editDoc, editDoc: DHSheetV2.#editDoc,
deleteDoc: DHSheetV2.#deleteDoc, deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat, toChat: DHSheetV2.#toChat,
@ -97,8 +102,8 @@ export default function DHApplicationMixin(Base) {
viewItem: DHSheetV2.#viewItem, viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect, toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended, toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#onAddNewItem, addNewItem: DHSheetV2.#addNewItem,
browseItem: DHSheetV2.#onBrowseItem, browseItem: DHSheetV2.#browseItem,
editAttribution: DHSheetV2.#editAttribution editAttribution: DHSheetV2.#editAttribution
}, },
contextMenus: [ contextMenus: [
@ -172,6 +177,7 @@ export default function DHApplicationMixin(Base) {
/**@inheritdoc */ /**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs // Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) { for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
@ -284,16 +290,6 @@ export default function DHApplicationMixin(Base) {
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this._createTagifyElements(this.options.tagifyConfigs); this._createTagifyElements(this.options.tagifyConfigs);
for (const d of this.options.dragDrop) {
new foundry.applications.ux.DragDrop.implementation({
...d,
callbacks: {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
}
}).bind(this.element);
}
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -354,6 +350,21 @@ export default function DHApplicationMixin(Base) {
/* Drag and Drop */ /* Drag and Drop */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Creates drag-drop handlers from the configured options.
* @returns {foundry.applications.ux.DragDrop[]}
* @private
*/
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
/** /**
* Handle dragStart event. * Handle dragStart event.
* @param {DragEvent} event * @param {DragEvent} event
@ -418,26 +429,26 @@ export default function DHApplicationMixin(Base) {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
const options = [ const options = [
{ {
label: 'disableEffect', name: 'disableEffect',
icon: 'fa-solid fa-lightbulb', icon: 'fa-solid fa-lightbulb',
visible: element => { condition: element => {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
return !target.dataset.disabled && target.dataset.itemType !== 'beastform'; return !target.dataset.disabled && target.dataset.itemType !== 'beastform';
}, },
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: true }) callback: async target => (await getDocFromElement(target)).update({ disabled: true })
}, },
{ {
label: 'enableEffect', name: 'enableEffect',
icon: 'fa-regular fa-lightbulb', icon: 'fa-regular fa-lightbulb',
visible: element => { condition: element => {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
return target.dataset.disabled && target.dataset.itemType !== 'beastform'; return target.dataset.disabled && target.dataset.itemType !== 'beastform';
}, },
onClick: async (_, target) => (await getDocFromElement(target)).update({ disabled: false }) callback: async target => (await getDocFromElement(target)).update({ disabled: false })
} }
].map(option => ({ ].map(option => ({
...option, ...option,
label: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.label}`, name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`,
icon: `<i class="${option.icon}"></i>` icon: `<i class="${option.icon}"></i>`
})); }));
@ -468,34 +479,29 @@ export default function DHApplicationMixin(Base) {
_getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) { _getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) {
const options = [ const options = [
{ {
label: 'CONTROLS.CommonEdit', name: 'CONTROLS.CommonEdit',
icon: 'fa-solid fa-pen-to-square', icon: 'fa-solid fa-pen-to-square',
visible: target => { condition: target => {
const { dataset } = target.closest('[data-item-uuid]'); const { dataset } = target.closest('[data-item-uuid]');
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return ( return (
(!dataset.noCompendiumEdit && !doc) || (!dataset.noCompendiumEdit && !doc) ||
(doc?.isOwner && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection)) (doc && (!doc?.hasOwnProperty('systemPath') || doc?.inCollection))
); );
}, },
onClick: async (_, target) => { callback: async target => (await getDocFromElement(target)).sheet.render({ force: true })
return (await getDocFromElement(target)).sheet.render({ force: true });
}
} }
]; ];
if (usable) { if (usable) {
options.unshift({ options.unshift({
label: 'DAGGERHEART.GENERAL.damage', name: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion', icon: 'fa-solid fa-explosion',
visible: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
const hasDamage = return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length;
!foundry.utils.isEmpty(doc?.system?.attack?.damage.parts) ||
!foundry.utils.isEmpty(doc?.damage?.parts);
return doc?.isOwner && hasDamage;
}, },
onClick: async (event, target) => { callback: async (target, event) => {
const doc = await getDocFromElement(target), const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc; action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event); const config = action.prepareConfig(event);
@ -509,33 +515,32 @@ export default function DHApplicationMixin(Base) {
}); });
options.unshift({ options.unshift({
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst', icon: 'fa-solid fa-burst',
visible: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc?.isOwner && !(doc.type === 'domainCard' && doc.system.inVault); return doc && !(doc.type === 'domainCard' && doc.system.inVault);
}, },
onClick: async (event, target) => (await getDocFromElement(target)).use(event) callback: async (target, event) => (await getDocFromElement(target)).use(event)
}); });
} }
if (toChat) if (toChat)
options.push({ options.push({
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message', icon: 'fa-solid fa-message',
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid) callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
}); });
if (deletable) if (deletable)
options.push({ options.push({
label: 'CONTROLS.CommonDelete', name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash', icon: 'fa-solid fa-trash',
visible: element => { condition: element => {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
const doc = getDocFromElementSync(target); return target.dataset.itemType !== 'beastform';
return doc?.isOwner !== false && target.dataset.itemType !== 'beastform';
}, },
onClick: async (event, target) => { callback: async (target, event) => {
const doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete(); if (event.shiftKey) return doc.delete();
else return doc.deleteDialog(); else return doc.deleteDialog();
@ -641,7 +646,7 @@ export default function DHApplicationMixin(Base) {
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
static async #onAddNewItem(event, target) { static async #addNewItem(event, target) {
const createChoice = await foundry.applications.api.DialogV2.wait({ const createChoice = await foundry.applications.api.DialogV2.wait({
classes: ['dh-style', 'two-big-buttons'], classes: ['dh-style', 'two-big-buttons'],
buttons: [ buttons: [
@ -660,11 +665,11 @@ export default function DHApplicationMixin(Base) {
if (!createChoice) return; if (!createChoice) return;
if (createChoice === 'browse') return DHSheetV2.#onBrowseItem.call(this, event, target); if (createChoice === 'browse') return DHSheetV2.#browseItem.call(this, event, target);
else return DHSheetV2.#onCreateDoc.call(this, event, target); else return DHSheetV2.#createDoc.call(this, event, target);
} }
static async #onBrowseItem(_event, target) { static async #browseItem(event, target) {
const type = target.dataset.compendium ?? target.dataset.type; const type = target.dataset.compendium ?? target.dataset.type;
const presets = { const presets = {
@ -715,17 +720,17 @@ export default function DHApplicationMixin(Base) {
* Create an embedded document. * Create an embedded document.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #onCreateDoc(event, target) { static async #createDoc(event, target) {
const { documentClass, type, inVault, disabled } = target.dataset; const { documentClass, type, inVault, disabled } = target.dataset;
const parentIsItem = this.document.documentName === 'Item'; const parentIsItem = this.document.documentName === 'Item';
const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature'; const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature';
const parent = featureOnCharacter const parent = featureOnCharacter
? this.document.parent ? this.document.parent
: parentIsItem && documentClass === 'Item' : parentIsItem && documentClass === 'Item'
? type === 'action' ? type === 'action'
? this.document.system ? this.document.system
: null : null
: this.document; : this.document;
let systemData = {}; let systemData = {};
if (featureOnCharacter) { if (featureOnCharacter) {
@ -737,21 +742,15 @@ export default function DHApplicationMixin(Base) {
const cls = const cls =
type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass); type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass);
const data = { const data = {
name: cls.defaultName({ type, parent }), name: cls.defaultName({ type, parent }),
type, type,
system: systemData system: systemData
}; };
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true; if (disabled) data.disabled = true;
if (type === 'domainCard' && parent?.system.domains?.length) {
if (type === 'domainCard') { data.system.domain = parent.system.domains[0];
if (parent?.system.domains?.length) data.system.domain = parent.system.domains[0];
if (inVault) data.system.inVault = true;
} else if (type === 'weapon') {
// Passing an empty system object to weapon causes validation failure due to attack action initialization
// todo: determine why, fix it at its source, then remove this fallback
delete data.system;
} }
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });

View file

@ -73,7 +73,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
.hideAttribution; .hideAttribution;
// Prepare inventory data // Prepare inventory data
if (this.document.system.metadata.hasInventory) { if (['party', 'character'].includes(this.document.type)) {
context.inventory = { context.inventory = {
currencies: {}, currencies: {},
weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort), weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort),
@ -160,21 +160,12 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
inactives: [] inactives: []
}; };
for (const effect of this.actor.allApplicableEffects({ noTransferArmor: true })) { for (const effect of this.actor.allApplicableEffects()) {
const list = effect.active ? context.effects.actives : context.effects.inactives; const list = effect.active ? context.effects.actives : context.effects.inactives;
list.push(effect); list.push(effect);
} }
} }
/** Add support for input content editables */
_toggleDisabled(disabled) {
super._toggleDisabled(disabled);
const form = this.form;
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Context Menu */ /* Context Menu */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -189,43 +180,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
} }
/**
* Get the set of ContextMenu options for the base attack.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static getBaseAttackContextOptions() {
/**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */
return [
{
label: 'DAGGERHEART.CONFIG.RollTypes.attack.name',
icon: 'fa-solid fa-burst',
onClick: async (event, target) => (await getDocFromElement(target)).use(event)
},
{
label: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
onClick: async (event, target) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(
this.document,
doc
);
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
}
},
{
label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid)
}
];
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Listener Actions */ /* Application Listener Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -274,6 +228,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
'systems/daggerheart/templates/ui/chat/action.hbs', 'systems/daggerheart/templates/ui/chat/action.hbs',
systemData systemData
), ),
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'),
speaker: cls.getSpeaker(), speaker: cls.getSpeaker(),
flags: { flags: {
daggerheart: { daggerheart: {
@ -329,7 +284,11 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
async _onDropItem(event, item) { async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const originActor = item.actor; const originActor = item.actor;
if (!originActor || originActor.uuid === this.document.uuid || !this.document.system.metadata.hasInventory) { if (
item.actor?.uuid === this.document.uuid ||
!originActor ||
!['character', 'party'].includes(this.document.type)
) {
return super._onDropItem(event, item); return super._onDropItem(event, item);
} }
@ -344,79 +303,47 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
); );
} }
// Perform the actual transfer, showing a dialog when doing it if (item.system.metadata.isQuantifiable) {
const availableQuantity = Math.max(1, item.system.quantity); const actorItem = originActor.items.get(data.originId);
const actorItem = originActor.items.get(data.originId) ?? item; const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
if (availableQuantity > 1) {
const quantityTransferred = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item, item,
targetActor: this.document targetActor: this.document
}); });
return this.#transferItem(actorItem, quantityTransferred);
if (quantityTransfered) {
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
if (existingItem) {
await existingItem.update({
'system.quantity': existingItem.system.quantity + quantityTransfered
});
} else {
const createData = item.toObject();
await this.document.createEmbeddedDocuments('Item', [
{
...createData,
system: {
...createData.system,
quantity: quantityTransfered
}
}
]);
}
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
}
} else { } else {
return this.#transferItem(actorItem, availableQuantity); await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} }
} }
} }
/**
* Helper to perform the actual transfer of an item to this actor, including stack/unstack logic based on target quantifiability.
* Make sure item is the actor item before calling this method or there will be issues
*/
async #transferItem(item, quantity) {
const originActor = item.actor;
const targetActor = this.document;
const allowStacking = targetActor.system.metadata.quantifiable?.includes(item.type);
const batch = [];
// First add/update the item to the target actor
const existing = allowStacking ? targetActor.items.find(x => itemIsIdentical(x, item)) : null;
if (existing) {
batch.push({
action: 'update',
documentName: 'Item',
parent: targetActor,
updates: [{ _id: existing.id, 'system.quantity': existing.system.quantity + quantity }]
});
} else {
const itemsToCreate = [];
if (allowStacking) {
itemsToCreate.push(foundry.utils.mergeObject(item.toObject(true), { system: { quantity } }));
} else {
const createData = new Array(Math.max(1, quantity))
.fill(0)
.map(() => foundry.utils.mergeObject(item.toObject(), { system: { quantity: 1 } }));
itemsToCreate.push(...createData);
}
batch.push({
action: 'create',
documentName: 'Item',
parent: targetActor,
data: itemsToCreate
});
}
// Remove the item from the original actor (by either deleting it, or updating its quantity)
if (quantity >= item.system.quantity) {
batch.push({
action: 'delete',
documentName: 'Item',
parent: originActor,
ids: [item.id]
});
} else {
batch.push({
action: 'update',
documentName: 'Item',
parent: originActor,
updates: [{ _id: item.id, 'system.quantity': item.system.quantity - quantity }]
});
}
return foundry.documents.modifyBatch(batch);
}
/** /**
* On dragStart on the item. * On dragStart on the item.
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event

View file

@ -126,7 +126,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
options.push({ options.push({
name: 'CONTROLS.CommonDelete', name: 'CONTROLS.CommonDelete',
icon: '<i class="fa-solid fa-trash"></i>', icon: '<i class="fa-solid fa-trash"></i>',
onClick: async (_, target) => { callback: async target => {
const feature = await getDocFromElement(target); const feature = await getDocFromElement(target);
if (!feature) return; if (!feature) return;
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({

View file

@ -29,6 +29,16 @@ export default function ItemAttachmentSheet(Base) {
} }
}; };
async _preparePartContext(partId, context) {
await super._preparePartContext(partId, context);
if (partId === 'attachments') {
context.attachedItems = await prepareAttachmentContext(this.document);
}
return context;
}
async _onDrop(event) { async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);

View file

@ -31,12 +31,11 @@ export default class AncestrySheet extends DHHeritageSheet {
if (data.type === 'ActiveEffect') return super._onDrop(event); if (data.type === 'ActiveEffect') return super._onDrop(event);
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
if (target) { const typeField =
const typeField = this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];
this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];
if (!typeField) { if (!typeField) {
super._onDrop(event); super._onDrop(event);
}
} }
} }
} }

View file

@ -47,15 +47,6 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
return context; return context;
} }
async updateArmorEffect(event) {
const value = Number.parseInt(event.target.value);
const armorEffect = this.document.system.armorEffect;
if (Number.isNaN(value) || !armorEffect) return;
await armorEffect.system.armorChange.updateArmorMax(value);
this.render();
}
/** /**
* Callback function used by `tagifyElement`. * Callback function used by `tagifyElement`.
* @param {Array<Object>} selectedOptions - The currently selected tag objects. * @param {Array<Object>} selectedOptions - The currently selected tag objects.

View file

@ -102,7 +102,7 @@ export default class BeastformSheet extends DHBaseItemSheet {
async advantageOnRemove(event) { async advantageOnRemove(event) {
await this.document.update({ await this.document.update({
[`system.advantageOn.${event.detail.data.value}`]: _del [`system.advantageOn.-=${event.detail.data.value}`]: null
}); });
} }
} }

View file

@ -104,10 +104,9 @@ export default class ClassSheet extends DHBaseItemSheet {
} }
/**@inheritdoc */ /**@inheritdoc */
async _prepareContext(options) { async _prepareContext(_options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(_options);
context.domains = this.document.system.domains; context.domains = this.document.system.domains;
context.subclasses = await this.document.system.fetchSubclasses();
return context; return context;
} }
@ -129,8 +128,20 @@ export default class ClassSheet extends DHBaseItemSheet {
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type; const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
if (itemType === 'subclass') {
if (['feature', 'ActiveEffect'].includes(itemType)) { if (item.system.linkedClass) {
return ui.notifications.warn(
game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', {
name: item.name,
class: this.document.name
})
);
}
await item.update({ 'system.linkedClass': this.document.uuid });
await this.document.update({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
} else if (['feature', 'ActiveEffect'].includes(itemType)) {
super._onDrop(event); super._onDrop(event);
} else if (this.document.parent?.type !== 'character') { } else if (this.document.parent?.type !== 'character') {
if (itemType === 'weapon') { if (itemType === 'weapon') {
@ -189,6 +200,12 @@ export default class ClassSheet extends DHBaseItemSheet {
static async #removeItemFromCollection(_event, element) { static async #removeItemFromCollection(_event, element) {
const { uuid, target } = element.dataset; const { uuid, target } = element.dataset;
const prop = foundry.utils.getProperty(this.document.system, target); const prop = foundry.utils.getProperty(this.document.system, target);
if (target === 'subclasses') {
const subclass = await foundry.utils.fromUuid(uuid);
await subclass?.update({ 'system.linkedClass': null });
}
await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) }); await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) });
} }

View file

@ -31,7 +31,7 @@ export default class FeatureSheet extends DHBaseItemSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
//Might be wrong location but testing out if here is okay. //Might be wrong location but testing out if here is okay.
/**@override */ /**@override */
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);

View file

@ -40,36 +40,4 @@ export default class SubclassSheet extends DHBaseItemSheet {
get relatedDocs() { get relatedDocs() {
return this.document.system.features.map(x => x.item); return this.document.system.features.map(x => x.item);
} }
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (this.document.system.linkedClass) {
const classData = await fromUuid(this.document.system.linkedClass);
context.class = classData ?? {
name: _loc('DAGGERHEART.GENERAL.missingX', { x: _loc('TYPES.Item.class') }),
missing: true
};
}
return context;
}
async _onDrop(event) {
event.stopPropagation();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
if (itemType === 'class') {
const uuid = item._stats.compendiumSource ?? item.uuid;
if (this.document.system.linkedClass !== uuid) {
await this.document.update({ 'system.linkedClass': uuid });
// Re-render all class sheets for instant feedback
for (const app of foundry.applications.instances.values()) {
if (app.document?.type === 'class') app.render();
}
}
return;
}
return super._onDrop(event);
}
} }

View file

@ -108,15 +108,14 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
getSystemFlagUpdate() { getSystemFlagUpdate() {
const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce( const deleteUpdate = Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {}).reduce(
(acc, formulaKey) => { (acc, formulaKey) => {
if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[formulaKey] = _del; if (!this.daggerheartFlag.altFormula[formulaKey]) acc.altFormula[`-=${formulaKey}`] = null;
return acc; return acc;
}, },
{ altFormula: {} } { altFormula: {} }
); );
const flagData = this.daggerheartFlag.toObject(); return { ['flags.daggerheart']: foundry.utils.mergeObject(this.daggerheartFlag.toObject(), deleteUpdate) };
return { ...flagData, altFormula: { ...flagData.altFormula, ...deleteUpdate.altFormula } };
} }
static async #addFormula() { static async #addFormula() {
@ -128,7 +127,7 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa
static async #removeFormula(_event, target) { static async #removeFormula(_event, target) {
await this.daggerheartFlag.updateSource({ await this.daggerheartFlag.updateSource({
[`altFormula.${target.dataset.key}`]: _del [`altFormula.-=${target.dataset.key}`]: null
}); });
this.render({ internalRefresh: true }); this.render({ internalRefresh: true });
} }

View file

@ -1,19 +1,52 @@
export default class DhSidebar extends foundry.applications.sidebar.Sidebar { export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
static buildTabs() {
const { settings, ...tabs } = super.TABS;
return {
...tabs,
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
gmOnly: true
},
settings
};
}
/** @override */ /** @override */
static TABS = DhSidebar.buildTabs(); static TABS = {
chat: {
documentName: 'ChatMessage'
},
combat: {
documentName: 'Combat'
},
scenes: {
documentName: 'Scene',
gmOnly: true
},
actors: {
documentName: 'Actor'
},
items: {
documentName: 'Item'
},
journal: {
documentName: 'JournalEntry',
tooltip: 'SIDEBAR.TabJournal'
},
tables: {
documentName: 'RollTable'
},
cards: {
documentName: 'Cards'
},
macros: {
documentName: 'Macro'
},
playlists: {
documentName: 'Playlist'
},
compendium: {
tooltip: 'SIDEBAR.TabCompendium',
icon: 'fa-solid fa-book-atlas'
},
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg',
gmOnly: true
},
settings: {
tooltip: 'SIDEBAR.TabSettings',
icon: 'fa-solid fa-gears'
}
};
/** @override */ /** @override */
static PARTS = { static PARTS = {

View file

@ -13,8 +13,8 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
return document.type === 'adversary' return document.type === 'adversary'
? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary') ? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary')
: document.type === 'environment' : document.type === 'environment'
? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment') ? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment')
: null; : null;
}; };
} }
@ -46,67 +46,50 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.
_getEntryContextOptions() { _getEntryContextOptions() {
const options = super._getEntryContextOptions(); const options = super._getEntryContextOptions();
options.push( options.push({
{ name: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier',
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.duplicateToNewTier', icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`,
icon: `<i class="fa-solid fa-arrow-trend-up" inert></i>`, condition: li => {
visible: li => { const actor = game.actors.get(li.dataset.entryId);
const actor = game.actors.get(li.dataset.entryId); return actor?.type === 'adversary' && actor.system.type !== 'social';
return actor?.type === 'adversary' && actor.system.type !== 'social';
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
const content = document.createElement('div');
const select = document.createElement('select');
select.name = 'tier';
select.append(
...tiers.map(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
return option;
})
);
content.append(select);
const tier = await foundry.applications.api.Dialog.input({
classes: ['dh-style', 'dialog'],
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
content,
ok: {
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.createAdversary',
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
}
});
if (tier === actor.system.tier) {
ui.notifications.warn('This actor is already at this tier');
} else if (tier) {
const source = actor.system.adjustForTier(tier);
await Actor.create(source);
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
}
}
}, },
{ callback: async li => {
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.activateParty', const actor = game.actors.get(li.dataset.entryId);
icon: `<i class="fa-regular fa-square"></i>`, if (!actor) throw new Error('Unexpected missing actor');
visible: li => {
const actor = game.actors.get(li.dataset.entryId);
return actor && actor.type === 'party' && !actor.system.active;
},
callback: async li => {
const actor = game.actors.get(li.dataset.entryId);
if (!actor) throw new Error('Unexpected missing actor');
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.ActiveParty, actor.id); const tiers = [1, 2, 3, 4].filter(t => t !== actor.system.tier);
ui.actors.render(); const content = document.createElement('div');
const select = document.createElement('select');
select.name = 'tier';
select.append(
...tiers.map(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${t}`);
return option;
})
);
content.append(select);
const tier = await foundry.applications.api.Dialog.input({
classes: ['dh-style', 'dialog'],
window: { title: 'DAGGERHEART.UI.Sidebar.actorDirectory.pickTierTitle' },
content,
ok: {
label: 'DAGGERHEART.UI.Sidebar.actorDirectory.createAdversary',
callback: (event, button, dialog) => Number(button.form.elements.tier.value)
}
});
if (tier === actor.system.tier) {
ui.notifications.warn('This actor is already at this tier');
} else if (tier) {
const source = actor.system.adjustForTier(tier);
await Actor.create(source);
ui.notifications.info(`Tier ${tier} ${actor.name} created`);
} }
} }
); });
return options; return options;
} }

View file

@ -31,8 +31,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
}, },
actions: { actions: {
selectRefreshable: DaggerheartMenu.#selectRefreshable, selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.#refreshActors, refreshActors: DaggerheartMenu.#refreshActors
createFallCollisionDamage: DaggerheartMenu.#createFallCollisionDamage
} }
}; };
@ -51,7 +50,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.refreshables = this.refreshSelections; context.refreshables = this.refreshSelections;
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected); context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
context.fallAndCollision = CONFIG.DH.GENERAL.fallAndCollisionDamage;
return context; return context;
} }
@ -73,22 +71,4 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
this.render(); this.render();
} }
static async #createFallCollisionDamage(_event, button) {
const data = CONFIG.DH.GENERAL.fallAndCollisionDamage[button.dataset.key];
const roll = new Roll(data.damageFormula);
await roll.evaluate();
/* class BaseRoll needed to get rendered by foundryRoll.hbs */
const rollJSON = roll.toJSON();
rollJSON.class = 'BaseRoll';
foundry.documents.ChatMessage.implementation.create({
title: game.i18n.localize(data.chatTitle),
author: game.user.id,
speaker: foundry.documents.ChatMessage.implementation.getSpeaker(),
rolls: [rollJSON],
sound: CONFIG.sounds.dice
});
}
} }

View file

@ -7,4 +7,3 @@ export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs'; export { default as DhHotbar } from './hotbar.mjs';
export { default as DhSceneNavigation } from './sceneNavigation.mjs'; export { default as DhSceneNavigation } from './sceneNavigation.mjs';
export { ItemBrowser } from './itemBrowser.mjs'; export { ItemBrowser } from './itemBrowser.mjs';
export { default as DhProgress } from './progress.mjs';

View file

@ -1,6 +1,5 @@
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; import { abilities } from '../../config/actorConfig.mjs';
import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) { constructor(options) {
@ -22,114 +21,35 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
classes: ['daggerheart'] classes: ['daggerheart']
}; };
static CHAT_COMMANDS = {
...super.CHAT_COMMANDS,
dr: {
rgx: /^(?:\/dr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const reaction = rollCommand.reaction;
const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true });
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(CONFIG.DH.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage,
grantResources
});
return false;
}
},
fr: {
rgx: /^(?:\/fr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
}
}
};
_getEntryContextOptions() { _getEntryContextOptions() {
return [ return [
...super._getEntryContextOptions(), ...super._getEntryContextOptions(),
// {
// name: 'Reroll',
// icon: '<i class="fa-solid fa-dice"></i>',
// condition: li => {
// const message = game.messages.get(li.dataset.messageId);
// return (game.user.isGM || message.isAuthor) && message.rolls.length > 0;
// },
// callback: li => {
// const message = game.messages.get(li.dataset.messageId);
// new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true });
// }
// },
{ {
label: 'DAGGERHEART.UI.ChatLog.rerollActionRoll', name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
icon: '<i class="fa-solid fa-dice"></i>', icon: '<i class="fa-solid fa-dice"></i>',
visible: li => { condition: li => {
const message = game.messages.get(li.dataset.messageId);
return message.system.hasRoll && (game.user.isGM || message.isAuthor);
},
callback: async li => {
const message = game.messages.get(li.dataset.messageId);
const reroll = await message.rolls[0].reroll({ liveRoll: true });
message.update({ rolls: [reroll] });
}
},
{
label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
icon: '<i class="fa-solid fa-dice"></i>',
visible: li => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
const hasRolledDamage = message.system.hasDamage const hasRolledDamage = message.system.hasDamage
? Object.keys(message.system.damage).length > 0 ? Object.keys(message.system.damage).length > 0
: false; : false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage; return (game.user.isGM || message.isAuthor) && hasRolledDamage;
}, },
callback: async li => { callback: li => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
const update = await message.system.getRerolledDamage(); new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true });
message.update(update);
} }
} }
]; ];
@ -149,6 +69,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.reroll-button').forEach(element => html.querySelectorAll('.reroll-button').forEach(element =>
element.addEventListener('click', event => this.rerollEvent(event, message)) element.addEventListener('click', event => this.rerollEvent(event, message))
); );
html.querySelectorAll('.group-roll-button').forEach(element =>
element.addEventListener('click', event => this.groupRollButton(event, message))
);
html.querySelectorAll('.group-roll-reroll').forEach(element =>
element.addEventListener('click', event => this.groupRollReroll(event, message))
);
html.querySelectorAll('.group-roll-success').forEach(element =>
element.addEventListener('click', event => this.groupRollSuccessEvent(event, message))
);
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
element.addEventListener('click', this.groupRollExpandSection)
);
html.querySelectorAll('.risk-it-all-button').forEach(element => html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data)) element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
); );
@ -243,7 +175,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
action.use(event); action.use(event);
} }
async rerollEvent(event, messageData) { async rerollEvent(event, message) {
event.stopPropagation(); event.stopPropagation();
if (!event.shiftKey) { if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
@ -255,43 +187,208 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (!confirmed) return; if (!confirmed) return;
} }
const message = game.messages.get(messageData._id);
const target = event.target.closest('[data-die-index]'); const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') { if (target.dataset.type === 'damage') {
const { damageType, part, dice, result } = target.dataset; game.system.api.dice.DamageRoll.reroll(target, message);
const damagePart = message.system.damage[damageType].parts[part];
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result);
const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => {
if (index !== Number(part)) return damagePart;
return {
...damagePart,
total: parsedRoll.total,
dice: rerolledDice
};
});
const updateMessage = game.messages.get(message._id);
await updateMessage.update({
[`system.damage.${damageType}`]: {
total: parsedRoll.total,
parts: damageParts
}
});
} else { } else {
const rerollDice = message.system.roll.dice[target.dataset.dieIndex]; let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
await rerollDice.reroll(`/r1=${rerollDice.total}`, { const rollClass =
liveRoll: { game.system.api.dice[
roll: message.system.roll, message.type === 'dualityRoll'
actor: message.system.actionActor, ? 'DualityRoll'
isReaction: message.system.roll.options.actionType === 'reaction' : target.dataset.type === 'damage'
} ? 'DHRoll'
: 'D20Roll'
];
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
}); });
await message.update({
rolls: [message.system.roll.toJSON()] Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
}); });
} }
} }
async groupRollButton(event, message) {
const path = event.currentTarget.dataset.path;
const isLeader = path === 'leader';
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor) {
return ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.documentIsMissing', {
documentType: game.i18n.localize('TYPES.Actor.character')
})
);
}
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
resources: !isLeader,
updateCountdowns: !isLeader
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
if (!result) return;
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll);
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollReroll(event, message) {
const path = event.currentTarget.dataset.path;
const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const traitLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
}),
roll: {
trait: trait,
advantage: 0,
modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }]
},
hasRoll: true,
skips: {
createMessage: true,
updateCountdowns: true
}
};
const result = await actor.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: traitLabel
})
});
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.result`, { ...result.roll, rerolled: true });
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollSuccessEvent(event, message) {
if (!game.user.isGM) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmOnly'));
}
const { path, success } = event.currentTarget.dataset;
const { actor: actorData } = foundry.utils.getProperty(message.system, path);
const actor = game.actors.get(actorData._id);
if (!actor.testUserPermission(game.user, 'OWNER')) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership'));
}
const newMessageData = foundry.utils.deepClone(message.system);
foundry.utils.setProperty(newMessageData, `${path}.manualSuccess`, Boolean(success));
const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) };
const updatedContent = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/groupRoll.hbs',
{ ...renderData, user: game.user }
);
const mess = game.messages.get(message._id);
await emitAsGM(
GMUpdateEvent.UpdateDocument,
mess.update.bind(mess),
{
...renderData,
content: updatedContent
},
mess.uuid
);
}
async groupRollExpandSection(event) {
event.target
.closest('.group-roll-header-expand-section')
.querySelectorAll('i')
.forEach(element => {
element.classList.toggle('fa-angle-up');
element.classList.toggle('fa-angle-down');
});
event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed');
}
async riskItAllClearStressAndHitPoints(event, data) { async riskItAllClearStressAndHitPoints(event, data) {
const resourceValue = event.target.dataset.resourceValue; const resourceValue = event.target.dataset.resourceValue;
const actor = game.actors.get(event.target.dataset.actorId); const actor = game.actors.get(event.target.dataset.actorId);

View file

@ -1,6 +1,4 @@
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs'; import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
import { expireActiveEffects } from '../../helpers/utils.mjs';
import { clearPreviousSpotlight } from '../../macros/spotlightCombatant.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -56,9 +54,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
async _prepareTrackerContext(context, options) { async _prepareTrackerContext(context, options) {
await super._prepareTrackerContext(context, options); await super._prepareTrackerContext(context, options);
const npcs = context.turns?.filter(x => x.isNPC) ?? []; const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const adversaries = npcs.filter(x => x.disposition !== CONST.TOKEN_DISPOSITIONS.FRIENDLY);
const friendlies = npcs.filter(x => x.disposition === CONST.TOKEN_DISPOSITIONS.FRIENDLY);
const characters = context.turns?.filter(x => !x.isNPC) ?? []; const characters = context.turns?.filter(x => !x.isNPC) ?? [];
const spotlightQueueEnabled = game.settings.get( const spotlightQueueEnabled = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
@ -77,56 +73,25 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
Object.assign(context, { Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries, adversaries,
friendlies,
allCharacters: characters, allCharacters: characters,
characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), characters: characters.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests spotlightRequests
}); });
} }
/**
* Open the dialog used to edit the name of the currently viewed Combat encounter.
* @this {CombatTracker}
* @returns {Promise<void>}
*/
static async #onEditName() {
const combat = this.viewed;
if (!combat || !game.user.isGM) return null;
const field = combat.schema.fields.name;
const inputHTML = field.toFormGroup({}, { name: 'name', value: combat.name, autofocus: true }).outerHTML;
const formData = await foundry.applications.api.DialogV2.input({
window: { icon: 'fa-solid fa-tag', title: 'COMBAT.ACTIONS.EditNameTitle' },
position: { width: 480 },
content: inputHTML
});
await combat.update({ name: formData.name || '' });
}
_getCombatContextOptions() { _getCombatContextOptions() {
return [ return [
{ {
label: 'COMBAT.ACTIONS.EditName', name: 'COMBAT.ClearMovementHistories',
icon: 'fa-solid fa-tag', icon: '<i class="fa-solid fa-shoe-prints"></i>',
visible: () => game.user.isGM && !!this.viewed, condition: () => game.user.isGM && this.viewed?.combatants.size > 0,
onClick: () => DhCombatTracker.#onEditName.call(this) callback: () => this.viewed.clearMovementHistories()
}, },
{ {
label: 'COMBAT.ACTIONS.LinkToScene', name: 'COMBAT.Delete',
icon: '<i class="fa-solid fa-link"></i>', icon: '<i class="fa-solid fa-trash"></i>',
visible: () => game.user.isGM && !this.scene, condition: () => game.user.isGM && !!this.viewed,
onClick: () => this.viewed.toggleSceneLink() callback: () => this.viewed.endCombat()
},
{
label: 'COMBAT.ACTIONS.UnlinkFromScene',
icon: '<i class="fa-solid fa-unlink"></i>',
visible: () => game.user.isGM && !!this.scene,
onClick: () => this.viewed.toggleSceneLink()
},
{
label: 'COMBAT.End',
icon: 'fa-solid fa-xmark',
visible: () => game.user.isGM && !!this.viewed,
onClick: () => this.viewed.endCombat()
} }
]; ];
} }
@ -162,8 +127,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
active: index === combat.turn, active: index === combat.turn,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor?.system?.type, type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant), img: await this._getCombatantThumbnail(combatant)
disposition: combatant.token?.disposition
}; };
turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin( turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin(
@ -185,13 +149,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
async setCombatantSpotlight(combatantId) { async setCombatantSpotlight(combatantId) {
const combatant = this.viewed.combatants.get(combatantId);
const update = { const update = {
system: { system: {
'spotlight.requesting': false, 'spotlight.requesting': false,
'spotlight.requestOrderIndex': 0 'spotlight.requestOrderIndex': 0
} }
}; };
const combatant = this.viewed.combatants.get(combatantId);
const toggleTurn = this.viewed.combatants.contents const toggleTurn = this.viewed.combatants.contents
.sort(this.viewed._sortCombatants) .sort(this.viewed._sortCombatants)
@ -213,8 +177,6 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (autoPoints) { if (autoPoints) {
update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0); update.system.actionTokens = Math.max(combatant.system.actionTokens - 1, 0);
} }
if (combatant.actor) expireActiveEffects(combatant.actor, [CONFIG.DH.GENERAL.activeEffectDurations.act.id]);
} }
await this.viewed.update({ await this.viewed.update({
@ -222,14 +184,6 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
round: this.viewed.round + 1 round: this.viewed.round + 1
}); });
await combatant.update(update); await combatant.update(update);
if (combatant.token) clearPreviousSpotlight();
}
async clearTurn() {
await this.viewed.update({
turn: null,
round: this.viewed.round + 1
});
} }
static async requestSpotlight(_, target) { static async requestSpotlight(_, target) {

View file

@ -1,6 +1,6 @@
import { DhCountdown } from '../../data/countdowns.mjs'; import { DhCountdown } from '../../data/countdowns.mjs';
import { waitForDiceSoNice } from '../../helpers/utils.mjs'; import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -56,8 +56,8 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop' ? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop' ? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop' : 'DAGGERHEART.UI.Countdowns.loop'
: null; : null;
const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic; const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic;
acc[key] = { acc[key] = {
@ -114,7 +114,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
} }
await this.data.updateSource(update); await this.data.updateSource(update);
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, { await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
@ -148,11 +148,11 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
} }
async gmSetSetting(data) { async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, { game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown } data: { refreshType: RefreshType.Countdown }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
} }
@ -233,6 +233,6 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
} }
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId); if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.${countdownId}`]: _del }); this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
} }
} }

View file

@ -1,5 +1,5 @@
import { waitForDiceSoNice } from '../../helpers/utils.mjs'; import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -21,19 +21,19 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
id: 'countdowns', id: 'countdowns',
tag: 'div', tag: 'div',
classes: ['daggerheart', 'dh-style', 'countdowns'], classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
window: { window: {
icon: 'fa-solid fa-clock-rotate-left', icon: 'fa-solid fa-clock-rotate-left',
frame: false, frame: true,
title: 'DAGGERHEART.UI.Countdowns.title', title: 'DAGGERHEART.UI.Countdowns.title',
positioned: false, positioned: false,
resizable: false, resizable: false,
minimizable: false minimizable: false
}, },
actions: { actions: {
toggleViewMode: DhCountdowns.#onToggleViewMode, toggleViewMode: DhCountdowns.#toggleViewMode,
editCountdowns: DhCountdowns.#onEditCountdowns, editCountdowns: DhCountdowns.#editCountdowns,
loopCountdown: DhCountdowns.#onLoopCountdown, loopCountdown: DhCountdowns.#loopCountdown,
decreaseCountdown: (_, target) => this.editCountdown(false, target), decreaseCountdown: (_, target) => this.editCountdown(false, target),
increaseCountdown: (_, target) => this.editCountdown(true, target) increaseCountdown: (_, target) => this.editCountdown(true, target)
}, },
@ -52,6 +52,10 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
} }
}; };
get element() {
return document.body.querySelector('.daggerheart.dh-style.countdowns');
}
/**@inheritdoc */ /**@inheritdoc */
async _renderFrame(options) { async _renderFrame(options) {
const frame = await super._renderFrame(options); const frame = await super._renderFrame(options);
@ -62,6 +66,19 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
if (iconOnly) frame.classList.add('icon-only'); if (iconOnly) frame.classList.add('icon-only');
else frame.classList.remove('icon-only'); else frame.classList.remove('icon-only');
const header = frame.querySelector('.window-header');
header.querySelector('button[data-action="close"]').remove();
if (game.user.isGM) {
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
const editButton = `<a style="margin-right: 8px;" class="header-control" data-tooltip="${editTooltip}" aria-label="${editTooltip}" data-action="editCountdowns"><i class="fa-solid fa-wrench"></i></a>`;
header.insertAdjacentHTML('beforeEnd', editButton);
}
const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="toggleViewMode"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
header.insertAdjacentHTML('beforeEnd', minimizeButton);
return frame; return frame;
} }
@ -101,8 +118,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop' ? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop' ? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop' : 'DAGGERHEART.UI.Countdowns.loop'
: null; : null;
const loopDisabled = const loopDisabled =
!countdownEditable || !countdownEditable ||
@ -123,8 +140,6 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
} }
static #getPlayerOwnership(user, setting, countdown) { static #getPlayerOwnership(user, setting, countdown) {
if (user.isGM) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const playerOwnership = countdown.ownership[user.id]; const playerOwnership = countdown.ownership[user.id];
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
? setting.defaultOwnership ? setting.defaultOwnership
@ -147,7 +162,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return true; return true;
} }
static async #onToggleViewMode() { static async #toggleViewMode() {
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode); const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
const appMode = CONFIG.DH.GENERAL.countdownAppMode; const appMode = CONFIG.DH.GENERAL.countdownAppMode;
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon; const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
@ -158,16 +173,15 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
this.render(); this.render();
} }
static async #onEditCountdowns() { static async #editCountdowns() {
new game.system.api.applications.ui.CountdownEdit().render(true); new game.system.api.applications.ui.CountdownEdit().render(true);
} }
static async #onLoopCountdown(_, target) { static async #loopCountdown(_, target) {
if (!DhCountdowns.canPerformEdit()) return; if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdownId = target.closest('[data-countdown]').dataset.countdown; const countdown = settings.countdowns[target.id];
const countdown = settings.countdowns[countdownId];
let progressMax = countdown.progress.start; let progressMax = countdown.progress.start;
let message = null; let message = null;
@ -181,17 +195,17 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? Number(progressMax) + 1 ? Number(progressMax) + 1
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? Math.max(Number(progressMax) - 1, 0) ? Math.max(Number(progressMax) - 1, 0)
: progressMax; : progressMax;
await waitForDiceSoNice(message); await waitForDiceSoNice(message);
await settings.updateSource({ await settings.updateSource({
[`countdowns.${countdownId}.progress`]: { [`countdowns.${target.id}.progress`]: {
current: newMax, current: newMax,
start: newMax start: newMax
} }
}); });
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
} }
@ -200,23 +214,22 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
if (!DhCountdowns.canPerformEdit()) return; if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdownId = target.closest('[data-countdown]').dataset.countdown; const countdown = settings.countdowns[target.id];
const countdown = settings.countdowns[countdownId];
const newCurrent = increase const newCurrent = increase
? Math.min(countdown.progress.current + 1, countdown.progress.start) ? Math.min(countdown.progress.current + 1, countdown.progress.start)
: Math.max(countdown.progress.current - 1, 0); : Math.max(countdown.progress.current - 1, 0);
await settings.updateSource({ [`countdowns.${countdownId}.progress.current`]: newCurrent }); await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
} }
static async gmSetSetting(data) { static async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, { game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown } data: { refreshType: RefreshType.Countdown }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
} }
@ -265,8 +278,10 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return acc; return acc;
}, {}) }, {})
}; };
await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { await emitAsGM(GMUpdateEvent.UpdateCountdowns,
refreshType: RefreshType.Countdown DhCountdowns.gmSetSetting.bind(settings),
settings, null, {
refreshType: RefreshType.Countdown
}); });
} }

View file

@ -1,4 +1,3 @@
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
import { RefreshType } from '../../systemRegistration/socket.mjs'; import { RefreshType } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -49,9 +48,11 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
for (const element of this.element?.querySelectorAll('.effect-container a') ?? []) {
element.addEventListener('click', e => this.#onClickEffect(e)); if (this.element) {
element.addEventListener('contextmenu', e => this.#onClickEffect(e, -1)); this.element.querySelectorAll('.effect-container a').forEach(element => {
element.addEventListener('contextmenu', this.removeEffect.bind(this));
});
} }
} }
@ -67,11 +68,11 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
const actor = token const actor = token
? token.actor ? token.actor
: canvas.tokens.controlled.length === 0 : canvas.tokens.controlled.length === 0
? !game.user.isGM ? !game.user.isGM
? game.user.character ? game.user.character
: null : null
: canvas.tokens.controlled[0].actor; : canvas.tokens.controlled[0].actor;
return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []); return actor?.getActiveEffects() ?? [];
}; };
toggleHidden(token, focused) { toggleHidden(token, focused) {
@ -85,22 +86,11 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
this.render(); this.render();
} }
async #onClickEffect(event, delta = 1) { async removeEffect(event) {
const element = event.target.closest('.effect-container'); const element = event.target.closest('.effect-container');
const effects = DhEffectsDisplay.getTokenEffects(); const effects = DhEffectsDisplay.getTokenEffects();
const effect = effects.find(x => x.id === element.dataset.effectId); const effect = effects.find(x => x.id === element.dataset.effectId);
if (!effect || (delta >= 0 && !effect.system.stacking)) { await effect.delete();
return;
}
const maxValue = effect.system.stacking?.max ?? Infinity;
const newValue = Math.clamp((effect.system.stacking?.value ?? 1) + delta, 0, maxValue);
if (newValue > 0) {
await effect.update({ 'system.stacking.value': newValue });
} else {
await effect.delete();
}
this.render();
} }
setupHooks() { setupHooks() {

View file

@ -1,4 +1,4 @@
import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -104,7 +104,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
} }
async updateFear(value) { async updateFear(value) {
return emitGMUpdate( return emitAsGM(
GMUpdateEvent.UpdateFear, GMUpdateEvent.UpdateFear,
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
value value

View file

@ -1,4 +1,3 @@
import { getDocFromElement } from '../../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -48,8 +47,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
expandContent: this.expandContent, expandContent: this.expandContent,
resetFilters: this.resetFilters, resetFilters: this.resetFilters,
sortList: this.sortList, sortList: this.sortList,
openSettings: this.openSettings, openSettings: this.openSettings
viewSheet: this.#onViewSheet
}, },
position: { position: {
left: 100, left: 100,
@ -111,8 +109,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
); );
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position; options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
delete options.position.zIndex;
if (!userPresetPosition) { if (!userPresetPosition) {
const width = noFolder === true || lite === true ? 600 : 850; const width = noFolder === true || lite === true ? 600 : 850;
@ -279,7 +277,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description)); (await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
} }
this.fieldFilter = await this._createFieldFilter(); this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) { if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k, v]) => { Object.entries(this.presets.filter).forEach(([k, v]) => {
@ -308,8 +306,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
{ {
items: this.items, items: this.items,
menu: this.selectedMenu, menu: this.selectedMenu,
formatLabel: this.formatLabel, formatLabel: this.formatLabel
viewSheet: this.items[0] instanceof Actor
} }
); );
@ -358,12 +355,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
); );
} }
async _createFieldFilter() { _createFieldFilter() {
const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters'); const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters');
for (const f of filters) { filters.forEach(f => {
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field); if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
else if (typeof f.choices === 'function') { else if (typeof f.choices === 'function') {
f.choices = await f.choices(this.items); f.choices = f.choices(this.items);
} }
// Clear field label so template uses our custom label parameter // Clear field label so template uses our custom label parameter
@ -373,8 +370,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
f.name ??= f.key; f.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null; f.value = this.presets?.filter?.[f.name]?.value ?? null;
} });
return filters; return filters;
} }
@ -571,11 +567,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
} }
} }
static async #onViewSheet(_, target) {
const document = await getDocFromElement(target);
document?.sheet?.render(true);
}
_createDragProcess() { _createDragProcess() {
new foundry.applications.ux.DragDrop.implementation({ new foundry.applications.ux.DragDrop.implementation({
dragSelector: '.item-container', dragSelector: '.item-container',
@ -614,16 +605,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
items: { items: {
folder: 'equipments', folder: 'equipments',
render: { render: {
folders: [ noFolder: true
'equipments',
'ancestries',
'classes',
'subclasses',
'domains',
'communities',
'beastforms'
// excluded: features
]
} }
}, },
compendium: {} compendium: {}

View file

@ -1,27 +0,0 @@
export default class DhProgress {
#notification;
constructor({ max, label = '' }) {
this.max = max;
this.label = label;
this.#notification = ui.notifications.info(this.label, { progress: true });
}
updateMax(newMax) {
this.max = newMax;
}
advance({ by = 1, label = this.label } = {}) {
if (this.value === this.max) return;
this.value = (this.value ?? 0) + Math.abs(by);
this.#notification.update({ message: label, pct: this.value / this.max });
}
close({ label = '' } = {}) {
this.#notification.update({ message: label, pct: 1 });
}
static createMigrationProgress(max = 0) {
return new DhProgress({ max, label: game.i18n.localize('DAGGERHEART.UI.Progress.migrationLabel') });
}
}

View file

@ -1,4 +1,4 @@
import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation { export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
/** @inheritdoc */ /** @inheritdoc */
@ -31,7 +31,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
const environments = daggerheartInfo.sceneEnvironments.filter( const environments = daggerheartInfo.sceneEnvironments.filter(
x => x && x.testUserPermission(game.user, 'LIMITED') x => x && x.testUserPermission(game.user, 'LIMITED')
); );
const hasEnvironments = environments.length > 0 && x.active; const hasEnvironments = environments.length > 0 && x.isView;
return { return {
...x, ...x,
hasEnvironments, hasEnvironments,
@ -39,10 +39,9 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
environments: environments environments: environments
}; };
}); });
context.scenes.active = extendScenes(context.scenes.active); context.scenes.active = extendScenes(context.scenes.active);
context.scenes.inactive = extendScenes(context.scenes.inactive); context.scenes.inactive = extendScenes(context.scenes.inactive);
context.scenes.viewed = context.scenes.viewed ? extendScenes([context.scenes.viewed])[0] : null;
return context; return context;
} }
@ -68,7 +67,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
1 1
)[0]; )[0];
newEnvironments.unshift(newFirst); newEnvironments.unshift(newFirst);
emitGMUpdate( emitAsGM(
GMUpdateEvent.UpdateDocument, GMUpdateEvent.UpdateDocument,
scene.update.bind(scene), scene.update.bind(scene),
{ 'flags.daggerheart.sceneEnvironments': newEnvironments }, { 'flags.daggerheart.sceneEnvironments': newEnvironments },

View file

@ -1,4 +1,97 @@
/**
* @typedef ContextMenuEntry
* @property {string} name The context menu label. Can be localized.
* @property {string} [icon] A string containing an HTML icon element for the menu item.
* @property {string} [classes] Additional CSS classes to apply to this menu item.
* @property {string} [group] An identifier for a group this entry belongs to.
* @property {ContextMenuJQueryCallback} callback The function to call when the menu item is clicked.
* @property {ContextMenuCondition|boolean} [condition] A function to call or boolean value to determine if this entry
* appears in the menu.
*/
/**
* @callback ContextMenuCondition
* @param {jQuery|HTMLElement} html The element of the context menu entry.
* @returns {boolean} Whether the entry should be rendered in the context menu.
*/
/**
* @callback ContextMenuCallback
* @param {HTMLElement} target The element that the context menu has been triggered for.
* @returns {unknown}
*/
/**
* @callback ContextMenuJQueryCallback
* @param {HTMLElement|jQuery} target The element that the context menu has been triggered for. Will
* either be a jQuery object or an HTMLElement instance, depending
* on how the ContextMenu was configured.
* @returns {unknown}
*/
/**
* @typedef ContextMenuOptions
* @property {string} [eventName="contextmenu"] Optionally override the triggering event which can spawn the menu. If
* the menu is using fixed positioning, this event must be a MouseEvent.
* @property {ContextMenuCallback} [onOpen] A function to call when the context menu is opened.
* @property {ContextMenuCallback} [onClose] A function to call when the context menu is closed.
* @property {boolean} [fixed=false] If true, the context menu is given a fixed position rather than being
* injected into the target.
* @property {boolean} [jQuery=true] If true, callbacks will be passed jQuery objects instead of HTMLElement
* instances.
*/
/**
* @typedef ContextMenuRenderOptions
* @property {Event} [event] The event that triggered the context menu opening.
* @property {boolean} [animate=true] Animate the context menu opening.
*/
/**
* A subclass of ContextMenu.
* @extends {foundry.applications.ux.ContextMenu}
*/
export default class DHContextMenu extends foundry.applications.ux.ContextMenu { export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
/**
* @param {HTMLElement|jQuery} container - The HTML element that contains the context menu targets.
* @param {string} selector - A CSS selector which activates the context menu.
* @param {ContextMenuEntry[]} menuItems - An Array of entries to display in the menu
* @param {ContextMenuOptions} [options] - Additional options to configure the context menu.
*/
constructor(container, selector, menuItems, options) {
super(container, selector, menuItems, options);
/** @deprecated since v13 until v15 */
this.#jQuery = options.jQuery;
}
/**
* Whether to pass jQuery objects or HTMLElement instances to callback.
* @type {boolean}
*/
#jQuery;
/**@inheritdoc */
activateListeners(menu) {
menu.addEventListener('click', this.#onClickItem.bind(this));
}
/**
* Handle click events on context menu items.
* @param {PointerEvent} event The click event
*/
#onClickItem(event) {
event.preventDefault();
event.stopPropagation();
const element = event.target.closest('.context-item');
if (!element) return;
const item = this.menuItems.find(i => i.element === element);
item?.callback(this.#jQuery ? $(this.target) : this.target, event);
this.close();
}
/* -------------------------------------------- */
/** /**
* Trigger a context menu event in response to a normal click on a additional options button. * Trigger a context menu event in response to a normal click on a additional options button.
* @param {PointerEvent} event * @param {PointerEvent} event
@ -6,11 +99,8 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
static triggerContextMenu(event, altSelector) { static triggerContextMenu(event, altSelector) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const selector = altSelector ?? '[data-item-uuid]';
if (ui.context?.selector === selector) return;
const { clientX, clientY } = event; const { clientX, clientY } = event;
const selector = altSelector ?? '[data-item-uuid]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent( target?.dispatchEvent(
new PointerEvent('contextmenu', { new PointerEvent('contextmenu', {

View file

@ -1,6 +1,5 @@
export { default as DhMeasuredTemplate } from './measuredTemplate.mjs'; export { default as DhMeasuredTemplate } from './measuredTemplate.mjs';
export { default as DhRuler } from './ruler.mjs'; export { default as DhRuler } from './ruler.mjs';
export { default as DhRegion } from './region.mjs'; export { default as DhTemplateLayer } from './templateLayer.mjs';
export { default as DhRegionLayer } from './regionLayer.mjs';
export { default as DhTokenPlaceable } from './token.mjs'; export { default as DhTokenPlaceable } from './token.mjs';
export { default as DhTokenRuler } from './tokenRuler.mjs'; export { default as DhTokenRuler } from './tokenRuler.mjs';

View file

@ -1,12 +0,0 @@
import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhRegion extends foundry.canvas.placeables.Region {
/**@inheritdoc */
_formatMeasuredDistance(distance) {
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (!range.enabled) return super._formatMeasuredDistance(distance);
const { distance: resultDistance, units } = DhMeasuredTemplate.getRangeLabels(distance, range);
return `${resultDistance} ${units}`;
}
}

View file

@ -1,155 +0,0 @@
export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
const { tools, ...rest } = super.prepareSceneControls();
return {
...rest,
tools: {
select: tools.select,
templateMode: tools.templateMode,
rectangle: tools.rectangle,
circle: tools.circle,
ellipse: tools.ellipse,
cone: tools.cone,
inFront: {
name: 'inFront',
order: 7,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ring: { ...tools.ring, order: 8 },
line: { ...tools.line, order: 9 },
emanation: { ...tools.emanation, order: 10 },
polygon: { ...tools.polygon, order: 11 },
hole: { ...tools.hole, order: 12 },
snap: { ...tools.snap, order: 13 },
clear: { ...tools.clear, order: 14 }
}
};
}
/** @inheritDoc */
_isCreationToolActive() {
return this.active && (game.activeTool === 'inFront' || game.activeTool in foundry.data.BaseShapeData.TYPES);
}
_createDragShapeData(event) {
const hole = ui.controls.controls[this.options.name].tools.hole?.active ?? false;
if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole };
const shape = super._createDragShapeData(event);
const token =
shape?.type === 'emanation' && shape.base?.type === 'token'
? this.#findTokenInBounds(event.interactionData.origin)
: null;
if (token) {
shape.base.width = token.width;
shape.base.height = token.height;
event.interactionData.origin = token.getCenterPoint();
}
return shape;
}
async placeRegion(data, options = {}) {
const preConfirm = data => {
const shape = data.document.shapes[0];
const isEmanation = shape.type === 'emanation';
if (isEmanation) {
const token = this.#findTokenInBounds(shape.base.origin);
if (!token) return options.preConfirm?.(data) ?? true;
const shapeData = shape.toObject();
data.document.updateSource({
shapes: [
{
...shapeData,
base: {
...shapeData.base,
height: token.height,
width: token.width,
x: token.x,
y: token.y
}
}
]
});
}
return options?.preConfirm?.(data) ?? true;
};
return await super.placeRegion(data, { ...options, preConfirm });
}
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */
#findTokenInBounds(origin) {
const { x, y } = origin;
const gridSize = canvas.grid.size;
const inBounds = canvas.scene.tokens.filter(t => {
return x.between(t.x, t.x + t.width * gridSize) && y.between(t.y, t.y + t.height * gridSize);
});
return inBounds.length === 1 ? inBounds[0] : null;
}
static getTemplateShape({ type, angle, range, direction } = {}) {
const { line, rectangle, inFront, cone, circle, emanation } = CONFIG.DH.GENERAL.templateTypes;
/* Length calculation */
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
const settings = canvas.scene?.rangeSettings;
const rangeNumber = Number(range);
const length = (!Number.isNaN(rangeNumber) ? rangeNumber : settings ? settings[range] : 0) * dimensionConstant;
/*----*/
const shapeData = {
...canvas.mousePosition,
type: type,
direction: direction ?? 0
};
switch (type) {
case rectangle.id:
shapeData.width = length;
shapeData.height = length;
break;
case line.id:
shapeData.length = length;
shapeData.width = 5 * dimensionConstant;
break;
case cone.id:
shapeData.angle = angle ?? CONFIG.MeasuredTemplate.defaults.angle;
shapeData.radius = length;
break;
case inFront.id:
shapeData.angle = '180';
shapeData.radius = length;
shapeData.type = cone.id;
break;
case circle.id:
shapeData.radius = length;
break;
case emanation.id:
shapeData.radius = length;
shapeData.base = {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
};
break;
}
return shapeData;
}
}

View file

@ -0,0 +1,116 @@
export default class DhTemplateLayer extends foundry.canvas.layers.TemplateLayer {
static prepareSceneControls() {
const sc = foundry.applications.ui.SceneControls;
return {
name: 'templates',
order: 2,
title: 'CONTROLS.GroupMeasure',
icon: 'fa-solid fa-ruler-combined',
visible: game.user.can('TEMPLATE_CREATE'),
onChange: (event, active) => {
if (active) canvas.templates.activate();
},
onToolChange: () => canvas.templates.setAllRenderFlags({ refreshState: true }),
tools: {
circle: {
name: 'circle',
order: 1,
title: 'CONTROLS.MeasureCircle',
icon: 'fa-regular fa-circle',
toolclip: {
src: 'toolclips/tools/measure-circle.webm',
heading: 'CONTROLS.MeasureCircle',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete'])
}
},
cone: {
name: 'cone',
order: 2,
title: 'CONTROLS.MeasureCone',
icon: 'fa-solid fa-angle-left',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.MeasureCone',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
inFront: {
name: 'inFront',
order: 3,
title: 'CONTROLS.inFront',
icon: 'fa-solid fa-eye',
toolclip: {
src: 'toolclips/tools/measure-cone.webm',
heading: 'CONTROLS.inFront',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
rect: {
name: 'rect',
order: 4,
title: 'CONTROLS.MeasureRect',
icon: 'fa-regular fa-square',
toolclip: {
src: 'toolclips/tools/measure-rect.webm',
heading: 'CONTROLS.MeasureRect',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
ray: {
name: 'ray',
order: 5,
title: 'CONTROLS.MeasureRay',
icon: 'fa-solid fa-up-down',
toolclip: {
src: 'toolclips/tools/measure-ray.webm',
heading: 'CONTROLS.MeasureRay',
items: sc.buildToolclipItems(['create', 'move', 'edit', 'hide', 'delete', 'rotate'])
}
},
clear: {
name: 'clear',
order: 6,
title: 'CONTROLS.MeasureClear',
icon: 'fa-solid fa-trash',
visible: game.user.isGM,
onChange: () => canvas.templates.deleteAll(),
button: true
}
},
activeTool: 'circle'
};
}
_onDragLeftStart(event) {
const interaction = event.interactionData;
// Snap the origin to the grid
if (!event.shiftKey) interaction.origin = this.getSnappedPoint(interaction.origin);
// Create a pending MeasuredTemplateDocument
const tool = game.activeTool === 'inFront' ? 'cone' : game.activeTool;
const previewData = {
user: game.user.id,
t: tool,
x: interaction.origin.x,
y: interaction.origin.y,
sort: Math.max(this.getMaxSort() + 1, 0),
distance: 1,
direction: 0,
fillColor: game.user.color || '#FF0000',
hidden: event.altKey
};
const defaults = CONFIG.MeasuredTemplate.defaults;
if (game.activeTool === 'cone') previewData.angle = defaults.angle;
else if (game.activeTool === 'inFront') previewData.angle = 180;
else if (game.activeTool === 'ray') previewData.width = defaults.width * canvas.dimensions.distance;
const cls = foundry.utils.getDocumentClass('MeasuredTemplate');
const doc = new cls(previewData, { parent: canvas.scene });
// Create a preview MeasuredTemplate object
const template = new this.constructor.placeableClass(doc);
doc._object = template;
interaction.preview = this.preview.addChild(template);
template.draw();
}
}

View file

@ -1,4 +1,3 @@
import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs';
import DhMeasuredTemplate from './measuredTemplate.mjs'; import DhMeasuredTemplate from './measuredTemplate.mjs';
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
@ -10,36 +9,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.previewHelp ||= this.addChild(this.#drawPreviewHelp()); this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
} }
/**@inheritdoc */
_refreshTurnMarker() {
// Should a Turn Marker be active?
const { turnMarker } = this.document;
const markersEnabled =
CONFIG.Combat.settings.turnMarker.enabled && turnMarker.mode !== CONST.TOKEN_TURN_MARKER_MODES.DISABLED;
const spotlighted = game.settings
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightTracker)
.spotlightedTokens.has(this.document.uuid);
const turnIsSet = typeof game.combat?.turn === 'number';
const isTurn = game.combat?.combatant?.tokenId === this.id;
const markerActive = markersEnabled && turnIsSet ? isTurn : spotlighted;
// Activate a Turn Marker
if (markerActive) {
if (!this.turnMarker)
this.turnMarker = this.addChildAt(new foundry.canvas.placeables.tokens.TokenTurnMarker(this), 0);
canvas.tokens.turnMarkers.add(this);
this.turnMarker.draw();
}
// Remove a Turn Marker
else if (this.turnMarker) {
canvas.tokens.turnMarkers.delete(this);
this.turnMarker.destroy();
this.turnMarker = null;
}
}
/** @inheritDoc */ /** @inheritDoc */
async _drawEffects() { async _drawEffects() {
this.effects.renderable = false; this.effects.renderable = false;
@ -51,7 +20,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.effects.overlay = null; this.effects.overlay = null;
// Categorize effects // Categorize effects
const activeEffects = getIconVisibleActiveEffects(Array.from(this.actor?.allApplicableEffects() ?? [])); const activeEffects = this.actor?.getActiveEffects() ?? [];
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay')); const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
// Draw effects // Draw effects
@ -60,8 +29,8 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
if (!effect.img) continue; if (!effect.img) continue;
const promise = const promise =
effect === overlayEffect effect === overlayEffect
? this._drawOverlay(effect.img, effect.tint, effect) ? this._drawOverlay(effect.img, effect.tint)
: this._drawEffect(effect.img, effect.tint, effect); : this._drawEffect(effect.img, effect.tint);
promises.push( promises.push(
promise.then(e => { promise.then(e => {
if (e) e.zIndex = i; if (e) e.zIndex = i;
@ -75,39 +44,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true }); this.renderFlags.set({ refreshEffects: true });
} }
/**@inheritdoc */
async _drawEffect(src, tint, effect) {
if (!src) return;
const tex = await foundry.canvas.loadTexture(src, { fallback: 'icons/svg/hazard.svg' });
const icon = new PIXI.Sprite(tex);
icon.tint = tint ?? 0xffffff;
if (effect.system.stacking?.value > 1) {
const stackOverlay = new PIXI.Text(effect.system.stacking.value, {
fill: '#f3c267',
stroke: '#000000',
fontSize: 96,
strokeThickness: 4
});
const nrDigits = Math.floor(Math.log10(effect.system.stacking.value)) + 1;
stackOverlay.y = -8;
/* This does not account for 1:s being much less wide than other digits. I don't think it's desired however as it makes it look jumpy */
stackOverlay.x = icon.width - 8 - nrDigits * 56;
stackOverlay.anchor.set(0, 0);
icon.addChild(stackOverlay);
}
return this.effects.addChild(icon);
}
async _drawOverlay(src, tint, effect) {
const icon = await this._drawEffect(src, tint, effect);
if (icon) icon.alpha = 0.8;
this.effects.overlay = icon ?? null;
return icon;
}
/** /**
* Returns the distance from this token to another token object. * Returns the distance from this token to another token object.
* This value is corrected to handle alternate token sizes and other grid types * This value is corrected to handle alternate token sizes and other grid types
@ -155,15 +91,15 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint); const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint);
const adjustedOriginPoint = originEdge const adjustedOriginPoint = originEdge
? canvas.grid.getTopLeftPoint({ ? canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x), x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
y: originEdge.y + Math.sign(originPoint.y - originEdge.y) y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
}) })
: originPoint; : originPoint;
const adjustDestinationPoint = targetEdge const adjustDestinationPoint = targetEdge
? canvas.grid.getTopLeftPoint({ ? canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y)
}) })
: targetPoint; : targetPoint;
const distance = canvas.grid.measurePath([ const distance = canvas.grid.measurePath([
{ ...adjustedOriginPoint, elevation: 0 }, { ...adjustedOriginPoint, elevation: 0 },
@ -249,6 +185,9 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritDoc */ /** @inheritDoc */
_drawBar(number, bar, data) { _drawBar(number, bar, data) {
const val = Number(data.value);
const pct = Math.clamp(val, 0, data.max) / data.max;
// Determine sizing // Determine sizing
const { width, height } = this.document.getSize(); const { width, height } = this.document.getSize();
const s = canvas.dimensions.uiScale; const s = canvas.dimensions.uiScale;
@ -256,19 +195,17 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
const bh = 8 * (this.document.height >= 2 ? 1.5 : 1) * s; const bh = 8 * (this.document.height >= 2 ? 1.5 : 1) * s;
// Determine the color to use // Determine the color to use
const Color = foundry.utils.Color; const fillColor =
const fillColor = number === 0 ? Color.fromRGB([1, 0, 0]) : Color.fromString('#0032b1'); number === 0 ? foundry.utils.Color.fromRGB([1, 0, 0]) : foundry.utils.Color.fromString('#0032b1');
const emptyColor = Color.fromRGB([0, 0, 0]);
// Draw the bar (accounting floating point numbers from bar animations) // Draw the bar
const widthUnit = bw / Math.ceil(data.max); const widthUnit = bw / data.max;
bar.clear().lineStyle(s, 0x000000, 1.0); bar.clear().lineStyle(s, 0x000000, 1.0);
const sections = [...Array(Math.ceil(data.max)).keys()]; const sections = [...Array(data.max).keys()];
for (const mark of sections) { for (let mark of sections) {
const x = mark * widthUnit; const x = mark * widthUnit;
const marked = mark < Math.ceil(data.value); const marked = mark + 1 <= data.value;
const remainder = mark === Math.ceil(data.value) - 1 ? data.value % 1 : 0; const color = marked ? fillColor : foundry.utils.Color.fromRGB([0, 0, 0]);
const color = !marked ? emptyColor : remainder ? emptyColor.mix(fillColor, remainder) : fillColor;
if (mark === 0 || mark === sections.length - 1) { if (mark === 0 || mark === sections.length - 1) {
bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); // Would like drawRoundedRect, but it's very troublsome with the corners. Leaving for now. bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); // Would like drawRoundedRect, but it's very troublsome with the corners. Leaving for now.
} else { } else {

View file

@ -1 +1,16 @@
export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {} export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {
async _createPreview(createData, options) {
if (options.actor) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
if (options.actor?.system.metadata.usesSize) {
const tokenSize = tokenSizes[options.actor.system.size];
if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
createData.width = tokenSize;
createData.height = tokenSize;
}
}
}
return super._createPreview(createData, options);
}
}

View file

@ -115,10 +115,3 @@ export const advantageState = {
value: 1 value: 1
} }
}; };
export const areaTypes = {
placed: {
id: 'placed',
label: 'Placed Area'
}
};

View file

@ -70,40 +70,10 @@ export const range = {
} }
}; };
export const groupAttackRange = {
melee: range.melee,
veryClose: range.veryClose,
close: range.close,
far: range.far,
veryFar: range.veryFar
};
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
export const templateTypes = { export const templateTypes = {
circle: { ...CONST.MEASURED_TEMPLATE_TYPES,
id: 'circle', EMANATION: 'emanation',
label: 'Circle' INFRONT: 'inFront'
},
cone: {
id: 'cone',
label: 'Cone'
},
rectangle: {
id: 'rectangle',
label: 'Rectangle'
},
line: {
id: 'line',
label: 'Line'
},
emanation: {
id: 'emanation',
label: 'Emanation'
},
inFront: {
id: 'inFront',
label: 'In Front'
}
}; };
export const rangeInclusion = { export const rangeInclusion = {
@ -271,8 +241,8 @@ export const defaultRestOptions = {
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
parts: { parts: [
hitPoints: { {
applyTo: healingTypes.hitPoints.id, applyTo: healingTypes.hitPoints.id,
value: { value: {
custom: { custom: {
@ -281,7 +251,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
} }
}, },
@ -305,8 +275,8 @@ export const defaultRestOptions = {
type: 'self' type: 'self'
}, },
damage: { damage: {
parts: { parts: [
stress: { {
applyTo: healingTypes.stress.id, applyTo: healingTypes.stress.id,
value: { value: {
custom: { custom: {
@ -315,7 +285,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
} }
}, },
@ -340,8 +310,8 @@ export const defaultRestOptions = {
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
parts: { parts: [
armor: { {
applyTo: healingTypes.armor.id, applyTo: healingTypes.armor.id,
value: { value: {
custom: { custom: {
@ -350,7 +320,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
} }
}, },
@ -374,8 +344,8 @@ export const defaultRestOptions = {
type: 'self' type: 'self'
}, },
damage: { damage: {
parts: { parts: [
hope: { {
applyTo: healingTypes.hope.id, applyTo: healingTypes.hope.id,
value: { value: {
custom: { custom: {
@ -384,7 +354,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
}, },
prepareWithFriends: { prepareWithFriends: {
@ -398,8 +368,8 @@ export const defaultRestOptions = {
type: 'self' type: 'self'
}, },
damage: { damage: {
parts: { parts: [
hope: { {
applyTo: healingTypes.hope.id, applyTo: healingTypes.hope.id,
value: { value: {
custom: { custom: {
@ -408,7 +378,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
} }
}, },
@ -435,8 +405,8 @@ export const defaultRestOptions = {
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
parts: { parts: [
hitPoints: { {
applyTo: healingTypes.hitPoints.id, applyTo: healingTypes.hitPoints.id,
value: { value: {
custom: { custom: {
@ -445,7 +415,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
} }
}, },
@ -469,8 +439,8 @@ export const defaultRestOptions = {
type: 'self' type: 'self'
}, },
damage: { damage: {
parts: { parts: [
stress: { {
applyTo: healingTypes.stress.id, applyTo: healingTypes.stress.id,
value: { value: {
custom: { custom: {
@ -479,7 +449,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
} }
}, },
@ -504,17 +474,17 @@ export const defaultRestOptions = {
type: 'friendly' type: 'friendly'
}, },
damage: { damage: {
parts: { parts: [
armor: { {
applyTo: healingTypes.armor.id, applyTo: healingTypes.armor.id,
value: { value: {
custom: { custom: {
enabled: true, enabled: true,
formula: '@system.armorScore.max' formula: '@system.armorScore'
} }
} }
} }
} ]
} }
} }
}, },
@ -538,8 +508,8 @@ export const defaultRestOptions = {
type: 'self' type: 'self'
}, },
damage: { damage: {
parts: { parts: [
hope: { {
applyTo: healingTypes.hope.id, applyTo: healingTypes.hope.id,
value: { value: {
custom: { custom: {
@ -548,7 +518,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
}, },
prepareWithFriends: { prepareWithFriends: {
@ -562,8 +532,8 @@ export const defaultRestOptions = {
type: 'self' type: 'self'
}, },
damage: { damage: {
parts: { parts: [
hope: { {
applyTo: healingTypes.hope.id, applyTo: healingTypes.hope.id,
value: { value: {
custom: { custom: {
@ -572,7 +542,7 @@ export const defaultRestOptions = {
} }
} }
} }
} ]
} }
} }
}, },
@ -734,14 +704,14 @@ const getDiceSoNiceSFX = sfxOptions => {
if (sfxOptions.critical && criticalAnimationData.class) { if (sfxOptions.critical && criticalAnimationData.class) {
return { return {
specialEffect: criticalAnimationData.class, specialEffect: criticalAnimationData.class,
options: { ...criticalAnimationData.options } options: {}
}; };
} }
if (sfxOptions.higher && sfxOptions.data.higher) { if (sfxOptions.higher && sfxOptions.data.higher) {
return { return {
specialEffect: sfxOptions.data.higher.class, specialEffect: sfxOptions.data.higher.class,
options: { ...sfxOptions.data.higher.options } options: {}
}; };
} }
@ -984,144 +954,3 @@ export const sceneRangeMeasurementSetting = {
label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.custom' label: 'DAGGERHEART.CONFIG.SceneRangeMeasurementTypes.custom'
} }
}; };
export const tagTeamRollTypes = {
trait: {
id: 'trait',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait'
},
ability: {
id: 'ability',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability'
},
damageAbility: {
id: 'damageAbility',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility'
}
};
export const baseActiveEffectModes = {
custom: {
id: 'custom',
priority: 0,
label: 'EFFECT.CHANGES.TYPES.custom'
},
multiply: {
id: 'multiply',
priority: 10,
label: 'EFFECT.CHANGES.TYPES.multiply'
},
add: {
id: 'add',
priority: 20,
label: 'EFFECT.CHANGES.TYPES.add'
},
subtract: {
id: 'subtract',
priority: 20,
label: 'EFFECT.CHANGES.TYPES.subtract'
},
downgrade: {
id: 'downgrade',
priority: 30,
label: 'EFFECT.CHANGES.TYPES.downgrade'
},
upgrade: {
id: 'upgrade',
priority: 40,
label: 'EFFECT.CHANGES.TYPES.upgrade'
},
override: {
id: 'override',
priority: 50,
label: 'EFFECT.CHANGES.TYPES.override'
}
};
export const activeEffectModes = {
armor: {
id: 'armor',
priority: 20,
label: 'TYPES.ActiveEffect.armor'
},
...baseActiveEffectModes
};
export const activeEffectArmorInteraction = {
none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' },
active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' },
inactive: { id: 'inactive', label: 'DAGGERHEART.CONFIG.ArmorInteraction.inactive.label' }
};
export const activeEffectDurations = {
temporary: {
id: 'temporary',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.temporary'
},
act: {
id: 'act',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.act'
},
scene: {
id: 'scene',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.scene'
},
shortRest: {
id: 'shortRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.shortRest'
},
longRest: {
id: 'longRest',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.longRest'
},
session: {
id: 'session',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.session'
},
custom: {
id: 'custom',
label: 'DAGGERHEART.CONFIG.ActiveEffectDuration.custom'
}
};
export const fallAndCollisionDamage = {
veryClose: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.veryClose.chatTitle',
damageFormula: '1d10 + 3'
},
close: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.close.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.close.chatTitle',
damageFormula: '1d20 + 5'
},
far: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.far.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.far.chatTitle',
damageFormula: '1d100 + 15'
},
collision: {
id: 'veryClose',
label: 'DAGGERHEART.CONFIG.fallAndCollision.collision.label',
chatTitle: 'DAGGERHEART.CONFIG.fallAndCollision.collision.chatTitle',
damageFormula: '1d20 + 5'
}
};
export const simpleDispositions = {
[-1]: {
id: -1,
label: 'TOKEN.DISPOSITION.HOSTILE'
},
[0]: {
id: 0,
label: 'TOKEN.DISPOSITION.NEUTRAL'
},
[1]: {
id: 1,
label: 'TOKEN.DISPOSITION.FRIENDLY'
}
};

View file

@ -1,6 +1,4 @@
export const hooksConfig = { export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle', effectDisplayToggle: 'DHEffectDisplayToggle',
lockedTooltipDismissed: 'DHLockedTooltipDismissed', lockedTooltipDismissed: 'DHLockedTooltipDismissed'
tagTeamStart: 'DHTagTeamRollStart',
groupRollStart: 'DHGroupRollStart'
}; };

View file

@ -70,65 +70,17 @@ export const typeConfig = {
} }
] ]
}, },
environments: {
columns: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
},
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type',
format: type => {
if (!type) return '-';
return CONFIG.DH.ACTOR.environmentTypes[type].label;
}
}
],
filters: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
field: 'system.api.models.actors.DhEnvironment.schema.fields.tier'
},
{
key: 'system.type',
label: 'DAGGERHEART.GENERAL.type',
field: 'system.api.models.actors.DhEnvironment.schema.fields.type'
},
{
key: 'system.difficulty',
name: 'difficulty.min',
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin',
field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty',
operator: 'gte'
},
{
key: 'system.difficulty',
name: 'difficulty.max',
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax',
field: 'system.api.models.actors.DhEnvironment.schema.fields.difficulty',
operator: 'lte'
}
]
},
items: { items: {
columns: [ columns: [
{ {
key: 'type', key: 'type',
label: 'DAGGERHEART.GENERAL.type', label: 'DAGGERHEART.GENERAL.type',
format: type => (type ? `TYPES.Item.${type}` : '-') format: type => type ? `TYPES.Item.${type}` : '-'
}, },
{ {
key: 'system.secondary', key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype', label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => format: isSecondary => (isSecondary ? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short' : isSecondary === false ? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' : '-')
isSecondary
? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short'
: isSecondary === false
? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short'
: '-'
}, },
{ {
key: 'system.tier', key: 'system.tier',
@ -220,8 +172,8 @@ export const typeConfig = {
key: 'system.secondary', key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype', label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [ choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.full' }, { value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.full' } { value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short' }
] ]
}, },
{ {
@ -308,12 +260,12 @@ export const typeConfig = {
{ {
key: 'system.type', key: 'system.type',
label: 'DAGGERHEART.GENERAL.type', label: 'DAGGERHEART.GENERAL.type',
format: type => (type ? `DAGGERHEART.CONFIG.DomainCardTypes.${type}` : '-') format: type => type ? `DAGGERHEART.CONFIG.DomainCardTypes.${type}` : '-'
}, },
{ {
key: 'system.domain', key: 'system.domain',
label: 'DAGGERHEART.GENERAL.Domain.single', label: 'DAGGERHEART.GENERAL.Domain.single',
format: domain => (domain ? CONFIG.DH.DOMAIN.allDomains()[domain].label : '-') format: domain => domain ? CONFIG.DH.DOMAIN.allDomains()[domain].label : '-'
}, },
{ {
key: 'system.level', key: 'system.level',
@ -426,8 +378,7 @@ export const typeConfig = {
{ {
key: 'system.linkedClass', key: 'system.linkedClass',
label: 'TYPES.Item.class', label: 'TYPES.Item.class',
format: linkedClass => format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
foundry.utils.fromUuidSync(linkedClass)?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
}, },
{ {
key: 'system.spellcastingTrait', key: 'system.spellcastingTrait',
@ -437,20 +388,15 @@ export const typeConfig = {
], ],
filters: [ filters: [
{ {
key: 'system.linkedClass', key: 'system.linkedClass.uuid',
label: 'TYPES.Item.class', label: 'TYPES.Item.class',
choices: async items => { choices: items => {
const list = []; const list = items
for (const item of items.filter(item => item.system.linkedClass)) { .filter(item => item.system.linkedClass)
const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass); .map(item => ({
if (linkedClass) { value: item.system.linkedClass.uuid,
list.push({ label: item.system.linkedClass.name
value: linkedClass.uuid, }));
label: linkedClass.name
});
}
}
return list.reduce((a, c) => { return list.reduce((a, c) => {
if (!a.find(i => i.value === c.value)) a.push(c); if (!a.find(i => i.value === c.value)) a.push(c);
return a; return a;
@ -604,8 +550,7 @@ export const compendiumConfig = {
id: 'environments', id: 'environments',
keys: ['environments'], keys: ['environments'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments', label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
type: ['environment'], type: ['environment']
listType: 'environments'
}, },
beastforms: { beastforms: {
id: 'beastforms', id: 'beastforms',

View file

@ -14,8 +14,8 @@ export const armorFeatures = {
type: 'hostile' type: 'hostile'
}, },
damage: { damage: {
parts: { parts: [
stress: { {
applyTo: 'stress', applyTo: 'stress',
value: { value: {
custom: { custom: {
@ -24,7 +24,7 @@ export const armorFeatures = {
} }
} }
} }
} ]
} }
} }
] ]
@ -453,7 +453,7 @@ export const allArmorFeatures = () => {
const feature = homebrewFeatures[key]; const feature = homebrewFeatures[key];
const actions = feature.actions.map(action => ({ const actions = feature.actions.map(action => ({
...action, ...action,
effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [], effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id))??[],
type: action.type type: action.type
})); }));
const actionEffects = actions.flatMap(a => a.effects); const actionEffects = actions.flatMap(a => a.effects);
@ -489,18 +489,15 @@ export const weaponFeatures = {
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
img: 'icons/skills/melee/shield-block-bash-blue.webp', img: 'icons/skills/melee/shield-block-bash-blue.webp',
changes: [ changes: [
{
key: 'system.armorScore',
mode: 2,
value: 'ITEM.@system.tier + 1'
},
{ {
key: 'system.evasion', key: 'system.evasion',
mode: 2, mode: 2,
value: '-1' value: '-1'
},
{
key: 'Armor',
type: 'armor',
typeData: {
type: 'armor',
max: 'ITEM.@system.tier + 1'
}
} }
] ]
} }
@ -735,8 +732,8 @@ export const weaponFeatures = {
type: 'hostile' type: 'hostile'
}, },
damage: { damage: {
parts: { parts: [
stress: { {
applyTo: 'stress', applyTo: 'stress',
value: { value: {
custom: { custom: {
@ -745,7 +742,7 @@ export const weaponFeatures = {
} }
} }
} }
} ]
} }
} }
], ],
@ -792,6 +789,11 @@ export const weaponFeatures = {
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
img: 'icons/skills/melee/sword-shield-stylized-white.webp', img: 'icons/skills/melee/sword-shield-stylized-white.webp',
changes: [ changes: [
{
key: 'system.armorScore',
mode: 2,
value: '1'
},
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2, mode: 2,
@ -806,22 +808,6 @@ export const weaponFeatures = {
type: 'withinRange' type: 'withinRange'
} }
} }
},
{
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
img: 'icons/skills/melee/sword-shield-stylized-white.webp',
changes: [
{
key: 'Armor',
type: 'armor',
value: 0,
typeData: {
type: 'armor',
max: 1
}
}
]
} }
] ]
}, },
@ -928,8 +914,8 @@ export const weaponFeatures = {
type: 'self' type: 'self'
}, },
damage: { damage: {
parts: { parts: [
hitPoints: { {
applyTo: 'hitPoints', applyTo: 'hitPoints',
value: { value: {
custom: { custom: {
@ -938,7 +924,7 @@ export const weaponFeatures = {
} }
} }
} }
} ]
} }
} }
] ]
@ -1205,13 +1191,9 @@ export const weaponFeatures = {
img: 'icons/skills/melee/shield-block-gray-orange.webp', img: 'icons/skills/melee/shield-block-gray-orange.webp',
changes: [ changes: [
{ {
key: 'Armor', key: 'system.armorScore',
type: 'armor', mode: 2,
value: 0, value: 'ITEM.@system.tier'
typeData: {
type: 'armor',
max: 'ITEM.@system.tier'
}
} }
] ]
} }
@ -1407,7 +1389,7 @@ export const allWeaponFeatures = () => {
const actions = feature.actions.map(action => ({ const actions = feature.actions.map(action => ({
...action, ...action,
effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id)) ?? [], effects: action.effects?.map(effect => feature.effects.find(x => x.id === effect._id))??[],
type: action.type type: action.type
})); }));
const actionEffects = actions.flatMap(a => a.effects); const actionEffects = actions.flatMap(a => a.effects);

View file

@ -60,7 +60,7 @@ const companionBaseResources = Object.freeze({
max: 3, max: 3,
reverse: true, reverse: true,
label: 'DAGGERHEART.GENERAL.stress' label: 'DAGGERHEART.GENERAL.stress'
} },
}); });
export const character = { export const character = {

View file

@ -1,6 +1,5 @@
export const keybindings = { export const keybindings = {
spotlight: 'DHSpotlight', spotlight: 'DHSpotlight'
partySheet: 'DHPartySheet'
}; };
export const menu = { export const menu = {
@ -39,10 +38,9 @@ export const gameSettings = {
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns', Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion', LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue', SpotlightRequestQueue: 'SpotlightRequestQueue',
CompendiumBrowserSettings: 'CompendiumBrowserSettings', CompendiumBrowserSettings: 'CompendiumBrowserSettings'
SpotlightTracker: 'SpotlightTracker',
ActiveParty: 'ActiveParty'
}; };
export const actionAutomationChoices = { export const actionAutomationChoices = {

View file

@ -1,11 +1,9 @@
export { default as DhCombat } from './combat.mjs'; export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs'; export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
export { default as DhRollTable } from './rollTable.mjs'; export { default as DhRollTable } from './rollTable.mjs';
export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs';
export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs'; export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs';
export { default as TagTeamData } from './tagTeamData.mjs';
export { default as GroupRollData } from './groupRollData.mjs';
export { default as SpotlightTracker } from './spotlightTracker.mjs';
export * as countdowns from './countdowns.mjs'; export * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs'; export * as actions from './action/_module.mjs';
@ -15,4 +13,3 @@ export * as chatMessages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs'; export * as fields from './fields/_module.mjs';
export * as items from './item/_module.mjs'; export * as items from './item/_module.mjs';
export * as scenes from './scene/_module.mjs'; export * as scenes from './scene/_module.mjs';
export * as regionBehaviors from './regionBehavior/_module.mjs';

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