Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

1685 changed files with 31687 additions and 41635 deletions

View file

@ -1,2 +0,0 @@
FOUNDRY_MAIN_PATH=/path/to/foundry/resources/app/main.js
FOUNDRY_DATA_PATH=/path/to/foundry/data

View file

@ -1,10 +1,10 @@
---
name: Bug report
about: Create a bug report to help us identify issues and resolve them
title: "[Bug] <Insert Title here> "
about: Create a report to help us improve
title: "[BUG] - "
labels: bug
type: bug
assignees: ''
---
**Describe the bug**
@ -24,10 +24,10 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Setup Information:**
- OS: [e.g. iOS, Windows]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Foundry Version [e.g. v13 b342]
- System Version [e.g. v.1.0, v.1.0.1]
- System Version [e.g. main-3593f44]
**Additional context**

View file

@ -1,14 +0,0 @@
---
name: Feature request
about: Create a feature request for suggestions on improving the system
title: "[Feature] <Insert Title here> "
labels: enhancement, discussion, maybe
type: feature
assignees: ''
---
**Description**
A clear and concise description of what feature needs to be implemented.
**Screenshots**
If applicable, add screenshots to help explain the feature that needs to be implemented.

View file

@ -1,9 +0,0 @@
---
name: Typo report
about: Create a new issue to report a compendium typo
title: "[TYPO] - "
labels: compendium, typo
type: bug
assignees: ''
---

View file

@ -1,55 +0,0 @@
---
name: Pull Request
about: Create a new pull request
title: "[Community PR] <Insert Title here>"
labels: community pr
assignees: ''
---
## Description
Please include a summary of the change and which issue is fixed (if applicable). Also include relevant context or motivation for the change.
- Fixes #(issue)
- Closes #(issue)
## Type of Change
Please check the relevant options:
- [ ] Bug fix
- [ ] New feature
- [ ] Code cleanup/refactor
- [ ] Documentation update
- [ ] Test coverage
- [ ] Dependency update
- [ ] Configuration change
- [ ] Other (please describe):
## How Has This Been Tested?
Please describe the tests you ran to verify your changes:
- [ ] Manual testing
- [ ] Other:
## Screenshots (if applicable)
Include screenshots or GIFs to help explain your changes visually.
## Checklist
- [ ] My code follows the project style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code where necessary
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings or errors
- [ ] I have added tests that prove my fix or feature works
- [ ] New and existing tests pass locally with my changes
## Additional Comments
Add any other context or questions here.
---
> Thank you for your contribution! 🎉

2
.gitignore vendored
View file

@ -1,8 +1,6 @@
.vscode
.env
node_modules
/packs
Build
build
foundry
styles/daggerheart.css

View file

@ -14,7 +14,7 @@ This is the community repo for the Foundry VTT system _Foundryborne_ Daggerheart
## User Install
1. **Recommended** Searching for _Daggerheart_ or _Foundryborne_ in the System Installation dialogue of the FoundryVTT admin settings.
1. **recommended** Searching for _Daggerheart_ or _Foundryborne_ in the System Instalaltion dialgoe of the FoundryVTT admin settings.
2. Pasting `https://raw.githubusercontent.com/Foundryborne/daggerheart/refs/heads/main/system.json` into the Install System dialog on the Setup menu of the application.
3. Downloading one of the .zip archives from the Releases page and extracting it into your foundry Data folder, under Data/systems/daggerheart.
@ -24,41 +24,24 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart
## Development Setup
1. **Navigate to the repo directory:**
- Open a terminal in the directory with the repo `cd <path>/<to>/<repo>`
- NOTE: The repo should be placed in the system files are or somewhere else and a link (if on linux) is placed in the system directory
- NOTE: Linux link can be made using `ln -snf <path to development folder> daggerheart` inside the system folder
- Install npm `npm install`
- Update package.json to match your profile
```bash
cd <path>/<to>/<repo>
```
```
"start": "concurrently \"rollup -c --watch\" \"node C:/FoundryDev/resources/app/main.js --dataPath=C:/FoundryDevFiles --noupnp\" \"gulp\"",
"start-test": "node C:/FoundryDev/resources/app/main.js --dataPath=C:/FoundryDevFiles && rollup -c --watch && gulp",
2. **Install dependencies:**
```
```bash
npm install
```
- Replace `C:/FoundryDev/resources/app/main.js` with `<your>/<path>/<to>/<foundry>/<main.js>`
- The main is likely in `<Foundry Install Location>/resouces/app/main.js`
- Replace `--dataPath=C:/FoundryDevFiles` with `<your>/<path>/<to>/<foundry>/<data>`
3. **Configure your Foundry paths:**
```bash
npm run setup:dev -- --foundry-path="/path/to/foundry/main.js" --data-path="/path/to/data"
```
4. **Start developing:**
```bash
npm start
```
### Available Scripts
- `npm start` - Start development with file watching and Foundry launching
- `npm run build` - One-time build
- `npm run setup:dev -- --foundry-path="<path>" --data-path="<path>"` - Configure development environment
### Notes
- The repo should be placed in your Foundry `Data/systems/` directory or symlinked there
- Linux symlink can be made using `ln -snf <path to development folder> daggerheart` inside the systems folder
- Your `.env` file is ignored by git, so each developer can have their own configuration
[Foundry VTT Website][1]
Now you should be able to build the app using `npm start`
[Foundry VTT Website][1]
[1]: https://foundryvtt.com/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="42.0746mm" height="50.8071mm"
viewBox="0 0 159 192">
<path id="Fear"
fill="#808080" stroke="black" stroke-width="1"
d="M 107.14,62.84
C 107.14,62.84 52.70,137.55 52.70,137.55
52.70,137.55 110.33,137.55 110.33,137.55
110.33,137.55 127.99,79.63 127.99,79.63
127.99,79.63 107.14,62.84 107.14,62.84 Z
M 48.07,144.21
C 48.07,144.21 13.61,192.28 13.61,192.28
14.01,191.90 30.68,177.55 47.81,176.06
47.81,176.06 64.48,176.30 64.48,176.30
64.69,176.22 79.31,180.77 79.31,180.77
79.31,180.77 126.04,165.15 126.04,165.15
126.04,165.15 110.91,143.92 110.91,143.92
110.91,143.92 48.07,144.21 48.07,144.21 Z
M 134.24,81.37
C 134.24,81.37 115.74,140.33 115.74,140.33
115.74,140.33 131.41,161.00 131.41,161.00
131.41,161.00 158.98,122.82 158.98,122.82
158.98,122.82 158.91,72.83 158.91,72.83
158.91,72.83 134.24,81.37 134.24,81.37 Z
M 130.43,30.45
C 130.43,30.45 110.41,59.27 110.41,59.27
110.41,59.27 131.77,75.21 131.77,75.21
131.77,75.21 157.51,67.23 157.51,67.23
157.25,67.32 130.42,30.45 130.43,30.45 Z" />
<path id="Hope"
fill="white" stroke="black" stroke-width="1"
d="M 143.84,1.80
C 143.84,1.80 105.59,54.92 105.59,54.92
105.59,54.92 83.04,39.72 83.04,39.72
83.04,39.72 82.87,13.08 82.87,13.08
82.23,14.32 111.40,18.59 112.13,17.16
113.68,18.32 145.11,2.74 143.84,1.80 Z
M 76.01,13.40
C 76.01,13.40 76.01,40.05 76.01,40.05
76.01,40.05 25.99,75.35 25.99,75.35
25.99,75.35 1.96,68.16 1.96,68.16
1.96,68.16 30.08,27.62 30.08,27.62
30.08,27.62 76.01,13.40 76.01,13.40 Z
M 79.11,44.62
C 79.11,44.62 101.34,60.81 101.34,60.81
101.34,60.81 48.38,133.54 48.38,133.54
48.38,133.54 30.57,80.09 30.57,80.09
30.57,80.09 79.11,44.62 79.11,44.62 Z
M 24.36,81.07
C 24.36,81.07 43.64,139.59 43.64,139.59
43.64,139.59 28.11,162.31 28.11,162.31
28.11,162.31 0.33,123.90 0.33,123.90
0.33,123.90 0.33,74.05 0.33,74.05
0.33,74.05 24.36,81.07 24.36,81.07 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

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="M369.1 21.22c-19.2 0-36.2 10.63-47.9 26.47-11.7 15.84-18.6 37.03-18.6 60.31 0 21.1 5.7 40.5 15.5 55.7-5.7 1.6-11 3.9-15.9 6.6-10.2-8.5-22.6-13.6-35.9-13.6-19.3 0-36.3 10.6-48 26.4-4.7 6.4-8.6 13.6-11.6 21.5-4.8-2.4-9.9-4.3-15.5-5.6 9.4-15.1 14.8-34.1 14.8-54.7 0-23.2-6.9-44.43-18.6-60.27-11.7-15.84-28.7-26.5-47.9-26.5s-36.2 10.66-47.94 26.5C79.87 99.87 73 121.1 73 144.3c0 21.1 5.69 40.5 15.47 55.8-32.07 9.1-50.29 37.1-59.44 70-9.79 35.2-10.87 77.3-10.87 115.6v9.4h45.5l6.78 99.3h18.75l-7.28-106.5-4.1-80-18.65 1 3.47 67.5H36.97c.24-35.2 1.97-72.1 10.09-101.2 8.78-31.6 23.32-52.8 51.25-58.2l4.69-.1c10.3 8.8 22.9 14.2 36.5 14.2 14.1 0 26.9-5.7 37.4-15h4.6c7.8 1.2 14.4 3.5 20.1 6.7-1.2 6.6-1.9 13.5-1.9 20.6 0 21.1 5.7 40.5 15.5 55.8-32.1 9.1-50.3 37.2-59.4 70-9.8 35.2-10.9 77.3-10.9 115.6v9.4c21.7-.3 42.8.2 64.3.2l-.5-7.3-4.1-80-18.7.9 3.4 67.5h-25.6c.3-35.2 2-72.1 10.1-101.2 8.7-31.6 23.3-52.7 51.1-58.2l4.9-.1c10.3 8.8 22.8 14.2 36.4 14.2 14.1 0 27-5.7 37.5-15h4.4c15.4 2.4 26.1 8.9 34.5 18.6 8.5 9.7 14.5 23.2 18.5 39.2 7.3 29.5 7.7 66.9 7.7 102.5h-23.4l3.5-67.5-18.7-.9-4.2 82-.3 5.3c20.8 0 43.3-.3 61.9-.2v-9.4c0-38.1.5-80.6-8.4-116.3-4.4-17.8-11.3-34.1-22.4-47-9.7-11.1-22.7-19.4-38.8-23.4 9.4-15.1 14.7-34.1 14.7-54.7 0-22.5-6.4-43.2-17.5-58.8 3.9-1.8 8.1-3.1 12.7-4h4.7c10.3 8.8 22.9 14.2 36.5 14.2 14.1 0 27-5.8 37.4-15l4.6-.1c15.4 2.5 26 8.9 34.4 18.6 8.5 9.8 14.5 23.3 18.5 39.3 7.3 29.4 7.7 66.8 7.7 102.4h-23.4l3.5-67.4-18.7-1-4.1 79.7-8.6 143.1h18.7l8.2-135.7h43.1v-9.3c0-38.2.6-80.7-8.3-116.3-4.5-17.9-11.4-34.2-22.5-47-9.6-11.2-22.6-19.5-38.8-23.5 9.4-15.1 14.8-34 14.8-54.6 0-23.28-6.9-44.47-18.6-60.31-11.6-15.29-31.5-26.13-47.9-26.47zm0 18.69c12.4 0 23.9 6.69 32.9 18.87 9 12.19 14.9 29.67 14.9 49.22 0 19.5-5.9 37-14.9 49.2-9 12.2-20.5 18.9-32.9 18.9-12.3 0-23.9-6.7-32.9-18.9s-14.9-29.7-14.9-49.2c0-19.55 5.9-37.03 14.9-49.22 9-12.18 20.6-18.87 32.9-18.87zM139.5 76.22c12.4 0 23.9 6.72 32.9 18.9s14.9 29.68 14.9 49.18-5.9 37-14.9 49.2c-9 12.2-20.5 18.9-32.9 18.9-12.4 0-23.9-6.7-32.9-18.9-8.97-12.2-14.91-29.7-14.91-49.2 0-19.5 5.94-37 14.91-49.17 9-12.19 20.5-18.91 32.9-18.91zm197.8 22.34v18.64h22.5V98.56h-22.5zm41.1 0v18.64h22.5V98.56h-22.5zM107.7 134.9v18.7h22.5v-18.7h-22.5zm41.1 0v18.7h22.5v-18.7h-22.5zm117.5 40.4c12.3 0 23.8 6.7 32.8 18.9 9 12.2 15 29.7 15 49.2 0 19.6-6 37-15 49.2-9 12.2-20.5 18.9-32.8 18.9-12.4 0-24-6.7-33-18.9-8.9-12.2-14.9-29.6-14.9-49.2 0-19.5 6-37 14.9-49.2 9-12.2 20.6-18.9 33-18.9zM234.5 234v18.7h22.4V234zm41.1 0v18.7H298V234h-22.4z" fill="#fff" fill-opacity="1"></path></g></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View file

View file

@ -17,10 +17,6 @@ We welcome contributions of all kinds:
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
@ -44,14 +40,12 @@ We encourage contributors to leave comments or open Discussions when proposing s
## 🧾 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
@ -73,6 +67,6 @@ Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Di
## 🤗 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**!
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

@ -1,6 +1,5 @@
import { SYSTEM } from './module/config/system.mjs';
import * as applications from './module/applications/_module.mjs';
import * as data from './module/data/_module.mjs';
import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs';
import * as dice from './module/dice/_module.mjs';
@ -8,185 +7,98 @@ import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
import { DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { registerCountdownHooks } from './module/data/countdowns.mjs';
import {
handlebarsRegistration,
runMigrations,
settingsRegistration,
socketRegistration
} from './module/systemRegistration/_module.mjs';
import { placeables, DhTokenLayer } from './module/canvas/_module.mjs';
import { placeables } from './module/canvas/_module.mjs';
import { registerRollDiceHooks } from './module/dice/dhRoll.mjs';
import './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs';
import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM;
CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll];
CONFIG.Dice.daggerheart = {
DHRoll: DHRoll,
DualityRoll: DualityRoll,
D20Roll: D20Roll,
DamageRoll: DamageRoll
};
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config;
CONFIG.Item.documentClass = documents.DHItem;
CONFIG.Item.dataModels = models.items.config;
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.Combat.dataModels = { base: models.DhCombat };
CONFIG.Combatant.documentClass = documents.DHCombatant;
CONFIG.Combatant.dataModels = { base: models.DhCombatant };
CONFIG.ChatMessage.dataModels = models.chatMessages.config;
CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Canvas.layers.tokens.layerClass = DhTokenLayer;
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
CONFIG.Scene.documentClass = documents.DhScene;
CONFIG.Token.documentClass = documents.DhToken;
CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenConfig;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.nav = applications.ui.DhSceneNavigation;
CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.effectsDisplay = applications.ui.DhEffectsDisplay;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.ui.sidebar = applications.sidebar.DhSidebar;
CONFIG.ui.actors = applications.sidebar.DhActorDirectory;
CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu;
CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
CONFIG.ux.TokenManager = new TokenManager();
Hooks.once('init', () => {
CONFIG.DH = SYSTEM;
game.system.api = {
applications,
data,
models,
documents,
dice,
fields
};
game.system.registeredTriggers = new RegisteredTriggers();
CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions).map(x => ({
...x,
name: game.i18n.localize(x.name),
systemEffect: true
}))
];
CONFIG.Dice.daggerheart = {
DHRoll: DHRoll,
DualityRoll: DualityRoll,
D20Roll: D20Roll,
DamageRoll: DamageRoll
};
CONFIG.Dice.rolls = [...CONFIG.Dice.rolls, DHRoll, DualityRoll, D20Roll, DamageRoll];
Roll.CHAT_TEMPLATE = 'systems/daggerheart/templates/ui/chat/foundryRoll.hbs';
Roll.TOOLTIP_TEMPLATE = 'systems/daggerheart/templates/ui/chat/foundryRollTooltip.hbs';
CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate;
const { DocumentSheetConfig } = foundry.applications.apps;
CONFIG.Token.documentClass = documents.DhToken;
CONFIG.Token.prototypeSheetClass = applications.sheetConfigs.DhPrototypeTokenConfig;
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
makeDefault: true
});
const sheetLabel = typePath => () =>
game.i18n.format('DAGGERHEART.GENERAL.typeSheet', {
type: game.i18n.localize(typePath)
});
CONFIG.Item.documentClass = documents.DHItem;
//Registering the Item DataModel
CONFIG.Item.dataModels = models.items.config;
const { Items, Actors } = foundry.documents.collections;
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, {
types: ['ancestry'],
makeDefault: true,
label: sheetLabel('TYPES.Item.ancestry')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Community, {
types: ['community'],
makeDefault: true,
label: sheetLabel('TYPES.Item.community')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Class, {
types: ['class'],
makeDefault: true,
label: sheetLabel('TYPES.Item.class')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Subclass, {
types: ['subclass'],
makeDefault: true,
label: sheetLabel('TYPES.Item.subclass')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Feature, {
types: ['feature'],
makeDefault: true,
label: sheetLabel('TYPES.Item.feature')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.DomainCard, {
types: ['domainCard'],
makeDefault: true,
label: sheetLabel('TYPES.Item.domainCard')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, { types: ['ancestry'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Community, { types: ['community'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Class, { types: ['class'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Subclass, { types: ['subclass'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Feature, { types: ['feature'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.DomainCard, { types: ['domainCard'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Loot, {
types: ['loot'],
makeDefault: true,
label: sheetLabel('TYPES.Item.loot')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Consumable, {
types: ['consumable'],
makeDefault: true,
label: sheetLabel('TYPES.Item.consumable')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Weapon, {
types: ['weapon'],
makeDefault: true,
label: sheetLabel('TYPES.Item.weapon')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Armor, {
types: ['armor'],
makeDefault: true,
label: sheetLabel('TYPES.Item.armor')
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Beastform, {
types: ['beastform'],
makeDefault: true,
label: sheetLabel('TYPES.Item.beastform')
makeDefault: true
});
Items.registerSheet(SYSTEM.id, applications.sheets.items.Consumable, { types: ['consumable'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Weapon, { types: ['weapon'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Armor, { types: ['armor'], makeDefault: true });
Items.registerSheet(SYSTEM.id, applications.sheets.items.Beastform, { types: ['beastform'], makeDefault: true });
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config;
Actors.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2);
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Character, {
types: ['character'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.character')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Companion, {
types: ['companion'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.companion')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Adversary, {
types: ['adversary'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.adversary')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Character, { types: ['character'], makeDefault: true });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Companion, { types: ['companion'], makeDefault: true });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Adversary, { types: ['adversary'], makeDefault: true });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Environment, {
types: ['environment'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.environment')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.party')
makeDefault: true
});
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
DocumentSheetConfig.unregisterSheet(
CONFIG.ActiveEffect.documentClass,
'core',
@ -197,56 +109,58 @@ Hooks.once('init', () => {
SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig,
{
makeDefault: true,
label: sheetLabel('DOCUMENT.ActiveEffect')
makeDefault: true
}
);
CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.Combat.dataModels = {
base: models.DhCombat
};
CONFIG.Combatant.dataModels = {
base: models.DhCombatant
};
CONFIG.ChatMessage.dataModels = models.chatMessages.config;
CONFIG.ChatMessage.documentClass = documents.DhChatMessage;
CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-message.hbs';
CONFIG.Canvas.rulerClass = placeables.DhRuler;
CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer;
CONFIG.Token.objectClass = placeables.DhTokenPlaceable;
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
CONFIG.ux.TemplateManager = new TemplateManager();
game.socket.on(`system.${SYSTEM.id}`, socketRegistration.handleSocketEvent);
// Make Compendium Dialog resizable
foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true;
DocumentSheetConfig.unregisterSheet(foundry.documents.Scene, 'core', foundry.applications.sheets.SceneConfig);
DocumentSheetConfig.registerSheet(foundry.documents.Scene, SYSTEM.id, applications.scene.DhSceneConfigSettings, {
makeDefault: true,
label: sheetLabel('DOCUMENT.Scene')
});
settingsRegistration.registerDHSettings();
RegisterHandlebarsHelpers.registerHelpers();
return handlebarsRegistration();
});
Hooks.on('setup', () => {
CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({
...x,
name: game.i18n.localize(x.name),
systemEffect: true
}))
];
});
Hooks.on('ready', async () => {
const appearanceSettings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance);
ui.resources = new CONFIG.ui.resources();
if (appearanceSettings.displayFear !== 'hide') ui.resources.render({ force: true });
if (appearanceSettings.displayCountdownUI) {
ui.countdowns = new CONFIG.ui.countdowns();
ui.countdowns.render({ force: true });
}
ui.effectsDisplay = new CONFIG.ui.effectsDisplay();
ui.effectsDisplay.render({ force: true });
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
ui.compendiumBrowser = new applications.ui.ItemBrowser();
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
ui.resources.render({ force: true });
registerCountdownHooks();
socketRegistration.registerSocketHooks();
registerRollDiceHooks();
socketRegistration.registerUserQueries();
if (!game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage)) {
@ -256,15 +170,13 @@ Hooks.on('ready', async () => {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true);
}
}
runMigrations();
});
Hooks.once('dicesoniceready', () => {});
Hooks.on('renderChatMessageHTML', (document, element) => {
Hooks.on('renderChatMessageHTML', (_, element, message) => {
enricherRenderSetup(element);
const cssClass = document.flags?.daggerheart?.cssClass;
const cssClass = message.message.flags?.daggerheart?.cssClass;
if (cssClass) cssClass.split(' ').forEach(cls => element.classList.add(cls));
});
@ -309,7 +221,7 @@ Hooks.on('chatMessage', (_, message) => {
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
label: 'test',
actionType: null,
advantage
});
@ -317,121 +229,73 @@ Hooks.on('chatMessage', (_, message) => {
}
});
const updateActorsRangeDependentEffects = async token => {
const rangeMeasurement = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
Hooks.on('renderJournalDirectory', async (tab, html, _, options) => {
if (tab.id === 'journal') {
if (options.parts && !options.parts.includes('footer')) return;
for (let effect of token.actor?.allApplicableEffects() ?? []) {
if (!effect.system.rangeDependence?.enabled) continue;
const { target, range, type } = effect.system.rangeDependence;
const buttons = tab.element.querySelector('.directory-footer.action-buttons');
const title = game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
type: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.types.narrative')
});
buttons.insertAdjacentHTML(
'afterbegin',
`
<button id="narrative-countdown-button">
<i class="fa-solid fa-stopwatch"></i>
<span style="font-weight: 400; font-family: var(--font-sans);">${title}</span>
</button>`
);
// If there are no targets, assume false. Otherwise, start with the effect enabled.
let enabledEffect = game.user.targets.size !== 0;
// Expect all targets to meet the rangeDependence requirements
for (let userTarget of game.user.targets) {
const disposition = userTarget.document.disposition;
if ((target === 'friendly' && disposition !== 1) || (target === 'hostile' && disposition !== -1)) {
enabledEffect = false;
break;
}
// Get required distance and special case 5 feet to test adjacency
const required = rangeMeasurement[range];
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const inRange =
required === 5
? userTarget.isAdjacentWith(token.object)
: userTarget.distanceTo(token.object) <= required;
if (reverse ? inRange : !inRange) {
enabledEffect = false;
break;
}
}
await effect.update({ disabled: !enabledEffect });
buttons.querySelector('#narrative-countdown-button').onclick = async () => {
new NarrativeCountdowns().open();
};
}
};
});
const updateAllRangeDependentEffects = async () => {
Hooks.on('moveToken', async (movedToken, data) => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return;
const tokens = canvas.scene?.tokens;
if (!tokens) return;
const rangeDependantEffects = movedToken.actor.effects.filter(effect => effect.system.rangeDependence?.enabled);
if (game.user.character) {
// The character updates their character's token. There can be only one token.
const characterToken = tokens.find(x => x.actor === game.user.character);
updateActorsRangeDependentEffects(characterToken);
} else if (game.user.isActiveGM) {
// The GM is responsible for all other tokens.
const playerCharacters = game.users.players.filter(x => x.active).map(x => x.character);
for (const token of tokens.filter(x => !playerCharacters.includes(x.actor))) {
updateActorsRangeDependentEffects(token);
const updateEffects = async (disposition, token, effects, effectUpdates) => {
const rangeMeasurement = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
for (let effect of effects.filter(x => x.system.rangeDependence?.enabled)) {
const { target, range, type } = effect.system.rangeDependence;
if ((target === 'friendly' && disposition !== 1) || (target === 'hostile' && disposition !== -1))
return false;
const distanceBetween = canvas.grid.measurePath([
{ ...movedToken.toObject(), x: data.destination.x, y: data.destination.y },
token
]).distance;
const distance = rangeMeasurement[range];
const reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const newDisabled = reverse ? distanceBetween <= distance : distanceBetween > distance;
const oldDisabled = effectUpdates[effect.uuid] ? effectUpdates[effect.uuid].disabled : newDisabled;
effectUpdates[effect.uuid] = {
disabled: oldDisabled || newDisabled,
value: effect
};
}
}
};
};
const debouncedRangeEffectCall = foundry.utils.debounce(updateAllRangeDependentEffects, 50);
Hooks.on('targetToken', () => {
debouncedRangeEffectCall();
});
Hooks.on('refreshToken', (_, options) => {
if (options.refreshPosition) {
debouncedRangeEffectCall();
}
});
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
class RegisteredTriggers extends Map {
constructor() {
super();
}
async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) {
const existingTrigger = this.get(trigger);
if (!existingTrigger) this.set(trigger, new Map());
this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
}
async runTrigger(trigger, currentActor, ...args) {
const updates = [];
const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers;
if (!triggerSettings.enabled) return updates;
const dualityTrigger = this.get(trigger);
if (dualityTrigger) {
for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) {
const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
if (triggerData.usesActor && triggeringActorType !== 'any') {
if (triggeringActorType === 'self' && currentActor?.uuid !== actor) continue;
else if (triggeringActorType === 'other' && currentActor?.uuid === actor) continue;
}
for (let command of commands) {
try {
const result = await command(...args);
if (result?.updates?.length) updates.push(...result.updates);
} catch (_) {
const triggerName = game.i18n.localize(triggerData.label);
ui.notifications.error(
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
trigger: triggerName,
actor: currentActor?.name
})
);
}
}
}
const effectUpdates = {};
for (let token of game.scenes.find(x => x.active).tokens) {
if (token.id !== movedToken.id) {
await updateEffects(token.disposition, token, rangeDependantEffects, effectUpdates);
}
return updates;
if (token.actor) await updateEffects(movedToken.disposition, token, token.actor.effects, effectUpdates);
}
}
for (let key in effectUpdates) {
const effect = effectUpdates[key];
await effect.value.update({ disabled: effect.disabled });
}
});

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,8 @@ export * as characterCreation from './characterCreation/_module.mjs';
export * as dialogs from './dialogs/_module.mjs';
export * as hud from './hud/_module.mjs';
export * as levelup from './levelup/_module.mjs';
export * as scene from './scene/_module.mjs';
export * as settings from './settings/_module.mjs';
export * as sheets from './sheets/_module.mjs';
export * as sheetConfigs from './sheets-configs/_module.mjs';
export * as sidebar from './sidebar/_module.mjs';
export * as ui from './ui/_module.mjs';
export * as ux from './ux/_module.mjs';

View file

@ -1,5 +1,6 @@
import { abilities } from '../../config/actorConfig.mjs';
import { burden } from '../../config/generalConfig.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -12,10 +13,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.setup = {
traits: this.character.system.traits,
ancestryName: {
primary: '',
secondary: ''
},
ancestryName: '',
mixedAncestry: false,
primaryAncestry: this.character.system.ancestry ?? {},
secondaryAncestry: {},
@ -45,6 +43,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
};
this._dragDrop = this._createDragDropHandlers();
this.itemBrowser = null;
}
get title() {
@ -83,70 +83,131 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static PARTS = {
tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' },
equipment: { template: 'systems/daggerheart/templates/characterCreation/equipment.hbs' },
// story: { template: 'systems/daggerheart/templates/characterCreation/story.hbs' },
setup: { template: 'systems/daggerheart/templates/characterCreation/tabs/setup.hbs' },
ancestry: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/community.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/class.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/domainCards.hbs' },
equipment: { template: 'systems/daggerheart/templates/characterCreation/tabs/equipment.hbs' },
// story: { template: 'systems/daggerheart/templates/characterCreation/tabs/story.hbs' },
footer: { template: 'systems/daggerheart/templates/characterCreation/footer.hbs' }
};
static TABS = {
class: {
setup: {
active: true,
cssClass: '',
group: 'primary',
id: 'setup',
label: 'DAGGERHEART.GENERAL.Tabs.setup'
},
equipment: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
group: 'primary',
id: 'equipment',
label: 'DAGGERHEART.GENERAL.Tabs.equipment',
optional: true
}
// story: {
// active: false,
// cssClass: '',
// group: 'primary',
// id: 'story',
// label: 'DAGGERHEART.GENERAL.Tabs.story',
// optional: true
// }
};
static SETUPTABS = {
ancestry: {
active: true,
cssClass: '',
group: 'setup',
id: 'ancestry',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.ancestry'
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.ancestry'
},
community: {
active: false,
cssClass: '',
group: 'setup',
id: 'community',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community'
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.community'
},
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.class'
},
traits: {
active: false,
cssClass: '',
group: 'setup',
id: 'traits',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.traits'
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.traits'
},
experience: {
active: false,
cssClass: '',
group: 'setup',
id: 'experience',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.experience'
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.experience'
},
domainCards: {
active: false,
cssClass: '',
group: 'setup',
id: 'domainCards',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.domainCards'
},
equipment: {
active: false,
cssClass: '',
group: 'setup',
id: 'equipment',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.equipment'
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.domainCards'
}
};
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
switch (v.id) {
case 'setup':
const ancestryFinished = this.setup.primaryAncestry.uuid;
const communityFinished = this.setup.community.uuid;
const classFinished = this.setup.class.uuid && this.setup.subclass.uuid;
const traitsFinished = Object.values(this.setup.traits).every(x => x.value !== null);
const experiencesFinished = Object.values(this.setup.experiences).every(x => x.name);
const domainCardsFinished = Object.values(this.setup.domainCards).every(x => x.uuid);
v.finished =
ancestryFinished &&
communityFinished &&
classFinished &&
traitsFinished &&
experiencesFinished &&
domainCardsFinished;
break;
case 'equipment':
const armorFinished = this.equipment.armor?.uuid;
const primaryFinished = this.equipment.primaryWeapon?.uuid;
const secondaryFinished =
this.equipment.secondaryWeapon?.uuid ||
(primaryFinished && this.equipment.primaryWeapon.system.burden == burden.twoHanded.value);
const choiceAFinished = this.equipment.inventory.choiceA?.uuid;
const choiceBFinished = this.equipment.inventory.choiceB?.uuid;
v.finished =
armorFinished && primaryFinished && secondaryFinished && choiceAFinished && choiceBFinished;
}
}
tabs.equipment.cssClass = tabs.setup.finished ? tabs.equipment.cssClass : 'disabled';
// tabs.story.cssClass = tabs.setup.finished ? tabs.story.cssClass : 'disabled';
return tabs;
}
_getSetupTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group]
? this.tabGroups[v.group] === v.id
@ -156,10 +217,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.cssClass = v.active ? 'active' : '';
switch (v.id) {
case 'ancestry':
case 'community':
v.disabled = this.setup.visibility < 2;
break;
case 'community':
case 'class':
v.disabled = this.setup.visibility < 3;
break;
case 'traits':
@ -171,15 +232,37 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 'domainCards':
v.disabled = this.setup.visibility < 6;
break;
case 'equipment':
v.disabled = this.setup.visibility < 7;
break;
}
}
return tabs;
}
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
if (group === 'primary') {
for (var listTab of Object.keys(this.constructor.TABS)) {
const marker = options.navElement.querySelector(`a[data-action="tab"].${listTab} .finish-marker`);
if (listTab === tab) {
marker.classList.add('active');
} else {
marker.classList.remove('active');
}
}
if (tab === 'equipment') {
this.tabGroups.setup = null;
this.element.querySelector('section[data-group="setup"].active')?.classList?.remove?.('active');
} else {
this.tabGroups.setup = 'domainCards';
this.element
.querySelector('section[data-group="setup"][data-tab="domainCards"]')
?.classList?.add?.('active');
}
}
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
@ -191,71 +274,14 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
});
}
async _preFirstRender(_context, _options) {
this.tabGroups.primary = 'setup';
this.tabGroups.setup = 'ancestry';
}
async _prepareContext(_options) {
this.tabGroups.setup = this.tabGroups.setup ?? 'class';
const context = await super._prepareContext(_options);
context.tabs = this._getTabs(this.constructor.TABS);
const availableTraitModifiers = game.settings
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew)
.traitArray.map(trait => ({ key: trait, name: trait }));
for (let trait of Object.values(this.setup.traits).filter(x => x.value !== null)) {
const index = availableTraitModifiers.findIndex(x => x.key === trait.value);
if (index !== -1) {
availableTraitModifiers.splice(index, 1);
}
}
context.suggestedTraits = this.setup.class.system
? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => {
const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey];
return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`;
})
: [];
context.traits = {
values: Object.keys(this.setup.traits).map(traitKey => {
const trait = this.setup.traits[traitKey];
const options = [...availableTraitModifiers];
if (trait.value !== null && !options.some(x => x.key === trait.value))
options.push({ key: trait.value, name: trait.value });
return {
...trait,
key: traitKey,
name: game.i18n.localize(abilities[traitKey].label),
options: options
};
})
};
context.traits.nrTotal = Object.keys(context.traits.values).length;
context.traits.nrSelected = this.getNrSelectedTrait();
context.experience = {
values: this.setup.experiences,
nrTotal: Object.keys(this.setup.experiences).length,
nrSelected: Object.values(this.setup.experiences).reduce((acc, exp) => acc + (exp.name ? 1 : 0), 0)
};
context.mixedAncestry = Number(this.setup.mixedAncestry);
const { primary, secondary, overwrite } = this.setup.ancestryName;
context.ancestryName = overwrite ?? (primary && secondary ? `${primary}/${secondary}` : primary);
context.primaryAncestry = { ...this.setup.primaryAncestry, compendium: 'ancestries' };
context.secondaryAncestry = { ...this.setup.secondaryAncestry, compendium: 'ancestries' };
context.community = { ...this.setup.community, compendium: 'communities' };
context.class = { ...this.setup.class, compendium: 'classes' };
context.subclass = { ...this.setup.subclass, compendium: 'subclasses' };
const allDomainData = CONFIG.DH.DOMAIN.allDomains();
context.classDomains = context.class.uuid
? context.class.system.domains.map(key => game.i18n.localize(allDomainData[key].label))
: [];
context.domainCards = Object.keys(this.setup.domainCards).reduce((acc, x) => {
acc[x] = { ...this.setup.domainCards[x], compendium: 'domains' };
return acc;
}, {});
context.visibility = this.setup.visibility;
return context;
}
@ -263,16 +289,16 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
async _preparePartContext(partId, context) {
switch (partId) {
case 'footer':
context.isLastTab = this.tabGroups.setup === 'equipment';
context.isLastTab = this.tabGroups.setup === 'domainCards' || this.tabGroups.primary !== 'setup';
switch (this.tabGroups.setup) {
case null:
case 'class':
case 'ancestry':
context.nextDisabled = this.setup.visibility === 1;
break;
case 'ancestry':
case 'community':
context.nextDisabled = this.setup.visibility === 2;
break;
case 'community':
case 'class':
context.nextDisabled = this.setup.visibility === 3;
break;
case 'traits':
@ -281,11 +307,64 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 'experience':
context.nextDisabled = this.setup.visibility === 5;
break;
case 'domainCards':
context.nextDisabled = this.setup.visibility === 6;
break;
}
break;
case 'setup':
context.setupTabs = this._getSetupTabs(this.constructor.SETUPTABS);
const availableTraitModifiers = game.settings
.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew)
.traitArray.map(trait => ({ key: trait, name: trait }));
for (let trait of Object.values(this.setup.traits).filter(x => x.value !== null)) {
const index = availableTraitModifiers.findIndex(x => x.key === trait.value);
if (index !== -1) {
availableTraitModifiers.splice(index, 1);
}
}
context.suggestedTraits = this.setup.class.system
? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => {
const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey];
return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`;
})
: [];
context.traits = {
values: Object.keys(this.setup.traits).map(traitKey => {
const trait = this.setup.traits[traitKey];
const options = [...availableTraitModifiers];
if (trait.value !== null && !options.some(x => x.key === trait.value))
options.push({ key: trait.value, name: trait.value });
return {
...trait,
key: traitKey,
name: game.i18n.localize(abilities[traitKey].label),
options: options
};
})
};
context.traits.nrTotal = Object.keys(context.traits.values).length;
context.traits.nrSelected = this.getNrSelectedTrait();
context.experience = {
values: this.setup.experiences,
nrTotal: Object.keys(this.setup.experiences).length,
nrSelected: Object.values(this.setup.experiences).reduce((acc, exp) => acc + (exp.name ? 1 : 0), 0)
};
context.mixedAncestry = Number(this.setup.mixedAncestry);
context.ancestryName = this.setup.ancestryName;
context.primaryAncestry = { ...this.setup.primaryAncestry, compendium: 'ancestries' };
context.secondaryAncestry = { ...this.setup.secondaryAncestry, compendium: 'ancestries' };
context.community = { ...this.setup.community, compendium: 'communities' };
context.class = { ...this.setup.class, compendium: 'classes' };
context.subclass = { ...this.setup.subclass, compendium: 'subclasses' };
context.domainCards = Object.keys(this.setup.domainCards).reduce((acc, x) => {
acc[x] = { ...this.setup.domainCards[x], compendium: 'domains' };
return acc;
}, {});
context.visibility = this.setup.visibility;
break;
case 'equipment':
const suggestions = await this.getEquipmentSuggestions(
@ -299,7 +378,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
uuid: suggestions.armor?.uuid,
taken: suggestions.armor?.uuid === this.equipment.armor?.uuid
},
compendium: 'armor'
compendium: 'armors'
};
context.primaryWeapon = {
...this.equipment.primaryWeapon,
@ -308,7 +387,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
uuid: suggestions.primaryWeapon?.uuid,
taken: suggestions.primaryWeapon?.uuid === this.equipment.primaryWeapon?.uuid
},
compendium: 'weapon'
compendium: 'weapons'
};
context.secondaryWeapon = {
...this.equipment.secondaryWeapon,
@ -318,7 +397,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
taken: suggestions.secondaryWeapon?.uuid === this.equipment.secondaryWeapon?.uuid
},
disabled: this.equipment.primaryWeapon?.system?.burden === burden.twoHanded.value,
compendium: 'weapon'
compendium: 'weapons'
};
context.inventory = {
take: suggestions.inventory.take,
@ -354,20 +433,18 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
getUpdateVisibility() {
switch (this.setup.visibility) {
case 7:
return 7;
case 6:
return Object.values(this.setup.domainCards).every(x => x.uuid) ? 7 : 6;
return 6;
case 5:
return Object.values(this.setup.experiences).every(x => x.name) ? 6 : 5;
case 4:
return this.getNrSelectedTrait() === 6 ? 5 : 4;
case 3:
return this.setup.community.uuid ? 4 : 3;
return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3;
case 2:
return this.setup.primaryAncestry.uuid ? 3 : 2;
return this.setup.community.uuid ? 3 : 2;
case 1:
return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1;
return this.setup.primaryAncestry.uuid ? 2 : 1;
}
}
@ -418,34 +495,23 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}
static async viewCompendium(event, target) {
const type = target.dataset.compendium ?? target.dataset.type,
equipment = ['armor', 'weapon'];
const type = target.dataset.compendium ?? target.dataset.type;
const presets = {
folder: equipment.includes(type) ? `equipments.folders.${type}s` : type,
compendium: 'daggerheart',
folder: type,
render: {
noFolder: true
}
};
if (type === 'domains')
if (type == 'domains')
presets.filter = {
'level.max': { key: 'level.max', value: 1 },
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
};
if (type === 'subclasses')
presets.filter = {
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
};
if (equipment.includes(type))
presets.filter = {
'system.tier': { key: 'system.tier', value: 1 },
'type': { key: 'type', value: type }
};
ui.compendiumBrowser.open(presets);
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true }));
}
static async viewItem(_, target) {
@ -473,10 +539,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static setupGoNext() {
switch (this.setup.visibility) {
case 2:
this.tabGroups.setup = 'ancestry';
this.tabGroups.setup = 'community';
break;
case 3:
this.tabGroups.setup = 'community';
this.tabGroups.setup = 'class';
break;
case 4:
this.tabGroups.setup = 'traits';
@ -487,26 +553,20 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 6:
this.tabGroups.setup = 'domainCards';
break;
case 7:
this.tabGroups.setup = 'equipment';
break;
}
this.render();
}
static async finish(_, button) {
button.disabled = true;
static async finish() {
const primaryAncestryFeature = this.setup.primaryAncestry.system.primaryFeature;
const secondaryAncestryFeature = this.setup.secondaryAncestry?.uuid
? this.setup.secondaryAncestry.system.secondaryFeature
: this.setup.primaryAncestry.system.secondaryFeature;
const { primary, secondary, overwrite } = this.setup.ancestryName;
const ancestry = {
...this.setup.primaryAncestry,
name: overwrite ?? (primary && secondary ? `${primary}/${secondary}` : primary),
name: this.setup.ancestryName ?? this.setup.primaryAncestry.name,
system: {
...this.setup.primaryAncestry.system,
features: [
@ -563,7 +623,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
{ overwrite: true }
);
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
if (this.itemBrowser) this.itemBrowser.close();
this.close();
}
@ -578,14 +638,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (item.type === 'ancestry' && event.target.closest('.primary-ancestry-card')) {
this.setup.ancestryName.primary = item.name;
this.setup.ancestryName = item.name;
this.setup.primaryAncestry = {
...item,
effects: Array.from(item.effects).map(x => x.toObject()),
uuid: item.uuid
};
} else if (item.type === 'ancestry' && event.target.closest('.secondary-ancestry-card')) {
this.setup.ancestryName.secondary = item.name;
this.setup.secondaryAncestry = {
...item,
effects: Array.from(item.effects).map(x => x.toObject()),

View file

@ -1,16 +1,11 @@
export { default as AttributionDialog } from './attributionDialog.mjs';
export { default as BeastformDialog } from './beastformDialog.mjs';
export { default as d20RollDialog } from './d20RollDialog.mjs';
export { default as DamageDialog } from './damageDialog.mjs';
export { default as DamageReductionDialog } from './damageReductionDialog.mjs';
export { default as DeathMove } from './deathMove.mjs';
export { default as Downtime } from './downtime.mjs';
export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
export { default as ItemTransferDialog } from './itemTransfer.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.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 ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs';

View file

@ -57,11 +57,7 @@ export default class ActionSelectionDialog extends HandlebarsApplicationMixin(Ap
/** @inheritDoc */
async _prepareContext(options) {
const actions = this.#item.system.actionsList.map(action => ({
...action.toObject(),
id: action.id,
img: action.baseAction ? action.parent.parent.img : action.img
})),
const actions = this.#item.system.actionsList,
itemName = this.#item.name;
return {
...(await super._prepareContext(options)),

View file

@ -1,93 +0,0 @@
import autocomplete from 'autocompleter';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class AttributionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(item) {
super({});
this.item = item;
this.sources = Object.keys(CONFIG.DH.GENERAL.attributionSources).flatMap(groupKey => {
const group = CONFIG.DH.GENERAL.attributionSources[groupKey];
return group.values.map(x => ({ group: group.label, ...x }));
});
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.Attribution.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'attribution'],
position: { width: 'auto', height: 'auto' },
window: { icon: 'fa-solid fa-signature' },
form: { handler: this.updateData, submitOnChange: false, closeOnSubmit: true }
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/attribution.hbs' }
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const sources = this.sources;
htmlElement.querySelectorAll('.attribution-input').forEach(element => {
autocomplete({
input: element,
fetch: function (text, update) {
if (!text) {
update(sources);
} else {
text = text.toLowerCase();
var suggestions = sources.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}`;
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 = item.label;
},
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.item = this.item;
context.data = this.item.system.attribution;
return context;
}
static async updateData(_event, _element, formData) {
await this.item.update({ 'system.attribution': formData.object });
this.item.sheet.refreshFrame();
}
}

View file

@ -276,29 +276,7 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
const featureItem = item;
app.addEventListener(
'close',
async () => {
const selected = app.selected.toObject();
const evolved = app.evolved.form ? app.evolved.form.toObject() : null;
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(
app.configData.data.parent,
evolved ?? app.selected
);
if (data) {
if (!data.selectedImage) selected = null;
else {
const imageSource = evolved ?? selected;
if (imageSource.usesDynamicToken) imageSource.system.tokenRingImg = data.selectedImage;
else imageSource.system.tokenImg = data.selectedImage;
}
}
resolve({
selected: selected,
evolved: { ...app.evolved, form: evolved },
hybrid: app.hybrid,
item: featureItem
});
},
() => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem }),
{ once: true }
);
app.render({ force: true });

View file

@ -1,5 +1,3 @@
import { abilities } from '../../config/actorConfig.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -9,21 +7,20 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll = roll;
this.config = config;
this.config.experiences = [];
this.reactionOverride = config.actionType === 'reaction';
this.selectedEffects = this.config.bonusEffects;
this.reactionOverride = config.roll?.type === 'reaction';
if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
this.action =
config.data.attack?._id == config.source.action
? config.data.attack
: this.item.system.actionsList?.find(a => a.id === config.source.action);
: this.item.system.actions.get(config.source.action);
}
}
static DEFAULT_OPTIONS = {
tag: 'form',
// id: 'roll-selection',
id: 'roll-selection',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'],
position: {
width: 'auto'
@ -35,8 +32,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@ -47,7 +42,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
};
get title() {
return `${this.config.title}${this.actor ? `: ${this.actor.name}` : ''}`;
return this.config.title;
}
get actor() {
@ -71,16 +66,13 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config;
context.hasRoll = !!this.config.roll;
context.canRoll = true;
context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
context.selectedRollMode = this.config.selectedRollMode;
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action,
label,
icon
}));
context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
this.config.costs ??= [];
if (this.config.costs?.length) {
const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(
@ -89,7 +81,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
);
context.costs = updatedCosts.map(x => ({
...x,
label: x.itemId
label: x.keyIsID
? this.action.parent.parent.name
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
}));
@ -109,7 +101,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.roll = this.roll;
context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices;
const experiences = this.config.data?.system?.experiences || {};
const experiences = this.config.data?.experiences || {};
context.experiences = Object.keys(experiences).map(id => ({
id,
...experiences[id]
@ -121,31 +113,15 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config);
if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers();
context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
context.showReaction = !context.rollConfig.type && context.rollType === 'DualityRoll';
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;
}
getTraitModifiers() {
return Object.values(abilities).map(a => ({
id: a.id,
label: `${game.i18n.localize(a.label)} (${this.actor.system.traits[a.id]?.value.signedString() ?? 0})`
}));
}
static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) {
@ -157,12 +133,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll[key] = value;
});
}
if (rest.hasOwnProperty('trait')) {
this.config.roll.trait = rest.trait;
this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.config.roll.trait]?.label)
});
}
this.config.extraFormula = rest.extraFormula;
this.render();
}
@ -181,43 +151,35 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key)
: [...this.config.experiences, button.dataset.key];
this.config.costs =
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,
{
extKey: button.dataset.key,
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope',
value: 1,
name: this.config.data?.system.experiences?.[button.dataset.key]?.name
}
];
if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') {
this.config.costs =
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,
{
extKey: button.dataset.key,
key: 'hope',
value: 1,
name: this.config.data?.experiences?.[button.dataset.key]?.name
}
];
}
this.render();
}
static toggleReaction() {
if (this.config.roll) {
this.reactionOverride = !this.reactionOverride;
this.config.actionType = this.reactionOverride
? 'reaction'
: this.config.actionType === 'reaction'
? 'action'
: this.config.actionType;
this.config.roll.type = this.reactionOverride
? CONFIG.DH.ITEM.actionTypes.reaction.id
: this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id
? null
: this.config.roll.type;
this.render();
}
}
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
}

View file

@ -6,7 +6,6 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.roll = roll;
this.config = config;
this.selectedEffects = this.config.bonusEffects;
}
static DEFAULT_OPTIONS = {
@ -21,7 +20,6 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon: 'fa-solid fa-dice'
},
actions: {
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
form: {
@ -59,9 +57,6 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon
}));
context.modifiers = this.config.modifiers;
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
return context;
}
@ -74,11 +69,6 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() {
await this.close({ submitted: true });
}

View file

@ -10,7 +10,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject;
this.actor = actor;
this.damage = damage;
this.damageType = damageType;
this.rulesDefault = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
@ -40,7 +39,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.availableStressReductions = Object.keys(actor.system.rules.damageReduction.stressDamageReduction).reduce(
(acc, key) => {
const dr = actor.system.rules.damageReduction.stressDamageReduction[key];
if (dr.cost) {
if (dr.enabled) {
if (acc === null) acc = {};
const damage = damageKeyToNumber(key);
@ -58,11 +57,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
null
);
this.reduceSeverity = this.damageType.reduce((value, curr) => {
return Math.max(this.actor.system.rules.damageReduction.reduceSeverity[curr], value);
}, 0);
this.actor.system.rules.damageReduction.reduceSeverity[this.damageType];
this.thresholdImmunities = Object.keys(actor.system.rules.damageReduction.thresholdImmunities).reduce(
(acc, key) => {
if (actor.system.rules.damageReduction.thresholdImmunities[key])
@ -117,9 +111,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
CONFIG.DH.GENERAL.ruleChoice.onWithToggle.id,
CONFIG.DH.GENERAL.ruleChoice.offWithToggle.id
].includes(this.rulesDefault);
context.reduceSeverity = this.reduceSeverity;
context.thresholdImmunities =
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
context.thresholdImmunities = this.thresholdImmunities;
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
this.getDamageInfo();
@ -181,9 +173,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0
);
if (this.reduceSeverity) {
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
}
if (this.thresholdImmunities[currentDamage]) currentDamage = 0;
@ -271,7 +260,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
const reducedDamage = currentDamage !== this.damage ? getDamageLabel(currentDamage) : null;
const currentDamageLabel = reducedDamage ?? getDamageLabel(this.damage);
if (!stressReduction.any && stressReduction.from !== currentDamageLabel) return;
if (stressReduction.from !== currentDamageLabel) return;
stressReduction.selected = true;
this.render();

View file

@ -1,5 +1,3 @@
import { refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) {
@ -93,34 +91,36 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}
getRefreshables() {
const actionItems = this.actor.items
.filter(x => this.actor.system.isItemAvailable(x))
.reduce((acc, x) => {
if (x.system.actions) {
const recoverable = x.system.actions.reduce((acc, action) => {
if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) {
acc.push({
title: x.name,
name: action.name,
uuid: action.uuid
});
}
return acc;
}, []);
if (recoverable) {
acc.push(...recoverable);
const actionItems = this.actor.items.reduce((acc, x) => {
if (x.system.actions) {
const recoverable = x.system.actions.reduce((acc, action) => {
if (
(action.uses.recovery && (action.uses.recovery === 'longRest') === !this.shortrest) ||
action.uses.recovery === 'shortRest'
) {
acc.push({
title: x.name,
name: action.name,
uuid: action.uuid
});
}
}
return acc;
}, []);
return acc;
}, []);
if (recoverable) {
acc.push(...recoverable);
}
}
return acc;
}, []);
const resourceItems = this.actor.items.reduce((acc, x) => {
if (
x.system.resource &&
x.system.resource.type &&
refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], x.system.resource.recovery)
((x.system.resource.recovery === 'longRest') === !this.shortrest ||
x.system.resource.recovery === 'shortRest')
) {
acc.push({
title: game.i18n.localize(`TYPES.Item.${x.type}`),
@ -177,24 +177,11 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
}
static async takeDowntime() {
const moves = Object.keys(this.moveData).flatMap(categoryKey => {
const category = this.moveData[categoryKey];
return Object.keys(category.moves)
.filter(x => category.moves[x].selected)
.flatMap(key => {
const move = category.moves[key];
const needsTarget = move.actions.filter(x => x.target?.type && x.target.type !== 'self').length > 0;
return [...Array(move.selected).keys()].map(_ => ({
...move,
movePath: `${categoryKey}.moves.${key}`,
needsTarget: needsTarget
}));
});
const moves = Object.values(this.moveData).flatMap(category => {
return Object.values(category.moves)
.filter(x => x.selected)
.flatMap(move => [...Array(move.selected).keys()].map(_ => move));
});
const characters = game.actors
.filter(x => x.type === 'character')
.filter(x => x.testUserPermission(game.user, 'LIMITED'))
.filter(x => x.uuid !== this.actor.uuid);
const cls = getDocumentClass('ChatMessage');
const msg = {
@ -214,9 +201,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`
),
actor: { name: this.actor.name, img: this.actor.img },
moves: moves,
characters: characters,
selfId: this.actor.uuid
moves: moves
}
),
flags: {

View file

@ -1,196 +0,0 @@
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}`;
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}`;
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,70 +0,0 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class ImageSelectDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(titleName, images) {
super();
this.titleName = titleName;
this.images = images;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dialog', 'dh-style', 'image-select'],
position: {
width: 612,
height: 'auto'
},
window: {
icon: 'fa-solid fa-paw'
},
actions: {
selectImage: ImageSelectDialog.#selectImage,
finishSelection: ImageSelectDialog.#finishSelection
}
};
get title() {
return this.titleName;
}
/** @override */
static PARTS = {
main: {
template: 'systems/daggerheart/templates/dialogs/image-select/main.hbs',
scrollable: ['.images-container']
},
footer: { template: 'systems/daggerheart/templates/dialogs/image-select/footer.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.images = this.images;
context.selectedImage = this.selectedImage;
return context;
}
static #selectImage(_event, button) {
this.selectedImage = button.dataset.image ?? button.querySelector('img').dataset.image;
this.render();
}
static #finishSelection() {
this.close({ submitted: true });
}
async close(options = {}) {
if (!options.submitted) this.selectedImage = null;
await super.close();
}
static async configure(title, images) {
return new Promise(resolve => {
const app = new this(title, images);
app.addEventListener('close', () => resolve(app.selectedImage), { once: true });
app.render({ force: true });
});
}
}

View file

@ -1,62 +0,0 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class ItemTransferDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(data) {
super({});
this.data = data;
}
get title() {
return this.data.title;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'item-transfer'],
position: { width: 400, height: 'auto' },
window: { icon: 'fa-solid fa-hand-holding-hand' },
actions: {
finish: ItemTransferDialog.#finish
}
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/item-transfer.hbs', root: true }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
return foundry.utils.mergeObject(context, this.data);
}
static async #finish() {
this.selected = this.form.elements.quantity.valueAsNumber || null;
this.close();
}
static #determineTransferOptions({ originActor, targetActor, item, currency }) {
originActor ??= item?.actor;
const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew;
const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null;
return {
originActor,
targetActor,
itemImage: item?.img,
currencyIcon: currencySetting?.icon,
max: item?.system.quantity ?? originActor.system.gold[currency] ?? 0,
title: item?.name ?? currencySetting?.label
};
}
static async configure(options) {
return new Promise(resolve => {
const data = this.#determineTransferOptions(options);
if (data.max <= 1) return resolve(data.max);
const app = new this(data);
app.addEventListener('close', () => resolve(app.selected), { once: true });
app.render({ force: true });
});
}
}

View file

@ -1,20 +1,18 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(name, ownership, defaultOwnership) {
constructor(resolve, reject, name, ownership) {
super({});
this.resolve = resolve;
this.reject = reject;
this.name = name;
this.ownership = foundry.utils.deepClone(ownership);
this.defaultOwnership = defaultOwnership;
this.ownership = ownership;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'],
window: {
icon: 'fa-solid fa-users'
},
classes: ['daggerheart', 'views', 'ownership-selection'],
position: {
width: 600,
height: 'auto'
@ -32,48 +30,43 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name });
}
getOwnershipData(id) {
return this.ownership[id] ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels;
context.defaultOwnership = this.defaultOwnership;
context.ownership = game.users.reduce((acc, user) => {
if (!user.isGM) {
acc[user.id] = {
...user,
img: user.character?.img ?? 'icons/svg/cowled.svg',
ownership: this.getOwnershipData(user.id)
};
}
context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({
value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level],
label: game.i18n.localize(`OWNERSHIP.${level}`)
}));
context.ownership = {
default: this.ownership.default,
players: Object.keys(this.ownership.players).reduce((acc, x) => {
const user = game.users.get(x);
if (!user.isGM) {
acc[x] = {
img: user.character?.img ?? 'icons/svg/cowled.svg',
name: user.name,
ownership: this.ownership.players[x].value
};
}
return acc;
}, {});
context.showOwnership = Boolean(Object.keys(context.ownership).length);
return acc;
}, {})
};
return context;
}
static async updateData(event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
this.close(data);
const { ownership } = foundry.utils.expandObject(formData.object);
this.resolve(ownership);
this.close(true);
}
async close(data) {
if (data) {
this.saveData = data;
async close(fromSave) {
if (!fromSave) {
this.reject();
}
await super.close();
}
static async configure(name, ownership, defaultOwnership) {
return new Promise(resolve => {
const app = new this(name, ownership, defaultOwnership);
app.addEventListener('close', () => resolve(app.saveData), { once: true });
app.render({ force: true });
});
}
}

View file

@ -1,5 +1,3 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -124,15 +122,6 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
}, {})
};
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();
}

View file

@ -1,347 +0,0 @@
import { getCritDamageBonus } from '../../helpers/utils.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class TagTeamDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(party) {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.party = party;
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.TagTeamRoll) {
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.render();
}
});
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
position: { width: 550, height: 'auto' },
actions: {
removeMember: TagTeamDialog.#removeMember,
unlinkMessage: TagTeamDialog.#unlinkMessage,
selectMessage: TagTeamDialog.#selectMessage,
createTagTeam: TagTeamDialog.#createTagTeam
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'tag-team-dialog',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.hopeCost = this.hopeCost;
context.data = this.data;
context.memberOptions = this.party.filter(c => !this.data.members[c.id]);
context.selectedCharacterOptions = this.party.filter(c => this.data.members[c.id]);
context.members = Object.keys(this.data.members).map(id => {
const roll = this.data.members[id].messageId ? game.messages.get(this.data.members[id].messageId) : null;
context.usesDamage =
context.usesDamage === undefined
? roll?.system.hasDamage
: context.usesDamage && roll?.system.hasDamage;
return {
character: this.party.find(x => x.id === id),
selected: this.data.members[id].selected,
roll: roll,
damageValues: roll
? Object.keys(roll.system.damage).map(key => ({
key: key,
name: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label),
total: roll.system.damage[key].total
}))
: null
};
});
const initiatorChar = this.party.find(x => x.id === this.data.initiator.id);
context.initiator = {
character: initiatorChar,
cost: this.data.initiator.cost
};
const selectedMember = Object.values(context.members).find(x => x.selected && x.roll);
const selectedIsCritical = selectedMember?.roll?.system?.isCritical;
context.selectedData = {
result: selectedMember
? `${selectedMember.roll.system.roll.total} ${selectedMember.roll.system.roll.result.label}`
: null,
damageValues: null
};
for (const member of Object.values(context.members)) {
if (!member.roll) continue;
if (context.usesDamage) {
if (!context.selectedData.damageValues) context.selectedData.damageValues = {};
for (let damage of member.damageValues) {
const damageTotal = member.roll.system.isCritical
? damage.total
: selectedIsCritical
? damage.total + (await getCritDamageBonus(member.roll.system.damage[damage.key].formula))
: damage.total;
if (context.selectedData.damageValues[damage.key]) {
context.selectedData.damageValues[damage.key].total += damageTotal;
} else {
context.selectedData.damageValues[damage.key] = {
...foundry.utils.deepClone(damage),
total: damageTotal
};
}
}
}
}
context.showResult = Object.values(context.members).reduce((enabled, member) => {
if (!member.roll) return enabled;
if (context.usesDamage) {
enabled = enabled === null ? member.damageValues.length > 0 : enabled && member.damageValues.length > 0;
} else {
enabled = enabled === null ? Boolean(member.roll) : enabled && Boolean(member.roll);
}
return enabled;
}, null);
context.createDisabled =
!context.selectedData.result ||
!this.data.initiator.id ||
Object.keys(this.data.members).length === 0 ||
Object.values(context.members).some(x =>
context.usesDamage ? !x.damageValues || x.damageValues.length === 0 : !x.roll
);
return context;
}
async updateSource(update) {
await this.data.updateSource(update);
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, this.data.toObject());
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: this.data.toObject(),
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
static async updateData(_event, _element, formData) {
const { selectedAddMember, initiator } = foundry.utils.expandObject(formData.object);
const update = { initiator: initiator };
if (selectedAddMember) {
const member = await foundry.utils.fromUuid(selectedAddMember);
update[`members.${member.id}`] = { messageId: null };
}
await this.updateSource(update);
this.render();
}
static async #removeMember(_, button) {
const update = { [`members.-=${button.dataset.characterId}`]: null };
if (this.data.initiator.id === button.dataset.characterId) {
update.iniator = { id: null };
}
await this.updateSource(update);
}
static async #unlinkMessage(_, button) {
await this.updateSource({ [`members.${button.id}.messageId`]: null });
}
static async #selectMessage(_, button) {
const member = this.data.members[button.id];
const currentSelected = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const curretSelectedUpdate =
currentSelected && currentSelected !== button.id ? { [`${currentSelected}`]: { selected: false } } : {};
await this.updateSource({
members: {
[`${button.id}`]: { selected: !member.selected },
...curretSelectedUpdate
}
});
}
static async #createTagTeam() {
const mainRollId = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const mainRoll = game.messages.get(this.data.members[mainRollId].messageId);
if (this.data.initiator.cost) {
const initiator = this.party.find(x => x.id === this.data.initiator.id);
if (initiator.system.resources.hope.value < this.data.initiator.cost) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope')
);
}
}
const secondaryRolls = Object.keys(this.data.members)
.filter(key => key !== mainRollId)
.map(key => game.messages.get(this.data.members[key].messageId));
const systemData = foundry.utils.deepClone(mainRoll).system.toObject();
const criticalRoll = systemData.roll.isCritical;
for (let roll of secondaryRolls) {
if (roll.system.hasDamage) {
for (let key in roll.system.damage) {
var damage = roll.system.damage[key];
const damageTotal =
!roll.system.isCritical && criticalRoll
? (await getCritDamageBonus(damage.formula)) + damage.total
: damage.total;
const updatedDamageParts = damage.parts;
if (systemData.damage[key]) {
if (!roll.system.isCritical && criticalRoll) {
for (let part of updatedDamageParts) {
const criticalDamage = await getCritDamageBonus(part.formula);
if (criticalDamage) {
damage.formula = `${damage.formula} + ${criticalDamage}`;
part.formula = `${part.formula} + ${criticalDamage}`;
part.modifierTotal = part.modifierTotal + criticalDamage;
part.total += criticalDamage;
part.roll = new Roll(part.formula);
}
}
}
systemData.damage[key].formula = `${systemData.damage[key].formula} + ${damage.formula}`;
systemData.damage[key].total += damageTotal;
systemData.damage[key].parts = [...systemData.damage[key].parts, ...updatedDamageParts];
} else {
systemData.damage[key] = { ...damage, total: damageTotal, parts: updatedDamageParts };
}
}
}
}
systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: 'dualityRoll',
user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: this.party.find(x => x.id === mainRollId) }),
system: systemData,
rolls: mainRoll.rolls,
sound: null,
flags: { core: { RollTable: true } }
};
await cls.create(msgData);
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId of Object.keys(this.data.members)) {
const resourceUpdates = [];
const rollGivesHope = systemData.roll.isCritical || systemData.roll.result.duality === 1;
if (memberId === this.data.initiator.id) {
const value = this.data.initiator.cost
? rollGivesHope
? 1 - this.data.initiator.cost
: -this.data.initiator.cost
: 1;
resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true });
} else if (rollGivesHope) {
resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true });
}
if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (systemData.roll.result.duality === -1) {
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
}
this.party.find(x => x.id === memberId).modifyResource(resourceUpdates);
}
if (fearUpdate.value) {
this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]);
}
/* Improve by fetching default from schema */
const update = { members: [], initiator: { id: null, cost: 3 } };
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: update,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
static async assignRoll(char, message) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
const character = settings.members[char.id];
if (!character) return;
await settings.updateSource({ [`members.${char.id}.messageId`]: message.id });
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, settings);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: settings,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
async close(options = {}) {
Hooks.off(socketEvent.Refresh, this.setupHooks);
await super.close(options);
}
}

View file

@ -1,12 +1,6 @@
import { shuffleArray } from '../../helpers/utils.mjs';
export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
static DEFAULT_OPTIONS = {
classes: ['daggerheart'],
actions: {
combat: DHTokenHUD.#onToggleCombat,
togglePartyTokens: DHTokenHUD.#togglePartyTokens
}
classes: ['daggerheart']
};
/** @override */
@ -17,28 +11,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
}
};
static #nonCombatTypes = ['environment', 'companion', 'party'];
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (!this.actor) return context;
context.partyOnCanvas =
this.actor.type === 'party' &&
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png';
context.actorType = this.actor.type;
context.usesEffects = this.actor.type !== 'party';
context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type)
? false
: context.canToggleCombat;
context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key];
if (effect.systemEffect) {
const disabled = !effect.isActive && this.actor.system.rules?.conditionImmunities?.[key];
acc[key] = { ...effect, disabled };
}
if (effect.systemEffect) acc[key] = effect;
return acc;
}, {});
@ -59,138 +36,6 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
return context;
}
static async #onToggleCombat() {
const tokensWithoutActors = canvas.tokens.controlled.filter(t => !t.actor);
const warning =
tokensWithoutActors.length === 1
? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', {
name: tokensWithoutActors[0].name
})
: game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', {
names: tokensWithoutActors.map(x => x.name).join(', ')
});
const tokens = canvas.tokens.controlled
.filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
.map(t => t.document);
if (!this.object.controlled && this.document.actor) tokens.push(this.document);
try {
if (this.document.inCombat) {
const tokensInCombat = tokens.filter(t => t.inCombat);
await TokenDocument.implementation.deleteCombatants([...tokensInCombat, ...tokensWithoutActors]);
} else {
if (tokensWithoutActors.length) {
ui.notifications.warn(warning);
}
const tokensOutOfCombat = tokens.filter(t => !t.inCombat);
await TokenDocument.implementation.createCombatants(tokensOutOfCombat);
}
} catch (err) {
ui.notifications.warn(err.message);
}
}
static async #togglePartyTokens(_, button) {
const icon = button.querySelector('img');
icon.classList.toggle('flipped');
button.dataset.tooltip = game.i18n.localize(
icon.classList.contains('flipped')
? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrievePartyTokens'
: 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens'
);
const animationDuration = 500;
const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens());
const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) {
for (let token of activeTokens) {
await token.document.update(
{ x: actorX, y: actorY, alpha: 0 },
{ animation: { duration: animationDuration } }
);
setTimeout(() => token.document.delete(), animationDuration);
}
} else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
const partyTokenData = [];
for (let member of this.actor.system.partyMembers) {
const data = await member.getTokenDocument();
partyTokenData.push(data.toObject());
}
const newTokens = await activeScene.createEmbeddedDocuments(
'Token',
partyTokenData.map(tokenData => ({
...tokenData,
alpha: 0,
x: actorX,
y: actorY
}))
);
const { sizeX, sizeY } = activeScene.grid;
const nrRandomPositions = Math.ceil(newTokens.length / 8) * 8;
/* This is an overcomplicated mess, but I'm stupid */
const positions = shuffleArray(
[...Array(nrRandomPositions).keys()].map((_, index) => {
const nonZeroIndex = index + 1;
const indexFloor = Math.floor(index / 8);
const distanceCoefficient = indexFloor + 1;
const side = 3 + indexFloor * 2;
const sideMiddle = Math.ceil(side / 2);
const inbetween = 1 + indexFloor * 2;
const inbetweenMiddle = Math.ceil(inbetween / 2);
if (index < side) {
const distance =
nonZeroIndex === sideMiddle
? 0
: nonZeroIndex < sideMiddle
? -nonZeroIndex
: nonZeroIndex - sideMiddle;
return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient };
} else if (index < side + inbetween) {
const inbetweenIndex = nonZeroIndex - side;
const distance =
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? -inbetweenIndex
: inbetweenIndex - inbetweenMiddle;
return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance };
} else if (index < 2 * side + inbetween) {
const sideIndex = nonZeroIndex - side - inbetween;
const distance =
sideIndex === sideMiddle
? 0
: sideIndex < sideMiddle
? sideIndex
: -(sideIndex - sideMiddle);
return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient };
} else {
const inbetweenIndex = nonZeroIndex - 2 * side - inbetween;
const distance =
inbetweenIndex === inbetweenMiddle
? 0
: inbetweenIndex < inbetweenMiddle
? inbetweenIndex
: -(inbetweenIndex - inbetweenMiddle);
return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance };
}
})
);
for (let token of newTokens) {
const position = positions.pop();
token.update(
{ x: position.x, y: position.y, alpha: 1 },
{ animation: { duration: animationDuration } }
);
}
}
}
_getStatusEffectChoices() {
// Include all HUD-enabled status effects
const choices = {};
@ -214,20 +59,16 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
}
// Update the status of effects which are active for the token actor
const activeEffects = this.actor?.getActiveEffects() || [];
const activeEffects = this.actor?.effects || [];
for (const effect of activeEffects) {
for (const statusId of effect.statuses) {
const status = choices[statusId];
if (!status) continue;
status.instances = 1 + (status.instances ?? 0);
status.locked = status.locked || effect.condition || status.instances > 1;
if (!status) continue;
if (status._id) {
if (status._id !== effect.id) continue;
}
status.isActive = true;
if (effect.getFlag?.('core', 'overlay')) status.isOverlay = true;
if (effect.getFlag('core', 'overlay')) status.isOverlay = true;
}
}

View file

@ -1,4 +1,3 @@
export { default as CharacterLevelup } from './characterLevelup.mjs';
export { default as CompanionLevelup } from './companionLevelup.mjs';
export { default as Levelup } from './levelup.mjs';
export { default as LevelupViewMode } from './levelupViewMode.mjs';

View file

@ -49,7 +49,12 @@ export default class DhCharacterLevelUp extends LevelUpBase {
const experienceIncreases = Object.values(advancementChoices.experience ?? {});
const experienceIncreaseValues = experienceIncreases
.filter(exp => exp.data.length > 0)
.flatMap(exp => exp.data);
.flatMap(exp =>
exp.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(x => x === data);
return this.actor.system.experiences[experience].name;
})
);
context.experienceIncreases = {
values: experienceIncreaseValues,
active: experienceIncreases.length > 0,
@ -280,19 +285,11 @@ export default class DhCharacterLevelUp extends LevelUpBase {
break;
case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = [];
const allExperiences = {
...this.actor.system.experiences,
...Object.values(this.levelup.levels).reduce((acc, level) => {
for (const key of Object.keys(level.achievements.experiences)) {
acc[key] = level.achievements.experiences[key];
}
return acc;
}, {})
};
const data = checkbox.data.map(data => {
const experience = Object.keys(allExperiences).find(x => x === data);
return allExperiences[experience]?.name ?? '';
const experience = Object.keys(this.actor.system.experiences).find(
x => x === data
);
return this.actor.system.experiences[experience]?.name ?? '';
});
advancement[choiceKey].push({ data: data, value: checkbox.value });
break;

View file

@ -37,7 +37,12 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
const experienceIncreases = Object.values(advancementChoices.experience ?? {});
const experienceIncreaseValues = experienceIncreases
.filter(exp => exp.data.length > 0)
.flatMap(exp => exp.data);
.flatMap(exp =>
exp.data.map(data => {
const experience = Object.keys(this.actor.system.experiences).find(x => x === data);
return this.actor.system.experiences[experience].name;
})
);
context.experienceIncreases = {
values: experienceIncreaseValues,
active: experienceIncreases.length > 0,
@ -67,28 +72,6 @@ export default class DhCompanionLevelUp extends BaseLevelUp {
const levelKeys = Object.keys(this.levelup.levels);
const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice;
const actorRange = this.actor.system.attack.range;
let achievementExperiences = [];
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];
if (Number(levelKey) < this.levelup.startLevel) continue;
achievementExperiences = level.achievements.experiences
? Object.values(level.achievements.experiences).reduce((acc, experience) => {
if (experience.name) acc.push(experience);
return acc;
}, [])
: [];
}
context.achievements = {
experiences: {
values: achievementExperiences,
shown: achievementExperiences.length > 0
}
};
context.achievements = context.achievements.experiences.shown ? context.achievements : undefined;
const advancement = {};
for (var levelKey of levelKeys) {
const level = this.levelup.levels[levelKey];

View file

@ -1,5 +1,6 @@
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -11,6 +12,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this._dragDrop = this._createDragDropHandlers();
this.tabGroups.primary = 'advancements';
this.itemBrowser = null;
}
get title() {
@ -357,23 +360,11 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) {
const allExperiences = {
...this.actor.system.experiences,
...Object.values(this.levelup.levels).reduce((acc, level) => {
for (const key of Object.keys(level.achievements.experiences)) {
acc[key] = level.achievements.experiences[key];
}
return acc;
}, {})
};
tagifyElement(
experienceIncreaseTagify,
Object.keys(allExperiences).reduce((acc, id) => {
const experience = allExperiences[id];
if (experience.name) {
acc.push({ id: id, label: experience.name });
}
Object.keys(this.actor.system.experiences).reduce((acc, id) => {
const experience = this.actor.system.experiences[id];
acc.push({ id: id, label: experience.name });
return acc;
}, []),
@ -549,6 +540,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const type = target.dataset.compendium ?? target.dataset.type;
const presets = {
compendium: 'daggerheart',
folder: type,
render: {
noFolder: true
@ -567,7 +559,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
};
}
ui.compendiumBrowser.open(presets);
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true }));
}
static async selectPreview(_, button) {
@ -658,9 +650,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this.render();
}
static async save(_, button) {
button.disabled = true;
static async save() {
const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => {
if (level >= this.levelup.startLevel) {
acc[level] = this.levelup.levels[level].toObject();
@ -670,8 +660,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}, {});
await this.actor.levelUp(levelupData);
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
if (this.itemBrowser) this.itemBrowser.close();
this.close();
}
}

View file

@ -1,95 +0,0 @@
import { chunkify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhlevelUpViewMode extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
}
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Levelup.viewModeTitle', { actor: this.actor.name });
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dialog', 'dh-style', 'levelup'],
position: { width: 1000, height: 'auto' },
window: {
resizable: true,
icon: 'fa-solid fa-arrow-turn-up'
}
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/levelup/tabs/viewMode.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const { tiers } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers);
const tierKeys = Object.keys(tiers);
const selections = Object.keys(this.actor.system.levelData.levelups).reduce(
(acc, key) => {
const level = this.actor.system.levelData.levelups[key];
Object.keys(level.selections).forEach(optionKey => {
const choice = level.selections[optionKey];
if (!acc[choice.tier][choice.optionKey]) acc[choice.tier][choice.optionKey] = {};
acc[choice.tier][choice.optionKey][choice.checkboxNr] = choice;
});
return acc;
},
tierKeys.reduce((acc, key) => {
acc[key] = {};
return acc;
}, {})
);
context.tiers = tierKeys.map((tierKey, tierIndex) => {
const tier = tiers[tierKey];
return {
name: tier.name,
active: true,
groups: Object.keys(tier.options).map(optionKey => {
const option = tier.options[optionKey];
const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => {
const checkboxNr = index + 1;
const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr];
const checkbox = { ...option, checkboxNr, tier: tierKey, disabled: true };
if (checkboxData) {
checkbox.level = checkboxData.level;
checkbox.selected = true;
}
return checkbox;
});
let label = game.i18n.localize(option.label);
return {
label: label,
checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => {
const anySelected = chunkedBoxes.some(x => x.selected);
const anyDisabled = chunkedBoxes.some(x => x.disabled);
return {
multi: option.minCost > 1,
checkboxes: chunkedBoxes.map(x => ({
...x,
selected: anySelected,
disabled: anyDisabled
}))
};
})
};
})
};
});
return context;
}
}

View file

@ -1 +0,0 @@
export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs';

View file

@ -1,108 +0,0 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
constructor(options) {
super(options);
Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.Scene) this.render();
});
}
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
actions: {
...super.DEFAULT_OPTIONS.actions,
removeSceneEnvironment: DhSceneConfigSettings.#removeSceneEnvironment
}
};
static buildParts() {
const { footer, tabs, ...parts } = super.PARTS;
const tmpParts = {
tabs: { template: 'systems/daggerheart/templates/scene/tabs.hbs' },
...parts,
dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' },
footer
};
return tmpParts;
}
static PARTS = DhSceneConfigSettings.buildParts();
static buildTabs() {
super.TABS.sheet.tabs.push({ id: 'dh', src: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg' });
return super.TABS;
}
static TABS = DhSceneConfigSettings.buildTabs();
async _preRender(context, options) {
await super._preFirstRender(context, options);
if (!options.internalRefresh)
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
switch (partId) {
case 'dh':
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } });
this.render({ internalRefresh: true });
});
const dragArea = htmlElement.querySelector('.scene-environments');
if (dragArea) dragArea.ondrop = this._onDrop.bind(this);
break;
}
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
await this.daggerheartFlag.updateSource({
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid]
});
this.render({ internalRefresh: true });
}
}
/** @inheritDoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'dh':
context.data = this.daggerheartFlag;
context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules);
break;
}
return context;
}
static async #removeSceneEnvironment(_event, button) {
await this.daggerheartFlag.updateSource({
sceneEnvironments: this.daggerheartFlag.sceneEnvironments.filter(
(_, index) => index !== Number.parseInt(button.dataset.index)
)
});
this.render({ internalRefresh: true });
}
/** @override */
async _processSubmitData(event, form, submitData, options) {
submitData.flags.daggerheart = this.daggerheartFlag.toObject();
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);
}
}

View file

@ -3,48 +3,43 @@ import { getDiceSoNicePreset } from '../../config/generalConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* @import {ApplicationClickAction} from "@client/applications/_types.mjs"
*/
export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) {
/**@inheritdoc */
constructor() {
super({});
this.settings = new DhAppearance(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' },
window: {
title: 'DAGGERHEART.SETTINGS.Menu.title',
icon: 'fa-solid fa-gears'
},
actions: {
reset: DHAppearanceSettings.#onReset,
preview: DHAppearanceSettings.#onPreview
reset: this.reset,
save: this.save,
preview: this.preview
},
form: {
closeOnSubmit: true,
handler: DHAppearanceSettings.#onSubmit
}
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/settings/appearance-settings/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' },
diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' },
footer: { template: 'templates/generic/form-footer.hbs' }
main: {
template: 'systems/daggerheart/templates/settings/appearance-settings.hbs'
}
};
/** @inheritdoc */
static TABS = {
general: {
tabs: [
{ id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' },
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }
],
initial: 'main'
},
diceSoNice: {
tabs: [
{ id: 'hope', label: 'DAGGERHEART.GENERAL.hope' },
@ -56,150 +51,79 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
}
};
/**@type {DhAppearance}*/
setting;
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
static #localized = false;
/** @inheritDoc */
async _preFirstRender(_context, _options) {
await super._preFirstRender(_context, _options);
if (!DHAppearanceSettings.#localized) {
foundry.helpers.Localization.localizeDataModel(this.setting.constructor);
DHAppearanceSettings.#localized = true;
}
this.render();
}
/** @inheritdoc */
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
if (!game.modules.get('dice-so-nice')?.active) {
delete parts.diceSoNice;
delete parts.tabs;
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
context.showDiceSoNice = game.modules.get('dice-so-nice')?.active;
if (game.dice3d) {
context.diceSoNiceTextures = game.dice3d.exports.TEXTURELIST;
context.diceSoNiceColorsets = game.dice3d.exports.COLORSETS;
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).map(key => ({
key: key,
name: `DICESONICE.Material${key.capitalize()}`
}));
context.diceSoNiceSystems = [];
for (const [key, system] of game.dice3d.DiceFactory.systems.entries()) {
context.diceSoNiceSystems.push({ key, name: system.name });
}
}
return parts;
}
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (options.isFirstRender)
this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
context.setting = this.setting;
context.fields = this.setting.schema.fields;
context.tabs = this._prepareTabs('general');
context.dsnTabs = this._prepareTabs('diceSoNice');
context.diceTab = {
key: this.tabGroups.diceSoNice,
source: this.settings._source.diceSoNice[this.tabGroups.diceSoNice],
fields: this.settings.schema.fields.diceSoNice.fields[this.tabGroups.diceSoNice].fields
};
return context;
}
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
if (partId in context.tabs) partContext.tab = partContext.tabs[partId];
switch (partId) {
case 'diceSoNice':
await this.prepareDiceSoNiceContext(partContext);
break;
case 'footer':
partContext.buttons = [
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
];
break;
static async updateData(event, element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource(updatedSettings);
this.render();
}
static async preview() {
const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice];
let faces = 'd12';
switch (this.tabGroups.diceSoNice) {
case 'advantage':
case 'disadvantage':
faces = 'd6';
}
return partContext;
}
/**
* Prepare render context for the DSN part.
* @param {ApplicationRenderContext} context
* @returns {Promise<void>}
* @protected
*/
async prepareDiceSoNiceContext(context) {
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
(acc, [k, v]) => ({
...acc,
[k]: v.name
}),
{}
);
context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce(
(acc, v) => ({
...acc,
[v.id]: v.description
}),
{}
);
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce(
(acc, key) => ({
...acc,
[key]: `DICESONICE.Material${key.capitalize()}`
}),
{}
);
context.diceSoNiceSystems = Object.fromEntries(
[...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name])
);
context.diceSoNiceFonts = game.dice3d.exports.Utils.prepareFontList();
foundry.utils.mergeObject(
context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
(acc, key) => ({
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
}
}),
{}
)
);
}
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @param {SubmitEvent} event
* @param {HTMLFormElement} form
* @param {foundry.applications.ux.FormDataExtended} formData
* @returns {Promise<void>}
*/
static async #onSubmit(event, form, formData) {
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
}
/* -------------------------------------------- */
/**
* Submit the configuration form.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
const preset = await getDiceSoNicePreset(source, faces);
const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
}
/**
* Reset the form back to default values.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
static async #onReset() {
this.setting = new this.setting.constructor();
this.render({ force: false });
static async reset() {
this.settings = new DhAppearance();
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, this.settings.toObject());
this.close();
}
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
}
}

View file

@ -31,20 +31,8 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
};
static PARTS = {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' },
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
};
/** @inheritdoc */
static TABS = {
main: {
tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
initial: 'general',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
template: 'systems/daggerheart/templates/settings/automation-settings.hbs'
}
};

View file

@ -1,6 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) {
@ -11,14 +10,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject()
);
this.selected = this.#getDefaultAdversaryType();
this.selected = {
domain: null
};
}
#getDefaultAdversaryType = () => ({
domain: null,
adversaryType: null
});
get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
}
@ -32,7 +28,6 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
icon: 'fa-solid fa-gears'
},
actions: {
editCurrencyIcon: this.changeCurrencyIcon,
addItem: this.addItem,
editItem: this.editItem,
removeItem: this.removeItem,
@ -40,11 +35,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addDomain: this.addDomain,
toggleSelectedDomain: this.toggleSelectedDomain,
deleteDomain: this.deleteDomain,
addAdversaryType: this.addAdversaryType,
deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType,
save: this.save,
resetTokenSizes: this.resetTokenSizes,
reset: this.reset
},
form: { handler: this.updateData, submitOnChange: true }
@ -54,8 +45,6 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' },
itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' },
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
};
@ -63,19 +52,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
/** @inheritdoc */
static TABS = {
main: {
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }],
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }],
initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
this.selected = this.#getDefaultAdversaryType();
this.render();
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
@ -97,11 +79,6 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
context.configDomains = CONFIG.DH.DOMAIN.domains;
context.homebrewDomains = this.settings.domains;
break;
case 'types':
context.selectedAdversaryType = this.selected.adversaryType
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null;
break;
}
return context;
@ -117,100 +94,34 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async changeCurrencyIcon(_, target) {
const type = target.dataset.currency;
const currentIcon = this.settings.currency[type].icon;
const icon = await foundry.applications.api.DialogV2.input({
classes: ['daggerheart', 'dh-style', 'change-currency-icon'],
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/settings/homebrew-settings/change-currency-icon.hbs',
{ currentIcon }
),
window: {
title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.currency.changeIcon'),
icon: 'fa-solid fa-coins'
},
render: (_, dialog) => {
const icon = dialog.element.querySelector('.displayed-icon i');
const input = dialog.element.querySelector('input');
const reset = dialog.element.querySelector('button[data-action=reset]');
input.addEventListener('input', () => {
icon.classList.value = input.value;
});
reset.addEventListener('click', () => {
const currencyField = DhHomebrew.schema.fields.currency.fields[type];
const initial = currencyField.fields.icon.getInitialValue();
input.value = icon.classList.value = initial;
});
},
ok: {
callback: (_, button) => button.form.elements.icon.value
static async addItem(_, target) {
await this.settings.updateSource({
[`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
img: 'icons/magic/life/cross-worn-green.webp',
description: '',
actions: []
}
});
if (icon !== null) {
await this.settings.updateSource({
[`currency.${type}.icon`]: icon
});
this.render();
}
}
static async addItem(_, target) {
const { type } = target.dataset;
if (['shortRest', 'longRest'].includes(type)) {
await this.settings.updateSource({
[`restMoves.${type}.moves.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
img: 'icons/magic/life/cross-worn-green.webp',
description: '',
actions: []
}
});
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
await this.settings.updateSource({
[`itemFeatures.${type}.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newFeature'),
img: 'icons/magic/life/cross-worn-green.webp',
description: '',
actions: [],
effects: []
}
});
}
this.render();
}
static async editItem(_, target) {
const { type, id } = target.dataset;
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`;
const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id];
const configTitle = isDowntime
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove')
: type === 'armorFeatures'
? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature')
: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature');
const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure(
configTitle,
featureBase,
const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id];
const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`;
const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure(
move,
path,
this.settings,
{ hasIcon: isDowntime, hasEffects: !isDowntime }
this.settings
);
if (!editedBase) return;
if (!editedMove) return;
await this.updateAction.bind(this)(editedBase, target.dataset.type, target.dataset.id);
await this.updateAction.bind(this)(editedMove, target.dataset.type, target.dataset.id);
}
async updateAction(data, type, id) {
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({
[`${path}.${id}`]: {
[`restMoves.${type}.moves.${id}`]: {
actions: data.actions,
name: data.name,
icon: data.icon,
@ -218,16 +129,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
description: data.description
}
});
this.render();
}
static async removeItem(_, target) {
const { type, id } = target.dataset;
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({
[`${path}.-=${id}`]: null
[`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null
});
this.render();
}
@ -394,45 +301,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async addAdversaryType(_, target) {
const newId = foundry.utils.randomID();
await this.settings.updateSource({
[`adversaryTypes.${newId}`]: {
id: newId,
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.adversaryType.newType')
}
});
this.selected.adversaryType = newId;
this.render();
}
static async deleteAdversaryType(_, target) {
const { key } = target.dataset;
await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null });
this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType;
this.render();
}
static async selectAdversaryType(_, target) {
this.selected.adversaryType = this.selected.adversaryType === target.dataset.type ? null : target.dataset.type;
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close();
}
static async resetTokenSizes() {
await this.settings.updateSource({
tokenSizes: this.settings.schema.fields.tokenSizes.initial
});
this.render();
}
static async reset() {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {

View file

@ -1,10 +1,8 @@
export { default as ActionConfig } from './action-config.mjs';
export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-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 DowntimeConfig } from './downtimeConfig.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs';
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
export { default as DhTokenConfig } from './token-config.mjs';

View file

@ -1,382 +0,0 @@
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) {
constructor(action) {
super({});
this.action = action;
this.openSection = null;
this.openTrigger = this.action.triggers.length > 0 ? 0 : null;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.GENERAL.Tabs.settings')}: ${this.action.name}`;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'action-config', 'dialog', 'max-800'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 600, height: 'auto' },
actions: {
toggleSection: this.toggleSection,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage,
editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
};
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
base: {
id: 'base',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/base.hbs'
},
configuration: {
id: 'configuration',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/configuration.hbs'
},
effect: {
id: 'effect',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
},
trigger: {
id: 'trigger',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/trigger.hbs'
}
};
static TABS = {
base: {
active: true,
cssClass: '',
group: 'primary',
id: 'base',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.base'
},
config: {
active: false,
cssClass: '',
group: 'primary',
id: 'config',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.configuration'
},
effect: {
active: false,
cssClass: '',
group: 'primary',
id: 'effect',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.effects'
},
trigger: {
active: false,
cssClass: '',
group: 'primary',
id: 'trigger',
icon: null,
label: 'DAGGERHEART.GENERAL.Tabs.triggers'
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
element.addEventListener('change', this.updateSummonCount.bind(this));
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(true);
context.summons = [];
for (const summon of context.source.summon ?? []) {
const actor = await foundry.utils.fromUuid(summon.actorUUID);
context.summons.push({ actor, count: summon.count });
}
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor?.isNPC;
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll;
context.triggers = context.source.triggers.map((trigger, index) => {
const { hint, returns, usesActor } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
return {
...trigger,
hint,
returns,
usesActor,
revealed: this.openTrigger === index
};
});
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
{ key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') },
...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name }))
];
return context;
}
static toggleSection(_, button) {
this.openSection = button.dataset.section === this.openSection ? null : button.dataset.section;
this.render(true);
}
getCostOptions() {
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
const resource = this.action.parent.resource;
if (resource) {
options.resource = {
label: 'DAGGERHEART.GENERAL.itemResource',
group: 'Global'
};
}
if (this.action.parent.metadata?.isQuantifiable) {
options.quantity = {
label: 'DAGGERHEART.GENERAL.itemQuantity',
group: 'Global'
};
}
return options;
}
getRollTypeOptions() {
const types = foundry.utils.deepClone(CONFIG.DH.GENERAL.rollTypes);
if (!this.action.actor) return types;
Object.values(types).forEach(t => {
if (this.action.actor.type !== 'character' && t.playerOnly) delete types[t.id];
});
return types;
}
disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => {
if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true;
});
return filtered;
}
_prepareSubmitData(_event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
const itemAbilityCostKeys = Object.keys(CONFIG.DH.GENERAL.itemAbilityCosts);
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
const data = foundry.utils.getProperty(submitData, keyPath);
const dataValues = data ? Object.values(data) : [];
if (keyPath === 'cost') {
for (var value of dataValues) {
value.itemId = itemAbilityCostKeys.includes(value.key) ? this.action.parent.parent.id : null;
}
}
if (data) foundry.utils.setProperty(submitData, keyPath, dataValues);
}
return submitData;
}
static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData);
const data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = await this.action.update(data);
this.sheetUpdate?.(this.action);
this.render();
}
static addElement(event) {
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key;
if (!this.action[key]) return;
data[key].push(this.action.defaultValues[key] ?? {});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeElement(event, button) {
event.stopPropagation();
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key;
// Prefer explicit index, otherwise find by uuid
let index = button?.dataset.index;
if (index === undefined || index === null || index === '') {
const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid;
index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid);
if (index === -1) return;
} else index = Number(index);
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async editDoc(_event, target) {
const element = target.closest('[data-item-uuid]');
const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null;
if (doc) return doc.sheet.render({ force: true });
}
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
part = {};
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
data.damage.parts.push(part);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeDamage(_event, button) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = button.dataset.index;
data.damage.parts.splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addTrigger() {
const data = this.action.toObject();
data.triggers.push({
trigger: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
triggeringActor: CONFIG.DH.TRIGGER.triggerActorTargetType.any.id
});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async removeTrigger(_event, button) {
const trigger = CONFIG.DH.TRIGGER.triggers[this.action.triggers[button.dataset.index].trigger];
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.deleteTriggerTitle')
},
content: game.i18n.format('DAGGERHEART.ACTIONS.Config.deleteTriggerContent', {
trigger: game.i18n.localize(trigger.label)
})
});
if (!confirmed) return;
const data = this.action.toObject();
data.triggers = data.triggers.filter((_, index) => index !== Number.parseInt(button.dataset.index));
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async expandTrigger(_event, button) {
const index = Number.parseInt(button.dataset.index);
const toggle = (element, codeMirror) => {
codeMirror.classList.toggle('revealed');
const button = element.querySelector('a > i');
button.classList.toggle('fa-angle-up');
button.classList.toggle('fa-angle-down');
};
const fieldset = button.closest('fieldset');
const codeMirror = fieldset.querySelector('.code-mirror-wrapper');
toggle(fieldset, codeMirror);
if (this.openTrigger !== null && this.openTrigger !== index) {
const previouslyExpanded = fieldset
.closest(`section`)
.querySelector(`fieldset[data-index="${this.openTrigger}"]`);
const codeMirror = previouslyExpanded.querySelector('.code-mirror-wrapper');
toggle(previouslyExpanded, codeMirror);
this.openTrigger = index;
} else if (this.openTrigger === index) {
this.openTrigger = null;
} else {
this.openTrigger = index;
}
}
updateSummonCount(event) {
event.stopPropagation();
const wrapper = event.target.closest('.summon-count-wrapper');
const index = wrapper.dataset.index;
const data = this.action.toObject();
data.summon[index].count = event.target.value;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** Specific implementation in extending classes **/
static async addEffect(_event) {}
static removeEffect(_event, _button) {}
static editEffect(_event) {}
async close(options) {
this.tabGroups.primary = 'base';
await super.close(options);
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (!(item instanceof game.system.api.documents.DhpActor)) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop'));
return;
}
const actionData = this.action.toObject();
let countvalue = 1;
for (const entry of actionData.summon) {
if (entry.actorUUID === data.uuid) {
entry.count += 1;
countvalue = entry.count;
await this.constructor.updateForm.bind(this)(null, null, {
object: foundry.utils.flattenObject(actionData)
});
return;
}
}
actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
}
}

View file

@ -1,32 +1,232 @@
import DHActionBaseConfig from './action-base-config.mjs';
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
constructor(action, sheetUpdate) {
super({});
this.action = action;
this.sheetUpdate = sheetUpdate;
this.openSection = null;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.GENERAL.Tabs.settings')}: ${this.action.name}`;
}
export default class DHActionConfig extends DHActionBaseConfig {
static DEFAULT_OPTIONS = {
...DHActionBaseConfig.DEFAULT_OPTIONS,
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'max-800'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 600, height: 'auto' },
actions: {
...DHActionBaseConfig.DEFAULT_OPTIONS.actions,
toggleSection: this.toggleSection,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
editEffect: this.editEffect
addElement: this.addElement,
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id));
context.getEffectDetails = this.getEffectDetails.bind(this);
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
base: {
id: 'base',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/base.hbs'
},
configuration: {
id: 'configuration',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/configuration.hbs'
},
effect: {
id: 'effect',
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
}
};
static TABS = {
base: {
active: true,
cssClass: '',
group: 'primary',
id: 'base',
icon: null,
label: 'Base'
},
config: {
active: false,
cssClass: '',
group: 'primary',
id: 'config',
icon: null,
label: 'Configuration'
},
effect: {
active: false,
cssClass: '',
group: 'primary',
id: 'effect',
icon: null,
label: 'Effect'
}
};
static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects'];
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options, 'action');
context.source = this.action.toObject(false);
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id));
if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
context.getEffectDetails = this.getEffectDetails.bind(this);
context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor?.isNPC;
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
{ key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') },
...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name }))
];
return context;
}
static async addEffect(_event) {
if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)();
const data = this.action.toObject();
static toggleSection(_, button) {
this.openSection = button.dataset.section === this.openSection ? null : button.dataset.section;
this.render(true);
}
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], {
render: false
getCostOptions() {
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
const resource = this.action.parent.resource;
if (resource) {
options[this.action.parent.parent.id] = {
label: 'DAGGERHEART.GENERAL.itemResource',
group: 'Global'
};
}
return options;
}
getRollTypeOptions() {
const types = foundry.utils.deepClone(CONFIG.DH.GENERAL.rollTypes);
if (!this.action.actor) return types;
Object.values(types).forEach(t => {
if (this.action.actor.type !== 'character' && t.playerOnly) delete types[t.id];
});
return types;
}
disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => {
if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true;
});
return filtered;
}
getEffectDetails(id) {
return this.action.item.effects.get(id);
}
_prepareSubmitData(_event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
const data = foundry.utils.getProperty(submitData, keyPath);
const dataValues = data ? Object.values(data) : [];
if (keyPath === 'cost') {
for (var value of dataValues) {
const item = this.action.parent.parent.id === value.key;
value.keyIsID = Boolean(item);
}
}
if (data) foundry.utils.setProperty(submitData, keyPath, dataValues);
}
return submitData;
}
static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData),
data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = await this.action.update(data);
this.sheetUpdate?.(this.action);
this.render();
}
static addElement(event) {
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key;
if (!this.action[key]) return;
data[key].push({});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeElement(event, button) {
event.stopPropagation();
const data = this.action.toObject(),
key = event.target.closest('[data-key]').dataset.key,
index = button.dataset.index;
data[key].splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addDamage(event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
part = {};
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
data.damage.parts.push(part);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeDamage(event, button) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
index = button.dataset.index;
data.damage.parts.splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static async addEffect(event) {
if (!this.action.effects) return;
const effectData = this._addEffectData.bind(this)(),
[created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { render: false }),
data = this.action.toObject();
data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created._id).sheet.render(true);
@ -46,10 +246,6 @@ export default class DHActionConfig extends DHActionBaseConfig {
};
}
getEffectDetails(id) {
return this.action.item.effects.get(id);
}
static removeEffect(event, button) {
if (!this.action.effects) return;
const index = button.dataset.index,

View file

@ -1,66 +0,0 @@
import DHActionBaseConfig from './action-base-config.mjs';
export default class DHActionSettingsConfig extends DHActionBaseConfig {
constructor(action, effects, sheetUpdate) {
super(action);
this.effects = effects;
this.sheetUpdate = sheetUpdate;
}
static DEFAULT_OPTIONS = {
...DHActionBaseConfig.DEFAULT_OPTIONS,
actions: {
...DHActionBaseConfig.DEFAULT_OPTIONS.actions,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
editEffect: this.editEffect
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.effects = this.effects;
context.getEffectDetails = this.getEffectDetails.bind(this);
return context;
}
getEffectDetails(id) {
return this.effects.find(x => x.id === id);
}
static async addEffect(_event) {
if (!this.action.effects) return;
const effectData = game.system.api.data.activeEffects.BaseEffect.getDefaultObject();
const data = this.action.toObject();
this.sheetUpdate(data, effectData);
this.effects = [...this.effects, effectData];
data.effects.push({ _id: effectData.id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static removeEffect(event, button) {
if (!this.action.effects) return;
const index = button.dataset.index,
effectId = this.action.effects[index]._id;
this.constructor.removeElement.bind(this)(event, button);
this.sheetUpdate(
this.action.toObject(),
this.effects.find(x => x.id === effectId),
true
);
}
static async editEffect(event) {
const id = event.target.closest('[data-effect-id]')?.dataset?.effectId;
const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(
this.getEffectDetails(id)
);
if (!updatedEffect) return;
this.effects = await this.sheetUpdate(this.action.toObject(), { ...updatedEffect, id });
this.render();
}
}

View file

@ -9,9 +9,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
// As per DHToken._getTrackedAttributesFromSchema, attributes.bar have a max version as well.
const maxAttributes = attributes.bar.map(x => [...x, 'max']);
attributes.value.push(...maxAttributes);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
@ -99,13 +96,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
});
}
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.systemFields = context.document.system.schema.fields;
return context;
}
async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context);
switch (partId) {
@ -115,7 +105,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects;
if (!useGeneric) {
partContext.statuses = Object.values(CONFIG.DH.GENERAL.conditions()).map(status => ({
partContext.statuses = Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({
value: status.id,
label: game.i18n.localize(status.name)
}));

View file

@ -51,19 +51,6 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
}
};
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;
}
/* -------------------------------------------- */
/**
@ -111,17 +98,11 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return;
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]);
if (item.type === 'feature') {
await this.actor.createEmbeddedDocuments('Item', [item]);
}
}
}

View file

@ -1,4 +1,5 @@
import { GMUpdateEvent, socketEvent } from '../../systemRegistration/socket.mjs';
import DhCompanionlevelUp from '../levelup/companionLevelup.mjs';
import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -10,7 +11,8 @@ export default class DHCompanionSettings extends DHBaseActorSettings {
position: { width: 455, height: 'auto' },
actions: {
addExperience: DHCompanionSettings.#addExperience,
removeExperience: DHCompanionSettings.#removeExperience
removeExperience: DHCompanionSettings.#removeExperience,
levelUp: DHCompanionSettings.#levelUp
}
};
@ -119,4 +121,12 @@ export default class DHCompanionSettings extends DHBaseActorSettings {
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
}
/**
* Opens the companion level-up dialog for the associated actor.
* @type {ApplicationClickAction}
*/
static async #levelUp() {
new DhCompanionlevelUp(this.actor).render({ force: true });
}
}

View file

@ -1,26 +1,21 @@
import { actionsTypes } from '../../data/action/_module.mjs';
import ActionSettingsConfig from './action-settings-config.mjs';
import DHActionConfig from './action-config.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(configTitle, move, movePath, settings, optionalParts, options) {
export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(move, movePath, settings, options) {
super(options);
this.configTitle = configTitle;
this.move = move;
this.movePath = movePath;
this.actionsPath = `${movePath}.actions`;
this.settings = settings;
const { hasIcon, hasEffects } = optionalParts;
this.hasIcon = hasIcon;
this.hasEffects = hasEffects;
}
get title() {
return this.configTitle;
return game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves');
}
static DEFAULT_OPTIONS = {
@ -35,7 +30,6 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
addItem: this.addItem,
editItem: this.editItem,
removeItem: this.removeItem,
addEffect: this.addEffect,
resetMoves: this.resetMoves,
saveForm: this.saveForm
},
@ -47,14 +41,13 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' },
actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' },
effects: { template: 'systems/daggerheart/templates/settings/downtime-config/effects.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' }
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }],
tabs: [{ id: 'main' }, { id: 'actions' }],
initial: 'main',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
@ -62,9 +55,6 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.tabs = this._filterTabs(context.tabs);
context.hasIcon = this.hasIcon;
context.hasEffects = this.hasEffects;
context.move = this.move;
context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(
context.move.description
@ -102,8 +92,6 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return (
(await foundry.applications.api.DialogV2.input({
window: { title: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType') },
position: { width: 300 },
classes: ['daggerheart', 'dh-style'],
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/actionTypes/actionType.hbs',
{ types: CONFIG.DH.ACTIONS.actionTypes }
@ -142,109 +130,31 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
}
static async editItem(_, target) {
const { type, id } = target.dataset;
if (type === 'effect') {
const effectIndex = this.move.effects.findIndex(x => x.id === id);
const effect = this.move.effects[effectIndex];
const updatedEffect =
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
if (!updatedEffect) return;
await this.settings.updateSource({
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
return acc;
}, [])
});
const actionId = target.dataset.id;
const action = this.move.actions.get(actionId);
await new DHActionConfig(action, async updatedMove => {
await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
} else {
const action = this.move.actions.get(id);
await new ActionSettingsConfig(action, this.move.effects, async (updatedMove, effectData, deleteEffect) => {
let updatedEffects = null;
if (effectData) {
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
const existingEffectIndex = currentEffects.findIndex(x => x.id === effectData.id);
updatedEffects = deleteEffect
? currentEffects.filter(x => x.id !== effectData.id)
: existingEffectIndex === -1
? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData);
await this.settings.updateSource({
[`${this.movePath}.effects`]: updatedEffects
});
}
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
return updatedEffects;
}).render(true);
}
}).render(true);
}
static async removeItem(_, target) {
const { type, id } = target.dataset;
if (type === 'effect') {
const move = foundry.utils.getProperty(this.settings, this.movePath);
for (const action of move.actions) {
const remainingEffects = action.effects.filter(x => x._id !== id);
if (action.effects.length !== remainingEffects.length) {
await action.update({
effects: remainingEffects.map(x => {
const { _id, ...rest } = x;
return { ...rest, _id: _id };
})
});
}
}
await this.settings.updateSource({
[this.movePath]: {
effects: move.effects.filter(x => x.id !== id),
actions: move.actions
}
});
} else {
await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
}
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
}
static async addEffect(_, target) {
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
await this.settings.updateSource({
[`${this.movePath}.effects`]: [
...currentEffects,
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
]
});
await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render();
}
static resetMoves() {}
_filterTabs(tabs) {
return this.hasEffects
? tabs
: Object.keys(tabs).reduce((acc, key) => {
if (key !== 'effects') acc[key] = tabs[key];
return acc;
}, {});
}
/** @override */
_onClose(options = {}) {
if (!options.submitted) this.move = null;
}
static async configure(configTitle, move, movePath, settings, optionalParts, options = {}) {
static async configure(move, movePath, settings, options = {}) {
return new Promise(resolve => {
const app = new this(configTitle, move, movePath, settings, optionalParts, options);
const app = new this(move, movePath, settings, options);
app.addEventListener('close', () => resolve(app.move), { once: true });
app.render({ force: true });
});

View file

@ -49,19 +49,6 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
}
};
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;
}
/**
* Adds a new category entry to the actor.
* @type {ApplicationClickAction}
@ -122,9 +109,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return;
if (data.fromInternal) return;
const item = await fromUuid(data.uuid);
if (item.type === 'adversary' && event.target.closest('.category-container')) {
const target = event.target.closest('.category-container');
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`;

View file

@ -1,30 +1,20 @@
import DHTokenConfigMixin from './token-config-mixin.mjs';
import { getActorSizeFromForm } from './token-config-mixin.mjs';
export default class DhPrototypeTokenConfig extends DHTokenConfigMixin(
foundry.applications.sheets.PrototypeTokenConfig
) {
export default class DhPrototypeTokenConfig extends foundry.applications.sheets.PrototypeTokenConfig {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
form: { handler: DhPrototypeTokenConfig.#onSubmit }
};
/**
* Process form submission for the sheet
* @this {PrototypeTokenConfig}
* @type {ApplicationFormSubmission}
*/
static async #onSubmit(event, form, formData) {
const submitData = this._processFormData(event, form, formData);
submitData.detectionModes ??= []; // Clear detection modes array
this._processChanges(submitData);
const changes = { prototypeToken: submitData };
const changedTokenSizeValue = getActorSizeFromForm(this.element, this.actor);
if (changedTokenSizeValue) changes.system = { size: changedTokenSizeValue };
this.actor.validate({ changes, clean: true, fallback: false });
await this.actor.update(changes);
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhPrototypeTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
}

View file

@ -1,231 +0,0 @@
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);
const ignoredActorKeys = ['config', 'DhEnvironment'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
}
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}`;
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

@ -1,114 +0,0 @@
export default function DHTokenConfigMixin(Base) {
class DHTokenConfigBase extends Base {
/** @override */
static PARTS = {
tabs: super.PARTS.tabs,
identity: super.PARTS.identity,
appearance: {
template: 'systems/daggerheart/templates/sheets-settings/token-config/appearance.hbs',
scrollable: ['']
},
vision: super.PARTS.vision,
light: super.PARTS.light,
resources: super.PARTS.resources,
footer: super.PARTS.footer
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
switch (partId) {
case 'appearance':
htmlElement
.querySelector('#dhTokenSize')
?.addEventListener('change', this.onTokenSizeChange.bind(this));
break;
}
}
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DHTokenConfigBase.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
async _prepareAppearanceTab() {
const context = await super._prepareAppearanceTab();
context.tokenSizes = CONFIG.DH.ACTOR.tokenSize;
context.tokenSize = this.actor?.system?.size;
context.usesActorSize = this.actor?.system?.metadata?.usesSize;
context.actorSizeDisable = context.usesActorSize && this.actor.system.size !== 'custom';
return context;
}
/** @inheritDoc */
_previewChanges(changes) {
if (!changes || !this._preview) return;
const tokenSizeSelect = this.element?.querySelector('#dhTokenSize');
if (this.actor && tokenSizeSelect && tokenSizeSelect.value !== 'custom') {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const tokenSize = tokenSizes[tokenSizeSelect.value];
changes.width = tokenSize;
changes.height = tokenSize;
}
const deletions = { '-=actorId': null, '-=actorLink': null };
const mergeOptions = { inplace: false, performDeletions: true };
this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
if (this._preview?.object?.destroyed === false) {
this._preview.object.initializeSources();
this._preview.object.renderFlags.set({ refresh: true });
}
}
async onTokenSizeChange(event) {
const value = event.target.value;
const tokenSizeDimensions = this.element.querySelector('#tokenSizeDimensions');
if (tokenSizeDimensions) {
const disabled = value !== 'custom';
tokenSizeDimensions.dataset.tooltip = disabled
? game.i18n.localize('DAGGERHEART.APPLICATIONS.TokenConfig.actorSizeUsed')
: '';
const disabledIcon = tokenSizeDimensions.querySelector('i');
if (disabledIcon) {
disabledIcon.style.opacity = disabled ? '' : '0';
}
const dimensionsInputs = tokenSizeDimensions.querySelectorAll('.form-fields input');
for (const input of dimensionsInputs) {
input.disabled = disabled;
}
}
}
}
return DHTokenConfigBase;
}
export function getActorSizeFromForm(element, actor) {
const tokenSizeSelect = element.querySelector('#dhTokenSize');
const isSizeDifferent = tokenSizeSelect?.value !== actor?.system?.size;
if (tokenSizeSelect && actor && isSizeDifferent) {
return tokenSizeSelect.value;
}
return null;
}

View file

@ -1,11 +1,20 @@
import DHTokenConfigMixin from './token-config-mixin.mjs';
import { getActorSizeFromForm } from './token-config-mixin.mjs';
export default class DhTokenConfig extends DHTokenConfigMixin(foundry.applications.sheets.TokenConfig) {
async _processSubmitData(event, form, submitData, options) {
const changedTokenSizeValue = getActorSizeFromForm(this.element, this.actor);
if (changedTokenSizeValue) this.token.actor.update({ 'system.size': changedTokenSizeValue });
super._processSubmitData(event, form, submitData, options);
export default class DhTokenConfig extends foundry.applications.sheets.TokenConfig {
/** @inheritDoc */
async _prepareResourcesTab() {
const token = this.token;
const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const attributeSource =
this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
: this.actor?.system;
const TokenDocument = foundry.utils.getDocumentClass('Token');
const attributes = TokenDocument.getTrackedAttributes(attributeSource);
return {
barAttributes: TokenDocument.getTrackedAttributeChoices(attributes, attributeSource),
bar1: token.getBarAttribute?.('bar1'),
bar2: token.getBarAttribute?.('bar2'),
turnMarkerModes: DhTokenConfig.TURN_MARKER_MODES,
turnMarkerAnimations: CONFIG.Combat.settings.turnMarkerAnimations
};
}
}

View file

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

View file

@ -1,60 +1,26 @@
import { getDocFromElement } from '../../../helpers/utils.mjs';
import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
export default class AdversarySheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['adversary'],
position: { width: 660, height: 766 },
window: { resizable: true },
actions: {
toggleHitPoints: AdversarySheet.#toggleHitPoints,
toggleStress: AdversarySheet.#toggleStress,
reactionRoll: AdversarySheet.#reactionRoll,
toggleResourceDice: AdversarySheet.#toggleResourceDice,
handleResourceDice: AdversarySheet.#handleResourceDice
reactionRoll: AdversarySheet.#reactionRoll
},
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
}
]
resizable: true
}
};
static PARTS = {
limited: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/limited.hbs',
scrollable: ['.limited-container']
},
sidebar: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs',
scrollable: ['.shortcut-items-section']
},
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' },
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs',
scrollable: ['.feature-section']
},
notes: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs'
},
effects: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs',
scrollable: ['.effects-sections']
}
features: { template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs' },
notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' },
effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' }
};
/** @inheritdoc */
@ -66,40 +32,10 @@ export default class AdversarySheet extends DHBaseActorSheet {
}
};
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.systemFields.attack.fields = this.document.system.attack.schema.fields;
context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => {
acc[key] = this.document.system.resources[key];
return acc;
}, {});
const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max);
context.resources.hitPoints.emptyPips =
context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0;
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
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
);
return context;
}
@ -108,11 +44,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
case 'limited':
await this._prepareHeaderContext(context, options);
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
context.adversaryType = game.i18n.localize(adversaryTypes[this.document.system.type].label);
break;
case 'notes':
await this._prepareNotesContext(context, options);
@ -121,16 +53,6 @@ export default class AdversarySheet extends DHBaseActorSheet {
return context;
}
/**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => {
element.addEventListener('change', this.updateItemResource.bind(this));
element.addEventListener('click', e => e.stopPropagation());
});
}
/**
* Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context
@ -176,40 +98,10 @@ export default class AdversarySheet extends DHBaseActorSheet {
});
}
/** @inheritdoc */
async _onDragStart(event) {
const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
}
super._onDragStart(event);
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Toggles hitpoint resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, button) {
const hitPointsValue = Number.parseInt(button.dataset.value);
const newValue =
this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/**
* Performs a reaction roll for an Adversary.
* @type {ApplicationClickAction}
@ -220,61 +112,13 @@ export default class AdversarySheet extends DHBaseActorSheet {
title: `Reaction Roll: ${this.actor.name}`,
headerTitle: 'Adversary Reaction Roll',
roll: {
type: 'trait'
type: 'reaction'
},
actionType: 'reaction',
type: 'trait',
hasRoll: true,
data: this.actor.getRollData()
};
this.actor.diceRoll(config);
}
/**
* Toggle the used state of a resource dice.
* @type {ApplicationClickAction}
*/
static async #toggleResourceDice(event, target) {
const item = await getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice];
await item.update({
[`system.resource.diceStates.${dice}.used`]: diceState ? !diceState.used : true
});
}
/**
* Handle the roll values of resource dice.
* @type {ApplicationClickAction}
*/
static async #handleResourceDice(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
if (!rollValues) return;
await item.update({
'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
acc[index] = { value: state.value, used: state.used };
return acc;
}, {})
});
}
/* -------------------------------------------- */
/* Application Listener Actions */
/* -------------------------------------------- */
async updateItemResource(event) {
const item = await getDocFromElement(event.currentTarget);
if (!item) return;
const max = event.currentTarget.max ? Number(event.currentTarget.max) : null;
const value = max ? Math.min(Number(event.currentTarget.value), max) : event.currentTarget.value;
await item.update({ 'system.resource.value': value });
this.render();
}
}

View file

@ -1,53 +1,40 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import DhpDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
const { TextEditor } = foundry.applications.ux;
export default class CharacterSheet extends DHBaseActorSheet {
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['character'],
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: {
toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute,
toggleHitPoints: CharacterSheet.#toggleHitPoints,
toggleStress: CharacterSheet.#toggleStress,
toggleArmor: CharacterSheet.#toggleArmor,
toggleHope: CharacterSheet.#toggleHope,
toggleLoadoutView: CharacterSheet.#toggleLoadoutView,
openPack: CharacterSheet.#openPack,
makeDeathMove: CharacterSheet.#makeDeathMove,
levelManagement: CharacterSheet.#levelManagement,
viewLevelups: CharacterSheet.#viewLevelups,
toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform,
useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty
tempBrowser: CharacterSheet.#tempBrowser
},
window: {
resizable: true,
controls: [
{
icon: 'fa-solid fa-angles-up',
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
action: 'viewLevelups'
}
]
resizable: true
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dragSelector: '[data-item-id][draggable="true"]',
dropSelector: null
}
],
@ -81,14 +68,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@override */
static PARTS = {
limited: {
id: 'limited',
scrollable: ['.limited-container'],
template: 'systems/daggerheart/templates/sheets/actors/character/limited.hbs'
},
sidebar: {
id: 'sidebar',
scrollable: ['.shortcut-items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs'
},
header: {
@ -97,27 +78,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
},
features: {
id: 'features',
scrollable: ['.features-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs'
},
loadout: {
id: 'loadout',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs'
},
inventory: {
id: 'inventory',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs'
},
biography: {
id: 'biography',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs'
},
effects: {
id: 'effects',
scrollable: ['.effects-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs'
}
};
@ -138,50 +114,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => {
element.addEventListener('change', this.updateItemResource.bind(this));
element.addEventListener('click', e => e.stopPropagation());
});
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
});
// Add listener for armor marks input
htmlElement.querySelectorAll('.armor-marks-input').forEach(element => {
element.addEventListener('change', this.updateArmorMarks.bind(this));
});
htmlElement.querySelectorAll('.item-resource.die').forEach(element => {
element.addEventListener('contextmenu', this.lowerResourceDie.bind(this));
});
}
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
if (!this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
this.element
.querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
this.element
.querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
const observer = this.document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER, {
exact: true
});
if (observer) {
this.element.querySelector('.window-content').classList.add('viewMode');
}
this._createFilterMenus();
this._createSearchFilter();
}
this._createFilterMenus();
this._createSearchFilter();
}
/* -------------------------------------------- */
@ -201,17 +154,24 @@ export default class CharacterSheet extends DHBaseActorSheet {
return acc;
}, {});
context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => {
acc[key] = this.document.system.resources[key];
return acc;
}, {});
const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max);
context.resources.hitPoints.emptyPips =
context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0;
context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
context.inventory = {
currency: {
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'),
handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'),
bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'),
chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests')
}
};
context.beastformActive = this.document.effects.find(x => x.type === 'beastform');
const homebrewCurrency = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).currency;
if (homebrewCurrency.enabled) {
context.inventory.currency = homebrewCurrency;
}
if (context.inventory.length === 0) {
context.inventory = Array(1).fill(Array(5).fill([]));
}
return context;
}
@ -249,7 +209,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @protected
*/
async _prepareLoadoutContext(context, _options) {
context.cardView = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard);
context.cardView = !game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList);
}
/**
@ -319,45 +279,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
}
},
{
name: 'recall',
icon: 'fa-solid fa-bolt-lightning',
condition: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system.inVault;
},
callback: async (target, event) => {
const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot;
if (!actorLoadout.available) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
return;
}
if (doc.system.recallCost == 0) {
return doc.update({ 'system.inVault': false });
}
const type = 'effect';
const cls = game.system.api.models.actions.actionsTypes[type];
const action = new cls(
{
...cls.getSourceConfig(doc.system),
type: type,
chatDisplay: false,
cost: [
{
key: 'stress',
value: doc.system.recallCost
}
]
},
{ parent: doc.system }
);
const config = await action.use(event);
if (config) {
return doc.update({ 'system.inVault': false });
}
}
},
{
name: 'toVault',
icon: 'fa-solid fa-arrow-down',
@ -629,6 +550,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.render();
}
async updateItemQuantity(event) {
const item = await getDocFromElement(event.currentTarget);
if (!item) return;
await item.update({ 'system.quantity': event.currentTarget.value });
this.render();
}
async updateArmorMarks(event) {
const armor = this.document.system.armor;
if (!armor) return;
@ -656,14 +585,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
if (!value || !subclass)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClassOrSubclass'));
new CharacterLevelup(this.document).render({ force: true });
}
/**
* Opens the charater level management window in viewMode.
*/
static #viewLevelups() {
new LevelupViewMode(this.document).render({ force: true });
new DhCharacterlevelUp(this.document).render({ force: true });
}
/**
@ -682,22 +604,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
const { key } = button.dataset;
const presets = {
compendium: 'daggerheart',
folder: key,
filter:
key === 'subclasses'
? {
'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource
}
}
: undefined,
render: {
noFolder: true
}
};
ui.compendiumBrowser.open(presets);
return new ItemBrowser({ presets }).render({ force: true });
}
/**
@ -712,27 +626,38 @@ export default class CharacterSheet extends DHBaseActorSheet {
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
effects: Array.from(await this.document.allApplicableEffects()),
roll: {
trait: button.dataset.attribute,
type: 'trait'
trait: button.dataset.attribute
},
hasRoll: true,
actionType: 'action',
hasRoll: true
};
const result = await this.document.diceRoll({
...config,
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;
});
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources = result.costs
.filter(x => x.enabled)
.map(cost => ({ ...cost, value: -cost.value, total: -cost.total }));
config.resourceUpdates.addResources(costResources);
await config.resourceUpdates.updateResources();
setTimeout(() => {
this.consumeResource(result?.costs);
}, 50);
}
async consumeResource(costs) {
if (!costs?.length) return;
const usefulResources = foundry.utils.deepClone(this.actor.system.resources);
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
await this.actor.modifyResource(resources);
}
//TODO: redo toggleEquipItem method
@ -777,42 +702,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction}
*/
static async #toggleLoadoutView(_, button) {
const newAbilityView = button.dataset.value === 'true';
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard, newAbilityView);
const newAbilityView = button.dataset.value !== 'true';
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList, newAbilityView);
this.render();
}
/**
* Toggles hitpoint resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, button) {
const hitPointsValue = Number.parseInt(button.dataset.value);
const newValue =
this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await this.document.update({ 'system.resources.hitPoints.value': newValue });
}
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/**
* Toggles ArmorScore resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmor(_, button, element) {
const ArmorValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue;
await this.document.system.armor.update({ 'system.marks.value': newValue });
}
/**
* Toggles a hope resource value.
* @type {ApplicationClickAction}
@ -830,7 +724,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
static async #toggleVault(_event, button) {
const doc = await getDocFromElement(button);
const { available } = this.document.system.loadoutSlot;
if (doc.system.inVault && !available && !doc.system.loadoutIgnore) {
if (doc.system.inVault && !available) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
}
@ -852,6 +746,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
/**
* Temp
*/
static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true });
}
/**
* Handle the roll values of resource dice.
* @type {ApplicationClickAction}
@ -871,71 +772,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
/** */
static #advanceResourceDie(_, target) {
this.updateResourceDie(target, true);
}
lowerResourceDie(event) {
event.preventDefault();
event.stopPropagation();
this.updateResourceDie(event.target, false);
}
async updateResourceDie(target, advance) {
const item = await getDocFromElement(target);
if (!item) return;
const advancedValue = item.system.resource.value + (advance ? 1 : -1);
await item.update({
'system.resource.value': Math.min(advancedValue, Number(item.system.resource.dieFaces.split('d')[1]))
});
}
/**
*
*/
static async #cancelBeastform(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(item);
}
static async #viewParty(_, target) {
const parties = this.document.parties;
if (parties.size <= 1) {
parties.first()?.sheet.render({ force: true });
return;
}
const buttons = parties.map(p => {
const button = document.createElement('button');
button.type = 'button';
button.classList.add('plain');
const img = document.createElement('img');
img.src = p.img;
button.append(img);
const name = document.createElement('span');
name.textContent = p.name;
button.append(name);
button.addEventListener('click', () => {
p.sheet?.render({ force: true });
game.tooltip.dismissLockedTooltips();
});
return button;
});
const html = document.createElement('div');
html.classList.add('party-list');
html.append(...buttons);
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true
});
}
/**
* Open the downtime application.
* @type {ApplicationClickAction}
@ -946,48 +782,40 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
/** @inheritdoc */
async _onDragStart(event) {
const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
}
const item = await getDocFromElement(event.target);
const dragData = {
type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
super._onDragStart(event);
}
async _onDropItem(event, item) {
if (this.document.uuid === item.parent?.uuid) {
return super._onDropItem(event, item);
async _onDrop(event) {
// Prevent event bubbling to avoid duplicate handling
event.preventDefault();
event.stopPropagation();
super._onDrop(event);
this._onDropItem(event, TextEditor.getDragEventData(event));
}
async _onDropItem(event, data) {
const item = await Item.implementation.fromDropData(data);
const itemData = item.toObject();
if (item.type === 'domainCard' && !this.document.system.loadoutSlot.available) {
itemData.system.inVault = true;
}
if (item.type === 'beastform') {
if (this.document.effects.find(x => x.type === 'beastform')) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied')
);
}
if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData);
const createdItem = await this._onDropItemCreate(itemData);
const itemData = item.toObject();
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData);
if (!data?.selectedImage) {
return;
} else if (data) {
if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage;
else itemData.system.tokenImg = data.selectedImage;
return await this._onDropItemCreate(itemData);
}
}
// If this is a type that gets deleted, delete it first (but still defer to super)
const typesThatReplace = ['ancestry', 'community'];
if (typesThatReplace.includes(item.type)) {
await this.document.deleteEmbeddedDocuments(
'Item',
this.document.items.filter(x => x.type === item.type).map(x => x.id)
);
}
return super._onDropItem(event, item);
return createdItem;
}
async _onDropItemCreate(itemData, event) {

View file

@ -1,4 +1,3 @@
import DhCompanionLevelUp from '../../levelup/companionLevelup.mjs';
import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -6,25 +5,14 @@ import DHBaseActorSheet from '../api/base-actor.mjs';
export default class DhCompanionSheet extends DHBaseActorSheet {
static DEFAULT_OPTIONS = {
classes: ['actor', 'companion'],
position: { width: 340 },
actions: {
toggleStress: DhCompanionSheet.#toggleStress,
actionRoll: DhCompanionSheet.#actionRoll,
levelManagement: DhCompanionSheet.#levelManagement
}
position: { width: 300 },
actions: {}
};
static PARTS = {
limited: {
template: 'systems/daggerheart/templates/sheets/actors/companion/limited.hbs',
scrollable: ['.limited-container']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' },
effects: {
template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs',
scrollable: ['.effects-sections']
}
effects: { template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs' }
};
/* -------------------------------------------- */
@ -37,80 +25,4 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
this.element
.querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Toggles stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, button) {
const StressValue = Number.parseInt(button.dataset.value);
const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue;
await this.document.update({ 'system.resources.stress.value': newValue });
}
/**
*
*/
static async #actionRoll(event) {
const partner = this.actor.system.partner;
const config = {
event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}: ${this.actor.name}`,
headerTitle: `Companion ${game.i18n.localize('DAGGERHEART.GENERAL.Roll.action')}`,
roll: {
trait: partner.system.spellcastModifierTrait?.key
},
hasRoll: true,
data: partner.getRollData()
};
const result = await partner.diceRoll(config);
this.consumeResource(result?.costs);
}
// Remove when Action Refactor part #2 done
async consumeResource(costs) {
if (!costs?.length) return;
const partner = this.actor.system.partner;
const usefulResources = {
...foundry.utils.deepClone(partner.system.resources),
fear: {
value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
reversed: false
}
};
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target
};
});
await partner.modifyResource(resources);
}
/**
* Opens the companions level management window.
* @type {ApplicationClickAction}
*/
static #levelManagement() {
new DhCompanionLevelUp(this.document).render({ force: true });
}
}

View file

@ -1,4 +1,3 @@
import { getDocFromElement } from '../../../helpers/utils.mjs';
import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -9,44 +8,21 @@ export default class DhpEnvironment extends DHBaseActorSheet {
classes: ['environment'],
position: {
width: 500,
height: 740
height: 725
},
window: {
resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
resizable: true
},
actions: {
toggleResourceDice: DhpEnvironment.#toggleResourceDice,
handleResourceDice: DhpEnvironment.#handleResourceDice
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
]
actions: {},
dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }]
};
/**@override */
static PARTS = {
limited: {
template: 'systems/daggerheart/templates/sheets/actors/environment/limited.hbs',
scrollable: ['.limited-container']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
features: {
template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs',
scrollable: ['.feature-section']
},
features: { template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs' },
potentialAdversaries: {
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs',
scrollable: ['.items-section']
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs'
},
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
};
@ -60,28 +36,12 @@ export default class DhpEnvironment extends DHBaseActorSheet {
}
};
/** @inheritdoc */
_initializeApplicationOptions(options) {
const applicationOptions = super._initializeApplicationOptions(options);
if (applicationOptions.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
applicationOptions.position.width = 360;
applicationOptions.position.height = 'auto';
}
return applicationOptions;
}
/**@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);
@ -118,22 +78,6 @@ export default class DhpEnvironment extends DHBaseActorSheet {
}
}
/**
* 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 Header part.
* @param {ApplicationRenderContext} context
@ -154,51 +98,12 @@ export default class DhpEnvironment extends DHBaseActorSheet {
/* -------------------------------------------- */
async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item[data-type=adversary]');
const item = event.currentTarget.closest('.inventory-item');
if (item) {
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0);
} else {
return super._onDragStart(event);
}
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
/**
* Toggle the used state of a resource dice.
* @type {ApplicationClickAction}
*/
static async #toggleResourceDice(event, target) {
const item = await getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice];
await item.update({
[`system.resource.diceStates.${dice}.used`]: diceState ? !diceState.used : true
});
}
/**
* Handle the roll values of resource dice.
* @type {ApplicationClickAction}
*/
static async #handleResourceDice(_, target) {
const item = await getDocFromElement(target);
if (!item) return;
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
if (!rollValues) return;
await item.update({
'system.resource.diceStates': rollValues.reduce((acc, state, index) => {
acc[index] = { value: state.value, used: state.used };
return acc;
}, {})
});
}
}

View file

@ -1,479 +0,0 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import { getDocFromElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
import DhpActor from '../../../documents/actor.mjs';
import DHItem from '../../../documents/item.mjs';
export default class Party extends DHBaseActorSheet {
constructor(options) {
super(options);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
}
/**@inheritdoc */
static DEFAULT_OPTIONS = {
classes: ['party'],
position: {
width: 550,
height: 900
},
window: {
resizable: true
},
actions: {
deletePartyMember: Party.#deletePartyMember,
deleteItem: Party.#deleteItem,
toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress,
toggleArmorSlot: Party.#toggleArmorSlot,
tempBrowser: Party.#tempBrowser,
refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest,
tagTeamRoll: Party.#tagTeamRoll,
groupRoll: Party.#groupRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
},
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
};
/**@override */
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
resources: {
template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs',
scrollable: ['']
},
/* NOT YET IMPLEMENTED */
// projects: {
// template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
// scrollable: ['']
// },
inventory: {
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
scrollable: ['.tab.inventory .items-section']
},
notes: { template: 'systems/daggerheart/templates/sheets/actors/party/notes.hbs' }
};
/** @inheritdoc */
static TABS = {
primary: {
tabs: [
{ id: 'partyMembers' },
{ id: 'resources' },
/* NOT YET IMPLEMENTED */
// { id: 'projects' },
{ id: 'inventory' },
{ id: 'notes' }
],
initial: 'partyMembers',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
};
static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary'];
static DICE_ROLL_ACTOR_TYPES = ['character'];
async _onRender(context, options) {
await super._onRender(context, options);
this._createFilterMenus();
this._createSearchFilter();
}
/* -------------------------------------------- */
/* Prepare Context */
/* -------------------------------------------- */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(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 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
})
};
}
}
/**
* Toggles a hope resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHope(_, target) {
const hopeValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue;
await actor.update({ 'system.resources.hope.value': newValue });
this.render();
}
/**
* Toggles a hp resource value.
* @type {ApplicationClickAction}
*/
static async #toggleHitPoints(_, target) {
const hitPointsValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
await actor.update({ 'system.resources.hitPoints.value': newValue });
this.render();
}
/**
* Toggles a stress resource value.
* @type {ApplicationClickAction}
*/
static async #toggleStress(_, target) {
const stressValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue;
await actor.update({ 'system.resources.stress.value': newValue });
this.render();
}
/**
* Toggles a armor slot resource value.
* @type {ApplicationClickAction}
*/
static async #toggleArmorSlot(_, target, element) {
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
const armorValue = Number.parseInt(target.dataset.value);
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
await armorItem.update({ 'system.marks.value': newValue });
this.render();
}
/**
* Opens Compedium Browser
*/
static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true });
}
static async #refeshActions() {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: 'New Section',
icon: 'fa-solid fa-campground'
},
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs',
{
refreshables: DaggerheartMenu.defaultRefreshSelections()
}
),
classes: ['daggerheart', 'dialog', 'dh-style', 'tab', 'sidebar-tab', 'daggerheartMenu-sidebar']
});
if (!confirmed) return;
}
static async #triggerRest(_, button) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`),
icon: button.dataset.type === 'shortRest' ? 'fa-solid fa-utensils' : 'fa-solid fa-bed'
},
content: 'This will trigger a dialog to players make their downtime moves, are you sure?',
classes: ['daggerheart', 'dialog', 'dh-style']
});
if (!confirmed) return;
this.document.system.partyMembers.forEach(actor => {
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.DowntimeTrigger,
data: {
actorId: actor.uuid,
downtimeType: button.dataset.type
}
});
});
}
static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId);
if (!actor || !actor?.isOwner) reject();
new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({
force: true
});
}
static async #tagTeamRoll() {
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) {
new GroupRollDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({ force: true });
}
/**
* Get the set of ContextMenu options for Consumable and Loot.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static #getItemContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */
/* Filter Tracking */
/* -------------------------------------------- */
/**
* The currently active search filter.
* @type {foundry.applications.ux.SearchFilter}
*/
#search = {};
/**
* The currently active search filter.
* @type {FilterMenu}
*/
#menu = {};
/**
* Tracks which item IDs are currently displayed, organized by filter type and section.
* @type {{
* inventory: {
* search: Set<string>,
* menu: Set<string>
* },
* loadout: {
* search: Set<string>,
* menu: Set<string>
* },
* }}
*/
#filteredItems = {
inventory: {
search: new Set(),
menu: new Set()
},
loadout: {
search: new Set(),
menu: new Set()
}
};
/* -------------------------------------------- */
/* Search Inputs */
/* -------------------------------------------- */
/**
* Create and initialize search filter instances for the inventory and loadout sections.
*
* Sets up two {@link foundry.applications.ux.SearchFilter} instances:
* - One for the inventory, which filters items in the inventory grid.
* - One for the loadout, which filters items in the loadout/card grid.
* @private
*/
_createSearchFilter() {
//Filters could be a application option if needed
const filters = [
{
key: 'inventory',
input: 'input[type="search"].search-inventory',
content: '[data-application-part="inventory"] .items-section',
callback: this._onSearchFilterInventory.bind(this)
}
];
for (const { key, input, content, callback } of filters) {
const filter = new foundry.applications.ux.SearchFilter({
inputSelector: input,
contentSelector: content,
callback
});
filter.bind(this.element);
this.#search[key] = filter;
}
}
/**
* Handle invetory items search and filtering.
* @param {KeyboardEvent} event The keyboard input event.
* @param {string} query The input search string.
* @param {RegExp} rgx The regular expression query that should be matched against.
* @param {HTMLElement} html The container to filter items from.
* @protected
*/
async _onSearchFilterInventory(_event, query, rgx, html) {
this.#filteredItems.inventory.search.clear();
for (const li of html.querySelectorAll('.inventory-item')) {
const item = await getDocFromElement(li);
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.inventory.search.add(item.id);
const { menu } = this.#filteredItems.inventory;
li.hidden = !(menu.has(item.id) && matchesSearch);
}
}
/* -------------------------------------------- */
/* Filter Menus */
/* -------------------------------------------- */
_createFilterMenus() {
//Menus could be a application option if needed
const menus = [
{
key: 'inventory',
container: '[data-application-part="inventory"]',
content: '.items-section',
callback: this._onMenuFilterInventory.bind(this),
target: '.filter-button',
filters: FilterMenu.invetoryFilters
}
];
menus.forEach(m => {
const container = this.element.querySelector(m.container);
this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, {
contentSelector: m.content
});
});
}
/**
* Callback when filters change
* @param {PointerEvent} event
* @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
*/
async _onMenuFilterInventory(_event, html, filters) {
this.#filteredItems.inventory.menu.clear();
for (const li of html.querySelectorAll('.inventory-item')) {
const item = await getDocFromElement(li);
const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id);
const { search } = this.#filteredItems.inventory;
li.hidden = !(search.has(item.id) && matchesMenu);
}
}
/* -------------------------------------------- */
async _onDropActor(event, document) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (document instanceof DhpActor && Party.ALLOWED_ACTOR_TYPES.includes(document.type)) {
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
if (currentMembers.includes(data.uuid)) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateCharacter'));
}
await this.document.update({ 'system.partyMembers': [...currentMembers, document.uuid] });
} else {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet'));
}
return null;
}
static async #deletePartyMember(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.adversary'),
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMemberdList = currentMembers.filter(uuid => uuid !== doc.uuid);
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

@ -44,8 +44,9 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC;
if (context.systemFields.attack)
if (context.systemFields.attack) {
context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
}
return context;
}

View file

@ -1,22 +1,6 @@
const { HandlebarsApplicationMixin } = foundry.applications.api;
import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs';
const typeSettingsMap = {
character: 'extendCharacterDescriptions',
adversary: 'extendAdversaryDescriptions',
environment: 'extendEnvironmentDescriptions',
ancestry: 'extendItemDescriptions',
community: 'extendItemDescriptions',
class: 'extendItemDescriptions',
subclass: 'extendItemDescriptions',
feature: 'extendItemDescriptions',
domainCard: 'extendItemDescriptions',
loot: 'extendItemDescriptions',
consumable: 'extendItemDescriptions',
weapon: 'extendItemDescriptions',
armor: 'extendItemDescriptions',
beastform: 'extendItemDescriptions'
};
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
/**
* @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction
@ -84,8 +68,6 @@ export default function DHApplicationMixin(Base) {
this._dragDrop = this._createDragDropHandlers();
}
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/**
* The default options for the sheet.
* @type {DHSheetV2Configuration}
@ -99,12 +81,10 @@ export default function DHApplicationMixin(Base) {
deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat,
useItem: DHSheetV2.#useItem,
viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem,
browseItem: DHSheetV2.#browseItem,
editAttribution: DHSheetV2.#editAttribution
browseItem: DHSheetV2.#browseItem
},
contextMenus: [
{
@ -124,47 +104,10 @@ export default function DHApplicationMixin(Base) {
}
}
],
dragDrop: [{ dragSelector: '.inventory-item[data-type="effect"]', dropSelector: null }],
dragDrop: [],
tagifyConfigs: []
};
/**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
const hideAttribution = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).hideAttribution;
const headerAttribution = !this.#nonHeaderAttribution.includes(this.document.type);
if (!hideAttribution && this.document.system.metadata.hasAttribution && headerAttribution) {
const { source, page } = this.document.system.attribution;
const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
const element = `<label class="attribution-header-label">${attribution}</label>`;
this.window.controls.insertAdjacentHTML('beforebegin', element);
}
return frame;
}
/**
* Refresh the custom parts of the application frame
*/
refreshFrame() {
const hideAttribution = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).hideAttribution;
const headerAttribution = !this.#nonHeaderAttribution.includes(this.document.type);
if (!hideAttribution && this.document.system.metadata.hasAttribution && headerAttribution) {
const { source, page } = this.document.system.attribution;
const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
const label = this.window.header.querySelector('.attribution-header-label');
label.innerHTML = attribution;
}
}
/**
* Related documents that should cause a rerender of this application when updated.
*/
@ -178,79 +121,6 @@ export default function DHApplicationMixin(Base) {
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
// Handle delta inputs
for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) {
deltaInput.dataset.numValue = deltaInput.value;
deltaInput.inputMode = 'numeric';
const handleUpdate = (delta = 0) => {
const min = Number(deltaInput.min) || 0;
const max = Number(deltaInput.max) || Infinity;
const current = Number(deltaInput.dataset.numValue);
const rawNumber = Number(deltaInput.value);
if (Number.isNaN(rawNumber)) {
deltaInput.value = delta ? Math.clamp(current + delta, min, max) : current;
return;
}
const newValue =
deltaInput.value.startsWith('+') || deltaInput.value.startsWith('-')
? Math.clamp(current + rawNumber + delta, min, max)
: Math.clamp(rawNumber + delta, min, max);
deltaInput.value = deltaInput.dataset.numValue = newValue;
};
// Force valid characters while inputting
deltaInput.addEventListener('input', () => {
deltaInput.value = /[+=\-]?\d*/.exec(deltaInput.value)?.at(0) ?? deltaInput.value;
});
// Recreate Keyup/Keydown support
deltaInput.addEventListener('keydown', event => {
const step = event.key === 'ArrowUp' ? 1 : event.key === 'ArrowDown' ? -1 : 0;
if (step !== 0) {
handleUpdate(step);
deltaInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});
// Mousewheel while focused support
deltaInput.addEventListener(
'wheel',
event => {
if (deltaInput === document.activeElement) {
event.preventDefault();
handleUpdate(Math.sign(-1 * event.deltaY));
deltaInput.dispatchEvent(new Event('change', { bubbles: true }));
}
},
{ passive: false }
);
deltaInput.addEventListener('change', () => {
handleUpdate();
});
}
// Handle contenteditable
for (const input of htmlElement.querySelectorAll('[contenteditable][data-property]')) {
const property = input.dataset.property;
input.addEventListener('blur', () => {
const selection = document.getSelection();
if (input.contains(selection.anchorNode)) {
selection.empty();
}
this.document.update({ [property]: input.textContent });
});
input.addEventListener('keydown', event => {
if (event.key === 'Enter') input.blur();
});
// Chrome sometimes add <br>, which aren't a problem for the value but are for the placeholder
input.addEventListener('input', () => input.querySelectorAll('br').forEach(i => i.remove()));
}
}
/**@inheritdoc */
@ -259,7 +129,6 @@ export default function DHApplicationMixin(Base) {
const docs = [];
for (const docData of this.relatedDocs) {
if (!docData) continue;
const doc = await foundry.utils.fromUuid(docData.uuid);
docs.push(doc);
}
@ -267,8 +136,6 @@ export default function DHApplicationMixin(Base) {
docs.filter(doc => doc).forEach(doc => (doc.apps[this.id] = this));
if (!!this.options.contextMenus.length) this._createContextMenus();
this.#autoExtendDescriptions(context);
}
/** @inheritDoc */
@ -277,15 +144,6 @@ export default function DHApplicationMixin(Base) {
this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]);
}
/** @inheritdoc */
async _renderHTML(context, options) {
const rendered = await super._renderHTML(context, options);
for (const result of Object.values(rendered)) {
await this.#prepareInventoryDescription(result);
}
return rendered;
}
/**@inheritdoc */
async _onRender(context, options) {
await super._onRender(context, options);
@ -297,13 +155,19 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */
/**@inheritdoc */
_preSyncPartState(partId, newElement, priorElement, state) {
super._preSyncPartState(partId, newElement, priorElement, state);
_syncPartState(partId, newElement, priorElement, state) {
super._syncPartState(partId, newElement, priorElement, state);
for (const el of priorElement.querySelectorAll('.extensible.extended')) {
const { actionId, itemUuid } = el.parentElement.dataset;
const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`;
const newExtensible = newElement.querySelector(selector);
newExtensible?.classList.add('extended');
if (!newExtensible) continue;
newExtensible.classList.add('extended');
const descriptionElement = newExtensible.querySelector('.invetory-description');
if (descriptionElement) {
this.#prepareInventoryDescription(newExtensible, descriptionElement);
}
}
}
@ -370,38 +234,14 @@ export default function DHApplicationMixin(Base) {
* @param {DragEvent} event
* @protected
*/
async _onDragStart(event) {
const inventoryItem = event.currentTarget.closest('.inventory-item');
if (inventoryItem) {
const { type, itemUuid } = inventoryItem.dataset;
if (type === 'effect') {
const effect = await foundry.utils.fromUuid(itemUuid);
const effectData = {
type: 'ActiveEffect',
data: { ...effect.toObject(), _id: null },
fromInternal: this.document.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(effectData));
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0);
}
}
}
_onDragStart(event) {}
/**
* Handle drop event.
* @param {DragEvent} event
* @protected
*/
_onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) {
this.document.createEmbeddedDocuments('ActiveEffect', [data.data]);
} else {
// Fallback to super, but note that item sheets do not have this function
return super._onDrop?.(event);
}
}
_onDrop(event) {}
/* -------------------------------------------- */
/* Context Menu */
@ -504,10 +344,7 @@ export default function DHApplicationMixin(Base) {
callback: async (target, event) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
config.effects = Array.from(await this.document.allApplicableEffects());
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
return action && action.use(event, { byPassRoll: true });
}
});
@ -526,7 +363,7 @@ export default function DHApplicationMixin(Base) {
options.push({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message',
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
callback: async target => (await getDocFromElement(target)).toChat(this.document.id)
});
if (deletable)
@ -557,7 +394,6 @@ export default function DHApplicationMixin(Base) {
context.source = this.document;
context.fields = this.document.schema.fields;
context.systemFields = this.document.system.schema.fields;
context.settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
return context;
}
@ -567,72 +403,32 @@ export default function DHApplicationMixin(Base) {
/**
* Prepares and enriches an inventory item or action description for display.
* @param {HTMLElement} element the element to enrich the inventory items of
* @param {HTMLElement} extensibleElement - The parent element containing the description.
* @param {HTMLElement} descriptionElement - The element where the enriched description will be rendered.
* @returns {Promise<void>}
*/
async #prepareInventoryDescription(element) {
// Get all inventory item elements with a data-item-uuid attribute
const inventoryItems = element.querySelectorAll('.inventory-item[data-item-uuid]');
for (const el of inventoryItems) {
// Get the doc uuid from the element
const { itemUuid } = el?.dataset || {};
if (!itemUuid) continue;
async #prepareInventoryDescription(extensibleElement, descriptionElement) {
const parent = extensibleElement.closest('[data-item-uuid], [data-action-id]');
const { actionId, itemUuid } = parent?.dataset || {};
if (!actionId && !itemUuid) return;
//get doc by uuid
const doc = await fromUuid(itemUuid);
const doc = itemUuid
? await getDocFromElement(extensibleElement)
: this.document.system.attack?.id === actionId
? this.document.system.attack
: this.document.system.actions?.get(actionId);
if (!doc) return;
//get inventory-item description element
const descriptionElement = el.querySelector('.invetory-description');
if (!doc || !descriptionElement) continue;
// localize the description (idk if it's still necessary)
const description = doc.system?.getEnrichedDescription
? await doc.system.getEnrichedDescription()
: game.i18n.localize(doc.system?.description ?? doc.description);
// Enrich the description and attach it;
const isAction = doc.documentName === 'Action';
descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
description,
{
relativeTo: isAction ? doc.parent : doc,
rollData: doc.getRollData?.(),
secrets: isAction ? doc.parent.isOwner : doc.isOwner
}
);
}
}
/* -------------------------------------------- */
/* Extend Descriptions by Settings */
/* -------------------------------------------- */
/**
* Extend inventory description when enabled in settings.
* @returns {Promise<void>}
*/
async #autoExtendDescriptions(context) {
const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]');
for (const el of inventoryItems) {
// Get the doc uuid from the element
const { itemUuid } = el?.dataset || {};
if (!itemUuid) continue;
//get doc by uuid
const doc = await fromUuid(itemUuid);
//check the type of the document
const actorType =
doc?.type === 'adversary' && context.document?.type === 'environment'
? typeSettingsMap[doc?.type]
: doc.actor?.type;
// If the actor type is defined and the setting is enabled, extend the description
if (typeSettingsMap[actorType]) {
const settingKey = typeSettingsMap[actorType];
if (context.settings[settingKey]) this.#activeExtended(el);
const description = doc.system?.description ?? doc.description;
const isAction = !!actionId;
descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
description,
{
relativeTo: isAction ? doc.parent : doc,
rollData: doc.getRollData?.(),
secrets: isAction ? doc.parent.isOwner : doc.isOwner
}
}
);
}
/* -------------------------------------------- */
@ -640,6 +436,8 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */
static async #addNewItem(event, target) {
const { type } = target.dataset;
const createChoice = await foundry.applications.api.DialogV2.wait({
classes: ['dh-style', 'two-big-buttons'],
buttons: [
@ -665,27 +463,28 @@ export default function DHApplicationMixin(Base) {
static async #browseItem(event, target) {
const type = target.dataset.compendium ?? target.dataset.type;
const presets = {
render: {
noFolder: true
}
};
const presets = {};
switch (type) {
case 'loot':
presets.folder = 'equipments.folders.loots';
break;
case 'consumable':
presets.folder = 'equipments.folders.consumables';
break;
case 'armor':
presets.folder = 'equipments.folders.armors';
break;
case 'weapon':
presets.folder = 'equipments.folders.weapons';
presets.compendium = 'daggerheart';
presets.folder = 'equipments';
presets.render = {
noFolder: true
};
presets.filter = {
type: { key: 'type', value: type, forced: true }
};
break;
case 'domainCard':
presets.compendium = 'daggerheart';
presets.folder = 'domains';
presets.render = {
noFolder: true
};
presets.filter = {
'level.max': { key: 'level.max', value: this.document.system.levelData.level.current },
'system.domain': { key: 'system.domain', value: this.document.system.domains }
@ -695,15 +494,7 @@ export default function DHApplicationMixin(Base) {
return;
}
ui.compendiumBrowser.open(presets);
}
/**
* Open the attribution dialog
* @type {ApplicationClickAction}
*/
static async #editAttribution() {
new game.system.api.applications.dialogs.AttributionDialog(this.document).render({ force: true });
return new ItemBrowser({ presets }).render({ force: true });
}
/**
@ -726,6 +517,7 @@ export default function DHApplicationMixin(Base) {
if (featureOnCharacter) {
systemData = {
originItemType: this.document.type,
originId: this.document.id,
identifier: this.document.system.isMulticlass ? 'multiclass' : null
};
}
@ -739,9 +531,6 @@ export default function DHApplicationMixin(Base) {
};
if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true;
if (type === 'domainCard' && parent?.system.domains?.length) {
data.system.domain = parent.system.domains[0];
}
const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
if (parentIsItem && type === 'feature') {
@ -778,7 +567,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction}
*/
static async #toChat(_event, target) {
const doc = await getDocFromElement(target);
let doc = await getDocFromElement(target);
return doc.toChat(doc.uuid);
}
@ -787,19 +576,10 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction}
*/
static async #useItem(event, target) {
const doc = await getDocFromElement(target);
let doc = await getDocFromElement(target);
await doc.use(event);
}
/**
* View an item by opening its sheet
* @type {ApplicationClickAction}
*/
static async #viewItem(_, target) {
const doc = await getDocFromElement(target);
await doc.sheet.render({ force: true });
}
/**
* Toggle a ActiveEffect
* @type {ApplicationClickAction}
@ -825,12 +605,10 @@ export default function DHApplicationMixin(Base) {
static async #toggleExtended(_, target) {
const container = target.closest('.inventory-item');
const extensible = container?.querySelector('.extensible');
extensible?.classList.toggle('extended');
}
const t = extensible?.classList.toggle('extended');
async #activeExtended(element) {
const extensible = element?.querySelector('.extensible');
extensible?.classList.add('extended');
const descriptionElement = extensible?.querySelector('.invetory-description');
if (t && !!descriptionElement) await this.#prepareInventoryDescription(extensible, descriptionElement);
}
}

View file

@ -1,4 +1,3 @@
import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs';
import DHBaseActorSettings from './actor-setting.mjs';
import DHApplicationMixin from './application-mixin.mjs';
@ -34,10 +33,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}
}
],
dragDrop: [
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
{ dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null }
]
dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
};
/* -------------------------------------------- */
@ -51,12 +47,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null);
}
get isVisible() {
const viewPermission = this.document.testUserPermission(game.user, this.options.viewPermission);
const limitedOnly = this.document.testUserPermission(game.user, this.options.viewPermission, { exact: true });
return limitedOnly ? this.document.system.metadata.hasLimitedView : viewPermission;
}
/* -------------------------------------------- */
/* Prepare Context */
/* -------------------------------------------- */
@ -65,36 +55,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isNPC = this.document.isNPC;
context.useResourcePips = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.appearance
).useResourcePips;
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution;
// Prepare inventory data
if (['party', 'character'].includes(this.document.type)) {
context.inventory = {
currencies: {},
weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort),
armor: this.document.itemTypes.armor.sort((a, b) => a.sort - b.sort),
consumables: this.document.itemTypes.consumable.sort((a, b) => a.sort - b.sort),
loot: this.document.itemTypes.loot.sort((a, b) => a.sort - b.sort)
};
const { title, ...currencies } = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Homebrew
).currency;
for (const key in currencies) {
context.inventory.currencies[key] = {
...currencies[key],
field: context.systemFields.gold.fields[key],
value: context.source.system.gold[key]
};
}
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled);
}
return context;
}
@ -109,39 +69,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return context;
}
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
if (!this.document.system.metadata.hasLimitedView) return parts;
if (this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) return { limited: parts.limited };
return Object.keys(parts).reduce((acc, key) => {
if (key !== 'limited') acc[key] = parts[key];
return acc;
}, {});
}
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
if (
this.document.system.metadata.hasLimitedView &&
this.document.testUserPermission(game.user, 'LIMITED', { exact: true })
) {
this.element.classList = `${this.element.classList} limited`;
}
}
/**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
element.addEventListener('change', this.updateItemQuantity.bind(this));
element.addEventListener('click', e => e.stopPropagation());
});
htmlElement.querySelectorAll('.item-button .action-uses-button').forEach(element => {
element.addEventListener('contextmenu', DHBaseActorSheet.#modifyActionUses);
});
@ -180,15 +111,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */
/* Application Listener Actions */
/* -------------------------------------------- */
async updateItemQuantity(event) {
const item = await getDocFromElement(event.currentTarget);
await item?.update({ 'system.quantity': event.currentTarget.value });
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
@ -257,98 +179,13 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Application Drag/Drop */
/* -------------------------------------------- */
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'Currency' && ['character', 'party'].includes(this.document.type)) {
const originActor = await foundry.utils.fromUuid(data.originActor);
if (!originActor || originActor.uuid === this.document.uuid) return;
const currency = data.currency;
const quantity = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
originActor,
targetActor: this.document,
currency
});
if (quantity) {
originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) });
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
}
return;
}
return super._onDrop(event);
}
async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const originActor = item.actor;
if (
item.actor?.uuid === this.document.uuid ||
!originActor ||
!['character', 'party'].includes(this.document.type)
) {
return super._onDropItem(event, item);
}
/* Handling transfer of inventoryItems */
if (item.system.metadata.isInventoryItem) {
if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item,
targetActor: this.document
});
if (quantityTransfered) {
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - 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
}
}
]);
}
}
} else {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
}
}
}
/**
* On dragStart on the item.
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
// Handle drag/dropping currencies
const currencyEl = event.currentTarget.closest(".currency[data-currency]");
if (currencyEl) {
const currency = currencyEl.dataset.currency;
const data = { type: 'Currency', currency, originActor: this.document.uuid };
event.dataTransfer.setData('text/plain', JSON.stringify(data));
return;
}
// Handle drag/dropping attacks
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) {
const attackData = {
type: 'Attack',
@ -358,20 +195,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
};
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
return;
}
const item = await getDocFromElement(event.target);
if (item) {
const dragData = {
originActor: this.document.uuid,
originId: item.id,
type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
}
super._onDragStart(event);
}
}

View file

@ -13,16 +13,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static DEFAULT_OPTIONS = {
classes: ['item'],
position: { width: 600 },
window: {
resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
},
window: { resizable: true },
form: {
submitOnChange: true
},
@ -33,9 +24,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
removeResource: DHBaseItemSheet.#removeResource
},
dragDrop: [
{ dragSelector: null, dropSelector: '.drop-section' },
{ dragSelector: null, dropSelector: '.tab.features .drop-section' },
{ dragSelector: '.feature-item', dropSelector: null },
{ dragSelector: '.inventory-item', dropSelector: null }
{ dragSelector: '.action-item', dropSelector: null }
],
contextMenus: [
{
@ -64,22 +55,19 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/* Prepare Context */
/* -------------------------------------------- */
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution;
return context;
}
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options);
const { TextEditor } = foundry.applications.ux;
switch (partId) {
case 'description':
context.enrichedDescription = await this.document.system.getEnrichedDescription();
const value = foundry.utils.getProperty(this.document, 'system.description') ?? '';
context.enrichedDescription = await TextEditor.enrichHTML(value, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
});
break;
case 'effects':
await this._prepareEffectsContext(context, options);
@ -161,12 +149,12 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const { type } = target.dataset;
const cls = foundry.documents.Item.implementation;
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
let systemData = {};
if (this.document.parent?.type === 'character') {
systemData = {
originItemType: this.document.type,
identifier: multiclass ?? type
originId: this.document.id,
identifier: this.document.system.isMulticlass ? 'multiclass' : null
};
}
@ -193,32 +181,12 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static async #deleteFeature(_, element) {
const target = element.closest('[data-item-uuid]');
const feature = await getDocFromElement(target);
if (!feature) {
await this.document.update({
'system.features': this.document.system.features
.filter(x => x.item)
.map(x => ({ ...x, item: x.item.uuid }))
});
} else {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
type: game.i18n.localize('TYPES.Item.feature'),
name: feature.name
})
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name })
});
if (!confirmed) return;
await this.document.update({
'system.features': this.document.system.features
.filter(x => target.dataset.type !== x.type || x.item.uuid !== feature.uuid)
.map(x => ({ ...x, item: x.item.uuid }))
});
}
if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
await this.document.update({
'system.features': this.document.system.features
.filter(x => target.dataset.type !== x.type || x.item.uuid !== feature.uuid)
.map(x => ({ ...x, item: x.item.uuid }))
});
}
/**
@ -250,30 +218,35 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
/* Can prolly be improved a lot, but I don't wanna >_< */
const featureItem = event.currentTarget.closest('.feature-item');
const inventoryItem = event.currentTarget.closest('.inventory-item');
const lineItem = event.currentTarget.closest('.item-line');
const dragItemData = featureItem ?? inventoryItem ?? lineItem;
const dragItem = await foundry.utils.fromUuid(dragItemData.dataset.itemUuid);
if (dragItem) {
if (!dragItem) {
if (featureItem) {
const feature = this.document.system.features.find(x => x?.id === featureItem.id);
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return;
}
let dragData = {};
if (dragItemData.dataset.type === 'effect')
dragData = {
type: 'ActiveEffect',
fromInternal: this.document.uuid,
data: { ...dragItem, uuid: dragItem.uuid, id: dragItem.id }
};
else dragData = { type: 'Item', uuid: dragItem.uuid, id: dragItem.id, fromInternal: this.document.id };
const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true };
event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
} else {
const actionItem = event.currentTarget.closest('.action-item');
if (actionItem) {
const action = this.document.system.actions[actionItem.dataset.index];
if (!action) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing'));
return;
}
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
event.dataTransfer.setDragImage(dragItemData.querySelector('img'), 60, 0);
const actionData = {
type: 'Action',
data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid },
fromInternal: true
};
event.dataTransfer.setData('text/plain', JSON.stringify(actionData));
event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0);
}
}
}
@ -282,52 +255,25 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @param {DragEvent} event - The drag event
*/
async _onDrop(event) {
super._onDrop(event);
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal === this.document.id) return;
if (data.fromInternal) return;
const target = event.target.closest('fieldset.drop-section');
let item = await fromUuid(data.uuid);
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
const cls = foundry.documents.Item.implementation;
if (this.document.parent?.type === 'character') {
const itemData = item.toObject();
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
item = await cls.create(
{
...itemData,
_stats: { compendiumSource: this.document.uuid },
system: {
...itemData.system,
originItemType: this.document.type,
identifier: multiclass ?? target.dataset.type
}
},
{ parent: this.document.parent }
);
}
if (target.dataset.type) {
await this.document.update(
{
'system.features': [...this.document.system.features, { type: target.dataset.type, item }].map(
x => ({
...x,
item: x.item?.uuid
})
)
},
{ parent: this.document.parent?.type === 'character' ? this.document.parent : undefined }
);
await this.document.update({
'system.features': [...this.document.system.features, { type: target.dataset.type, item }].map(
x => ({
...x,
item: x.item?.uuid
})
)
});
} else {
await this.document.update(
{
'system.features': [...this.document.system.features, item].map(x => x.uuid)
},
{ parent: this.document.parent?.type === 'character' ? this.document.parent : undefined }
);
await this.document.update({
'system.features': [...this.document.system.features, item].map(x => x.uuid)
});
}
}
}

View file

@ -24,7 +24,7 @@ export default function ItemAttachmentSheet(Base) {
...super.TABS,
primary: {
...super.TABS?.primary,
tabs: [...(super.TABS?.primary?.tabs || []) /*{ id: 'attachments' }*/], // Disabled until fixed
tabs: [...(super.TABS?.primary?.tabs || []), { id: 'attachments' }],
initial: super.TABS?.primary?.initial || 'description',
labelPrefix: super.TABS?.primary?.labelPrefix || 'DAGGERHEART.GENERAL.Tabs'
}
@ -41,7 +41,7 @@ export default function ItemAttachmentSheet(Base) {
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const data = TextEditor.getDragEventData(event);
const attachmentsSection = event.target.closest('.attachments-section');
if (!attachmentsSection) return super._onDrop(event);

View file

@ -27,9 +27,6 @@ export default class AncestrySheet extends DHHeritageSheet {
* @param {DragEvent} event - The drag event
*/
async _onDrop(event) {
const data = TextEditor.getDragEventData(event);
if (data.type === 'ActiveEffect') return super._onDrop(event);
const target = event.target.closest('fieldset.drop-section');
const typeField =
this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];

View file

@ -8,7 +8,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
tagifyConfigs: [
{
selector: '.features-input',
options: () => CONFIG.DH.ITEM.orderedArmorFeatures(),
options: () => CONFIG.DH.ITEM.armorFeatures,
callback: ArmorSheet.#onFeatureSelect
}
]

View file

@ -77,7 +77,6 @@ export default class BeastformSheet extends DHBaseItemSheet {
name: context.document.system.advantageOn[key].value
}))
);
context.dimensionsDisabled = context.document.system.tokenSize.size !== 'custom';
break;
case 'effects':
context.effects.actives = context.effects.actives.map(effect => {

View file

@ -46,10 +46,6 @@ export default class ClassSheet extends DHBaseItemSheet {
template: 'systems/daggerheart/templates/sheets/items/class/settings.hbs',
scrollable: ['.settings']
},
questions: {
template: 'systems/daggerheart/templates/sheets/items/class/questions.hbs',
scrollable: ['.questions']
},
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
@ -59,13 +55,7 @@ export default class ClassSheet extends DHBaseItemSheet {
/** @inheritdoc */
static TABS = {
primary: {
tabs: [
{ id: 'description' },
{ id: 'features' },
{ id: 'settings' },
{ id: 'questions' },
{ id: 'effects' }
],
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }],
initial: 'description',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
@ -126,25 +116,15 @@ export default class ClassSheet extends DHBaseItemSheet {
event.stopPropagation();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
const target = event.target.closest('fieldset.drop-section');
if (itemType === 'subclass') {
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 });
if (item.type === 'subclass') {
await this.document.update({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
} else if (['feature', 'ActiveEffect'].includes(itemType)) {
} else if (item.type === 'feature') {
super._onDrop(event);
} else if (this.document.parent?.type !== 'character') {
if (itemType === 'weapon') {
if (item.type === 'weapon') {
if (target.classList.contains('primary-weapon-section')) {
if (!item.system.secondary)
await this.document.update({
@ -156,21 +136,21 @@ export default class ClassSheet extends DHBaseItemSheet {
'system.characterGuide.suggestedSecondaryWeapon': item.uuid
});
}
} else if (itemType === 'armor') {
} else if (item.type === 'armor') {
if (target.classList.contains('armor-section')) {
await this.document.update({
'system.characterGuide.suggestedArmor': item.uuid
});
}
} else if (target.classList.contains('choice-a-section')) {
if (itemType === 'loot' || itemType === 'consumable') {
if (item.type === 'loot' || item.type === 'consumable') {
const filteredChoiceA = this.document.system.inventory.choiceA;
if (filteredChoiceA.length < 2)
await this.document.update({
'system.inventory.choiceA': [...filteredChoiceA.map(x => x.uuid), item.uuid]
});
}
} else if (itemType === 'loot') {
} else if (item.type === 'loot') {
if (target.classList.contains('take-section')) {
const filteredTake = this.document.system.inventory.take.filter(x => x);
if (filteredTake.length < 3)
@ -200,13 +180,7 @@ export default class ClassSheet extends DHBaseItemSheet {
static async #removeItemFromCollection(_event, element) {
const { uuid, target } = element.dataset;
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.uuid !== uuid).map(x => x.uuid) });
}
/**

View file

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

View file

@ -8,7 +8,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
tagifyConfigs: [
{
selector: '.features-input',
options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(),
options: () => CONFIG.DH.ITEM.weaponFeatures,
callback: WeaponSheet.#onFeatureSelect
}
]

View file

@ -1,3 +0,0 @@
export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs';
export { default as DhActorDirectory } from './tabs/actorDirectory.mjs';
export { default as DhSidebar } from './sidebar.mjs';

View file

@ -1,73 +0,0 @@
export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
/** @override */
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 */
static PARTS = {
tabs: {
id: 'tabs',
template: 'systems/daggerheart/templates/sidebar/tabs.hbs'
}
};
/** @override */
async _prepareTabContext(context, options) {
context.tabs = Object.entries(this.constructor.TABS).reduce((obj, [k, v]) => {
let { documentName, gmOnly, tooltip, icon, img } = v;
if (gmOnly && !game.user.isGM) return obj;
if (documentName) {
tooltip ??= getDocumentClass(documentName).metadata.labelPlural;
icon ??= CONFIG[documentName]?.sidebarIcon;
}
obj[k] = { tooltip, icon, img };
obj[k].active = this.tabGroups.primary === k;
return obj;
}, {});
}
}

View file

@ -1,46 +0,0 @@
export default class DhActorDirectory extends foundry.applications.sidebar.tabs.ActorDirectory {
static DEFAULT_OPTIONS = {
renderUpdateKeys: ['system.levelData.level.current', 'system.partner', 'system.tier']
};
static _entryPartial = 'systems/daggerheart/templates/ui/sidebar/actor-document-partial.hbs';
async _prepareDirectoryContext(context, options) {
await super._prepareDirectoryContext(context, options);
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
const environmentTypes = CONFIG.DH.ACTOR.environmentTypes;
context.getTypeLabel = document => {
return document.type === 'adversary'
? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary')
: document.type === 'environment'
? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment')
: null;
};
}
/** @inheritDoc */
_onDragStart(event) {
let actor;
const { entryId } = event.currentTarget.dataset;
if (entryId) {
actor = this.collection.get(entryId);
if (!actor?.visible) return false;
}
super._onDragStart(event);
// Create the drag preview.
if (actor && canvas.ready) {
const img = event.currentTarget.querySelector('img');
const pt = actor.prototypeToken;
const usesSize = actor.system.metadata.usesSize;
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const width = usesSize ? tokenSizes[actor.system.size] : pt.width;
const height = usesSize ? tokenSizes[actor.system.size] : pt.height;
const w = width * canvas.dimensions.size * Math.abs(pt.texture.scaleX) * canvas.stage.scale.x;
const h = height * canvas.dimensions.size * Math.abs(pt.texture.scaleY) * canvas.stage.scale.y;
const preview = foundry.applications.ux.DragDrop.implementation.createDragImage(img, w, h);
event.dataTransfer.setDragImage(preview, w / 2, h / 2);
}
}
}

View file

@ -1,162 +0,0 @@
import { refreshIsAllowed } from '../../../helpers/utils.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar;
/**
* The daggerheart menu tab.
* @extends {AbstractSidebarTab}
* @mixes HandlebarsApplication
*/
export default class DaggerheartMenu extends HandlebarsApplicationMixin(AbstractSidebarTab) {
constructor(options) {
super(options);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
}
static defaultRefreshSelections() {
return {
session: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.session') },
scene: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.scene') },
longRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.longrest') },
shortRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.shortrest') }
};
}
/** @override */
static DEFAULT_OPTIONS = {
classes: ['dh-style'],
window: {
title: 'SIDEBAR.TabSettings'
},
actions: {
selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.#refreshActors
}
};
/** @override */
static tabName = 'daggerheartMenu';
/** @override */
static PARTS = {
main: { template: 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs' }
};
/* -------------------------------------------- */
/** @inheritDoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.refreshables = this.refreshSelections;
context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected);
return context;
}
async getRefreshables(types) {
const refreshedActors = {};
for (let actor of game.actors) {
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) {
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
const increasing =
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
updates[item.id].system = {
...updates[item.id].system,
'resource.value': increasing
? 0
: Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
};
}
if (item.system.metadata?.hasActions) {
const refreshTypes = new Set();
const actions = item.system.actions.filter(action => {
if (refreshIsAllowed(types, action.uses.recovery)) {
refreshTypes.add(action.uses.recovery);
return true;
}
return false;
});
if (actions.length === 0) continue;
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
updates[item.id].system = {
...updates[item.id].system,
...actions.reduce(
(acc, action) => {
acc.actions[action.id] = { 'uses.value': 0 };
return acc;
},
{ actions: updates[item.id].system.actions ?? {} }
)
};
}
}
for (let key in updates) {
const update = updates[key];
await actor.items.get(key).update(update);
}
}
}
return refreshedActors;
}
/* -------------------------------------------- */
/* Application Clicks Actions */
/* -------------------------------------------- */
static async #selectRefreshable(_event, button) {
const { type } = button.dataset;
this.refreshSelections[type].selected = !this.refreshSelections[type].selected;
this.render();
}
static async #refreshActors() {
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
await this.getRefreshables(refreshKeys);
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
{
types: types
}
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
speaker: cls.getSpeaker()
};
cls.create(msg);
this.render();
}
}

View file

@ -1,9 +1,5 @@
export { default as CountdownEdit } from './countdownEdit.mjs';
export { default as DhCountdowns } from './countdowns.mjs';
export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs';
export { default as DhEffectsDisplay } from './effectsDisplay.mjs';
export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
export { ItemBrowser } from './itemBrowser.mjs';

View file

@ -1,5 +1,4 @@
import { abilities } from '../../config/actorConfig.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) {
@ -38,7 +37,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
// }
// },
{
name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
name: 'Reroll Damage',
icon: '<i class="fa-solid fa-dice"></i>',
condition: li => {
const message = game.messages.get(li.dataset.messageId);
@ -55,31 +54,30 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
];
}
addChatListeners = async (document, html, data) => {
const message = data?.message ?? document.toObject(false);
addChatListeners = async (app, html, data) => {
html.querySelectorAll('.duality-action-damage').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message))
);
html.querySelectorAll('.target-save').forEach(element =>
element.addEventListener('click', event => this.onRollSave(event, data.message))
);
html.querySelectorAll('.roll-all-save-button').forEach(element =>
element.addEventListener('click', event => this.onRollAllSave(event, data.message))
);
html.querySelectorAll('.simple-roll-button').forEach(element =>
element.addEventListener('click', event => this.onRollSimple(event, message))
element.addEventListener('click', event => this.onRollSimple(event, data.message))
);
html.querySelectorAll('.healing-button').forEach(element =>
element.addEventListener('click', event => this.onHealing(event, data.message))
);
html.querySelectorAll('.ability-use-button').forEach(element =>
element.addEventListener('click', event => this.abilityUseButton(event, message))
element.addEventListener('click', event => this.abilityUseButton(event, data.message))
);
html.querySelectorAll('.action-use-button').forEach(element =>
element.addEventListener('click', event => this.actionUseButton(event, message))
element.addEventListener('click', event => this.actionUseButton(event, data.message))
);
html.querySelectorAll('.reroll-button').forEach(element =>
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)
element.addEventListener('click', event => this.rerollEvent(event, data.message))
);
};
@ -92,6 +90,80 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options);
}
async getActor(uuid) {
return await foundry.utils.fromUuid(uuid);
}
getAction(actor, itemId, actionId) {
const item = actor.items.get(itemId),
action =
actor.system.attack?._id === actionId
? actor.system.attack
: item.system.attack?._id === actionId
? item.system.attack
: item?.system?.actions?.get(actionId);
return action;
}
async onRollDamage(event, message) {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor);
if (game.user.character?.id !== actor.id && !game.user.isGM) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.rollDamage) return;
await action.rollDamage(event, message);
}
}
async onRollSave(event, message) {
event.stopPropagation();
const actor = await this.getActor(message.system.source.actor),
tokenId = event.target.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token?.actor || !token.isOwner) return true;
if (message.system.source.item && message.system.source.action) {
const action = this.getAction(actor, message.system.source.item, message.system.source.action);
if (!action || !action?.hasSave) return;
action.rollSave(token.actor, event, message).then(result =>
emitAsGM(
GMUpdateEvent.UpdateSaveMessage,
action.updateSaveMessage.bind(action, result, message, token.id),
{
action: action.uuid,
message: message._id,
token: token.id,
result
}
)
);
}
}
async onRollAllSave(event, message) {
event.stopPropagation();
if (!game.user.isGM) return;
const targets = event.target.parentElement.querySelectorAll('[data-token] .target-save');
const actor = await this.getActor(message.system.source.actor),
action = this.getAction(actor, message.system.source.item, message.system.source.action);
targets.forEach(async el => {
const tokenId = el.closest('[data-token]')?.dataset.token,
token = game.canvas.tokens.get(tokenId);
if (!token.actor) return;
if (game.user === token.actor.owner) el.dispatchEvent(new PointerEvent('click', { shiftKey: true }));
else {
token.actor.owner
.query('reactionRoll', {
actionId: action.uuid,
actorId: token.actor.uuid,
event,
message
})
.then(result => action.updateSaveMessage(result, message, token.id));
}
});
}
async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
@ -125,32 +197,17 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
item.system.attack?.id === event.currentTarget.id
? item.system.attack
: item.system.actions.get(event.currentTarget.id);
if (event.currentTarget.dataset.directDamage) {
const config = action.prepareConfig(event);
config.hasRoll = false;
action.workflow.get('damage').execute(config, null, true);
} else action.use(event);
if (event.currentTarget.dataset.directDamage) action.use(event, { byPassRoll: true });
else action.use(event);
}
async actionUseButton(event, message) {
const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset;
const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value;
const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor);
const { moveIndex, actionIndex } = event.currentTarget.dataset;
const parent = await foundry.utils.fromUuid(message.system.actor);
const actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = game.system.api.models.actions.actionsTypes[actionType.type];
const action = new cls(
{
...actionType,
_id: foundry.utils.randomID(),
name: game.i18n.localize(actionType.name),
originItem: {
type: CONFIG.DH.ITEM.originItemType.restMove,
itemPath: movePath,
actionIndex: actionIndex
},
targetUuid: targetUuid
},
{ ...actionType, _id: foundry.utils.randomID(), name: game.i18n.localize(actionType.name) },
{ parent: parent.system }
);
@ -192,182 +249,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
'system.roll': newRoll,
'rolls': [parsedRoll]
});
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');
}
}

View file

@ -1,11 +1,12 @@
import { AdversaryBPPerEncounter } from '../../config/encounterConfig.mjs';
import { EncounterCountdowns } from '../ui/countdowns.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
actions: {
requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens
setActionTokens: this.setActionTokens,
openCountdowns: this.openCountdowns
}
};
@ -21,33 +22,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}
};
/** @inheritDoc */
async _preparePartContext(_partId, context, _options) {
return context;
}
async _prepareContext(options) {
const context = await super._prepareContext(options);
await this._prepareTrackerContext(context, options);
await this._prepareCombatContext(context, options);
return context;
}
async _prepareCombatContext(context, options) {
await super._prepareCombatContext(context, options);
const modifierBP =
this.combats
.find(x => x.active)
?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null;
const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP;
const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters);
Object.assign(context, {
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
battlepoints: { max: maxBP, current: currentBP, hasModifierBP: modifierBP !== null }
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
});
}
@ -56,30 +35,19 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const characters = context.turns?.filter(x => !x.isNPC) ?? [];
const spotlightQueueEnabled = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.SpotlightRequestQueue
);
const spotlightRequests = characters
?.filter(x => !x.isNPC && spotlightQueueEnabled)
.filter(x => x.system.spotlight.requestOrderIndex > 0)
.sort((a, b) => {
const valueA = a.system.spotlight.requestOrderIndex;
const valueB = b.system.spotlight.requestOrderIndex;
return valueA - valueB;
});
Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries,
characters: characters
?.filter(x => !x.isNPC)
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
characters
});
}
async _prepareTurnContext(combat, combatant, index) {
const turn = await super._prepareTurnContext(combat, combatant, index);
return { ...turn, isNPC: combatant.isNPC, system: combatant.system.toObject() };
}
_getCombatContextOptions() {
return [
{
@ -97,63 +65,10 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
];
}
getDefeatedId(combatant) {
if (!combatant.actor) return CONFIG.specialStatusEffects.DEFEATED;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
return settings[`${combatant.actor.type}Default`];
}
/** @inheritdoc */
async _onToggleDefeatedStatus(combatant) {
const isDefeated = !combatant.isDefeated;
await combatant.update({ defeated: isDefeated });
await combatant.actor?.toggleStatusEffect(this.getDefeatedId(combatant), { overlay: true, active: isDefeated });
}
/** @inheritdoc */
async _prepareTurnContext(combat, combatant, index) {
const { id, name, isOwner, isDefeated, hidden, initiative, permission } = combatant;
const resource = permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null;
const hasDecimals = Number.isFinite(initiative) && !Number.isInteger(initiative);
const turn = {
hasDecimals,
hidden,
id,
isDefeated,
initiative,
isOwner,
name,
resource,
active: index === combat.turn,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant)
};
turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin(
' '
);
const defeatedId = this.getDefeatedId(combatant);
const effects = [];
for (const effect of combatant.actor?.temporaryEffects ?? []) {
if (effect.statuses.has(defeatedId)) turn.isDefeated = true;
else if (effect.img) effects.push({ img: effect.img, name: effect.name });
}
turn.effects = {
icons: effects,
tooltip: this._formatEffectsTooltip(effects)
};
return { ...turn, isNPC: combatant.isNPC, system: combatant.system.toObject() };
}
async setCombatantSpotlight(combatantId) {
const update = {
system: {
'spotlight.requesting': false,
'spotlight.requestOrderIndex': 0
'spotlight.requesting': false
}
};
const combatant = this.viewed.combatants.get(combatantId);
@ -165,14 +80,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (this.viewed.turn !== toggleTurn) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
if (combatant.actor?.type === 'character') {
await updateCountdowns(
CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id,
CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id
);
} else {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id);
}
await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.spotlight.id);
const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints;
if (autoPoints) {
@ -188,15 +96,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}
static async requestSpotlight(_, target) {
const characters = this.viewed.turns?.filter(x => !x.isNPC) ?? [];
const orderValues = characters.map(character => character.system.spotlight.requestOrderIndex);
const maxRequestIndex = Math.max(...orderValues);
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({
'system.spotlight': {
requesting: !combatant.system.spotlight.requesting,
requestOrderIndex: !combatant.system.spotlight.requesting ? maxRequestIndex + 1 : 0
requesting: !combatant.system.spotlight.requesting
}
});
@ -218,4 +122,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex });
this.render();
}
static openCountdowns() {
new EncounterCountdowns().open();
}
}

View file

@ -1,238 +0,0 @@
import { DhCountdown } from '../../data/countdowns.mjs';
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class CountdownEdit extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
this.editingCountdowns = new Set();
this.currentEditCountdown = null;
this.hideNewCountdowns = false;
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dialog', 'dh-style', 'countdown-edit'],
tag: 'form',
position: { width: 600 },
window: {
title: 'DAGGERHEART.APPLICATIONS.CountdownEdit.title',
icon: 'fa-solid fa-clock-rotate-left'
},
actions: {
addCountdown: CountdownEdit.#addCountdown,
toggleCountdownEdit: CountdownEdit.#toggleCountdownEdit,
editCountdownImage: CountdownEdit.#editCountdownImage,
editCountdownOwnership: CountdownEdit.#editCountdownOwnership,
randomiseCountdownStart: CountdownEdit.#randomiseCountdownStart,
removeCountdown: CountdownEdit.#removeCountdown
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
countdowns: {
template: 'systems/daggerheart/templates/ui/countdown-edit.hbs',
scrollable: ['.expanded-view', '.edit-content']
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isGM = game.user.isGM;
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
context.defaultOwnership = this.data.defaultOwnership;
context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes;
context.countdownProgressionTypes = CONFIG.DH.GENERAL.countdownProgressionTypes;
context.countdownLoopingTypes = CONFIG.DH.GENERAL.countdownLoopingTypes;
context.hideNewCountdowns = this.hideNewCountdowns;
context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => {
const countdown = this.data.countdowns[key];
const isLooping = countdown.progress.looping !== CONFIG.DH.GENERAL.countdownLoopingTypes.noLooping;
const loopTooltip = isLooping
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop'
: null;
const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic;
acc[key] = {
...countdown,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].label),
progress: {
...countdown.progress,
typeName: game.i18n.localize(
CONFIG.DH.GENERAL.countdownProgressionTypes[countdown.progress.type].label
)
},
editing: this.editingCountdowns.has(key),
randomizeValid,
loopTooltip
};
return acc;
}, {});
return context;
}
/** @override */
async _postRender(_context, _options) {
if (this.currentEditCountdown) {
setTimeout(() => {
const input = this.element.querySelector(
`.countdown-edit-container[data-id="${this.currentEditCountdown}"] input`
);
if (input) {
input.select();
this.currentEditCountdown = null;
}
}, 100);
}
}
canPerformEdit() {
if (game.user.isGM) return true;
if (!game.users.activeGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
}
return true;
}
async updateSetting(update) {
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return;
}
await this.data.updateSource(update);
await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown
});
this.render();
}
static async updateData(_event, _, formData) {
const { hideNewCountdowns, ...settingsData } = foundry.utils.expandObject(formData.object);
// Sync current and max if max is changing and they were equal before
for (const [id, countdown] of Object.entries(settingsData.countdowns ?? {})) {
const existing = this.data.countdowns[id];
countdown.progress.current = this.getMatchingCurrentValue(
existing,
countdown.progress.start,
countdown.progress.current
);
}
this.hideNewCountdowns = hideNewCountdowns;
this.updateSetting(settingsData);
}
getMatchingCurrentValue(oldCount, newStart, newCurrent) {
const wasEqual = oldCount && oldCount.progress.current === oldCount.progress.start;
if (wasEqual && newStart !== oldCount.progress.start) {
return newStart;
} else {
return Math.min(newCurrent, newStart);
}
}
async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
static #addCountdown() {
const id = foundry.utils.randomID();
this.editingCountdowns.add(id);
this.currentEditCountdown = id;
this.updateSetting({
[`countdowns.${id}`]: DhCountdown.defaultCountdown(null, this.hideNewCountdowns)
});
}
static #editCountdownImage(_, target) {
const countdown = this.data.countdowns[target.id];
const fp = new foundry.applications.apps.FilePicker.implementation({
current: countdown.img,
type: 'image',
callback: async path => this.updateSetting({ [`countdowns.${target.id}.img`]: path }),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
static #toggleCountdownEdit(_, button) {
const { countdownId } = button.dataset;
const isEditing = this.editingCountdowns.has(countdownId);
if (isEditing) this.editingCountdowns.delete(countdownId);
else {
this.editingCountdowns.add(countdownId);
this.currentEditCountdown = countdownId;
}
this.render();
}
static async #editCountdownOwnership(_, button) {
const countdown = this.data.countdowns[button.dataset.countdownId];
const data = await game.system.api.applications.dialogs.OwnershipSelection.configure(
countdown.name,
countdown.ownership,
this.data.defaultOwnership
);
if (!data) return;
this.updateSetting({ [`countdowns.${button.dataset.countdownId}`]: data });
}
static async #randomiseCountdownStart(_, button) {
const countdown = this.data.countdowns[button.dataset.countdownId];
const roll = await new Roll(countdown.progress.startFormula).roll();
const message = await roll.toMessage({ title: 'Countdown' });
await waitForDiceSoNice(message);
await this.updateSetting({
[`countdowns.${button.dataset.countdownId}.progress`]: {
start: roll.total,
current: this.getMatchingCurrentValue(countdown, roll.total, countdown.progress.current)
}
});
this.render();
}
static async #removeCountdown(event, button) {
const { countdownId } = button.dataset;
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownText', {
name: this.data.countdowns[countdownId].name
})
});
if (!confirmed) return;
}
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
}
}

View file

@ -1,295 +1,355 @@
import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import constructHTMLButton from '../../helpers/utils.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* A UI element which displays the countdowns in this world.
*
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(basePath) {
super({});
export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options = {}) {
super(options);
this.setupHooks();
this.basePath = basePath;
}
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`)
});
}
/** @inheritDoc */
static DEFAULT_OPTIONS = {
id: 'countdowns',
tag: 'div',
classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
classes: ['daggerheart', 'dh-style', 'countdown'],
tag: 'form',
position: { width: 740, height: 700 },
window: {
icon: 'fa-solid fa-clock-rotate-left',
frame: true,
title: 'DAGGERHEART.UI.Countdowns.title',
positioned: false,
resizable: false,
title: 'Countdowns',
resizable: true,
minimizable: false
},
actions: {
toggleViewMode: DhCountdowns.#toggleViewMode,
editCountdowns: DhCountdowns.#editCountdowns,
loopCountdown: DhCountdowns.#loopCountdown,
decreaseCountdown: (_, target) => this.editCountdown(false, target),
increaseCountdown: (_, target) => this.editCountdown(true, target)
addCountdown: this.addCountdown,
removeCountdown: this.removeCountdown,
editImage: this.onEditImage,
openOwnership: this.openOwnership,
openCountdownOwnership: this.openCountdownOwnership,
toggleSimpleView: this.toggleSimpleView
},
position: {
width: 400,
height: 222,
top: 50
}
form: { handler: this.updateData, submitOnChange: true }
};
/** @override */
static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/ui/countdowns.hbs'
countdowns: {
template: 'systems/daggerheart/templates/ui/countdowns.hbs',
scrollable: ['.expanded-view']
}
};
get element() {
return document.body.querySelector('.daggerheart.dh-style.countdowns');
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => {
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false));
element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true));
});
}
async _preFirstRender(context, options) {
options.position =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position) ??
Countdowns.DEFAULT_OPTIONS.position;
const viewSetting =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple) ?? !game.user.isGM;
this.simpleView =
game.user.isGM || !this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ? viewSetting : true;
context.simple = this.simpleView;
}
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position, position);
}
/**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
const iconOnly =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
if (iconOnly) frame.classList.add('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);
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) {
const button = constructHTMLButton({
label: '',
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'],
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' }
});
this.window.controls.after(button);
}
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;
}
/** Returns countdown data filtered by ownership */
#getCountdowns() {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const values = Object.entries(setting.countdowns).map(([key, countdown]) => ({
key,
countdown,
ownership: DhCountdowns.#getPlayerOwnership(game.user, setting, countdown)
}));
return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE);
testUserPermission(level, exact, altSettings) {
if (game.user.isGM) return true;
const settings =
altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level;
const userAllowed = exact
? settings.playerOwnership[game.user.id]?.value === level
: settings.playerOwnership[game.user.id]?.value >= level;
return defaultAllowed || userAllowed;
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[
this.basePath
];
context.isGM = game.user.isGM;
context.base = this.basePath;
context.iconOnly =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
context.countdowns = this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => {
const playersWithAccess = game.users.reduce((acc, user) => {
const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown);
if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
acc.push(user);
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true);
context.source = {
...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const countdown = countdownData.countdowns[key];
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
acc[key] = {
...countdown,
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown)
};
}
return acc;
}, []);
const nonGmPlayers = game.users.filter(x => !x.isGM);
const countdownEditable = game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const isLooping = countdown.progress.looping !== CONFIG.DH.GENERAL.countdownLoopingTypes.noLooping;
const loopTooltip = isLooping
? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? 'DAGGERHEART.UI.Countdowns.increasingLoop'
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? 'DAGGERHEART.UI.Countdowns.decreasingLoop'
: 'DAGGERHEART.UI.Countdowns.loop'
: null;
const loopDisabled =
!countdownEditable ||
(isLooping && (countdown.progress.current > 0 || countdown.progress.start === '0'));
acc[key] = {
...countdown,
editable: countdownEditable,
noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0,
shouldLoop: isLooping && countdown.progress.current === 0 && countdown.progress.start > 0,
loopDisabled: isLooping ? loopDisabled : null,
loopTooltip: isLooping && game.i18n.localize(loopTooltip)
};
return acc;
}, {});
}, {})
};
context.systemFields = countdownData.schema.fields;
context.countdownFields = context.systemFields.countdowns.element.fields;
context.simple = this.simpleView;
return context;
}
static #getPlayerOwnership(user, setting, countdown) {
const playerOwnership = countdown.ownership[user.id];
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
? setting.defaultOwnership
: playerOwnership;
}
static async updateData(event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
const newSetting = foundry.utils.mergeObject(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(),
data
);
cooldownRefresh = ({ refreshType }) => {
if (refreshType === RefreshType.Countdown) this.render();
};
static canPerformEdit() {
if (game.user.isGM) return true;
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting);
this.render();
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: newSetting
}
});
}
return true;
}
static async #toggleViewMode() {
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
const appMode = CONFIG.DH.GENERAL.countdownAppMode;
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
async updateSetting(update) {
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update);
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.Countdown,
application: `${this.basePath}-countdowns`
}
});
if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
else this.element.classList.remove('icon-only');
this.render();
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
}
static onEditImage(_, target) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const current = setting.countdowns[target.dataset.countdown].img;
const fp = new foundry.applications.apps.FilePicker.implementation({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
async updateImage(path, countdown) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdown}.img`]: path
});
await this.updateSetting(setting);
}
static openOwnership(_, target) {
new Promise((resolve, reject) => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const ownership = { default: setting.ownership.default, players: setting.playerOwnership };
new OwnershipSelection(resolve, reject, this.title, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting.toObject());
this.render();
});
}
static openCountdownOwnership(_, target) {
const countdownId = target.dataset.countdown;
new Promise((resolve, reject) => {
const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]
.countdowns[countdownId];
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting);
this.render();
});
}
static async toggleSimpleView() {
this.simpleView = !this.simpleView;
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
this.render();
}
static async #editCountdowns() {
new game.system.api.applications.ui.CountdownEdit().render(true);
}
async updateCountdownValue(event, increase) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
static async #loopCountdown(_, target) {
if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = settings.countdowns[target.id];
let progressMax = countdown.progress.start;
let message = null;
if (countdown.progress.startFormula) {
const roll = await new Roll(countdown.progress.startFormula).evaluate();
progressMax = roll.total;
message = await roll.toMessage();
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
return;
}
const newMax =
countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id
? Number(progressMax) + 1
: countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id
? Math.max(Number(progressMax) - 1, 0)
: progressMax;
const currentValue = countdown.progress.current;
await waitForDiceSoNice(message);
await settings.updateSource({
[`countdowns.${target.id}.progress`]: {
current: newMax,
start: newMax
}
});
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
if (increase && currentValue === countdown.progress.max) return;
if (!increase && currentValue === 0) return;
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
? currentValue + 1
: currentValue - 1
});
await this.updateSetting(countdownSetting.toObject());
}
static async editCountdown(increase, target) {
if (!DhCountdowns.canPerformEdit()) return;
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdown = settings.countdowns[target.id];
const newCurrent = increase
? Math.min(countdown.progress.current + 1, countdown.progress.start)
: Math.max(countdown.progress.current - 1, 0);
await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown
});
}
static async gmSetSetting(data) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
setupHooks() {
Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
}
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.cooldownRefresh);
return super.close(options);
}
/**
* Sends updates of the countdowns to the GM player. Since this is asynchronous, be sure to
* update all the countdowns at the same time.
*
* @param {...any} progressTypes Countdowns to be updated
*/
static async updateCountdowns(...progressTypes) {
const { countdownAutomation } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (!countdownAutomation) return;
static async addCountdown() {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => {
const countdown = countdownSetting.countdowns[key];
if (progressTypes.indexOf(countdown.progress.type) !== -1 && countdown.progress.current > 0) {
acc.push(key);
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
ownership: game.user.isGM
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
}
return acc;
}, []);
const countdownData = countdownSetting.toObject();
const settings = {
...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const countdown = foundry.utils.deepClone(countdownData.countdowns[key]);
if (updatedCountdowns.includes(key)) {
countdown.progress.current -= 1;
}
acc[key] = countdown;
return acc;
}, {})
};
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
DhCountdowns.gmSetSetting.bind(settings),
settings, null, {
refreshType: RefreshType.Countdown
});
await this.updateSetting(countdownSetting.toObject());
}
async _onRender(context, options) {
await super._onRender(context, options);
this.element.hidden = !game.user.isGM && this.#getCountdowns().length === 0;
if (options?.force) {
document.getElementById('ui-right-column-1')?.appendChild(this.element);
static async removeCountdown(_, target) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownText', { name: countdownName })
});
if (!confirmed) return;
await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null });
await this.updateSetting(countdownSetting.toObject());
}
async open() {
await this.render(true);
if (
Object.keys(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns
).length > 0
) {
this.minimize();
}
}
}
export class NarrativeCountdowns extends Countdowns {
constructor() {
super('narrative');
}
static DEFAULT_OPTIONS = {
id: 'narrative-countdowns'
};
}
export class EncounterCountdowns extends Countdowns {
constructor() {
super('encounter');
}
static DEFAULT_OPTIONS = {
id: 'encounter-countdowns'
};
}
export async function updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
return foundry.utils.mergeObject(
update,
Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
const countdown = countdownSetting[typeKey].countdowns[countdownKey];
if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
}
return acc;
}, {})
);
}, {});
await countdownSetting.updateSource(update);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting);
const data = { refreshType: RefreshType.Countdown };
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data
});
Hooks.callAll(socketEvent.Refresh, data);
}

View file

@ -1,117 +0,0 @@
import { RefreshType } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* A UI element which displays the Active Effects on a selected token.
*
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
export default class DhEffectsDisplay extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options = {}) {
super(options);
this.setupHooks();
}
/** @inheritDoc */
static DEFAULT_OPTIONS = {
id: 'effects-display',
tag: 'div',
classes: ['daggerheart', 'dh-style', 'effects-display'],
window: {
frame: false,
positioned: false,
resizable: false,
minimizable: false
},
actions: {}
};
/** @override */
static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/ui/effects-display.hbs'
}
};
get element() {
return document.body.querySelector('.daggerheart.dh-style.effects-display');
}
get hidden() {
return this.element.classList.contains('hidden');
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
if (this.element) {
this.element.querySelectorAll('.effect-container a').forEach(element => {
element.addEventListener('contextmenu', this.removeEffect.bind(this));
});
}
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.effects = DhEffectsDisplay.getTokenEffects();
return context;
}
static getTokenEffects = token => {
const actor = token
? token.actor
: canvas.tokens.controlled.length === 0
? !game.user.isGM
? game.user.character
: null
: canvas.tokens.controlled[0].actor;
return actor?.getActiveEffects() ?? [];
};
toggleHidden(token, focused) {
const effects = DhEffectsDisplay.getTokenEffects(focused ? token : null);
this.element.hidden = effects.length === 0;
Hooks.callAll(CONFIG.DH.HOOKS.effectDisplayToggle, this.element.hidden, token);
this.render();
}
async removeEffect(event) {
const element = event.target.closest('.effect-container');
const effects = DhEffectsDisplay.getTokenEffects();
const effect = effects.find(x => x.id === element.dataset.effectId);
await effect.delete();
this.render();
}
setupHooks() {
Hooks.on('controlToken', this.toggleHidden.bind(this));
Hooks.on(RefreshType.EffectsDisplay, this.toggleHidden.bind(this));
}
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('controlToken', this.toggleHidden);
Hooks.off(RefreshType.EffectsDisplay, this.toggleHidden);
return super.close(options);
}
async _onRender(context, options) {
await super._onRender(context, options);
this.element.hidden = context.effects.length === 0;
if (options?.force) {
document.getElementById('ui-right-column-1')?.appendChild(this.element);
}
}
}

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
import { emitAsGM, GMUpdateEvent, socketEvent } from "../../systemRegistration/socket.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -78,7 +78,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
/** @override */
async _preRender(context, options) {
if (this.currentFear > this.maxFear && game.user.isGM)
if (this.currentFear > this.maxFear)
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
}
@ -106,10 +106,19 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
}
async updateFear(value) {
return emitAsGM(
GMUpdateEvent.UpdateFear,
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
value
);
return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value);
/* if(!game.user.isGM)
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateFear,
update: value
}
});
else
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
/* if (!game.user.isGM) return;
value = Math.max(0, Math.min(this.maxFear, value));
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */
}
}

View file

@ -15,14 +15,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.fieldFilter = [];
this.selectedMenu = { path: [], data: null };
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = {};
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
this.presets = options.presets;
if (this.presets?.compendium && this.presets?.folder)
ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder);
}
/** @inheritDoc */
static DEFAULT_OPTIONS = {
id: 'itemBrowser',
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser', 'daggerheart-loader'],
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'],
tag: 'div',
window: {
frame: true,
@ -82,29 +84,17 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
}
};
/** @inheritDoc */
async _preFirstRender(context, options) {
if (context.presets?.render?.noFolder || context.presets?.render?.lite) options.position.width = 600;
await super._preFirstRender(context, options);
}
/** @inheritDoc */
async _preRender(context, options) {
this.presets = options.presets ?? {};
const noFolder = this.presets?.render?.noFolder;
if (noFolder === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserNoFolder';
}
const lite = this.presets?.render?.lite;
if (lite === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserLite';
}
const userPresetPosition = game.user.getFlag(
CONFIG.DH.id,
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
);
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
if (!userPresetPosition) {
const width = noFolder === true || lite === true ? 600 : 850;
if (this.rendered) this.setPosition({ width });
else options.position.width = width;
}
if (context.presets?.render?.noFolder || context.presets?.render?.lite)
options.parts.splice(options.parts.indexOf('sidebar'), 1);
await super._preRender(context, options);
}
@ -113,35 +103,22 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _onRender(context, options) {
await super._onRender(context, options);
this.element
.querySelectorAll('[data-action="selectFolder"]')
.forEach(element =>
element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.'))
);
this._createSearchFilter();
this._createFilterInputs();
this._createDragProcess();
this.element.classList.toggle('lite', this.presets?.render?.lite === true);
this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true);
this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true);
this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => {
element.hidden =
this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
});
}
if (context.presets?.render?.lite) this.element.classList.add('lite');
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position, position);
}
if (context.presets?.render?.noFolder) this.element.classList.add('no-folder');
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
if (context.presets?.render?.noFilter) this.element.classList.add('no-filter');
htmlElement.querySelectorAll('[data-action="selectFolder"]').forEach(element =>
element.addEventListener('contextmenu', event => {
event.target.classList.toggle('expanded');
})
);
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(
([k, v]) => (this.fieldFilter.find(c => c.name === k).value = v.value)
);
await this._onInputFilterBrowser();
}
}
/* -------------------------------------------- */
@ -152,26 +129,22 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config));
// context.pathTitle = this.pathTile;
context.menu = this.selectedMenu;
context.formatLabel = this.formatLabel;
context.formatChoices = this.formatChoices;
context.fieldFilter = this.fieldFilter = this._createFieldFilter();
context.items = this.items;
context.presets = this.presets;
return context;
}
open(presets = {}) {
this.presets = presets;
ItemBrowser.selectFolder.call(this);
}
getCompendiumFolders(config, parent = null, depth = 0) {
let folders = [];
Object.values(config).forEach(c => {
// if(this.presets.render?.folders?.length && !this.presets.render.folders.includes(c.id)) return;
const folder = {
id: c.id,
label: game.i18n.localize(c.label),
label: c.label,
selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id
};
folder.folders = c.folders
@ -179,114 +152,34 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
: [];
folders.push(folder);
});
folders.sort((a, b) => a.label.localeCompare(b.label));
return folders;
}
static async selectFolder(_, target) {
const folderId = target?.dataset?.folderId ?? this.presets.folder,
folderData = foundry.utils.getProperty(this.config, folderId) ?? {};
const columns = ItemBrowser.getFolderConfig(folderData).map(col => ({
...col,
label: game.i18n.localize(col.label)
}));
static async selectFolder(_, target, compend, folder) {
const config = foundry.utils.deepClone(this.config),
compendium = compend ?? target.closest('[data-compendium-id]').dataset.compendiumId,
folderId = folder ?? target.dataset.folderId,
folderPath = `${compendium}.folders.${folderId}`,
folderData = foundry.utils.getProperty(config, folderPath);
this.selectedMenu = {
path: folderId?.split('.') ?? [],
path: folderPath.split('.'),
data: {
...folderData,
columns: columns
columns: ItemBrowser.getFolderConfig(folderData)
}
};
await this.render({ force: true, presets: this.presets });
let items = [];
for (const key of folderData.keys) {
const comp = game.packs.get(`${compendium}.${key}`);
if (!comp) return;
items = items.concat(await comp.getDocuments({ type__in: folderData.type }));
}
if (this.selectedMenu?.data?.type?.length) this.loadItems();
}
_replaceHTML(result, content, options) {
if (!options.isFirstRender) delete result.sidebar;
super._replaceHTML(result, content, options);
}
loadItems() {
let loadTimeout = this.toggleLoader(true);
const promises = [];
game.packs.forEach(pack => {
promises.push(
new Promise(async resolve => {
const items = await pack.getDocuments({ type__in: this.selectedMenu?.data?.type });
resolve(items);
})
);
});
Promise.all(promises).then(async result => {
this.items = ItemBrowser.sortBy(
result.flatMap(r => r),
'name'
);
/* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
for (const item of this.items) {
item.system.enrichedDescription =
(await item.system.getEnrichedDescription?.()) ??
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
}
this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k, v]) => {
const filter = this.fieldFilter.find(c => c.name === k);
if (filter) filter.value = v.value;
});
// await this._onInputFilterBrowser();
}
const filterList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs',
{
fieldFilter: this.fieldFilter,
presets: this.presets,
formatChoices: this.formatChoices
}
);
this.element.querySelector('.filter-content .wrapper').innerHTML = filterList;
const filterContainer = this.element.querySelector('.filter-header > [data-action="expandContent"]');
if (this.fieldFilter.length === 0) filterContainer.setAttribute('disabled', '');
else filterContainer.removeAttribute('disabled');
const itemList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
{
items: this.items,
menu: this.selectedMenu,
formatLabel: this.formatLabel
}
);
this.element.querySelector('.item-list').innerHTML = itemList;
this._createFilterInputs();
await this._onInputFilterBrowser();
this._createDragProcess();
clearTimeout(loadTimeout);
this.toggleLoader(false);
});
}
toggleLoader(state) {
const container = this.element.querySelector('.item-list');
return setTimeout(() => {
container.classList.toggle('daggerheart-loader', state);
}, 100);
this.items = ItemBrowser.sortBy(items, 'name');
this.render({ force: true });
}
static expandContent(_, target) {
@ -302,7 +195,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const property = foundry.utils.getProperty(item, field.key);
if (Array.isArray(property)) property.join(', ');
if (typeof field.format !== 'function') return property ?? '-';
return game.i18n.localize(field.format(property));
return field.format(property);
}
formatChoices(data) {
@ -321,14 +214,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
filters.forEach(f => {
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
else if (typeof f.choices === 'function') {
f.choices = f.choices(this.items);
f.choices = f.choices();
}
// Clear field label so template uses our custom label parameter
if (f.field && f.label) {
f.field.label = undefined;
}
f.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null;
});
@ -340,8 +227,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/* -------------------------------------------- */
/**
* Create and initialize search filter instance.
* Create and initialize search filter instances for the inventory and loadout sections.
*
* Sets up two {@link foundry.applications.ux.SearchFilter} instances:
* - One for the inventory, which filters items in the inventory grid.
* - One for the loadout, which filters items in the loadout/card grid.
* @private
*/
_createSearchFilter() {
@ -406,7 +296,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
for (const li of html.querySelectorAll('.item-container')) {
const itemUUID = li.dataset.itemUuid,
item = this.items.find(i => i.uuid === itemUUID);
if (!item) continue;
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.browser.search.add(item.id);
const { input } = this.#filteredItems.browser;
@ -438,7 +327,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
if (matchesMenu) this.#filteredItems.browser.input.add(item.id);
const { search } = this.#filteredItems.browser;
li.hidden = !((this.#search.browser.query.length === 0 || search.has(item.id)) && matchesMenu);
li.hidden = !(search.has(item.id) && matchesMenu);
}
}
@ -477,7 +366,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
static resetFilters() {
this.render({ force: true });
this.loadItems();
}
static getFolderConfig(folder, property = 'columns') {
@ -499,13 +387,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const newOrder = [...itemList].reverse().sort((a, b) => {
const aProp = a.querySelector(`[data-item-key="${key}"]`),
bProp = b.querySelector(`[data-item-key="${key}"]`),
aValue = isNaN(aProp.innerText) ? aProp.innerText : Number(aProp.innerText),
bValue = isNaN(bProp.innerText) ? bProp.innerText : Number(bProp.innerText);
bProp = b.querySelector(`[data-item-key="${key}"]`);
if (type === 'DESC') {
return aValue < bValue ? 1 : -1;
return aProp.innerText < bProp.innerText ? 1 : -1;
} else {
return aValue > bValue ? 1 : -1;
return aProp.innerText > bProp.innerText ? 1 : -1;
}
});
@ -534,41 +420,4 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
_canDragStart() {
return true;
}
static injectSidebarButton(html) {
if (!game.user.isGM) return;
const sectionId = html.dataset.tab,
menus = {
actors: {
folder: 'adversaries',
render: {
folders: ['adversaries', 'characters', 'environments']
}
},
items: {
folder: 'equipments',
render: {
noFolder: true
}
},
compendium: {}
};
if (Object.keys(menus).includes(sectionId)) {
const headerActions = html.querySelector('.header-actions');
const button = document.createElement('button');
button.type = 'button';
button.classList.add('open-compendium-browser');
button.innerHTML = `
<i class="fa-solid fa-book-atlas"></i>
${game.i18n.localize('DAGGERHEART.UI.Tooltip.compendiumBrowser')}
`;
button.addEventListener('click', event => {
ui.compendiumBrowser.open(menus[sectionId]);
});
headerActions.append(button);
}
}
}

View file

@ -1,89 +0,0 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
/** @inheritdoc */
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: ['faded-ui', 'flexcol', 'scene-navigation'],
actions: {
openSceneEnvironment: DhSceneNavigation.#openSceneEnvironment
}
};
/** @inheritdoc */
static PARTS = {
scenes: {
root: true,
template: 'systems/daggerheart/templates/ui/sceneNavigation/scene-navigation.hbs'
}
};
/** @inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const extendScenes = scenes =>
scenes.map(x => {
const scene = game.scenes.get(x.id);
if (!scene.flags.daggerheart) return x;
const daggerheartInfo = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
const environments = daggerheartInfo.sceneEnvironments.filter(
x => x && x.testUserPermission(game.user, 'LIMITED')
);
const hasEnvironments = environments.length > 0 && x.isView;
return {
...x,
hasEnvironments,
environmentImage: hasEnvironments ? environments[0].img : null,
environments: environments
};
});
context.scenes.active = extendScenes(context.scenes.active);
context.scenes.inactive = extendScenes(context.scenes.inactive);
return context;
}
static async #openSceneEnvironment(event, button) {
const scene = game.scenes.get(button.dataset.sceneId);
const sceneEnvironments = new game.system.api.data.scenes.DHScene(
scene.flags.daggerheart
).sceneEnvironments.filter(x => x.testUserPermission(game.user, 'LIMITED'));
if (sceneEnvironments.length === 1 || event.shiftKey) {
sceneEnvironments[0].sheet.render(true);
} else {
new foundry.applications.ux.ContextMenu.implementation(
button,
'.scene-environment',
sceneEnvironments.map(environment => ({
name: environment.name,
callback: () => {
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
const newFirst = newEnvironments.splice(
newEnvironments.findIndex(x => x === environment.uuid)
)[0];
newEnvironments.unshift(newFirst);
emitAsGM(
GMUpdateEvent.UpdateDocument,
scene.update.bind(scene),
{ 'flags.daggerheart.sceneEnvironments': newEnvironments },
scene.uuid
);
}
environment.sheet.render({ force: true });
}
})),
{
jQuery: false,
fixed: true
}
);
CONFIG.ux.ContextMenu.triggerContextMenu(event, '.scene-environment');
}
}
}

View file

@ -96,11 +96,11 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
* Trigger a context menu event in response to a normal click on a additional options button.
* @param {PointerEvent} event
*/
static triggerContextMenu(event, altSelector) {
static triggerContextMenu(event) {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } = event;
const selector = altSelector ?? '[data-item-uuid]';
const selector = '[data-item-uuid]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent(
new PointerEvent('contextmenu', {

View file

@ -1,2 +1 @@
export * as placeables from './placeables/_module.mjs';
export { default as DhTokenLayer } from './tokens.mjs';

View file

@ -10,48 +10,29 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur
const splitRulerText = this.ruler.text.split(' ');
if (splitRulerText.length > 0) {
const rulerValue = Number(splitRulerText[0]);
const result = DhMeasuredTemplate.getRangeLabels(rulerValue, rangeMeasurementSettings);
this.ruler.text = result.distance + (result.units ? ' ' + result.units : '');
const vagueLabel = this.constructor.getDistanceLabel(rulerValue, rangeMeasurementSettings);
this.ruler.text = vagueLabel;
}
}
}
static getRangeLabels(distanceValue, settings) {
let result = { distance: distanceValue, units: '' };
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
if (sceneRangeMeasurement?.setting === disable.id) {
result.distance = distanceValue;
result.units = canvas.scene?.grid?.units;
return result;
static getDistanceLabel(distance, settings) {
if (distance <= settings.melee) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
}
if (distance <= settings.veryClose) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
}
if (distance <= settings.close) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
}
if (distance <= settings.far) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
}
if (distance > settings.far) {
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
}
const melee = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.melee : settings.melee;
const veryClose =
sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.veryClose : settings.veryClose;
const close = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.close : settings.close;
const far = sceneRangeMeasurement?.setting === custom.id ? sceneRangeMeasurement.far : settings.far;
if (distanceValue <= melee) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
return result;
}
if (distanceValue <= veryClose) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
return result;
}
if (distanceValue <= close) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
return result;
}
if (distanceValue <= far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
return result;
}
if (distanceValue > far) {
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
}
return result;
return '';
}
}

View file

@ -8,9 +8,9 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler {
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) {
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: result.distance, units: result.units };
context.distance = { total: result.distance, units: result.units };
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null };
context.distance = { total: distance, units: null };
}
return context;

View file

@ -1,12 +1,4 @@
export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
/** @inheritdoc */
async _draw(options) {
await super._draw(options);
if (this.document.flags.daggerheart?.createPlacement)
this.previewHelp ||= this.addChild(this.#drawPreviewHelp());
}
/** @inheritDoc */
async _drawEffects() {
this.effects.renderable = false;
@ -18,7 +10,27 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.effects.overlay = null;
// Categorize effects
const activeEffects = this.actor?.getActiveEffects() ?? [];
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status]));
const activeEffects = (this.actor ? this.actor.effects.filter(x => !x.disabled) : []).reduce((acc, effect) => {
acc.push(effect);
const currentStatusActiveEffects = acc.filter(
x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first()).name)
);
for (var status of effect.statuses) {
if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) {
const statusData = statusMap.get(status);
acc.push({
name: game.i18n.localize(statusData.name),
statuses: [status],
img: statusData.icon,
tint: effect.tint
});
}
}
return acc;
}, []);
const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
// Draw effects
@ -42,69 +54,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true });
}
/**
* Returns the distance from this token to another token object.
* This value is corrected to handle alternate token sizes and other grid types
* according to the diagonal rules.
*/
distanceTo(target) {
if (!canvas.ready) return NaN;
if (this === target) return 0;
const originPoint = this.center;
const destinationPoint = target.center;
// Compute for gridless. This version returns circular edge to edge + grid distance,
// so that tokens that are touching return 5.
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
const boundsCorrection = canvas.grid.distance / canvas.grid.size;
const originRadius = (this.bounds.width * boundsCorrection) / 2;
const targetRadius = (target.bounds.width * boundsCorrection) / 2;
const distance = canvas.grid.measurePath([originPoint, destinationPoint]).distance;
return distance - originRadius - targetRadius + canvas.grid.distance;
}
// Compute what the closest grid space of each token is, then compute that distance
const originEdge = this.#getEdgeBoundary(this.bounds, originPoint, destinationPoint);
const targetEdge = this.#getEdgeBoundary(target.bounds, originPoint, destinationPoint);
const adjustedOriginPoint = canvas.grid.getTopLeftPoint({
x: originEdge.x + Math.sign(originPoint.x - originEdge.x),
y: originEdge.y + Math.sign(originPoint.y - originEdge.y)
});
const adjustDestinationPoint = canvas.grid.getTopLeftPoint({
x: targetEdge.x + Math.sign(destinationPoint.x - targetEdge.x),
y: targetEdge.y + Math.sign(destinationPoint.y - targetEdge.y)
});
return canvas.grid.measurePath([adjustedOriginPoint, adjustDestinationPoint]).distance;
}
/** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */
#getEdgeBoundary(bounds, originPoint, destinationPoint) {
const points = [
{ x: bounds.x, y: bounds.y },
{ x: bounds.x + bounds.width, y: bounds.y },
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height },
{ x: bounds.x, y: bounds.y + bounds.height }
];
const pairsToTest = [
[points[0], points[1]],
[points[1], points[2]],
[points[2], points[3]],
[points[3], points[0]]
];
for (const pair of pairsToTest) {
const result = foundry.utils.lineSegmentIntersection(originPoint, destinationPoint, pair[0], pair[1]);
if (result) return result;
}
return null;
}
/** Tests if the token is at least adjacent with another, with some leeway for diagonals */
isAdjacentWith(token) {
return this.distanceTo(token) <= canvas.grid.distance * 1.5;
}
/** @inheritDoc */
_drawBar(number, bar, data) {
const val = Number(data.value);
@ -140,25 +89,4 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
bar.position.set(0, posY);
return true;
}
/**
* Draw a helptext for previews as a text object
* @returns {PreciseText} The Text object for the preview helper
*/
#drawPreviewHelp() {
const { uiScale } = canvas.dimensions;
const textStyle = CONFIG.canvasTextStyle.clone();
textStyle.fontSize = 18;
textStyle.wordWrapWidth = this.w * 2.5;
textStyle.fontStyle = 'italic';
const helpText = new foundry.canvas.containers.PreciseText(
`(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`,
textStyle
);
helpText.anchor.set(helpText.width / 900, 1);
helpText.scale.set(uiScale, uiScale);
return helpText;
}
}

View file

@ -8,9 +8,9 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke
const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) {
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: result.distance, units: result.units };
context.distance = { total: result.distance, units: result.units };
const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: distance, units: null };
context.distance = { total: distance, units: null };
}
return context;

View file

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

@ -2,12 +2,9 @@ export * as actionConfig from './actionConfig.mjs';
export * as actorConfig from './actorConfig.mjs';
export * as domainConfig from './domainConfig.mjs';
export * as effectConfig from './effectConfig.mjs';
export * as encounterConfig from './encounterConfig.mjs';
export * as flagsConfig from './flagsConfig.mjs';
export * as generalConfig from './generalConfig.mjs';
export * as hooksConfig from './hooksConfig.mjs';
export * as itemConfig from './itemConfig.mjs';
export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs';
export * as itemBrowserConfig from './itemBrowserConfig.mjs';
export * as triggerConfig from './triggerConfig.mjs';

View file

@ -2,15 +2,9 @@ export const actionTypes = {
attack: {
id: 'attack',
name: 'DAGGERHEART.ACTIONS.TYPES.attack.name',
icon: 'fa-hand-fist',
icon: 'fa-khanda',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.attack.tooltip'
},
countdown: {
id: 'countdown',
name: 'DAGGERHEART.ACTIONS.TYPES.countdown.name',
icon: 'fa-hourglass-half',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.countdown.tooltip'
},
healing: {
id: 'healing',
name: 'DAGGERHEART.ACTIONS.TYPES.healing.name',

View file

@ -108,72 +108,55 @@ export const adversaryTypes = {
bruiser: {
id: 'bruiser',
label: 'DAGGERHEART.CONFIG.AdversaryType.bruiser.label',
description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description',
bpCost: 4
description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description'
},
horde: {
id: 'horde',
label: 'DAGGERHEART.CONFIG.AdversaryType.horde.label',
description: 'DAGGERHEART.ACTORS.Adversary.horde.description',
bpCost: 2
description: 'DAGGERHEART.ACTORS.Adversary.horde.description'
},
leader: {
id: 'leader',
label: 'DAGGERHEART.CONFIG.AdversaryType.leader.label',
description: 'DAGGERHEART.ACTORS.Adversary.leader.description',
bpCost: 3,
bpDescription: 'DAGGERHEART.CONFIG.AdversaryType.leader.'
description: 'DAGGERHEART.ACTORS.Adversary.leader.description'
},
minion: {
id: 'minion',
label: 'DAGGERHEART.CONFIG.AdversaryType.minion.label',
description: 'DAGGERHEART.ACTORS.Adversary.minion.description',
bpCost: 1,
partyAmountPerBP: true
description: 'DAGGERHEART.ACTORS.Adversary.minion.description'
},
ranged: {
id: 'ranged',
label: 'DAGGERHEART.CONFIG.AdversaryType.ranged.label',
description: 'DAGGERHEART.ACTORS.Adversary.ranged.description',
bpCost: 2
description: 'DAGGERHEART.ACTORS.Adversary.ranged.description'
},
skulk: {
id: 'skulk',
label: 'DAGGERHEART.CONFIG.AdversaryType.skulk.label',
description: 'DAGGERHEART.ACTORS.Adversary.skulk.description',
bpCost: 2
description: 'DAGGERHEART.ACTORS.Adversary.skulk.description'
},
social: {
id: 'social',
label: 'DAGGERHEART.CONFIG.AdversaryType.social.label',
description: 'DAGGERHEART.ACTORS.Adversary.social.description',
bpCost: 1
description: 'DAGGERHEART.ACTORS.Adversary.social.description'
},
solo: {
id: 'solo',
label: 'DAGGERHEART.CONFIG.AdversaryType.solo.label',
description: 'DAGGERHEART.ACTORS.Adversary.solo.description',
bpCost: 5
description: 'DAGGERHEART.ACTORS.Adversary.solo.description'
},
standard: {
id: 'standard',
label: 'DAGGERHEART.CONFIG.AdversaryType.standard.label',
description: 'DAGGERHEART.ACTORS.Adversary.standard.description',
bpCost: 2
description: 'DAGGERHEART.ACTORS.Adversary.standard.description'
},
support: {
id: 'support',
label: 'DAGGERHEART.CONFIG.AdversaryType.support.label',
description: 'DAGGERHEART.ACTORS.Adversary.support.description',
bpCost: 1
description: 'DAGGERHEART.ACTORS.Adversary.support.description'
}
};
export const allAdversaryTypes = () => ({
...adversaryTypes,
...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).adversaryTypes
});
export const environmentTypes = {
exploration: {
label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label',
@ -211,44 +194,6 @@ export const adversaryTraits = {
}
};
export const tokenSize = {
custom: {
id: 'custom',
value: 0,
label: 'DAGGERHEART.GENERAL.custom'
},
tiny: {
id: 'tiny',
value: 1,
label: 'DAGGERHEART.CONFIG.TokenSize.tiny'
},
small: {
id: 'small',
value: 2,
label: 'DAGGERHEART.CONFIG.TokenSize.small'
},
medium: {
id: 'medium',
value: 3,
label: 'DAGGERHEART.CONFIG.TokenSize.medium'
},
large: {
id: 'large',
value: 4,
label: 'DAGGERHEART.CONFIG.TokenSize.large'
},
huge: {
id: 'huge',
value: 5,
label: 'DAGGERHEART.CONFIG.TokenSize.huge'
},
gargantuan: {
id: 'gargantuan',
value: 6,
label: 'DAGGERHEART.CONFIG.TokenSize.gargantuan'
}
};
export const levelChoices = {
attributes: {
name: 'attributes',

View file

@ -1,146 +0,0 @@
export const BaseBPPerEncounter = nrCharacters => 3 * nrCharacters + 2;
export const AdversaryBPPerEncounter = (adversaries, characters) => {
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
return adversaries
.reduce((acc, adversary) => {
const existingEntry = acc.find(
x => x.adversary.name === adversary.name && x.adversary.type === adversary.type
);
if (existingEntry) {
existingEntry.nr += 1;
} else if (adversary.type) {
acc.push({ adversary, nr: 1 });
}
return acc;
}, [])
.reduce((acc, entry) => {
const adversary = entry.adversary;
const type = adversaryTypes[adversary.type];
const bpCost = type.bpCost ?? 0;
if (type.partyAmountPerBP) {
acc += characters.length === 0 ? 0 : Math.ceil(entry.nr / characters.length);
} else {
acc += bpCost * entry.nr;
}
return acc;
}, 0);
};
export const adversaryTypeCostBrackets = {
1: [
{
sort: 1,
types: ['minion'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.minion'
},
{
sort: 2,
types: ['social', 'support'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.support'
}
],
2: [
{
sort: 1,
types: ['horde', 'ranged', 'skulk', 'standard'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.standard'
}
],
3: [
{
sort: 1,
types: ['leader'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.leader'
}
],
4: [
{
sort: 1,
types: ['bruiser'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.bruiser'
}
],
5: [
{
sort: 1,
types: ['solo'],
description: 'DAGGERHEART.CONFIG.AdversaryTypeCost.solo'
}
]
};
export const BPModifiers = {
[-2]: {
manySolos: {
sort: 1,
description: 'DAGGERHEART.CONFIG.BPModifiers.manySolos.description',
automatic: true,
conditional: (_combat, adversaries) => {
return adversaries.filter(x => x.system.type === 'solo').length > 1;
}
},
increaseDamage: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.description',
effectTargetTypes: ['adversary'],
effects: [
{
name: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.name',
description: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.description',
img: 'icons/magic/control/buff-flight-wings-red.webp',
changes: [
{
key: 'system.bonuses.damage.physical.dice',
mode: 2,
value: '1d4'
},
{
key: 'system.bonuses.damage.magical.dice',
mode: 2,
value: '1d4'
}
]
}
]
}
},
[-1]: {
lessDifficult: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.lessDifficult.description'
}
},
1: {
lowerTier: {
sort: 1,
description: 'DAGGERHEART.CONFIG.BPModifiers.lowerTier.description',
automatic: true,
conditional: (_combat, adversaries, characters) => {
const characterMaxTier = characters.reduce((maxTier, character) => {
return character.system.tier > maxTier ? character.system.tier : maxTier;
}, 1);
return adversaries.some(adversary => adversary.system.tier < characterMaxTier);
}
},
noToughies: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.noToughies.description',
automatic: true,
conditional: (_combat, adversaries) => {
const toughyTypes = ['bruiser', 'horde', 'leader', 'solo'];
return (
adversaries.length > 0 &&
!adversaries.some(adversary => toughyTypes.includes(adversary.system.type))
);
}
}
},
2: {
moreDangerous: {
sort: 2,
description: 'DAGGERHEART.CONFIG.BPModifiers.moreDangerous.description'
}
}
};

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