Compare commits

..

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

1633 changed files with 30654 additions and 37851 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 name: Bug report
about: Create a bug report to help us identify issues and resolve them about: Create a report to help us improve
title: "[Bug] <Insert Title here> " title: "[BUG] - "
labels: bug labels: bug
type: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** **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. If applicable, add screenshots to help explain your problem.
**Setup Information:** **Setup Information:**
- OS: [e.g. iOS, Windows] - OS: [e.g. iOS]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]
- Foundry Version [e.g. v13 b342] - 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** **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! 🎉

1
.gitignore vendored
View file

@ -1,5 +1,4 @@
.vscode .vscode
.env
node_modules node_modules
/packs /packs
Build Build

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. 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 ## 🧭 General Guidelines
@ -44,14 +40,12 @@ We encourage contributors to leave comments or open Discussions when proposing s
## 🧾 Issue & PR Best Practices ## 🧾 Issue & PR Best Practices
**For Issues:** **For Issues:**
- Use clear, descriptive titles - Use clear, descriptive titles
- Provide a concise explanation of the problem or idea - Provide a concise explanation of the problem or idea
- Include reproduction steps or example scenarios if it's a bug - Include reproduction steps or example scenarios if it's a bug
- Add screenshots or logs if helpful - Add screenshots or logs if helpful
**For Pull Requests:** **For Pull Requests:**
- Use a clear title summarizing the change - Use a clear title summarizing the change
- Provide a brief description of what your code does and why - Provide a brief description of what your code does and why
- Link to any related Issues - Link to any related Issues
@ -73,6 +67,6 @@ Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Di
## 🤗 Thank You! ## 🤗 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

@ -24,41 +24,24 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart
## Development Setup ## 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 - Replace `C:/FoundryDev/resources/app/main.js` with `<your>/<path>/<to>/<foundry>/<main.js>`
npm install - 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:** Now you should be able to build the app using `npm start`
[Foundry VTT Website][1]
```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]
[1]: https://foundryvtt.com/ [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

View file

@ -1,6 +1,5 @@
import { SYSTEM } from './module/config/system.mjs'; import { SYSTEM } from './module/config/system.mjs';
import * as applications from './module/applications/_module.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 models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs'; import * as documents from './module/documents/_module.mjs';
import * as dice from './module/dice/_module.mjs'; import * as dice from './module/dice/_module.mjs';
@ -8,178 +7,98 @@ import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { 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 { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { registerCountdownHooks } from './module/data/countdowns.mjs';
import { import {
handlebarsRegistration, handlebarsRegistration,
runMigrations,
settingsRegistration, settingsRegistration,
socketRegistration socketRegistration
} from './module/systemRegistration/_module.mjs'; } from './module/systemRegistration/_module.mjs';
import { placeables } 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 './node_modules/@yaireo/tagify/dist/tagify.css';
import TemplateManager from './module/documents/templateManager.mjs'; import TemplateManager from './module/documents/templateManager.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.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.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();
Hooks.once('init', () => { Hooks.once('init', () => {
CONFIG.DH = SYSTEM;
game.system.api = { game.system.api = {
applications, applications,
data,
models, models,
documents, documents,
dice, dice,
fields fields
}; };
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; 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.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, { DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
makeDefault: true makeDefault: true
}); });
const sheetLabel = typePath => () => CONFIG.Item.documentClass = documents.DHItem;
game.i18n.format('DAGGERHEART.GENERAL.typeSheet', {
type: game.i18n.localize(typePath) //Registering the Item DataModel
}); CONFIG.Item.dataModels = models.items.config;
const { Items, Actors } = foundry.documents.collections; const { Items, Actors } = foundry.documents.collections;
Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2); Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2);
Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, { Items.registerSheet(SYSTEM.id, applications.sheets.items.Ancestry, { types: ['ancestry'], makeDefault: true });
types: ['ancestry'], Items.registerSheet(SYSTEM.id, applications.sheets.items.Community, { types: ['community'], makeDefault: true });
makeDefault: true, Items.registerSheet(SYSTEM.id, applications.sheets.items.Class, { types: ['class'], makeDefault: true });
label: sheetLabel('TYPES.Item.ancestry') 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.Community, { Items.registerSheet(SYSTEM.id, applications.sheets.items.DomainCard, { types: ['domainCard'], makeDefault: true });
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.Loot, { Items.registerSheet(SYSTEM.id, applications.sheets.items.Loot, {
types: ['loot'], types: ['loot'],
makeDefault: true, 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')
}); });
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.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2);
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Character, { Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Character, { types: ['character'], makeDefault: true });
types: ['character'], Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Companion, { types: ['companion'], makeDefault: true });
makeDefault: true, Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Adversary, { types: ['adversary'], 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.Environment, { Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Environment, {
types: ['environment'], types: ['environment'],
makeDefault: true, makeDefault: true
label: sheetLabel('TYPES.Actor.environment')
});
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'],
makeDefault: true,
label: sheetLabel('TYPES.Actor.party')
}); });
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
DocumentSheetConfig.unregisterSheet( DocumentSheetConfig.unregisterSheet(
CONFIG.ActiveEffect.documentClass, CONFIG.ActiveEffect.documentClass,
'core', 'core',
@ -190,56 +109,58 @@ Hooks.once('init', () => {
SYSTEM.id, SYSTEM.id,
applications.sheetConfigs.ActiveEffectConfig, applications.sheetConfigs.ActiveEffectConfig,
{ {
makeDefault: true, makeDefault: true
label: sheetLabel('DOCUMENT.ActiveEffect')
} }
); );
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); game.socket.on(`system.${SYSTEM.id}`, socketRegistration.handleSocketEvent);
// Make Compendium Dialog resizable // Make Compendium Dialog resizable
foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true; 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(); settingsRegistration.registerDHSettings();
RegisterHandlebarsHelpers.registerHelpers(); RegisterHandlebarsHelpers.registerHelpers();
return handlebarsRegistration(); 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 () => { Hooks.on('ready', async () => {
const appearanceSettings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance);
ui.resources = new CONFIG.ui.resources(); ui.resources = new CONFIG.ui.resources();
if (appearanceSettings.displayFear !== 'hide') ui.resources.render({ force: true }); if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).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();
registerCountdownHooks();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerRollDiceHooks();
socketRegistration.registerUserQueries(); socketRegistration.registerUserQueries();
if (!game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage)) { if (!game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage)) {
@ -249,15 +170,13 @@ Hooks.on('ready', async () => {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true); game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true);
} }
} }
runMigrations();
}); });
Hooks.once('dicesoniceready', () => {}); Hooks.once('dicesoniceready', () => {});
Hooks.on('renderChatMessageHTML', (document, element) => { Hooks.on('renderChatMessageHTML', (_, element, message) => {
enricherRenderSetup(element); 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)); if (cssClass) cssClass.split(' ').forEach(cls => element.classList.add(cls));
}); });
@ -310,72 +229,73 @@ Hooks.on('chatMessage', (_, message) => {
} }
}); });
const updateActorsRangeDependentEffects = async token => { Hooks.on('renderJournalDirectory', async (tab, html, _, options) => {
if (tab.id === 'journal') {
if (options.parts && !options.parts.includes('footer')) return;
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>`
);
buttons.querySelector('#narrative-countdown-button').onclick = async () => {
new NarrativeCountdowns().open();
};
}
});
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 rangeDependantEffects = movedToken.actor.effects.filter(effect => effect.system.rangeDependence?.enabled);
const updateEffects = async (disposition, token, effects, effectUpdates) => {
const rangeMeasurement = game.settings.get( const rangeMeasurement = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement; ).rangeMeasurement;
for (let effect of token.actor?.allApplicableEffects() ?? []) { for (let effect of effects.filter(x => x.system.rangeDependence?.enabled)) {
if (!effect.system.rangeDependence?.enabled) continue;
const { target, range, type } = effect.system.rangeDependence; const { target, range, type } = effect.system.rangeDependence;
if ((target === 'friendly' && disposition !== 1) || (target === 'hostile' && disposition !== -1))
return false;
// If there are no targets, assume false. Otherwise, start with the effect enabled. const distanceBetween = canvas.grid.measurePath([
let enabledEffect = game.user.targets.size !== 0; { ...movedToken.toObject(), x: data.destination.x, y: data.destination.y },
// Expect all targets to meet the rangeDependence requirements token
for (let userTarget of game.user.targets) { ]).distance;
const disposition = userTarget.document.disposition; const distance = rangeMeasurement[range];
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 reverse = type === CONFIG.DH.GENERAL.rangeInclusion.outsideRange.id;
const inRange = const newDisabled = reverse ? distanceBetween <= distance : distanceBetween > distance;
required === 5 const oldDisabled = effectUpdates[effect.uuid] ? effectUpdates[effect.uuid].disabled : newDisabled;
? userTarget.isAdjacentWith(token.object) effectUpdates[effect.uuid] = {
: userTarget.distanceTo(token.object) <= required; disabled: oldDisabled || newDisabled,
if (reverse ? inRange : !inRange) { value: effect
enabledEffect = false; };
break;
} }
};
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);
} }
await effect.update({ disabled: !enabledEffect }); if (token.actor) await updateEffects(movedToken.disposition, token, token.actor.effects, effectUpdates);
} }
};
const updateAllRangeDependentEffects = async () => { for (let key in effectUpdates) {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects; const effect = effectUpdates[key];
if (!effectsAutomation.rangeDependent) return; await effect.value.update({ disabled: effect.disabled });
const tokens = canvas.scene.tokens;
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 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));

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 dialogs from './dialogs/_module.mjs';
export * as hud from './hud/_module.mjs'; export * as hud from './hud/_module.mjs';
export * as levelup from './levelup/_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 settings from './settings/_module.mjs';
export * as sheets from './sheets/_module.mjs'; export * as sheets from './sheets/_module.mjs';
export * as sheetConfigs from './sheets-configs/_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 ui from './ui/_module.mjs';
export * as ux from './ux/_module.mjs'; export * as ux from './ux/_module.mjs';

View file

@ -1,5 +1,6 @@
import { abilities } from '../../config/actorConfig.mjs'; import { abilities } from '../../config/actorConfig.mjs';
import { burden } from '../../config/generalConfig.mjs'; import { burden } from '../../config/generalConfig.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs'; import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -45,6 +46,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}; };
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
this.itemBrowser = null;
} }
get title() { get title() {
@ -83,9 +86,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static PARTS = { static PARTS = {
tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' }, 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' }, ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' }, community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' }, traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' }, experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' }, domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' },
@ -95,13 +98,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}; };
static TABS = { static TABS = {
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
ancestry: { ancestry: {
active: true, active: true,
cssClass: '', cssClass: '',
@ -116,6 +112,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
id: 'community', id: 'community',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community' label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community'
}, },
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
traits: { traits: {
active: false, active: false,
cssClass: '', cssClass: '',
@ -156,10 +159,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.cssClass = v.active ? 'active' : ''; v.cssClass = v.active ? 'active' : '';
switch (v.id) { switch (v.id) {
case 'ancestry': case 'community':
v.disabled = this.setup.visibility < 2; v.disabled = this.setup.visibility < 2;
break; break;
case 'community': case 'class':
v.disabled = this.setup.visibility < 3; v.disabled = this.setup.visibility < 3;
break; break;
case 'traits': case 'traits':
@ -192,7 +195,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
} }
async _prepareContext(_options) { async _prepareContext(_options) {
this.tabGroups.setup = this.tabGroups.setup ?? 'class'; this.tabGroups.setup = this.tabGroups.setup ?? 'ancestry';
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
@ -266,13 +269,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.isLastTab = this.tabGroups.setup === 'equipment'; context.isLastTab = this.tabGroups.setup === 'equipment';
switch (this.tabGroups.setup) { switch (this.tabGroups.setup) {
case null: case null:
case 'class': case 'ancestry':
context.nextDisabled = this.setup.visibility === 1; context.nextDisabled = this.setup.visibility === 1;
break; break;
case 'ancestry': case 'community':
context.nextDisabled = this.setup.visibility === 2; context.nextDisabled = this.setup.visibility === 2;
break; break;
case 'community': case 'class':
context.nextDisabled = this.setup.visibility === 3; context.nextDisabled = this.setup.visibility === 3;
break; break;
case 'traits': case 'traits':
@ -363,11 +366,11 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 4: case 4:
return this.getNrSelectedTrait() === 6 ? 5 : 4; return this.getNrSelectedTrait() === 6 ? 5 : 4;
case 3: case 3:
return this.setup.community.uuid ? 4 : 3; return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3;
case 2: case 2:
return this.setup.primaryAncestry.uuid ? 3 : 2; return this.setup.community.uuid ? 3 : 2;
case 1: case 1:
return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1; return this.setup.primaryAncestry.uuid ? 2 : 1;
} }
} }
@ -422,30 +425,26 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
equipment = ['armor', 'weapon']; equipment = ['armor', 'weapon'];
const presets = { const presets = {
folder: equipment.includes(type) ? `equipments.folders.${type}s` : type, compendium: 'daggerheart',
folder: equipment.includes(type) ? 'equipments' : type,
render: { render: {
noFolder: true noFolder: true
} }
}; };
if (type === 'domains') if (type == 'domains')
presets.filter = { presets.filter = {
'level.max': { key: 'level.max', value: 1 }, 'level.max': { key: 'level.max', value: 1 },
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null } 'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
}; };
if (type === 'subclasses')
presets.filter = {
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
};
if (equipment.includes(type)) if (equipment.includes(type))
presets.filter = { presets.filter = {
'system.tier': { key: 'system.tier', value: 1 }, 'system.tier': { key: 'system.tier', value: 1 },
'type': { key: 'type', value: type } 'type': { key: 'type', value: type }
}; };
ui.compendiumBrowser.open(presets); return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true }));
} }
static async viewItem(_, target) { static async viewItem(_, target) {
@ -473,10 +472,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static setupGoNext() { static setupGoNext() {
switch (this.setup.visibility) { switch (this.setup.visibility) {
case 2: case 2:
this.tabGroups.setup = 'ancestry'; this.tabGroups.setup = 'community';
break; break;
case 3: case 3:
this.tabGroups.setup = 'community'; this.tabGroups.setup = 'class';
break; break;
case 4: case 4:
this.tabGroups.setup = 'traits'; this.tabGroups.setup = 'traits';
@ -495,9 +494,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
this.render(); this.render();
} }
static async finish(_, button) { static async finish() {
button.disabled = true;
const primaryAncestryFeature = this.setup.primaryAncestry.system.primaryFeature; const primaryAncestryFeature = this.setup.primaryAncestry.system.primaryFeature;
const secondaryAncestryFeature = this.setup.secondaryAncestry?.uuid const secondaryAncestryFeature = this.setup.secondaryAncestry?.uuid
? this.setup.secondaryAncestry.system.secondaryFeature ? this.setup.secondaryAncestry.system.secondaryFeature
@ -563,7 +560,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
{ overwrite: true } { overwrite: true }
); );
if (ui.compendiumBrowser) ui.compendiumBrowser.close(); if (this.itemBrowser) this.itemBrowser.close();
this.close(); this.close();
} }

View file

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

View file

@ -57,11 +57,7 @@ export default class ActionSelectionDialog extends HandlebarsApplicationMixin(Ap
/** @inheritDoc */ /** @inheritDoc */
async _prepareContext(options) { async _prepareContext(options) {
const actions = this.#item.system.actionsList.map(action => ({ const actions = this.#item.system.actionsList,
...action.toObject(),
id: action.id,
img: action.baseAction ? action.parent.parent.img : action.img
})),
itemName = this.#item.name; itemName = this.#item.name;
return { return {
...(await super._prepareContext(options)), ...(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; const featureItem = item;
app.addEventListener( app.addEventListener(
'close', 'close',
async () => { () => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem }),
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
});
},
{ once: true } { once: true }
); );
app.render({ force: true }); app.render({ force: true });

View file

@ -1,5 +1,3 @@
import { abilities } from '../../config/actorConfig.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -9,20 +7,20 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll = roll; this.roll = roll;
this.config = config; this.config = config;
this.config.experiences = []; this.config.experiences = [];
this.reactionOverride = config.actionType === 'reaction'; this.reactionOverride = config.roll?.type === 'reaction';
if (config.source?.action) { if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
this.action = this.action =
config.data.attack?._id == config.source.action config.data.attack?._id == config.source.action
? config.data.attack ? 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 = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
// id: 'roll-selection', id: 'roll-selection',
classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'], classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'],
position: { position: {
width: 'auto' width: 'auto'
@ -34,7 +32,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage, updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction, toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -45,7 +42,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}; };
get title() { get title() {
return `${this.config.title}${this.actor ? `: ${this.actor.name}` : ''}`; return this.config.title;
} }
get actor() { get actor() {
@ -69,7 +66,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config; context.rollConfig = this.config;
context.hasRoll = !!this.config.roll; context.hasRoll = !!this.config.roll;
context.canRoll = true; context.canRoll = true;
context.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 }]) => ({ context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
action, action,
label, label,
@ -84,7 +81,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
); );
context.costs = updatedCosts.map(x => ({ context.costs = updatedCosts.map(x => ({
...x, ...x,
label: x.itemId label: x.keyIsID
? this.action.parent.parent.name ? this.action.parent.parent.name
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label) : game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
})); }));
@ -104,7 +101,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.roll = this.roll; context.roll = this.roll;
context.rollType = this.roll?.constructor.name; context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices; 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 => ({ context.experiences = Object.keys(experiences).map(id => ({
id, id,
...experiences[id] ...experiences[id]
@ -116,31 +113,15 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.isLite = this.config.roll?.lite; context.isLite = this.config.roll?.lite;
context.extraFormula = this.config.extraFormula; context.extraFormula = this.config.extraFormula;
context.formula = this.roll.constructFormula(this.config); context.formula = this.roll.constructFormula(this.config);
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; context.reactionOverride = this.reactionOverride;
} }
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
context.activeTagTeamRoll = true;
context.tagTeamSelected = this.config.tagTeamSelected;
}
return context; return context;
} }
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) { static updateRollConfiguration(event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object); const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode; this.config.selectedRollMode = rest.selectedRollMode;
if (this.config.costs) { if (this.config.costs) {
@ -152,12 +133,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.roll[key] = value; 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.config.extraFormula = rest.extraFormula;
this.render(); this.render();
} }
@ -176,6 +151,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.experiences.indexOf(button.dataset.key) > -1 this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key) ? this.config.experiences.filter(x => x !== button.dataset.key)
: [...this.config.experiences, button.dataset.key]; : [...this.config.experiences, button.dataset.key];
if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') {
this.config.costs = this.config.costs =
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
? this.config.costs.filter(x => x.extKey !== button.dataset.key) ? this.config.costs.filter(x => x.extKey !== button.dataset.key)
@ -183,31 +159,27 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
...this.config.costs, ...this.config.costs,
{ {
extKey: button.dataset.key, extKey: button.dataset.key,
key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', key: 'hope',
value: 1, value: 1,
name: this.config.data?.system.experiences?.[button.dataset.key]?.name name: this.config.data?.experiences?.[button.dataset.key]?.name
} }
]; ];
}
this.render(); this.render();
} }
static toggleReaction() { static toggleReaction() {
if (this.config.roll) { if (this.config.roll) {
this.reactionOverride = !this.reactionOverride; this.reactionOverride = !this.reactionOverride;
this.config.actionType = this.reactionOverride this.config.roll.type = this.reactionOverride
? 'reaction' ? CONFIG.DH.ITEM.actionTypes.reaction.id
: this.config.actionType === 'reaction' : this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id
? 'action' ? null
: this.config.actionType; : this.config.roll.type;
this.render(); this.render();
} }
} }
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static async submitRoll() { static async submitRoll() {
await this.close({ submitted: true }); await this.close({ submitted: true });
} }

View file

@ -10,7 +10,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject; this.reject = reject;
this.actor = actor; this.actor = actor;
this.damage = damage; this.damage = damage;
this.damageType = damageType;
this.rulesDefault = game.settings.get( this.rulesDefault = game.settings.get(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation CONFIG.DH.SETTINGS.gameSettings.Automation
@ -58,11 +57,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
null 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( this.thresholdImmunities = Object.keys(actor.system.rules.damageReduction.thresholdImmunities).reduce(
(acc, key) => { (acc, key) => {
if (actor.system.rules.damageReduction.thresholdImmunities[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.onWithToggle.id,
CONFIG.DH.GENERAL.ruleChoice.offWithToggle.id CONFIG.DH.GENERAL.ruleChoice.offWithToggle.id
].includes(this.rulesDefault); ].includes(this.rulesDefault);
context.reduceSeverity = this.reduceSeverity; context.thresholdImmunities = this.thresholdImmunities;
context.thresholdImmunities =
Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null;
const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } = const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } =
this.getDamageInfo(); this.getDamageInfo();
@ -181,9 +173,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length, this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length,
0 0
); );
if (this.reduceSeverity) {
currentDamage = Math.max(currentDamage - this.reduceSeverity, 0);
}
if (this.thresholdImmunities[currentDamage]) currentDamage = 0; if (this.thresholdImmunities[currentDamage]) currentDamage = 0;

View file

@ -1,5 +1,3 @@
import { refreshIsAllowed } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) { export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) {
@ -93,10 +91,14 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
} }
getRefreshables() { getRefreshables() {
const actionItems = this.actor.items.filter(x => this.actor.system.isItemAvailable(x)).reduce((acc, x) => { const actionItems = this.actor.items.reduce((acc, x) => {
if (x.system.actions) { if (x.system.actions) {
const recoverable = x.system.actions.reduce((acc, action) => { const recoverable = x.system.actions.reduce((acc, action) => {
if (refreshIsAllowed([this.shortrest ? 'shortRest' : 'longRest'], action.uses.recovery)) { if (
action.uses.recovery &&
((action.uses.recovery === 'longRest' && !this.shortrest) ||
action.uses.recovery === 'shortRest')
) {
acc.push({ acc.push({
title: x.name, title: x.name,
name: action.name, name: action.name,
@ -118,7 +120,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
if ( if (
x.system.resource && x.system.resource &&
x.system.resource.type && 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({ acc.push({
title: game.i18n.localize(`TYPES.Item.${x.type}`), title: game.i18n.localize(`TYPES.Item.${x.type}`),
@ -175,23 +178,11 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
} }
static async takeDowntime() { static async takeDowntime() {
const moves = Object.keys(this.moveData).flatMap(categoryKey => { const moves = Object.values(this.moveData).flatMap(category => {
const category = this.moveData[categoryKey]; return Object.values(category.moves)
return Object.keys(category.moves) .filter(x => x.selected)
.filter(x => category.moves[x].selected) .flatMap(move => [...Array(move.selected).keys()].map(_ => move));
.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 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 cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
@ -211,9 +202,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title` `DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`
), ),
actor: { name: this.actor.name, img: this.actor.img }, actor: { name: this.actor.name, img: this.actor.img },
moves: moves, moves: moves
characters: characters,
selfId: this.actor.uuid
} }
), ),
flags: { 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; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(name, ownership, defaultOwnership) { constructor(resolve, reject, name, ownership) {
super({}); super({});
this.resolve = resolve;
this.reject = reject;
this.name = name; this.name = name;
this.ownership = foundry.utils.deepClone(ownership); this.ownership = ownership;
this.defaultOwnership = defaultOwnership;
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'], classes: ['daggerheart', 'views', 'ownership-selection'],
window: {
icon: 'fa-solid fa-users'
},
position: { position: {
width: 600, width: 600,
height: 'auto' height: 'auto'
@ -32,48 +30,43 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name }); 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) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels; context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({
context.defaultOwnership = this.defaultOwnership; value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level],
context.ownership = game.users.reduce((acc, user) => { 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) { if (!user.isGM) {
acc[user.id] = { acc[x] = {
...user,
img: user.character?.img ?? 'icons/svg/cowled.svg', img: user.character?.img ?? 'icons/svg/cowled.svg',
ownership: this.getOwnershipData(user.id) name: user.name,
ownership: this.ownership.players[x].value
}; };
} }
return acc; return acc;
}, {}); }, {})
context.showOwnership = Boolean(Object.keys(context.ownership).length); };
return context; return context;
} }
static async updateData(event, _, formData) { static async updateData(event, _, formData) {
const data = foundry.utils.expandObject(formData.object); const { ownership } = foundry.utils.expandObject(formData.object);
this.close(data);
this.resolve(ownership);
this.close(true);
} }
async close(data) { async close(fromSave) {
if (data) { if (!fromSave) {
this.saveData = data; this.reject();
} }
await super.close(); 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; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -124,15 +122,6 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
}, {}) }, {})
}; };
await this.message.update(update); 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(); 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 { export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart'], classes: ['daggerheart']
actions: {
combat: DHTokenHUD.#onToggleCombat,
togglePartyTokens: DHTokenHUD.#togglePartyTokens
}
}; };
/** @override */ /** @override */
@ -17,28 +11,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
} }
}; };
static #nonCombatTypes = ['environment', 'companion', 'party'];
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._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) => { context.systemStatusEffects = Object.keys(context.statusEffects).reduce((acc, key) => {
const effect = context.statusEffects[key]; const effect = context.statusEffects[key];
if (effect.systemEffect) { if (effect.systemEffect) acc[key] = effect;
const disabled = !effect.isActive && this.actor.system.rules?.conditionImmunities?.[key];
acc[key] = { ...effect, disabled };
}
return acc; return acc;
}, {}); }, {});
@ -59,138 +36,6 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
return context; 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() { _getStatusEffectChoices() {
// Include all HUD-enabled status effects // Include all HUD-enabled status effects
const choices = {}; 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 // 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 effect of activeEffects) {
for (const statusId of effect.statuses) { for (const statusId of effect.statuses) {
const status = choices[statusId]; const status = choices[statusId];
if (!status) continue; 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) {
if (status._id !== effect.id) continue; if (status._id !== effect.id) continue;
} }
status.isActive = true; 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 CharacterLevelup } from './characterLevelup.mjs';
export { default as CompanionLevelup } from './companionLevelup.mjs'; export { default as CompanionLevelup } from './companionLevelup.mjs';
export { default as Levelup } from './levelup.mjs'; export { default as Levelup } from './levelup.mjs';
export { default as LevelupViewMode } from './levelupViewMode.mjs';

View file

@ -280,19 +280,11 @@ export default class DhCharacterLevelUp extends LevelUpBase {
break; break;
case 'experience': case 'experience':
if (!advancement[choiceKey]) advancement[choiceKey] = []; 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 data = checkbox.data.map(data => {
const experience = Object.keys(allExperiences).find(x => x === data); const experience = Object.keys(this.actor.system.experiences).find(
return allExperiences[experience]?.name ?? ''; x => x === data
);
return this.actor.system.experiences[experience]?.name ?? '';
}); });
advancement[choiceKey].push({ data: data, value: checkbox.value }); advancement[choiceKey].push({ data: data, value: checkbox.value });
break; break;

View file

@ -1,5 +1,6 @@
import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs'; import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs';
import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs'; import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs';
import { ItemBrowser } from '../ui/itemBrowser.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -11,6 +12,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
this.tabGroups.primary = 'advancements'; this.tabGroups.primary = 'advancements';
this.itemBrowser = null;
} }
get title() { get title() {
@ -357,23 +360,11 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
if (experienceIncreaseTagify) { if (experienceIncreaseTagify) {
const allExperiences = {
...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( tagifyElement(
experienceIncreaseTagify, experienceIncreaseTagify,
Object.keys(allExperiences).reduce((acc, id) => { Object.keys(this.actor.system.experiences).reduce((acc, id) => {
const experience = allExperiences[id]; const experience = this.actor.system.experiences[id];
if (experience.name) {
acc.push({ id: id, label: experience.name }); acc.push({ id: id, label: experience.name });
}
return acc; return acc;
}, []), }, []),
@ -549,6 +540,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const type = target.dataset.compendium ?? target.dataset.type; const type = target.dataset.compendium ?? target.dataset.type;
const presets = { const presets = {
compendium: 'daggerheart',
folder: type, folder: type,
render: { render: {
noFolder: true 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) { static async selectPreview(_, button) {
@ -658,9 +650,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
this.render(); this.render();
} }
static async save(_, button) { static async save() {
button.disabled = true;
const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => { const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => {
if (level >= this.levelup.startLevel) { if (level >= this.levelup.startLevel) {
acc[level] = this.levelup.levels[level].toObject(); acc[level] = this.levelup.levels[level].toObject();
@ -670,8 +660,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
}, {}); }, {});
await this.actor.levelUp(levelupData); await this.actor.levelUp(levelupData);
if (this.itemBrowser) this.itemBrowser.close();
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
this.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,63 +0,0 @@
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
// static DEFAULT_OPTIONS = {
// ...super.DEFAULT_OPTIONS,
// form: {
// handler: this.updateData,
// closeOnSubmit: true
// }
// };
static buildParts() {
const { footer, tabs, ...parts } = super.PARTS;
const tmpParts = {
// tabs,
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();
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
switch (partId) {
case 'dh':
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
const flagData = foundry.utils.mergeObject(this.document.flags.daggerheart, {
rangeMeasurement: { setting: event.target.value }
});
this.document.flags.daggerheart = flagData;
this.render();
});
break;
}
}
/** @inheritDoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'dh':
context.data = new game.system.api.data.scenes.DHScene(canvas.scene.flags.daggerheart);
context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules);
break;
}
return context;
}
// static async updateData(event, _, formData) {
// const data = foundry.utils.expandObject(formData.object);
// this.close(data);
// }
}

View file

@ -3,48 +3,43 @@ import { getDiceSoNicePreset } from '../../config/generalConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/**
* @import {ApplicationClickAction} from "@client/applications/_types.mjs"
*/
export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) { 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 = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
id: 'daggerheart-appearance-settings', id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'dialog', 'dh-style', 'setting'], classes: ['daggerheart', 'dialog', 'dh-style', 'setting'],
position: { width: '600', height: 'auto' }, position: { width: '600', height: 'auto' },
window: { window: {
title: 'DAGGERHEART.SETTINGS.Menu.title',
icon: 'fa-solid fa-gears' icon: 'fa-solid fa-gears'
}, },
actions: { actions: {
reset: DHAppearanceSettings.#onReset, reset: this.reset,
preview: DHAppearanceSettings.#onPreview save: this.save,
preview: this.preview
}, },
form: { form: { handler: this.updateData, submitOnChange: true }
closeOnSubmit: true,
handler: DHAppearanceSettings.#onSubmit
}
}; };
static PARTS = { static PARTS = {
header: { template: 'systems/daggerheart/templates/settings/appearance-settings/header.hbs' }, main: {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, template: 'systems/daggerheart/templates/settings/appearance-settings.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' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
general: {
tabs: [
{ id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' },
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }
],
initial: 'main'
},
diceSoNice: { diceSoNice: {
tabs: [ tabs: [
{ id: 'hope', label: 'DAGGERHEART.GENERAL.hope' }, { id: 'hope', label: 'DAGGERHEART.GENERAL.hope' },
@ -56,150 +51,79 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
} }
}; };
/**@type {DhAppearance}*/ changeTab(tab, group, options) {
setting; super.changeTab(tab, group, options);
static #localized = false; this.render();
}
/** @inheritDoc */ async _prepareContext(_options) {
async _preFirstRender(_context, _options) { const context = await super._prepareContext(_options);
await super._preFirstRender(_context, _options); context.settingFields = this.settings;
if (!DHAppearanceSettings.#localized) {
foundry.helpers.Localization.localizeDataModel(this.setting.constructor); context.showDiceSoNice = game.modules.get('dice-so-nice')?.active;
DHAppearanceSettings.#localized = true; 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 });
} }
} }
/** @inheritdoc */ context.diceTab = {
_configureRenderParts(options) { key: this.tabGroups.diceSoNice,
const parts = super._configureRenderParts(options); source: this.settings._source.diceSoNice[this.tabGroups.diceSoNice],
if (!game.modules.get('dice-so-nice')?.active) { fields: this.settings.schema.fields.diceSoNice.fields[this.tabGroups.diceSoNice].fields
delete parts.diceSoNice; };
delete parts.tabs;
}
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');
return context; return context;
} }
/**@inheritdoc */ static async updateData(event, element, formData) {
async _preparePartContext(partId, context, options) { const updatedSettings = foundry.utils.expandObject(formData.object);
const partContext = await super._preparePartContext(partId, context, options);
if (partId in context.tabs) partContext.tab = partContext.tabs[partId]; await this.settings.updateSource(updatedSettings);
switch (partId) { this.render();
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;
}
return partContext;
} }
/** static async preview() {
* Prepare render context for the DSN part. const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice];
* @param {ApplicationRenderContext} context let faces = 'd12';
* @returns {Promise<void>} switch (this.tabGroups.diceSoNice) {
* @protected case 'advantage':
*/ case 'disadvantage':
async prepareDiceSoNiceContext(context) { faces = 'd6';
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
} }
}), const preset = await getDiceSoNicePreset(source, faces);
{} const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate();
)
);
}
/**
* 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();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance; diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile; diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false); await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
} }
/** static async reset() {
* Reset the form back to default values. this.settings = new DhAppearance();
* @this {DHAppearanceSettings} this.render();
* @type {ApplicationClickAction} }
*/
static async #onReset() { static async save() {
this.setting = new this.setting.constructor(); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, this.settings.toObject());
this.render({ force: false });
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

@ -35,14 +35,13 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' }, header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' }, general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.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' } footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { main: {
tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }], tabs: [{ id: 'general' }, { id: 'rules' }],
initial: 'general', initial: 'general',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }

View file

@ -1,6 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs'; import { DhHomebrew } from '../../data/settings/_module.mjs';
import { slugify } from '../../helpers/utils.mjs'; import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) { 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() 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() { get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.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' icon: 'fa-solid fa-gears'
}, },
actions: { actions: {
editCurrencyIcon: this.changeCurrencyIcon,
addItem: this.addItem, addItem: this.addItem,
editItem: this.editItem, editItem: this.editItem,
removeItem: this.removeItem, removeItem: this.removeItem,
@ -40,11 +35,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addDomain: this.addDomain, addDomain: this.addDomain,
toggleSelectedDomain: this.toggleSelectedDomain, toggleSelectedDomain: this.toggleSelectedDomain,
deleteDomain: this.deleteDomain, deleteDomain: this.deleteDomain,
addAdversaryType: this.addAdversaryType,
deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType,
save: this.save, save: this.save,
resetTokenSizes: this.resetTokenSizes,
reset: this.reset reset: this.reset
}, },
form: { handler: this.updateData, submitOnChange: true } 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' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' }, settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.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' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
}; };
@ -63,19 +52,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { main: {
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }], tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }],
initial: 'settings', initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
changeTab(tab, group, options) {
super.changeTab(tab, group, options);
this.selected = this.#getDefaultAdversaryType();
this.render();
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.settingFields = this.settings; context.settingFields = this.settings;
@ -97,11 +79,6 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
context.configDomains = CONFIG.DH.DOMAIN.domains; context.configDomains = CONFIG.DH.DOMAIN.domains;
context.homebrewDomains = this.settings.domains; context.homebrewDomains = this.settings.domains;
break; break;
case 'types':
context.selectedAdversaryType = this.selected.adversaryType
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null;
break;
} }
return context; return context;
@ -117,100 +94,34 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); 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
}
});
if (icon !== null) {
await this.settings.updateSource({
[`currency.${type}.icon`]: icon
});
this.render();
}
}
static async addItem(_, target) { static async addItem(_, target) {
const { type } = target.dataset;
if (['shortRest', 'longRest'].includes(type)) {
await this.settings.updateSource({ await this.settings.updateSource({
[`restMoves.${type}.moves.${foundry.utils.randomID()}`]: { [`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
img: 'icons/magic/life/cross-worn-green.webp', img: 'icons/magic/life/cross-worn-green.webp',
description: '', description: '',
actions: [] 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(); this.render();
} }
static async editItem(_, target) { static async editItem(_, target) {
const { type, id } = target.dataset; const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id];
const isDowntime = ['shortRest', 'longRest'].includes(type); const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`;
const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`; const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure(
const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id]; move,
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,
path, path,
this.settings, this.settings
{ hasIcon: isDowntime, hasEffects: !isDowntime }
); );
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) { async updateAction(data, type, id) {
const isDowntime = ['shortRest', 'longRest'].includes(type);
const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`;
await this.settings.updateSource({ await this.settings.updateSource({
[`${path}.${id}`]: { [`restMoves.${type}.moves.${id}`]: {
actions: data.actions, actions: data.actions,
name: data.name, name: data.name,
icon: data.icon, icon: data.icon,
@ -218,16 +129,12 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
description: data.description description: data.description
} }
}); });
this.render(); this.render();
} }
static async removeItem(_, target) { 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({ await this.settings.updateSource({
[`${path}.-=${id}`]: null [`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null
}); });
this.render(); this.render();
} }
@ -394,45 +301,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); 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() { static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close(); this.close();
} }
static async resetTokenSizes() {
await this.settings.updateSource({
tokenSizes: this.settings.schema.fields.tokenSizes.initial
});
this.render();
}
static async reset() { static async reset() {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {

View file

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

View file

@ -1,236 +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;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.GENERAL.Tabs.settings')}: ${this.action.name}`;
}
static 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: {
toggleSection: this.toggleSection,
addEffect: this.addEffect,
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
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: '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'
}
};
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(true);
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;
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),
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,
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) });
}
/** 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);
}
}

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 = { 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: { actions: {
...DHActionBaseConfig.DEFAULT_OPTIONS.actions, toggleSection: this.toggleSection,
addEffect: this.addEffect, addEffect: this.addEffect,
removeEffect: this.removeEffect, 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) { static PARTS = {
const context = await super._prepareContext(options); header: {
if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); id: 'header',
context.getEffectDetails = this.getEffectDetails.bind(this); 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; return context;
} }
static async addEffect(_event) { static toggleSection(_, button) {
if (!this.action.effects) return; this.openSection = button.dataset.section === this.openSection ? null : button.dataset.section;
const effectData = this._addEffectData.bind(this)(); this.render(true);
const data = this.action.toObject(); }
const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { getCostOptions() {
render: false 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 }); data.effects.push({ _id: created._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created._id).sheet.render(true); 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) { static removeEffect(event, button) {
if (!this.action.effects) return; if (!this.action.effects) return;
const index = button.dataset.index, 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)) { if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key]; const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model); 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 group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model) .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) { async _preparePartContext(partId, context) {
const partContext = await super._preparePartContext(partId, context); const partContext = await super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
@ -115,7 +105,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
CONFIG.DH.SETTINGS.gameSettings.appearance CONFIG.DH.SETTINGS.gameSettings.appearance
).showGenericStatusEffects; ).showGenericStatusEffects;
if (!useGeneric) { 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, value: status.id,
label: game.i18n.localize(status.name) 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;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**

View file

@ -1,26 +1,21 @@
import { actionsTypes } from '../../data/action/_module.mjs'; 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; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) { export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(configTitle, move, movePath, settings, optionalParts, options) { constructor(move, movePath, settings, options) {
super(options); super(options);
this.configTitle = configTitle;
this.move = move; this.move = move;
this.movePath = movePath; this.movePath = movePath;
this.actionsPath = `${movePath}.actions`; this.actionsPath = `${movePath}.actions`;
this.settings = settings; this.settings = settings;
const { hasIcon, hasEffects } = optionalParts;
this.hasIcon = hasIcon;
this.hasEffects = hasEffects;
} }
get title() { get title() {
return this.configTitle; return game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves');
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -35,7 +30,6 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
addItem: this.addItem, addItem: this.addItem,
editItem: this.editItem, editItem: this.editItem,
removeItem: this.removeItem, removeItem: this.removeItem,
addEffect: this.addEffect,
resetMoves: this.resetMoves, resetMoves: this.resetMoves,
saveForm: this.saveForm 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' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' }, main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' },
actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.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' } footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' }
}; };
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
primary: { primary: {
tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }], tabs: [{ id: 'main' }, { id: 'actions' }],
initial: 'main', initial: 'main',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -62,9 +55,6 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._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 = this.move;
context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML( context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(
context.move.description context.move.description
@ -102,8 +92,6 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return ( return (
(await foundry.applications.api.DialogV2.input({ (await foundry.applications.api.DialogV2.input({
window: { title: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType') }, window: { title: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType') },
position: { width: 300 },
classes: ['daggerheart', 'dh-style'],
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/actionTypes/actionType.hbs', 'systems/daggerheart/templates/actionTypes/actionType.hbs',
{ types: CONFIG.DH.ACTIONS.actionTypes } { types: CONFIG.DH.ACTIONS.actionTypes }
@ -142,109 +130,31 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
} }
static async editItem(_, target) { static async editItem(_, target) {
const { type, id } = target.dataset; const actionId = target.dataset.id;
if (type === 'effect') { const action = this.move.actions.get(actionId);
const effectIndex = this.move.effects.findIndex(x => x.id === id); await new DHActionConfig(action, async updatedMove => {
const effect = this.move.effects[effectIndex]; await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove });
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;
}, [])
});
this.move = foundry.utils.getProperty(this.settings, this.movePath); this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); 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) { 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 }); 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()
]
});
this.move = foundry.utils.getProperty(this.settings, this.movePath); this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} }
static resetMoves() {} static resetMoves() {}
_filterTabs(tabs) {
return this.hasEffects
? tabs
: Object.keys(tabs).reduce((acc, key) => {
if (key !== 'effects') acc[key] = tabs[key];
return acc;
}, {});
}
/** @override */ /** @override */
_onClose(options = {}) { _onClose(options = {}) {
if (!options.submitted) this.move = null; 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 => { 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.addEventListener('close', () => resolve(app.move), { once: true });
app.render({ force: 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. * Adds a new category entry to the actor.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -122,9 +109,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
async _onDrop(event) { async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid); if (data.fromInternal) return;
if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return;
const item = await fromUuid(data.uuid);
if (item.type === 'adversary' && event.target.closest('.category-container')) { if (item.type === 'adversary' && event.target.closest('.category-container')) {
const target = event.target.closest('.category-container'); const target = event.target.closest('.category-container');
const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`; const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`;

View file

@ -1,30 +1,20 @@
import DHTokenConfigMixin from './token-config-mixin.mjs'; export default class DhPrototypeTokenConfig extends foundry.applications.sheets.PrototypeTokenConfig {
import { getActorSizeFromForm } from './token-config-mixin.mjs';
export default class DhPrototypeTokenConfig extends DHTokenConfigMixin(
foundry.applications.sheets.PrototypeTokenConfig
) {
/** @inheritDoc */ /** @inheritDoc */
static DEFAULT_OPTIONS = { async _prepareResourcesTab() {
...super.DEFAULT_OPTIONS, const token = this.token;
form: { handler: DhPrototypeTokenConfig.#onSubmit } 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
}; };
/**
* 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);
} }
} }

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'; export default class DhTokenConfig extends foundry.applications.sheets.TokenConfig {
import { getActorSizeFromForm } from './token-config-mixin.mjs'; /** @inheritDoc */
async _prepareResourcesTab() {
export default class DhTokenConfig extends DHTokenConfigMixin(foundry.applications.sheets.TokenConfig) { const token = this.token;
async _processSubmitData(event, form, submitData, options) { const usesTrackableAttributes = !foundry.utils.isEmpty(CONFIG.Actor.trackableAttributes);
const changedTokenSizeValue = getActorSizeFromForm(this.element, this.actor); const attributeSource =
if (changedTokenSizeValue) this.token.actor.update({ 'system.size': changedTokenSizeValue }); this.actor?.system instanceof foundry.abstract.DataModel && usesTrackableAttributes
? this.actor?.type
super._processSubmitData(event, form, submitData, options); : 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 Character } from './character.mjs';
export { default as Companion } from './companion.mjs'; export { default as Companion } from './companion.mjs';
export { default as Environment } from './environment.mjs'; export { default as Environment } from './environment.mjs';
export { default as Party } from './party.mjs';

View file

@ -1,60 +1,26 @@
import { getDocFromElement } from '../../../helpers/utils.mjs';
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
export default class AdversarySheet extends DHBaseActorSheet { export default class AdversarySheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['adversary'], classes: ['adversary'],
position: { width: 660, height: 766 }, position: { width: 660, height: 766 },
window: { resizable: true }, window: { resizable: true },
actions: { actions: {
toggleHitPoints: AdversarySheet.#toggleHitPoints, reactionRoll: AdversarySheet.#reactionRoll
toggleStress: AdversarySheet.#toggleStress,
reactionRoll: AdversarySheet.#reactionRoll,
toggleResourceDice: AdversarySheet.#toggleResourceDice,
handleResourceDice: AdversarySheet.#handleResourceDice
}, },
window: { window: {
resizable: true, resizable: true
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
} }
]
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
],
}; };
static PARTS = { static PARTS = {
limited: { sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' },
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']
},
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
features: { features: { template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs' },
template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs', notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' },
scrollable: ['.feature-section'] effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' }
},
notes: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs'
},
effects: {
template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs',
scrollable: ['.effects-sections']
}
}; };
/** @inheritdoc */ /** @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 */ /**@inheritdoc */
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.systemFields.attack.fields = this.document.system.attack.schema.fields; 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; return context;
} }
@ -108,11 +44,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header': case 'header':
case 'limited':
await this._prepareHeaderContext(context, options); await this._prepareHeaderContext(context, options);
const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes();
context.adversaryType = game.i18n.localize(adversaryTypes[this.document.system.type].label);
break; break;
case 'notes': case 'notes':
await this._prepareNotesContext(context, options); await this._prepareNotesContext(context, options);
@ -121,16 +53,6 @@ export default class AdversarySheet extends DHBaseActorSheet {
return context; 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. * Prepare render context for the Biography part.
* @param {ApplicationRenderContext} context * @param {ApplicationRenderContext} context
@ -176,41 +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 */ /* 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. * Performs a reaction roll for an Adversary.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -221,61 +112,13 @@ export default class AdversarySheet extends DHBaseActorSheet {
title: `Reaction Roll: ${this.actor.name}`, title: `Reaction Roll: ${this.actor.name}`,
headerTitle: 'Adversary Reaction Roll', headerTitle: 'Adversary Reaction Roll',
roll: { roll: {
type: 'trait' type: 'reaction'
}, },
actionType: 'reaction', type: 'trait',
hasRoll: true, hasRoll: true,
data: this.actor.getRollData() data: this.actor.getRollData()
}; };
this.actor.diceRoll(config); 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 DHBaseActorSheet from '../api/base-actor.mjs';
import DhpDeathMove from '../../dialogs/deathMove.mjs'; import DhpDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.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 DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
const { TextEditor } = foundry.applications.ux;
export default class CharacterSheet extends DHBaseActorSheet { export default class CharacterSheet extends DHBaseActorSheet {
/**@inheritdoc */ /**@inheritdoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['character'], classes: ['character'],
position: { width: 850, height: 800 }, position: { width: 850, height: 800 },
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
actions: { actions: {
toggleVault: CharacterSheet.#toggleVault, toggleVault: CharacterSheet.#toggleVault,
rollAttribute: CharacterSheet.#rollAttribute, rollAttribute: CharacterSheet.#rollAttribute,
toggleHitPoints: CharacterSheet.#toggleHitPoints,
toggleStress: CharacterSheet.#toggleStress,
toggleArmor: CharacterSheet.#toggleArmor,
toggleHope: CharacterSheet.#toggleHope, toggleHope: CharacterSheet.#toggleHope,
toggleLoadoutView: CharacterSheet.#toggleLoadoutView, toggleLoadoutView: CharacterSheet.#toggleLoadoutView,
openPack: CharacterSheet.#openPack, openPack: CharacterSheet.#openPack,
makeDeathMove: CharacterSheet.#makeDeathMove, makeDeathMove: CharacterSheet.#makeDeathMove,
levelManagement: CharacterSheet.#levelManagement, levelManagement: CharacterSheet.#levelManagement,
viewLevelups: CharacterSheet.#viewLevelups,
toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleEquipItem: CharacterSheet.#toggleEquipItem,
toggleResourceDice: CharacterSheet.#toggleResourceDice, toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform,
useDowntime: this.useDowntime, useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty, tempBrowser: CharacterSheet.#tempBrowser
}, },
window: { window: {
resizable: true, resizable: true
controls: [
{
icon: 'fa-solid fa-angles-up',
label: 'DAGGERHEART.ACTORS.Character.viewLevelups',
action: 'viewLevelups'
}
]
}, },
dragDrop: [ dragDrop: [
{ {
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', dragSelector: '[data-item-id][draggable="true"]',
dropSelector: null dropSelector: null
} }
], ],
@ -81,14 +68,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
/**@override */ /**@override */
static PARTS = { static PARTS = {
limited: {
id: 'limited',
scrollable: ['.limited-container'],
template: 'systems/daggerheart/templates/sheets/actors/character/limited.hbs'
},
sidebar: { sidebar: {
id: 'sidebar', id: 'sidebar',
scrollable: ['.shortcut-items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/sidebar.hbs'
}, },
header: { header: {
@ -97,27 +78,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
}, },
features: { features: {
id: 'features', id: 'features',
scrollable: ['.features-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/features.hbs'
}, },
loadout: { loadout: {
id: 'loadout', id: 'loadout',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/loadout.hbs'
}, },
inventory: { inventory: {
id: 'inventory', id: 'inventory',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/inventory.hbs'
}, },
biography: { biography: {
id: 'biography', id: 'biography',
scrollable: ['.items-section'],
template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/biography.hbs'
}, },
effects: { effects: {
id: 'effects', id: 'effects',
scrollable: ['.effects-sections'],
template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs' template: 'systems/daggerheart/templates/sheets/actors/character/effects.hbs'
} }
}; };
@ -138,51 +114,28 @@ export default class CharacterSheet extends DHBaseActorSheet {
htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => { htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => {
element.addEventListener('change', this.updateItemResource.bind(this)); 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 // Add listener for armor marks input
htmlElement.querySelectorAll('.armor-marks-input').forEach(element => { htmlElement.querySelectorAll('.armor-marks-input').forEach(element => {
element.addEventListener('change', this.updateArmorMarks.bind(this)); 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 */ /** @inheritDoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
if (!this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
this.element this.element
.querySelector('.level-value') .querySelector('.level-value')
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.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._createFilterMenus();
this._createSearchFilter(); this._createSearchFilter();
} }
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Prepare Context */ /* Prepare Context */
@ -201,17 +154,24 @@ export default class CharacterSheet extends DHBaseActorSheet {
return acc; return acc;
}, {}); }, {});
context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => { context.inventory = {
acc[key] = this.document.system.resources[key]; currency: {
return acc; title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
}, {}); coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'),
const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max); handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'),
context.resources.hitPoints.emptyPips = bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'),
context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0; chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests')
context.resources.stress.emptyPips = }
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; };
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; return context;
} }
@ -249,7 +209,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @protected * @protected
*/ */
async _prepareLoadoutContext(context, _options) { 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,40 +279,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached')); 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', name: 'toVault',
icon: 'fa-solid fa-arrow-down', icon: 'fa-solid fa-arrow-down',
@ -624,6 +550,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
this.render(); 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) { async updateArmorMarks(event) {
const armor = this.document.system.armor; const armor = this.document.system.armor;
if (!armor) return; if (!armor) return;
@ -651,14 +585,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
if (!value || !subclass) if (!value || !subclass)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClassOrSubclass')); return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClassOrSubclass'));
new CharacterLevelup(this.document).render({ force: true }); new DhCharacterlevelUp(this.document).render({ force: true });
}
/**
* Opens the charater level management window in viewMode.
*/
static #viewLevelups() {
new LevelupViewMode(this.document).render({ force: true });
} }
/** /**
@ -677,22 +604,14 @@ export default class CharacterSheet extends DHBaseActorSheet {
const { key } = button.dataset; const { key } = button.dataset;
const presets = { const presets = {
compendium: 'daggerheart',
folder: key, folder: key,
filter:
key === 'subclasses'
? {
'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource
}
}
: undefined,
render: { render: {
noFolder: true noFolder: true
} }
}; };
ui.compendiumBrowser.open(presets); return new ItemBrowser({ presets }).render({ force: true });
} }
/** /**
@ -710,21 +629,41 @@ export default class CharacterSheet extends DHBaseActorSheet {
roll: { roll: {
trait: button.dataset.attribute trait: button.dataset.attribute
}, },
hasRoll: true, hasRoll: true
actionType: 'action', };
const result = await this.document.diceRoll({
...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel ability: abilityLabel
}) })
}; });
const result = await this.document.diceRoll(config);
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ this.consumeResource(result?.costs);
const costResources = result.costs }
.filter(x => x.enabled)
.map(cost => ({ ...cost, value: -cost.value, total: -cost.total })); // Remove when Action Refactor part #2 done
config.resourceUpdates.addResources(costResources); async consumeResource(costs) {
await config.resourceUpdates.updateResources(); if (!costs?.length) return;
const usefulResources = {
...foundry.utils.deepClone(this.actor.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,
keyIsID: resource.keyIsID
};
});
await this.actor.modifyResource(resources);
} }
//TODO: redo toggleEquipItem method //TODO: redo toggleEquipItem method
@ -769,42 +708,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleLoadoutView(_, button) { static async #toggleLoadoutView(_, button) {
const newAbilityView = button.dataset.value === 'true'; const newAbilityView = button.dataset.value !== 'true';
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard, newAbilityView); await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsList, newAbilityView);
this.render(); 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. * Toggles a hope resource value.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -844,6 +752,13 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/**
* Temp
*/
static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true });
}
/** /**
* Handle the roll values of resource dice. * Handle the roll values of resource dice.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -863,71 +778,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. * Open the downtime application.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
@ -938,48 +788,40 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/** @inheritdoc */
async _onDragStart(event) { async _onDragStart(event) {
const inventoryItem = event.currentTarget.closest('.inventory-item'); const item = await getDocFromElement(event.target);
if (inventoryItem) {
event.dataTransfer.setDragImage(inventoryItem.querySelector('img'), 60, 0); const dragData = {
} type: item.documentName,
uuid: item.uuid
};
event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
super._onDragStart(event); super._onDragStart(event);
} }
async _onDropItem(event, item) { async _onDrop(event) {
if (this.document.uuid === item.parent?.uuid) { // Prevent event bubbling to avoid duplicate handling
return super._onDropItem(event, item); event.preventDefault();
} event.stopPropagation();
if (item.type === 'beastform') { super._onDrop(event);
if (this.document.effects.find(x => x.type === 'beastform')) { this._onDropItem(event, TextEditor.getDragEventData(event));
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied')
);
} }
async _onDropItem(event, data) {
const item = await Item.implementation.fromDropData(data);
const itemData = item.toObject(); const itemData = item.toObject();
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData);
if (!data?.selectedImage) { if (item.type === 'domainCard' && !this.document.system.loadoutSlot.available) {
return; itemData.system.inVault = true;
} 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) if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData);
const typesThatReplace = ['ancestry', 'community']; const createdItem = await this._onDropItemCreate(itemData);
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) { async _onDropItemCreate(itemData, event) {

View file

@ -8,23 +8,14 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
classes: ['actor', 'companion'], classes: ['actor', 'companion'],
position: { width: 340 }, position: { width: 340 },
actions: { actions: {
toggleStress: DhCompanionSheet.#toggleStress,
actionRoll: DhCompanionSheet.#actionRoll,
levelManagement: DhCompanionSheet.#levelManagement levelManagement: DhCompanionSheet.#levelManagement
} }
}; };
static PARTS = { 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' }, header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' }, details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' },
effects: { effects: { template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs' }
template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs',
scrollable: ['.effects-sections']
}
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -51,61 +42,6 @@ export default class DhCompanionSheet extends DHBaseActorSheet {
/* Application Clicks Actions */ /* 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. * Opens the companions level management window.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -1,4 +1,3 @@
import { getDocFromElement } from '../../../helpers/utils.mjs';
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -9,44 +8,21 @@ export default class DhpEnvironment extends DHBaseActorSheet {
classes: ['environment'], classes: ['environment'],
position: { position: {
width: 500, width: 500,
height: 740 height: 725
}, },
window: { window: {
resizable: true, resizable: true
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
}, },
actions: { actions: {},
toggleResourceDice: DhpEnvironment.#toggleResourceDice, dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }]
handleResourceDice: DhpEnvironment.#handleResourceDice
},
dragDrop: [
{
dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
dropSelector: null
}
]
}; };
/**@override */ /**@override */
static PARTS = { 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' }, header: { template: 'systems/daggerheart/templates/sheets/actors/environment/header.hbs' },
features: { features: { template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs' },
template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs',
scrollable: ['.feature-section']
},
potentialAdversaries: { potentialAdversaries: {
template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs', template: 'systems/daggerheart/templates/sheets/actors/environment/potentialAdversaries.hbs'
scrollable: ['.items-section']
}, },
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.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 */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header': case 'header':
await this._prepareHeaderContext(context, options); await this._prepareHeaderContext(context, options);
break;
case 'features':
await this._prepareFeaturesContext(context, options);
break; break;
case 'notes': case 'notes':
await this._prepareNotesContext(context, options); 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. * Prepare render context for the Header part.
* @param {ApplicationRenderContext} context * @param {ApplicationRenderContext} context
@ -154,51 +98,12 @@ export default class DhpEnvironment extends DHBaseActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
async _onDragStart(event) { async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item[data-type=adversary]'); const item = event.currentTarget.closest('.inventory-item');
if (item) { if (item) {
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid }; const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0); 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); const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC; context.isNPC = this.actor.isNPC;
if (context.systemFields.attack) if (context.systemFields.attack) {
context.systemFields.attack.fields = this.actor.system.attack.schema.fields; context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
}
return context; return context;
} }

View file

@ -1,5 +1,6 @@
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs';
const typeSettingsMap = { const typeSettingsMap = {
character: 'extendCharacterDescriptions', character: 'extendCharacterDescriptions',
@ -84,8 +85,6 @@ export default function DHApplicationMixin(Base) {
this._dragDrop = this._createDragDropHandlers(); this._dragDrop = this._createDragDropHandlers();
} }
#nonHeaderAttribution = ['environment', 'ancestry', 'community', 'domainCard'];
/** /**
* The default options for the sheet. * The default options for the sheet.
* @type {DHSheetV2Configuration} * @type {DHSheetV2Configuration}
@ -99,12 +98,10 @@ export default function DHApplicationMixin(Base) {
deleteDoc: DHSheetV2.#deleteDoc, deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat, toChat: DHSheetV2.#toChat,
useItem: DHSheetV2.#useItem, useItem: DHSheetV2.#useItem,
viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect, toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended, toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem, addNewItem: DHSheetV2.#addNewItem,
browseItem: DHSheetV2.#browseItem, browseItem: DHSheetV2.#browseItem
editAttribution: DHSheetV2.#editAttribution
}, },
contextMenus: [ contextMenus: [
{ {
@ -124,47 +121,10 @@ export default function DHApplicationMixin(Base) {
} }
} }
], ],
dragDrop: [{ dragSelector: '.inventory-item[data-type="effect"]', dropSelector: null }], dragDrop: [],
tagifyConfigs: [] 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. * Related documents that should cause a rerender of this application when updated.
*/ */
@ -178,79 +138,6 @@ export default function DHApplicationMixin(Base) {
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement)); 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 */ /**@inheritdoc */
@ -277,19 +164,11 @@ export default function DHApplicationMixin(Base) {
this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]); 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 */ /**@inheritdoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this._createTagifyElements(this.options.tagifyConfigs); this._createTagifyElements(this.options.tagifyConfigs);
await this.#prepareInventoryDescription(context);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -297,8 +176,8 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@inheritdoc */ /**@inheritdoc */
_preSyncPartState(partId, newElement, priorElement, state) { _syncPartState(partId, newElement, priorElement, state) {
super._preSyncPartState(partId, newElement, priorElement, state); super._syncPartState(partId, newElement, priorElement, state);
for (const el of priorElement.querySelectorAll('.extensible.extended')) { for (const el of priorElement.querySelectorAll('.extensible.extended')) {
const { actionId, itemUuid } = el.parentElement.dataset; const { actionId, itemUuid } = el.parentElement.dataset;
const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`; const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`;
@ -370,38 +249,14 @@ export default function DHApplicationMixin(Base) {
* @param {DragEvent} event * @param {DragEvent} event
* @protected * @protected
*/ */
async _onDragStart(event) { _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);
}
}
}
/** /**
* Handle drop event. * Handle drop event.
* @param {DragEvent} event * @param {DragEvent} event
* @protected * @protected
*/ */
_onDrop(event) { _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);
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Context Menu */ /* Context Menu */
@ -504,9 +359,7 @@ export default function DHApplicationMixin(Base) {
callback: async (target, event) => { callback: async (target, event) => {
const doc = await getDocFromElement(target), const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc; action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event); return action && action.use(event, { byPassRoll: true });
config.hasRoll = false;
return action && action.workflow.get('damage').execute(config, null, true);
} }
}); });
@ -525,7 +378,7 @@ export default function DHApplicationMixin(Base) {
options.push({ options.push({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message', 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) if (deletable)
@ -566,12 +419,11 @@ export default function DHApplicationMixin(Base) {
/** /**
* Prepares and enriches an inventory item or action description for display. * Prepares and enriches an inventory item or action description for display.
* @param {HTMLElement} element the element to enrich the inventory items of
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async #prepareInventoryDescription(element) { async #prepareInventoryDescription(context) {
// Get all inventory item elements with a data-item-uuid attribute // Get all inventory item elements with a data-item-uuid attribute
const inventoryItems = element.querySelectorAll('.inventory-item[data-item-uuid]'); const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]');
for (const el of inventoryItems) { for (const el of inventoryItems) {
// Get the doc uuid from the element // Get the doc uuid from the element
const { itemUuid } = el?.dataset || {}; const { itemUuid } = el?.dataset || {};
@ -662,27 +514,28 @@ export default function DHApplicationMixin(Base) {
static async #browseItem(event, target) { static async #browseItem(event, target) {
const type = target.dataset.compendium ?? target.dataset.type; const type = target.dataset.compendium ?? target.dataset.type;
const presets = { const presets = {};
render: {
noFolder: true
}
};
switch (type) { switch (type) {
case 'loot': case 'loot':
presets.folder = 'equipments.folders.loots';
break;
case 'consumable': case 'consumable':
presets.folder = 'equipments.folders.consumables';
break;
case 'armor': case 'armor':
presets.folder = 'equipments.folders.armors';
break;
case 'weapon': 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; break;
case 'domainCard': case 'domainCard':
presets.compendium = 'daggerheart';
presets.folder = 'domains'; presets.folder = 'domains';
presets.render = {
noFolder: true
};
presets.filter = { presets.filter = {
'level.max': { key: 'level.max', value: this.document.system.levelData.level.current }, 'level.max': { key: 'level.max', value: this.document.system.levelData.level.current },
'system.domain': { key: 'system.domain', value: this.document.system.domains } 'system.domain': { key: 'system.domain', value: this.document.system.domains }
@ -692,15 +545,7 @@ export default function DHApplicationMixin(Base) {
return; return;
} }
ui.compendiumBrowser.open(presets); return new ItemBrowser({ presets }).render({ force: true });
}
/**
* Open the attribution dialog
* @type {ApplicationClickAction}
*/
static async #editAttribution() {
new game.system.api.applications.dialogs.AttributionDialog(this.document).render({ force: true });
} }
/** /**
@ -723,6 +568,7 @@ export default function DHApplicationMixin(Base) {
if (featureOnCharacter) { if (featureOnCharacter) {
systemData = { systemData = {
originItemType: this.document.type, originItemType: this.document.type,
originId: this.document.id,
identifier: this.document.system.isMulticlass ? 'multiclass' : null identifier: this.document.system.isMulticlass ? 'multiclass' : null
}; };
} }
@ -736,9 +582,6 @@ export default function DHApplicationMixin(Base) {
}; };
if (inVault) data['system.inVault'] = true; if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = 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 }); const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey });
if (parentIsItem && type === 'feature') { if (parentIsItem && type === 'feature') {
@ -775,7 +618,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toChat(_event, target) { static async #toChat(_event, target) {
const doc = await getDocFromElement(target); let doc = await getDocFromElement(target);
return doc.toChat(doc.uuid); return doc.toChat(doc.uuid);
} }
@ -784,19 +627,10 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #useItem(event, target) { static async #useItem(event, target) {
const doc = await getDocFromElement(target); let doc = await getDocFromElement(target);
await doc.use(event); 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 * Toggle a ActiveEffect
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -1,4 +1,3 @@
import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs';
import DHBaseActorSettings from './actor-setting.mjs'; import DHBaseActorSettings from './actor-setting.mjs';
import DHApplicationMixin from './application-mixin.mjs'; import DHApplicationMixin from './application-mixin.mjs';
@ -34,10 +33,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
} }
} }
], ],
dragDrop: [ dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
{ dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null }
]
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -51,12 +47,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return (this.#settingSheet ??= SheetClass ? new SheetClass({ document: this.document }) : null); 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 */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -65,36 +55,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.isNPC = this.document.isNPC; 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; return context;
} }
@ -109,39 +69,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
return context; 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 */ /**@inheritdoc */
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._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 => { htmlElement.querySelectorAll('.item-button .action-uses-button').forEach(element => {
element.addEventListener('contextmenu', DHBaseActorSheet.#modifyActionUses); 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 }); 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 */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -257,98 +179,13 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Application Drag/Drop */ /* 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. * On dragStart on the item.
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDragStart(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"]'); const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) { if (attackItem) {
const attackData = { const attackData = {
type: 'Attack', type: 'Attack',
@ -358,20 +195,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}; };
event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); 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 = { static DEFAULT_OPTIONS = {
classes: ['item'], classes: ['item'],
position: { width: 600 }, position: { width: 600 },
window: { window: { resizable: true },
resizable: true,
controls: [
{
icon: 'fa-solid fa-signature',
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
action: 'editAttribution'
}
]
},
form: { form: {
submitOnChange: true submitOnChange: true
}, },
@ -33,9 +24,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
removeResource: DHBaseItemSheet.#removeResource removeResource: DHBaseItemSheet.#removeResource
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.drop-section' }, { dragSelector: null, dropSelector: '.tab.features .drop-section' },
{ dragSelector: '.feature-item', dropSelector: null }, { dragSelector: '.feature-item', dropSelector: null },
{ dragSelector: '.inventory-item', dropSelector: null } { dragSelector: '.action-item', dropSelector: null }
], ],
contextMenus: [ contextMenus: [
{ {
@ -64,15 +55,6 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/* Prepare Context */ /* 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 */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options); await super._preparePartContext(partId, context, options);
@ -167,12 +149,12 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const { type } = target.dataset; const { type } = target.dataset;
const cls = foundry.documents.Item.implementation; const cls = foundry.documents.Item.implementation;
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
let systemData = {}; let systemData = {};
if (this.document.parent?.type === 'character') { if (this.document.parent?.type === 'character') {
systemData = { systemData = {
originItemType: this.document.type, originItemType: this.document.type,
identifier: multiclass ?? type originId: this.document.id,
identifier: this.document.system.isMulticlass ? 'multiclass' : null
}; };
} }
@ -199,33 +181,19 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
static async #deleteFeature(_, element) { static async #deleteFeature(_, element) {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
const feature = await getDocFromElement(target); const feature = await getDocFromElement(target);
if (!feature) { if (!feature) {
await this.document.update({ await this.document.update({
'system.features': this.document.system.features 'system.features': this.document.system.features
.filter(x => x.item) .filter(x => x.item)
.map(x => ({ ...x, item: x.item.uuid })) .map(x => ({ ...x, item: x.item.uuid }))
}); });
} else { } 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({ await this.document.update({
'system.features': this.document.system.features 'system.features': this.document.system.features
.filter(x => target.dataset.type !== x.type || x.item.uuid !== feature.uuid) .filter(x => target.dataset.type !== x.type || x.item.uuid !== feature.uuid)
.map(x => ({ ...x, item: x.item.uuid })) .map(x => ({ ...x, item: x.item.uuid }))
}); });
} }
}
/** /**
* Add a resource to the item. * Add a resource to the item.
@ -256,30 +224,35 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDragStart(event) { async _onDragStart(event) {
/* Can prolly be improved a lot, but I don't wanna >_< */
const featureItem = event.currentTarget.closest('.feature-item'); 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 (featureItem) {
if (dragItem) { const feature = this.document.system.features.find(x => x?.id === featureItem.id);
if (!dragItem) { if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return; return;
} }
let dragData = {}; const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true };
if (dragItemData.dataset.type === 'effect') event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
dragData = { event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
type: 'ActiveEffect', } else {
fromInternal: this.document.uuid, const actionItem = event.currentTarget.closest('.action-item');
data: { ...dragItem, uuid: dragItem.uuid, id: dragItem.id } if (actionItem) {
}; const action = this.document.system.actions[actionItem.dataset.index];
else dragData = { type: 'Item', uuid: dragItem.uuid, id: dragItem.id, fromInternal: this.document.id }; if (!action) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing'));
return;
}
event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); const actionData = {
event.dataTransfer.setDragImage(dragItemData.querySelector('img'), 60, 0); 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);
}
} }
} }
@ -288,10 +261,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDrop(event) { async _onDrop(event) {
super._onDrop(event);
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(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'); const target = event.target.closest('fieldset.drop-section');
let item = await fromUuid(data.uuid); let item = await fromUuid(data.uuid);
@ -300,15 +271,14 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
if (this.document.parent?.type === 'character') { if (this.document.parent?.type === 'character') {
const itemData = item.toObject(); const itemData = item.toObject();
const multiclass = this.document.system.isMulticlass ? 'multiclass' : null;
item = await cls.create( item = await cls.create(
{ {
...itemData, ...itemData,
_stats: { compendiumSource: this.document.uuid },
system: { system: {
...itemData.system, ...itemData.system,
originItemType: this.document.type, originItemType: this.document.type,
identifier: multiclass ?? target.dataset.type originId: this.document.id,
identifier: this.document.system.isMulticlass ? 'multiclass' : null
} }
}, },
{ parent: this.document.parent } { parent: this.document.parent }

View file

@ -41,7 +41,7 @@ export default function ItemAttachmentSheet(Base) {
} }
async _onDrop(event) { async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = TextEditor.getDragEventData(event);
const attachmentsSection = event.target.closest('.attachments-section'); const attachmentsSection = event.target.closest('.attachments-section');
if (!attachmentsSection) return super._onDrop(event); if (!attachmentsSection) return super._onDrop(event);

View file

@ -27,9 +27,6 @@ export default class AncestrySheet extends DHHeritageSheet {
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDrop(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 target = event.target.closest('fieldset.drop-section');
const typeField = const typeField =
this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature']; this.document.system[target.dataset.type === 'primary' ? 'primaryFeature' : 'secondaryFeature'];

View file

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

View file

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

View file

@ -31,11 +31,4 @@ export default class FeatureSheet extends DHBaseItemSheet {
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
}; };
//Might be wrong location but testing out if here is okay.
/**@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: [ tagifyConfigs: [
{ {
selector: '.features-input', selector: '.features-input',
options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(), options: () => CONFIG.DH.ITEM.weaponFeatures,
callback: WeaponSheet.#onFeatureSelect 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,8 +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 DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.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 DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs'; export { default as DhHotbar } from './hotbar.mjs';
export { ItemBrowser } from './itemBrowser.mjs';

View file

@ -1,5 +1,4 @@
import { abilities } from '../../config/actorConfig.mjs'; import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) { constructor(options) {
@ -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>', icon: '<i class="fa-solid fa-dice"></i>',
condition: li => { condition: li => {
const message = game.messages.get(li.dataset.messageId); 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) => { addChatListeners = async (app, html, data) => {
const message = data?.message ?? document.toObject(false); 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 => 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 => 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 => 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 => html.querySelectorAll('.reroll-button').forEach(element =>
element.addEventListener('click', event => this.rerollEvent(event, message)) element.addEventListener('click', event => this.rerollEvent(event, data.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)
); );
}; };
@ -92,6 +90,80 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options); 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) { async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage', const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), 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?.id === event.currentTarget.id
? item.system.attack ? item.system.attack
: item.system.actions.get(event.currentTarget.id); : item.system.actions.get(event.currentTarget.id);
if (event.currentTarget.dataset.directDamage) { if (event.currentTarget.dataset.directDamage) action.use(event, { byPassRoll: true });
const config = action.prepareConfig(event); else action.use(event);
config.hasRoll = false;
action.workflow.get('damage').execute(config, null, true);
} else action.use(event);
} }
async actionUseButton(event, message) { async actionUseButton(event, message) {
const { moveIndex, actionIndex, movePath } = event.currentTarget.dataset; const { moveIndex, actionIndex } = event.currentTarget.dataset;
const targetUuid = event.currentTarget.closest('.action-use-button-parent').querySelector('select')?.value; const parent = await foundry.utils.fromUuid(message.system.actor);
const parent = await foundry.utils.fromUuid(targetUuid || message.system.actor)
const actionType = message.system.moves[moveIndex].actions[actionIndex]; const actionType = message.system.moves[moveIndex].actions[actionIndex];
const cls = game.system.api.models.actions.actionsTypes[actionType.type]; const cls = game.system.api.models.actions.actionsTypes[actionType.type];
const action = new cls( const action = new cls(
{ { ...actionType, _id: foundry.utils.randomID(), name: game.i18n.localize(actionType.name) },
...actionType,
_id: foundry.utils.randomID(),
name: game.i18n.localize(actionType.name),
originItem: {
type: CONFIG.DH.ITEM.originItemType.restMove,
itemPath: movePath,
actionIndex: actionIndex
},
targetUuid: targetUuid
},
{ parent: parent.system } { parent: parent.system }
); );
@ -192,182 +249,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
'system.roll': newRoll, 'system.roll': newRoll,
'rolls': [parsedRoll] '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 { export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
actions: { actions: {
requestSpotlight: this.requestSpotlight, requestSpotlight: this.requestSpotlight,
toggleSpotlight: this.toggleSpotlight, 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) { async _prepareCombatContext(context, options) {
await super._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, { Object.assign(context, {
fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
battlepoints: { max: maxBP, current: currentBP, hasModifierBP: modifierBP !== null }
}); });
} }
@ -56,27 +35,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const adversaries = context.turns?.filter(x => x.isNPC) ?? []; const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const characters = 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, { Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries, adversaries,
characters: characters characters
?.filter(x => !x.isNPC)
.filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
}); });
} }
@ -127,7 +90,6 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
resource, resource,
active: index === combat.turn, active: index === combat.turn,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant) img: await this._getCombatantThumbnail(combatant)
}; };
@ -152,8 +114,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
async setCombatantSpotlight(combatantId) { async setCombatantSpotlight(combatantId) {
const update = { const update = {
system: { system: {
'spotlight.requesting': false, 'spotlight.requesting': false
'spotlight.requestOrderIndex': 0
} }
}; };
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
@ -165,14 +126,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (this.viewed.turn !== toggleTurn) { if (this.viewed.turn !== toggleTurn) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
if (combatant.actor?.type === 'character') { await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.spotlight.id);
await updateCountdowns(
CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id,
CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id
);
} else {
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id);
}
const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints; const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints;
if (autoPoints) { if (autoPoints) {
@ -188,15 +142,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
static async requestSpotlight(_, target) { 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 { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({ await combatant.update({
'system.spotlight': { 'system.spotlight': {
requesting: !combatant.system.spotlight.requesting, requesting: !combatant.system.spotlight.requesting
requestOrderIndex: !combatant.system.spotlight.requesting ? maxRequestIndex + 1 : 0
} }
}); });
@ -218,4 +168,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex }); await combatant.update({ 'system.actionTokens': newIndex });
this.render(); 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 { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { emitAsGM, 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; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/** class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
* A UI element which displays the countdowns in this world. constructor(basePath) {
* super({});
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) { this.basePath = basePath;
constructor(options = {}) { }
super(options);
get title() {
this.setupHooks(); return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`)
});
} }
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
id: 'countdowns', classes: ['daggerheart', 'dh-style', 'countdown'],
tag: 'div', tag: 'form',
classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'], position: { width: 740, height: 700 },
window: { window: {
icon: 'fa-solid fa-clock-rotate-left',
frame: true, frame: true,
title: 'DAGGERHEART.UI.Countdowns.title', title: 'Countdowns',
positioned: false, resizable: true,
resizable: false,
minimizable: false minimizable: false
}, },
actions: { actions: {
toggleViewMode: DhCountdowns.#toggleViewMode, addCountdown: this.addCountdown,
editCountdowns: DhCountdowns.#editCountdowns, removeCountdown: this.removeCountdown,
loopCountdown: DhCountdowns.#loopCountdown, editImage: this.onEditImage,
decreaseCountdown: (_, target) => this.editCountdown(false, target), openOwnership: this.openOwnership,
increaseCountdown: (_, target) => this.editCountdown(true, target) openCountdownOwnership: this.openCountdownOwnership,
toggleSimpleView: this.toggleSimpleView
}, },
position: { form: { handler: this.updateData, submitOnChange: true }
width: 400,
height: 222,
top: 50
}
}; };
/** @override */
static PARTS = { static PARTS = {
resources: { countdowns: {
root: true, template: 'systems/daggerheart/templates/ui/countdowns.hbs',
template: 'systems/daggerheart/templates/ui/countdowns.hbs' scrollable: ['.expanded-view']
} }
}; };
get element() { _attachPartListeners(partId, htmlElement, options) {
return document.body.querySelector('.daggerheart.dh-style.countdowns'); 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) { async _renderFrame(options) {
const frame = await super._renderFrame(options); const frame = await super._renderFrame(options);
const iconOnly = if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) {
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) === const button = constructHTMLButton({
CONFIG.DH.GENERAL.countdownAppMode.iconOnly; label: '',
if (iconOnly) frame.classList.add('icon-only'); classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'],
else frame.classList.remove('icon-only'); dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' }
});
const header = frame.querySelector('.window-header'); this.window.controls.after(button);
header.querySelector('button[data-action="close"]').remove();
if (game.user.isGM) {
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
const editButton = `<a style="margin-right: 8px;" class="header-control" data-tooltip="${editTooltip}" aria-label="${editTooltip}" data-action="editCountdowns"><i class="fa-solid fa-wrench"></i></a>`;
header.insertAdjacentHTML('beforeEnd', editButton);
} }
const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="toggleViewMode"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
header.insertAdjacentHTML('beforeEnd', minimizeButton);
return frame; return frame;
} }
/** Returns countdown data filtered by ownership */ testUserPermission(level, exact, altSettings) {
#getCountdowns() { if (game.user.isGM) return true;
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const values = Object.entries(setting.countdowns).map(([key, countdown]) => ({ const settings =
key, altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
countdown, const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level;
ownership: DhCountdowns.#getPlayerOwnership(game.user, setting, countdown) const userAllowed = exact
})); ? settings.playerOwnership[game.user.id]?.value === level
return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE); : settings.playerOwnership[game.user.id]?.value >= level;
return defaultAllowed || userAllowed;
} }
/** @override */ async _prepareContext(_options) {
async _prepareContext(options) { const context = await super._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.isGM = game.user.isGM;
context.base = this.basePath;
context.iconOnly = context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true);
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) === context.source = {
CONFIG.DH.GENERAL.countdownAppMode.iconOnly; ...countdownData,
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
context.countdowns = this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { const countdown = countdownData.countdowns[key];
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);
}
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'));
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
acc[key] = { acc[key] = {
...countdown, ...countdown,
editable: countdownEditable, canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown)
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; return acc;
}, {}); }, {})
};
context.systemFields = countdownData.schema.fields;
context.countdownFields = context.systemFields.countdowns.element.fields;
context.simple = this.simpleView;
return context; return context;
} }
static #getPlayerOwnership(user, setting, countdown) { static async updateData(event, _, formData) {
const playerOwnership = countdown.ownership[user.id]; const data = foundry.utils.expandObject(formData.object);
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT const newSetting = foundry.utils.mergeObject(
? setting.defaultOwnership game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(),
: playerOwnership; data
);
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
}
});
}
} }
cooldownRefresh = ({ refreshType }) => { async updateSetting(update) {
if (refreshType === RefreshType.Countdown) this.render(); 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`
}
});
static canPerformEdit() { this.render();
if (game.user.isGM) return true; } else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
const noGM = !game.users.find(x => x.isGM && x.active); action: socketEvent.GMUpdate,
if (noGM) { data: {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired')); action: GMUpdateEvent.UpdateSetting,
return false; uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
} }
return true; 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();
} }
static async #toggleViewMode() { async updateImage(path, countdown) {
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode); const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const appMode = CONFIG.DH.GENERAL.countdownAppMode; await setting.updateSource({
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon; [`${this.basePath}.countdowns.${countdown}.img`]: path
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode); });
if (newMode === appMode.iconOnly) this.element.classList.add('icon-only'); await this.updateSetting(setting);
else this.element.classList.remove('icon-only'); }
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(); this.render();
} }
static async #editCountdowns() { async updateCountdownValue(event, increase) {
new game.system.api.applications.ui.CountdownEdit().render(true);
}
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();
}
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;
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
});
}
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;
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); 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[this.basePath].countdowns[event.currentTarget.dataset.countdown];
const countdown = countdownSetting.countdowns[key];
if (progressTypes.indexOf(countdown.progress.type) !== -1 && countdown.progress.current > 0) { if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
acc.push(key); return;
} }
return acc; const currentValue = countdown.progress.current;
}, []);
const countdownData = countdownSetting.toObject(); if (increase && currentValue === countdown.progress.max) return;
const settings = { if (!increase && currentValue === 0) return;
...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; await countdownSetting.updateSource({
return acc; [`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
}, {}) ? currentValue + 1
}; : currentValue - 1
await emitAsGM(GMUpdateEvent.UpdateCountdowns,
DhCountdowns.gmSetSetting.bind(settings),
settings, null, {
refreshType: RefreshType.Countdown
}); });
await this.updateSetting(countdownSetting.toObject());
} }
async _onRender(context, options) { static async addCountdown() {
await super._onRender(context, options); const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
this.element.hidden = !game.user.isGM && this.#getCountdowns().length === 0; await countdownSetting.updateSource({
if (options?.force) { [`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
document.getElementById('ui-right-column-1')?.appendChild(this.element); name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
ownership: game.user.isGM
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
}
});
await this.updateSetting(countdownSetting.toObject());
}
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; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -78,7 +78,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
/** @override */ /** @override */
async _preRender(context, options) { 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); 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) { async updateFear(value) {
return emitAsGM( return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value);
GMUpdateEvent.UpdateFear, /* if(!game.user.isGM)
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), await game.socket.emit(`system.${CONFIG.DH.id}`, {
value 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.fieldFilter = [];
this.selectedMenu = { path: [], data: null }; this.selectedMenu = { path: [], data: null };
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = {}; this.presets = options.presets;
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
if (this.presets?.compendium && this.presets?.folder)
ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder);
} }
/** @inheritDoc */ /** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
id: 'itemBrowser', id: 'itemBrowser',
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser', 'daggerheart-loader'], classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'],
tag: 'div', tag: 'div',
window: { window: {
frame: true, 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 */ /** @inheritDoc */
async _preRender(context, options) { async _preRender(context, options) {
this.presets = options.presets ?? {}; if (context.presets?.render?.noFolder || context.presets?.render?.lite)
const noFolder = this.presets?.render?.noFolder; options.parts.splice(options.parts.indexOf('sidebar'), 1);
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;
}
await super._preRender(context, options); await super._preRender(context, options);
} }
@ -113,35 +103,32 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _onRender(context, options) { async _onRender(context, options) {
await super._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._createSearchFilter();
this._createFilterInputs();
this._createDragProcess();
this.element.classList.toggle('lite', this.presets?.render?.lite === true); if (context.presets?.render?.lite) this.element.classList.add('lite');
this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true);
this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true); if (context.presets?.render?.noFolder) this.element.classList.add('no-folder');
this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => {
element.hidden = if (context.presets?.render?.noFilter) this.element.classList.add('no-filter');
this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
}); 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();
} }
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position, position);
} }
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelectorAll('[data-action="selectFolder"]').forEach(element => htmlElement
element.addEventListener('contextmenu', event => { .querySelectorAll('[data-action="selectFolder"]')
.forEach(element => element.addEventListener("contextmenu", (event) => {
event.target.classList.toggle('expanded'); event.target.classList.toggle('expanded');
}) }))
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -152,26 +139,22 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config)); context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config));
// context.pathTitle = this.pathTile;
context.menu = this.selectedMenu; context.menu = this.selectedMenu;
context.formatLabel = this.formatLabel; context.formatLabel = this.formatLabel;
context.formatChoices = this.formatChoices; context.formatChoices = this.formatChoices;
context.fieldFilter = this.fieldFilter = this._createFieldFilter();
context.items = this.items; context.items = this.items;
context.presets = this.presets; context.presets = this.presets;
return context; return context;
} }
open(presets = {}) {
this.presets = presets;
ItemBrowser.selectFolder.call(this);
}
getCompendiumFolders(config, parent = null, depth = 0) { getCompendiumFolders(config, parent = null, depth = 0) {
let folders = []; let folders = [];
Object.values(config).forEach(c => { Object.values(config).forEach(c => {
// if(this.presets.render?.folders?.length && !this.presets.render.folders.includes(c.id)) return;
const folder = { const folder = {
id: c.id, id: c.id,
label: game.i18n.localize(c.label), label: c.label,
selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id
}; };
folder.folders = c.folders folder.folders = c.folders
@ -179,108 +162,47 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
: []; : [];
folders.push(folder); folders.push(folder);
}); });
folders.sort((a, b) => a.label.localeCompare(b.label));
return folders; return folders;
} }
static async selectFolder(_, target) { static async selectFolder(_, target, compend, folder) {
const folderId = target?.dataset?.folderId ?? this.presets.folder, const config = foundry.utils.deepClone(this.config),
folderData = foundry.utils.getProperty(this.config, folderId) ?? {}; compendium = compend ?? target.closest('[data-compendium-id]').dataset.compendiumId,
folderId = folder ?? target.dataset.folderId,
const columns = ItemBrowser.getFolderConfig(folderData).map(col => ({ folderPath = `${compendium}.folders.${folderId}`,
...col, folderData = foundry.utils.getProperty(config, folderPath);
label: game.i18n.localize(col.label)
}));
this.selectedMenu = { this.selectedMenu = {
path: folderId?.split('.') ?? [], path: folderPath.split('.'),
data: { data: {
...folderData, ...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(); this.items = ItemBrowser.sortBy(items, 'name');
if(target) {
target.closest('.compendium-sidebar').querySelectorAll('[data-action="selectFolder"]').forEach(element => element.classList.remove("is-selected"))
target.classList.add('is-selected');
}
this.render({ force: true });
} }
_replaceHTML(result, content, options) { _replaceHTML(result, content, options) {
if (!options.isFirstRender) delete result.sidebar; if(!options.isFirstRender) delete result.sidebar;
super._replaceHTML(result, content, options); 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'
);
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);
}
static expandContent(_, target) { static expandContent(_, target) {
const parent = target.parentElement; const parent = target.parentElement;
parent.classList.toggle('expanded'); parent.classList.toggle('expanded');
@ -294,7 +216,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const property = foundry.utils.getProperty(item, field.key); const property = foundry.utils.getProperty(item, field.key);
if (Array.isArray(property)) property.join(', '); if (Array.isArray(property)) property.join(', ');
if (typeof field.format !== 'function') return property ?? '-'; if (typeof field.format !== 'function') return property ?? '-';
return game.i18n.localize(field.format(property)); return field.format(property);
} }
formatChoices(data) { formatChoices(data) {
@ -313,14 +235,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
filters.forEach(f => { filters.forEach(f => {
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field); if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
else if (typeof f.choices === 'function') { else if (typeof f.choices === 'function') {
f.choices = 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.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null; f.value = this.presets?.filter?.[f.name]?.value ?? null;
}); });
@ -332,8 +248,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 * @private
*/ */
_createSearchFilter() { _createSearchFilter() {
@ -398,7 +317,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
for (const li of html.querySelectorAll('.item-container')) { for (const li of html.querySelectorAll('.item-container')) {
const itemUUID = li.dataset.itemUuid, const itemUUID = li.dataset.itemUuid,
item = this.items.find(i => i.uuid === itemUUID); item = this.items.find(i => i.uuid === itemUUID);
if (!item) continue;
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.browser.search.add(item.id); if (matchesSearch) this.#filteredItems.browser.search.add(item.id);
const { input } = this.#filteredItems.browser; const { input } = this.#filteredItems.browser;
@ -430,7 +348,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
if (matchesMenu) this.#filteredItems.browser.input.add(item.id); if (matchesMenu) this.#filteredItems.browser.input.add(item.id);
const { search } = this.#filteredItems.browser; 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);
} }
} }
@ -469,7 +387,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
static resetFilters() { static resetFilters() {
this.render({ force: true }); this.render({ force: true });
this.loadItems();
} }
static getFolderConfig(folder, property = 'columns') { static getFolderConfig(folder, property = 'columns') {
@ -491,13 +408,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
const newOrder = [...itemList].reverse().sort((a, b) => { const newOrder = [...itemList].reverse().sort((a, b) => {
const aProp = a.querySelector(`[data-item-key="${key}"]`), const aProp = a.querySelector(`[data-item-key="${key}"]`),
bProp = b.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);
if (type === 'DESC') { if (type === 'DESC') {
return aValue < bValue ? 1 : -1; return aProp.innerText < bProp.innerText ? 1 : -1;
} else { } else {
return aValue > bValue ? 1 : -1; return aProp.innerText > bProp.innerText ? 1 : -1;
} }
}); });
@ -526,41 +441,4 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
_canDragStart() { _canDragStart() {
return true; 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

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

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; const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) { if (range.enabled) {
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range); const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: result.distance, units: result.units }; context.cost = { total: distance, units: null };
context.distance = { total: result.distance, units: result.units }; context.distance = { total: distance, units: null };
} }
return context; return context;

View file

@ -10,7 +10,29 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.effects.overlay = null; this.effects.overlay = null;
// Categorize effects // 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);
if (statusData) {
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')); const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay'));
// Draw effects // Draw effects
@ -34,69 +56,6 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true }); 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 */ /** @inheritDoc */
_drawBar(number, bar, data) { _drawBar(number, bar, data) {
const val = Number(data.value); const val = Number(data.value);

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; const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement;
if (range.enabled) { if (range.enabled) {
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range); const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
context.cost = { total: result.distance, units: result.units }; context.cost = { total: distance, units: null };
context.distance = { total: result.distance, units: result.units }; context.distance = { total: distance, units: null };
} }
return context; return context;

View file

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

View file

@ -2,15 +2,9 @@ export const actionTypes = {
attack: { attack: {
id: 'attack', id: 'attack',
name: 'DAGGERHEART.ACTIONS.TYPES.attack.name', name: 'DAGGERHEART.ACTIONS.TYPES.attack.name',
icon: 'fa-hand-fist', icon: 'fa-khanda',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.attack.tooltip' 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: { healing: {
id: 'healing', id: 'healing',
name: 'DAGGERHEART.ACTIONS.TYPES.healing.name', name: 'DAGGERHEART.ACTIONS.TYPES.healing.name',

View file

@ -108,72 +108,55 @@ export const adversaryTypes = {
bruiser: { bruiser: {
id: 'bruiser', id: 'bruiser',
label: 'DAGGERHEART.CONFIG.AdversaryType.bruiser.label', label: 'DAGGERHEART.CONFIG.AdversaryType.bruiser.label',
description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description', description: 'DAGGERHEART.ACTORS.Adversary.bruiser.description'
bpCost: 4
}, },
horde: { horde: {
id: 'horde', id: 'horde',
label: 'DAGGERHEART.CONFIG.AdversaryType.horde.label', label: 'DAGGERHEART.CONFIG.AdversaryType.horde.label',
description: 'DAGGERHEART.ACTORS.Adversary.horde.description', description: 'DAGGERHEART.ACTORS.Adversary.horde.description'
bpCost: 2
}, },
leader: { leader: {
id: 'leader', id: 'leader',
label: 'DAGGERHEART.CONFIG.AdversaryType.leader.label', label: 'DAGGERHEART.CONFIG.AdversaryType.leader.label',
description: 'DAGGERHEART.ACTORS.Adversary.leader.description', description: 'DAGGERHEART.ACTORS.Adversary.leader.description'
bpCost: 3,
bpDescription: 'DAGGERHEART.CONFIG.AdversaryType.leader.'
}, },
minion: { minion: {
id: 'minion', id: 'minion',
label: 'DAGGERHEART.CONFIG.AdversaryType.minion.label', label: 'DAGGERHEART.CONFIG.AdversaryType.minion.label',
description: 'DAGGERHEART.ACTORS.Adversary.minion.description', description: 'DAGGERHEART.ACTORS.Adversary.minion.description'
bpCost: 1,
partyAmountPerBP: true
}, },
ranged: { ranged: {
id: 'ranged', id: 'ranged',
label: 'DAGGERHEART.CONFIG.AdversaryType.ranged.label', label: 'DAGGERHEART.CONFIG.AdversaryType.ranged.label',
description: 'DAGGERHEART.ACTORS.Adversary.ranged.description', description: 'DAGGERHEART.ACTORS.Adversary.ranged.description'
bpCost: 2
}, },
skulk: { skulk: {
id: 'skulk', id: 'skulk',
label: 'DAGGERHEART.CONFIG.AdversaryType.skulk.label', label: 'DAGGERHEART.CONFIG.AdversaryType.skulk.label',
description: 'DAGGERHEART.ACTORS.Adversary.skulk.description', description: 'DAGGERHEART.ACTORS.Adversary.skulk.description'
bpCost: 2
}, },
social: { social: {
id: 'social', id: 'social',
label: 'DAGGERHEART.CONFIG.AdversaryType.social.label', label: 'DAGGERHEART.CONFIG.AdversaryType.social.label',
description: 'DAGGERHEART.ACTORS.Adversary.social.description', description: 'DAGGERHEART.ACTORS.Adversary.social.description'
bpCost: 1
}, },
solo: { solo: {
id: 'solo', id: 'solo',
label: 'DAGGERHEART.CONFIG.AdversaryType.solo.label', label: 'DAGGERHEART.CONFIG.AdversaryType.solo.label',
description: 'DAGGERHEART.ACTORS.Adversary.solo.description', description: 'DAGGERHEART.ACTORS.Adversary.solo.description'
bpCost: 5
}, },
standard: { standard: {
id: 'standard', id: 'standard',
label: 'DAGGERHEART.CONFIG.AdversaryType.standard.label', label: 'DAGGERHEART.CONFIG.AdversaryType.standard.label',
description: 'DAGGERHEART.ACTORS.Adversary.standard.description', description: 'DAGGERHEART.ACTORS.Adversary.standard.description'
bpCost: 2
}, },
support: { support: {
id: 'support', id: 'support',
label: 'DAGGERHEART.CONFIG.AdversaryType.support.label', label: 'DAGGERHEART.CONFIG.AdversaryType.support.label',
description: 'DAGGERHEART.ACTORS.Adversary.support.description', description: 'DAGGERHEART.ACTORS.Adversary.support.description'
bpCost: 1
} }
}; };
export const allAdversaryTypes = () => ({
...adversaryTypes,
...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).adversaryTypes
});
export const environmentTypes = { export const environmentTypes = {
exploration: { exploration: {
label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label', 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 = { export const levelChoices = {
attributes: { attributes: {
name: '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'
}
}
};

View file

@ -1,4 +1,4 @@
export const displayDomainCardsAsCard = 'displayDomainCardsAsCard'; export const displayDomainCardsAsList = 'displayDomainCardsAsList';
export const narrativeCountdown = { export const narrativeCountdown = {
simple: 'countdown-narrative-simple', simple: 'countdown-narrative-simple',
position: 'countdown-narrative-position' position: 'countdown-narrative-position'
@ -8,23 +8,8 @@ export const encounterCountdown = {
position: 'countdown-encounter-position' position: 'countdown-encounter-position'
}; };
export const compendiumBrowserDefault = {
position: 'compendium-browser-default-position'
};
export const compendiumBrowserNoFolder = {
position: 'compendium-browser-no-folder-position'
};
export const compendiumBrowserLite = {
position: 'compendium-browser-lite-position'
};
export const itemAttachmentSource = 'attachmentSource'; export const itemAttachmentSource = 'attachmentSource';
export const userFlags = { export const userFlags = {
welcomeMessage: 'welcome-message', welcomeMessage: 'welcome-message'
countdownMode: 'countdown-mode'
}; };
export const combatToggle = 'combat-toggle-origin';

View file

@ -90,22 +90,22 @@ export const rangeInclusion = {
export const otherTargetTypes = { export const otherTargetTypes = {
friendly: { friendly: {
id: 'friendly', id: 'friendly',
label: 'DAGGERHEART.CONFIG.TargetTypes.friendly' label: 'Friendly'
}, },
hostile: { hostile: {
id: 'hostile', id: 'hostile',
label: 'DAGGERHEART.CONFIG.TargetTypes.hostile' label: 'Hostile'
}, },
any: { any: {
id: 'any', id: 'any',
label: 'DAGGERHEART.CONFIG.TargetTypes.any' label: 'Any'
} }
}; };
export const targetTypes = { export const targetTypes = {
self: { self: {
id: 'self', id: 'self',
label: 'DAGGERHEART.CONFIG.TargetTypes.self' label: 'Self'
}, },
...otherTargetTypes ...otherTargetTypes
}; };
@ -164,36 +164,28 @@ export const healingTypes = {
} }
}; };
export const defeatedConditions = () => { export const defeatedConditions = {
const defeated = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).defeated;
return Object.keys(defeatedConditionChoices).reduce((acc, key) => {
const choice = defeatedConditionChoices[key];
acc[key] = {
...choice,
img: defeated[`${choice.id}Icon`],
description: `DAGGERHEART.CONFIG.Condition.${choice.id}.description`
};
return acc;
}, {});
};
export const defeatedConditionChoices = {
defeated: { defeated: {
id: 'defeated', id: 'defeated',
name: 'DAGGERHEART.CONFIG.Condition.defeated.name' name: 'DAGGERHEART.CONFIG.Condition.defeated.name',
img: 'icons/magic/control/fear-fright-mask-orange.webp',
description: 'DAGGERHEART.CONFIG.Condition.defeated.description'
}, },
unconscious: { unconscious: {
id: 'unconscious', id: 'unconscious',
name: 'DAGGERHEART.CONFIG.Condition.unconscious.name' name: 'DAGGERHEART.CONFIG.Condition.unconscious.name',
img: 'icons/magic/control/sleep-bubble-purple.webp',
description: 'DAGGERHEART.CONFIG.Condition.unconscious.description'
}, },
dead: { dead: {
id: 'dead', id: 'dead',
name: 'DAGGERHEART.CONFIG.Condition.dead.name' name: 'DAGGERHEART.CONFIG.Condition.dead.name',
img: 'icons/magic/death/grave-tombstone-glow-teal.webp',
description: 'DAGGERHEART.CONFIG.Condition.dead.description'
} }
}; };
export const conditions = () => ({ export const conditions = {
vulnerable: { vulnerable: {
id: 'vulnerable', id: 'vulnerable',
name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name',
@ -212,8 +204,8 @@ export const conditions = () => ({
img: 'icons/magic/control/debuff-chains-shackle-movement-red.webp', img: 'icons/magic/control/debuff-chains-shackle-movement-red.webp',
description: 'DAGGERHEART.CONFIG.Condition.restrained.description' description: 'DAGGERHEART.CONFIG.Condition.restrained.description'
}, },
...defeatedConditions() ...defeatedConditions
}); };
export const defaultRestOptions = { export const defaultRestOptions = {
shortRest: () => ({ shortRest: () => ({
@ -232,7 +224,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'friendly' type: 'self'
}, },
damage: { damage: {
parts: [ parts: [
@ -298,7 +290,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'friendly' type: 'self'
}, },
damage: { damage: {
parts: [ parts: [
@ -341,7 +333,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'friendly' type: 'self'
}, },
damage: { damage: {
parts: [ parts: [
@ -407,7 +399,7 @@ export const defaultRestOptions = {
actionType: 'action', actionType: 'action',
chatDisplay: false, chatDisplay: false,
target: { target: {
type: 'friendly' type: 'self'
}, },
damage: { damage: {
parts: [ parts: [
@ -569,19 +561,6 @@ export const refreshTypes = {
} }
}; };
export const itemAbilityCosts = {
resource: {
id: 'resource',
label: 'DAGGERHEART.GENERAL.resource',
group: 'Global'
},
quantity: {
id: 'quantity',
label: 'DAGGERHEART.GENERAL.quantity',
group: 'Global'
}
};
export const abilityCosts = { export const abilityCosts = {
hitPoints: { hitPoints: {
id: 'hitPoints', id: 'hitPoints',
@ -595,46 +574,33 @@ export const abilityCosts = {
}, },
hope: { hope: {
id: 'hope', id: 'hope',
label: 'DAGGERHEART.CONFIG.HealingType.hope.name', label: 'Hope',
group: 'TYPES.Actor.character' group: 'TYPES.Actor.character'
}, },
armor: { armor: {
id: 'armor', id: 'armor',
label: 'DAGGERHEART.CONFIG.HealingType.armor.name', label: 'Armor Slot',
group: 'TYPES.Actor.character' group: 'TYPES.Actor.character'
}, },
fear: { fear: {
id: 'fear', id: 'fear',
label: 'DAGGERHEART.CONFIG.HealingType.fear.name', label: 'Fear',
group: 'TYPES.Actor.adversary' group: 'TYPES.Actor.adversary'
}, }
resource: itemAbilityCosts.resource
}; };
export const countdownProgressionTypes = { export const countdownTypes = {
actionRoll: { spotlight: {
id: 'actionRoll', id: 'spotlight',
label: 'DAGGERHEART.CONFIG.CountdownType.actionRoll' label: 'DAGGERHEART.CONFIG.CountdownType.spotlight'
}, },
characterAttack: { characterAttack: {
id: 'characterAttack', id: 'characterAttack',
label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack' label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack'
}, },
characterSpotlight: {
id: 'characterSpotlight',
label: 'DAGGERHEART.CONFIG.CountdownType.characterSpotlight'
},
custom: { custom: {
id: 'custom', id: 'custom',
label: 'DAGGERHEART.CONFIG.CountdownType.custom' label: 'DAGGERHEART.CONFIG.CountdownType.custom'
},
fear: {
id: 'fear',
label: 'DAGGERHEART.CONFIG.CountdownType.fear'
},
spotlight: {
id: 'spotlight',
label: 'DAGGERHEART.CONFIG.CountdownType.spotlight'
} }
}; };
export const rollTypes = { export const rollTypes = {
@ -658,76 +624,8 @@ export const rollTypes = {
} }
}; };
export const attributionSources = {
daggerheart: {
label: 'Daggerheart',
values: [{ label: 'Daggerheart SRD' }]
}
};
export const fearDisplay = { export const fearDisplay = {
token: { value: 'token', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.token' }, token: { value: 'token', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.token' },
bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' }, bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' },
hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' }
}; };
export const basicOwnershiplevels = {
0: { value: 0, label: 'OWNERSHIP.NONE' },
2: { value: 2, label: 'OWNERSHIP.OBSERVER' },
3: { value: 3, label: 'OWNERSHIP.OWNER' }
};
export const simpleOwnershiplevels = {
[-1]: { value: -1, label: 'OWNERSHIP.INHERIT' },
...basicOwnershiplevels
};
export const countdownBaseTypes = {
narrative: {
id: 'narrative',
label: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative'
},
encounter: {
id: 'encounter',
label: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter'
}
};
export const countdownLoopingTypes = {
noLooping: {
id: 'noLooping',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.noLooping'
},
looping: {
id: 'looping',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.looping'
},
increasing: {
id: 'increasing',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.increasing'
},
decreasing: {
id: 'decreasing',
label: 'DAGGERHEART.APPLICATIONS.Countdown.loopingTypes.decreasing'
}
};
export const countdownAppMode = {
textIcon: 'text-icon',
iconOnly: 'icon-only'
};
export const sceneRangeMeasurementSetting = {
disable: {
id: 'disable',
label: 'Disable Daggerheart Range Measurement'
},
default: {
id: 'default',
label: 'Default'
},
custom: {
id: 'custom',
label: 'Custom'
}
};

View file

@ -1,5 +0,0 @@
const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle'
};
export default hooksConfig;

View file

@ -2,558 +2,404 @@ export const typeConfig = {
adversaries: { adversaries: {
columns: [ columns: [
{ {
key: 'system.tier', key: "system.tier",
label: 'DAGGERHEART.GENERAL.Tiers.singular' label: "Tier"
}, },
{ {
key: 'system.type', key: "system.type",
label: 'DAGGERHEART.GENERAL.type' label: "Type"
} }
], ],
filters: [ filters: [
{ {
key: 'system.tier', key: "system.tier",
label: 'DAGGERHEART.GENERAL.Tiers.singular', label: "Tier",
field: 'system.api.models.actors.DhAdversary.schema.fields.tier' field: 'system.api.models.actors.DhAdversary.schema.fields.tier'
}, },
{ {
key: 'system.type', key: "system.type",
label: 'DAGGERHEART.GENERAL.type', label: "Type",
field: 'system.api.models.actors.DhAdversary.schema.fields.type' field: 'system.api.models.actors.DhAdversary.schema.fields.type'
}, },
{ {
key: 'system.difficulty', key: "system.difficulty",
name: 'difficulty.min', name: "difficulty.min",
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin', label: "Difficulty (Min)",
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.difficulty', key: "system.difficulty",
name: 'difficulty.max', name: "difficulty.max",
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax', label: "Difficulty (Max)",
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: 'lte' operator: "lte"
}, },
{ {
key: 'system.resources.hitPoints.max', key: "system.resources.hitPoints.max",
name: 'hp.min', name: "hp.min",
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin', label: "Hit Points (Min)",
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.resources.hitPoints.max', key: "system.resources.hitPoints.max",
name: 'hp.max', name: "hp.max",
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax', label: "Hit Points (Max)",
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
operator: 'lte' operator: "lte"
}, },
{ {
key: 'system.resources.stress.max', key: "system.resources.stress.max",
name: 'stress.min', name: "stress.min",
label: 'DAGGERHEART.UI.ItemBrowser.stressMin', label: "Stress (Min)",
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.resources.stress.max', key: "system.resources.stress.max",
name: 'stress.max', name: "stress.max",
label: 'DAGGERHEART.UI.ItemBrowser.stressMax', label: "Stress (Max)",
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: 'lte' operator: "lte"
} },
] ]
}, },
items: { items: {
columns: [ columns: [
{ {
key: 'type', key: "type",
label: 'DAGGERHEART.GENERAL.type' label: "Type"
}, },
{ {
key: 'system.secondary', key: "system.secondary",
label: 'DAGGERHEART.UI.ItemBrowser.subtype', label: "Subtype",
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-') format: (isSecondary) => isSecondary ? "secondary" : (isSecondary === false ? "primary" : '-')
}, },
{ {
key: 'system.tier', key: "system.tier",
label: 'DAGGERHEART.GENERAL.Tiers.singular' label: "Tier"
} }
], ],
filters: [ filters: [
{ {
key: 'type', key: "type",
label: 'DAGGERHEART.GENERAL.type', label: "Type",
choices: () => choices: () => CONFIG.Item.documentClass.TYPES.filter(t => ["armor", "weapon", "consumable", "loot"].includes(t)).map(t => ({ value: t, label: t }))
CONFIG.Item.documentClass.TYPES.filter(t =>
['armor', 'weapon', 'consumable', 'loot'].includes(t)
).map(t => ({ value: t, label: t }))
}, },
{ {
key: 'system.secondary', key: "system.secondary",
label: 'DAGGERHEART.UI.ItemBrowser.subtype', label: "Subtype",
choices: [ choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' }, { value: false, label: "Primary Weapon"},
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' } { value: true, label: "Secondary Weapon"}
] ]
}, },
{ {
key: 'system.tier', key: "system.tier",
label: 'DAGGERHEART.GENERAL.Tiers.singular', label: "Tier",
choices: [ choices: [{ value: "1", label: "1"}, { value: "2", label: "2"}, { value: "3", label: "3"}, { value: "4", label: "4"}]
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
}, },
{ {
key: 'system.burden', key: "system.burden",
label: 'DAGGERHEART.GENERAL.burden', label: "Burden",
field: 'system.api.models.items.DHWeapon.schema.fields.burden' field: 'system.api.models.items.DHWeapon.schema.fields.burden'
}, },
{ {
key: 'system.attack.roll.trait', key: "system.attack.roll.trait",
label: 'DAGGERHEART.GENERAL.Trait.single', label: "Trait",
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait' field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
}, },
{ {
key: 'system.attack.range', key: "system.attack.range",
label: 'DAGGERHEART.GENERAL.range', label: "Range",
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range' field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
}, },
{ {
key: 'system.baseScore', key: "system.baseScore",
name: 'armor.min', name: "armor.min",
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin', label: "Armor Score (Min)",
field: 'system.api.models.items.DHArmor.schema.fields.baseScore', field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.baseScore', key: "system.baseScore",
name: 'armor.max', name: "armor.max",
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax', label: "Armor Score (Max)",
field: 'system.api.models.items.DHArmor.schema.fields.baseScore', field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'lte' operator: "lte"
}, },
{ {
key: 'system.itemFeatures', key: "system.itemFeatures",
label: 'DAGGERHEART.GENERAL.features', label: "Features",
choices: () => choices: () => [...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.armorFeatures)].map(([k,v]) => ({ value: k, label: v.label})),
[ operator: "contains3"
...Object.entries(CONFIG.DH.ITEM.weaponFeatures),
...Object.entries(CONFIG.DH.ITEM.armorFeatures)
].map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
}
]
},
weapons: {
columns: [
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
},
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
}
],
filters: [
{
key: 'system.secondary',
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
choices: [
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
]
},
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
},
{
key: 'system.burden',
label: 'DAGGERHEART.GENERAL.burden',
field: 'system.api.models.items.DHWeapon.schema.fields.burden'
},
{
key: 'system.attack.roll.trait',
label: 'DAGGERHEART.GENERAL.Trait.single',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait'
},
{
key: 'system.attack.range',
label: 'DAGGERHEART.GENERAL.range',
field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range'
},
{
key: 'system.itemFeatures',
label: 'DAGGERHEART.GENERAL.features',
choices: () =>
Object.entries(CONFIG.DH.ITEM.weaponFeatures).map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
}
]
},
armors: {
columns: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
}
],
filters: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular',
choices: [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
{ value: '4', label: '4' }
]
},
{
key: 'system.baseScore',
name: 'armor.min',
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'gte'
},
{
key: 'system.baseScore',
name: 'armor.max',
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: 'lte'
},
{
key: 'system.itemFeatures',
label: 'DAGGERHEART.GENERAL.features',
choices: () =>
Object.entries(CONFIG.DH.ITEM.armorFeatures).map(([k, v]) => ({ value: k, label: v.label })),
operator: 'contains3'
} }
] ]
}, },
features: { features: {
columns: [], columns: [
filters: []
],
filters: [
]
}, },
cards: { cards: {
columns: [ columns: [
{ {
key: 'system.type', key: "system.type",
label: 'DAGGERHEART.GENERAL.type' label: "Type"
}, },
{ {
key: 'system.domain', key: "system.domain",
label: 'DAGGERHEART.GENERAL.Domain.single' label: "Domain"
}, },
{ {
key: 'system.level', key: "system.level",
label: 'DAGGERHEART.GENERAL.level' label: "Level"
} }
], ],
filters: [ filters: [
{ {
key: 'system.type', key: "system.type",
label: 'DAGGERHEART.GENERAL.type', label: "Type",
field: 'system.api.models.items.DHDomainCard.schema.fields.type' field: 'system.api.models.items.DHDomainCard.schema.fields.type'
}, },
{ {
key: 'system.domain', key: "system.domain",
label: 'DAGGERHEART.GENERAL.Domain.single', label: "Domain",
field: 'system.api.models.items.DHDomainCard.schema.fields.domain', field: 'system.api.models.items.DHDomainCard.schema.fields.domain',
operator: 'contains2' operator: "contains2"
}, },
{ {
key: 'system.level', key: "system.level",
name: 'level.min', name: "level.min",
label: 'DAGGERHEART.UI.ItemBrowser.levelMin', label: "Level (Min)",
field: 'system.api.models.items.DHDomainCard.schema.fields.level', field: 'system.api.models.items.DHDomainCard.schema.fields.level',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.level', key: "system.level",
name: 'level.max', name: "level.max",
label: 'DAGGERHEART.UI.ItemBrowser.levelMax', label: "Level (Max)",
field: 'system.api.models.items.DHDomainCard.schema.fields.level', field: 'system.api.models.items.DHDomainCard.schema.fields.level',
operator: 'lte' operator: "lte"
}, },
{ {
key: 'system.recallCost', key: "system.recallCost",
name: 'recall.min', name: "recall.min",
label: 'DAGGERHEART.UI.ItemBrowser.recallCostMin', label: "Recall Cost (Min)",
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.recallCost', key: "system.recallCost",
name: 'recall.max', name: "recall.max",
label: 'DAGGERHEART.UI.ItemBrowser.recallCostMax', label: "Recall Cost (Max)",
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
operator: 'lte' operator: "lte"
} }
] ]
}, },
classes: { classes: {
columns: [ columns: [
{ {
key: 'system.evasion', key: "system.evasion",
label: 'DAGGERHEART.GENERAL.evasion' label: "Evasion"
}, },
{ {
key: 'system.hitPoints', key: "system.hitPoints",
label: 'DAGGERHEART.GENERAL.HitPoints.plural' label: "Hit Points"
}, },
{ {
key: 'system.domains', key: "system.domains",
label: 'DAGGERHEART.GENERAL.Domain.plural' label: "Domains"
} }
], ],
filters: [ filters: [
{ {
key: 'system.evasion', key: "system.evasion",
name: 'evasion.min', name: "evasion.min",
label: 'DAGGERHEART.UI.ItemBrowser.evasionMin', label: "Evasion (Min)",
field: 'system.api.models.items.DHClass.schema.fields.evasion', field: 'system.api.models.items.DHClass.schema.fields.evasion',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.evasion', key: "system.evasion",
name: 'evasion.max', name: "evasion.max",
label: 'DAGGERHEART.UI.ItemBrowser.evasionMax', label: "Evasion (Max)",
field: 'system.api.models.items.DHClass.schema.fields.evasion', field: 'system.api.models.items.DHClass.schema.fields.evasion',
operator: 'lte' operator: "lte"
}, },
{ {
key: 'system.hitPoints', key: "system.hitPoints",
name: 'hp.min', name: "hp.min",
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin', label: "Hit Points (Min)",
field: 'system.api.models.items.DHClass.schema.fields.hitPoints', field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
operator: 'gte' operator: "gte"
}, },
{ {
key: 'system.hitPoints', key: "system.hitPoints",
name: 'hp.max', name: "hp.max",
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax', label: "Hit Points (Max)",
field: 'system.api.models.items.DHClass.schema.fields.hitPoints', field: 'system.api.models.items.DHClass.schema.fields.hitPoints',
operator: 'lte' operator: "lte"
}, },
{ {
key: 'system.domains', key: "system.domains",
label: 'DAGGERHEART.GENERAL.Domain.plural', label: "Domains",
choices: () => Object.values(CONFIG.DH.DOMAIN.allDomains()).map(d => ({ value: d.id, label: d.label })), choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label})),
operator: 'contains2' operator: "contains2"
} }
] ]
}, },
subclasses: { subclasses: {
columns: [ columns: [
{ {
key: 'system.linkedClass', key: "id",
label: 'Class', label: "Class",
format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing' format: (id) => {
return "";
}
}, },
{ {
key: 'system.spellcastingTrait', key: "system.spellcastingTrait",
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait' label: "Spellcasting Trait"
} }
], ],
filters: [ filters: []
{
key: 'system.linkedClass.uuid',
label: 'Class',
choices: items => {
const list = items
.filter(item => item.system.linkedClass)
.map(item => ({
value: item.system.linkedClass.uuid,
label: item.system.linkedClass.name
}));
return list.reduce((a, c) => {
if (!a.find(i => i.value === c.value)) a.push(c);
return a;
}, []);
}
}
]
}, },
beastforms: { beastforms: {
columns: [ columns: [
{ {
key: 'system.tier', key: "system.tier",
label: 'DAGGERHEART.GENERAL.Tiers.singular' label: "Tier"
}, },
{ {
key: 'system.mainTrait', key: "system.mainTrait",
label: 'DAGGERHEART.GENERAL.Trait.single' label: "Main Trait"
} }
], ],
filters: [ filters: [
{ {
key: 'system.tier', key: "system.tier",
label: 'DAGGERHEART.GENERAL.Tiers.singular', label: "Tier",
field: 'system.api.models.items.DHBeastform.schema.fields.tier' field: 'system.api.models.items.DHBeastform.schema.fields.tier'
}, },
{ {
key: 'system.mainTrait', key: "system.mainTrait",
label: 'DAGGERHEART.GENERAL.Trait.single', label: "Main Trait",
field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait' field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait'
} }
] ]
} }
}; }
export const compendiumConfig = { export const compendiumConfig = {
characters: { "daggerheart": {
id: 'characters', id: "daggerheart",
keys: ['characters'], label: "DAGGERHEART",
label: 'DAGGERHEART.UI.ItemBrowser.folders.characters',
type: ['character']
// listType: 'characters'
},
adversaries: {
id: 'adversaries',
keys: ['adversaries'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.adversaries',
type: ['adversary'],
listType: 'adversaries'
},
ancestries: {
id: 'ancestries',
keys: ['ancestries'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.ancestries',
type: ['ancestry']
/* folders: {
features: {
id: 'features',
keys: ['ancestries'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
}
} */
},
equipments: {
id: 'equipments',
keys: ['armors', 'weapons', 'consumables', 'loot'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.equipment',
type: ['armor', 'weapon', 'consumable', 'loot'],
listType: 'items',
folders: { folders: {
weapons: { "adversaries": {
id: 'weapons', id: "adversaries",
keys: ['weapons'], keys: ["adversaries"],
label: 'DAGGERHEART.UI.ItemBrowser.folders.weapons', label: "Adversaries",
type: ['weapon'], type: ["adversary"],
listType: 'weapons' listType: "adversaries"
}, },
armors: { "ancestries": {
id: 'armors', id: "ancestries",
keys: ['armors'], keys: ["ancestries"],
label: 'DAGGERHEART.UI.ItemBrowser.folders.armors', label: "Ancestries",
type: ['armor'], type: ["ancestry"],
listType: 'armors' folders: {
}, "features": {
consumables: { id: "features",
id: 'consumables', keys: ["ancestries"],
keys: ['consumables'], label: "Features",
label: 'DAGGERHEART.UI.ItemBrowser.folders.consumables', type: ["feature"]
type: ['consumable']
},
loots: {
id: 'loots',
keys: ['loots'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.loots',
type: ['loot']
} }
} }
}, },
classes: { "equipments": {
id: 'classes', id: "equipments",
keys: ['classes'], keys: ["armors", "weapons", "consumables", "loot"],
label: 'DAGGERHEART.UI.ItemBrowser.folders.classes', label: "Equipment",
type: ['class'], type: ["armor", "weapon", "consumable", "loot"],
/* folders: { listType: "items"
features: {
id: 'features',
keys: ['classes'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
}, },
items: { "classes": {
id: 'items', id: "classes",
keys: ['classes'], keys: ["classes"],
label: 'DAGGERHEART.UI.ItemBrowser.folders.items', label: "Classes",
type: ['armor', 'weapon', 'consumable', 'loot'], type: ["class"],
listType: 'items' folders: {
"features": {
id: "features",
keys: ["classes"],
label: "Features",
type: ["feature"]
},
"items": {
id: "items",
keys: ["classes"],
label: "Items",
type: ["armor", "weapon", "consumable", "loot"],
listType: "items"
} }
}, */
listType: 'classes'
}, },
subclasses: { listType: "classes"
id: 'subclasses',
keys: ['subclasses'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.subclasses',
type: ['subclass'],
listType: 'subclasses'
}, },
domains: { "subclasses": {
id: 'domains', id: "subclasses",
keys: ['domains'], keys: ["subclasses"],
label: 'DAGGERHEART.UI.ItemBrowser.folders.domainCards', label: "Subclasses",
type: ['domainCard'], type: ["subclass"],
listType: 'cards' listType: "subclasses"
}, },
communities: { "domains": {
id: 'communities', id: "domains",
keys: ['communities'], keys: ["domains"],
label: 'DAGGERHEART.UI.ItemBrowser.folders.communities', label: "Domain Cards",
type: ['community'] type: ["domainCard"],
/* folders: { listType: "cards"
features: { },
id: 'features', "communities": {
keys: ['communities'], id: "communities",
label: 'DAGGERHEART.UI.ItemBrowser.folders.features', keys: ["communities"],
type: ['feature'] label: "Communities",
type: ["community"],
folders: {
"features": {
id: "features",
keys: ["communities"],
label: "Features",
type: ["feature"]
} }
} */
},
environments: {
id: 'environments',
keys: ['environments'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.environments',
type: ['environment']
},
beastforms: {
id: 'beastforms',
keys: ['beastforms'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.beastforms',
type: ['beastform'],
listType: 'beastforms'
/* folders: {
features: {
id: 'features',
keys: ['beastforms'],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
type: ['feature']
} }
} */
}, },
features: { "environments": {
id: 'features', id: "environments",
keys: ['features'], keys: ["environments"],
label: 'DAGGERHEART.UI.ItemBrowser.folders.features', label: "Environments",
type: ['feature'] type: ["environment"]
},
"beastforms": {
id: "beastforms",
keys: ["beastforms"],
label: "Beastforms",
type: ["beastform"],
listType: "beastforms",
folders: {
"features": {
id: "features",
keys: ["beastforms"],
label: "Features",
type: ["feature"]
} }
}; }
}
}
}
}

View file

@ -5,6 +5,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'damage', type: 'damage',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.burning.actions.burn.description',
@ -173,6 +174,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.hopeful.actions.hope.description',
@ -186,6 +188,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.impenetrable.actions.impenetrable.description',
@ -228,6 +231,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.painful.actions.pain.description',
@ -265,6 +269,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.quiet.actions.quiet.description',
@ -301,6 +306,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'attack', type: 'attack',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.resilient.actions.resilient.description',
@ -347,6 +353,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.shifting.actions.shift.description',
@ -366,6 +373,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'attack', type: 'attack',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.timeslowing.actions.slowTime.description',
@ -393,6 +401,7 @@ export const armorFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.truthseeking.actions.truthseeking.description',
@ -435,8 +444,7 @@ export const armorFeatures = {
{ {
key: 'system.resistance.magical.reduction', key: 'system.resistance.magical.reduction',
mode: 2, mode: 2,
value: '@system.armorScore', value: '@system.armorScore'
priority: 21
} }
] ]
} }
@ -444,43 +452,6 @@ export const armorFeatures = {
} }
}; };
export const allArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.armorFeatures;
return {
...armorFeatures,
...Object.keys(homebrewFeatures).reduce((acc, key) => {
const feature = homebrewFeatures[key];
const actions = feature.actions.map(action => ({
...action,
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
type: action.type
}));
const actionEffects = actions.flatMap(a => a.effects);
const effects = feature.effects.filter(effect => !actionEffects.some(x => x.id === effect.id));
acc[key] = { ...feature, label: feature.name, effects, actions };
return acc;
}, {})
};
};
export const orderedArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.armorFeatures;
const allFeatures = { ...armorFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key];
return {
...feature,
id: key,
label: feature.label ?? feature.name
};
});
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
};
export const weaponFeatures = { export const weaponFeatures = {
barrier: { barrier: {
label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name', label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name',
@ -529,6 +500,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.bouncing.actions.bounce.description',
@ -573,6 +545,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.brutal.actions.addDamage.description',
@ -586,6 +559,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.burning.actions.burn.description',
@ -599,6 +573,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.charged.actions.markStress.description',
@ -635,6 +610,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.concussive.actions.attack.description',
@ -675,6 +651,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.deadly.actions.extraDamage.description',
@ -688,6 +665,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.actions.deflect.description',
@ -710,8 +688,7 @@ export const weaponFeatures = {
{ {
key: 'system.evasion', key: 'system.evasion',
mode: 2, mode: 2,
value: '@system.armorScore', value: '@system.armorScore'
priority: 21
} }
] ]
} }
@ -725,6 +702,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'damage', type: 'damage',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.descriptive', description: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.actions.attack.descriptive',
@ -769,6 +747,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.devastating.actions.devastate.description',
@ -819,6 +798,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.doubledUp.actions.doubleUp.description',
@ -832,6 +812,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.dueling.actions.duel.description',
@ -845,6 +826,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', // Should prompt a dc 14 reaction save on adversaries type: 'effect', // Should prompt a dc 14 reaction save on adversaries
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.eruptive.actions.erupt.description',
@ -858,6 +840,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.grappling.actions.grapple.description',
@ -877,13 +860,11 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.description',
img: 'icons/commodities/currency/coins-crown-stack-gold.webp', img: 'icons/commodities/currency/coins-crown-stack-gold.webp',
target: {
type: 'self'
},
// Should cost handful of gold, // Should cost handful of gold,
effects: [ effects: [
{ {
@ -908,6 +889,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'healing', type: 'healing',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.healing.actions.heal.description',
@ -955,6 +937,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.hooked.actions.hook.description',
@ -968,6 +951,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.hot.actions.hot.description',
@ -981,6 +965,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.invigorating.actions.invigorate.description',
@ -994,6 +979,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.lifestealing.actions.lifesteal.description',
@ -1007,6 +993,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.lockedOn.actions.lockOn.description',
@ -1020,6 +1007,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.long.actions.long.description',
@ -1033,6 +1021,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.lucky.actions.luck.description',
@ -1070,6 +1059,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.painful.actions.pain.description',
@ -1115,6 +1105,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.parry.actions.parry.description',
@ -1128,6 +1119,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.actions.persuade.description',
@ -1164,6 +1156,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.pompous.actions.pompous.description',
@ -1207,6 +1200,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.quick.actions.quick.description',
@ -1244,6 +1238,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.reloading.actions.reload.description',
@ -1257,6 +1252,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.retractable.actions.retract.description',
@ -1270,6 +1266,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.returning.actions.return.description',
@ -1283,6 +1280,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.scary.actions.scare.description',
@ -1326,8 +1324,7 @@ export const weaponFeatures = {
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2, mode: 2,
value: '@system.traits.agility.value', value: '@system.traits.agility.value'
priority: 21
} }
] ]
} }
@ -1339,6 +1336,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.sheltering.actions.shelter.description',
@ -1352,6 +1350,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.startling.actions.startle.description',
@ -1371,6 +1370,7 @@ export const weaponFeatures = {
actions: [ actions: [
{ {
type: 'effect', type: 'effect',
actionType: 'action',
chatDisplay: true, chatDisplay: true,
name: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.timebending.actions.bendTime.description',
@ -1380,50 +1380,6 @@ export const weaponFeatures = {
} }
}; };
export const allWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.weaponFeatures;
return {
...weaponFeatures,
...Object.keys(homebrewFeatures).reduce((acc, key) => {
const feature = homebrewFeatures[key];
const actions = feature.actions.map(action => ({
...action,
effects: action.effects.map(effect => feature.effects.find(x => x.id === effect._id)),
type: action.type
}));
const actionEffects = actions.flatMap(a => a.effects);
const effects = feature.effects.filter(effect => !actionEffects.some(x => x.id === effect.id));
acc[key] = { ...feature, label: feature.name, effects, actions };
return acc;
}, {})
};
};
export const orderedWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures
.weaponFeatures;
const allFeatures = { ...weaponFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key];
return {
...feature,
id: key,
label: feature.label ?? feature.name
};
});
return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label)));
};
export const featureForm = {
passive: 'DAGGERHEART.CONFIG.FeatureForm.passive',
action: 'DAGGERHEART.CONFIG.FeatureForm.action',
reaction: 'DAGGERHEART.CONFIG.FeatureForm.reaction'
};
export const featureTypes = { export const featureTypes = {
ancestry: { ancestry: {
id: 'ancestry', id: 'ancestry',
@ -1481,6 +1437,21 @@ export const featureSubTypes = {
mastery: 'mastery' mastery: 'mastery'
}; };
export const actionTypes = {
passive: {
id: 'passive',
label: 'DAGGERHEART.CONFIG.ActionType.passive'
},
action: {
id: 'action',
label: 'DAGGERHEART.CONFIG.ActionType.action'
},
reaction: {
id: 'reaction',
label: 'DAGGERHEART.CONFIG.ActionType.reaction'
}
};
export const itemResourceTypes = { export const itemResourceTypes = {
simple: { simple: {
id: 'simple', id: 'simple',
@ -1489,10 +1460,6 @@ export const itemResourceTypes = {
diceValue: { diceValue: {
id: 'diceValue', id: 'diceValue',
label: 'DAGGERHEART.CONFIG.ItemResourceType.diceValue' label: 'DAGGERHEART.CONFIG.ItemResourceType.diceValue'
},
die: {
id: 'die',
label: 'DAGGERHEART.CONFIG.ItemResourceType.die'
} }
}; };
@ -1521,8 +1488,3 @@ export const beastformTypes = {
label: 'DAGGERHEART.CONFIG.BeastformType.hybrid' label: 'DAGGERHEART.CONFIG.BeastformType.hybrid'
} }
}; };
export const originItemType = {
itemCollection: 'itemCollection',
restMove: 'restMove'
};

View file

@ -26,27 +26,5 @@ export const gameSettings = {
Fear: 'ResourcesFear' Fear: 'ResourcesFear'
}, },
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns', Countdowns: 'Countdowns'
LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue',
};
export const actionAutomationChoices = {
never: {
id: 'never',
label: 'Never'
},
showDialog: {
id: 'showDialog',
label: 'Show Dialog only'
},
// npcOnly: {
// id: "npcOnly",
// label: "Always for non-characters"
// },
always: {
id: 'always',
label: 'Always'
}
}; };

View file

@ -1,20 +1,17 @@
import * as GENERAL from './generalConfig.mjs'; import * as GENERAL from './generalConfig.mjs';
import * as DOMAIN from './domainConfig.mjs'; import * as DOMAIN from './domainConfig.mjs';
import * as ENCOUNTER from './encounterConfig.mjs';
import * as ACTOR from './actorConfig.mjs'; import * as ACTOR from './actorConfig.mjs';
import * as ITEM from './itemConfig.mjs'; import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs'; import * as EFFECTS from './effectConfig.mjs';
import * as ACTIONS from './actionConfig.mjs'; import * as ACTIONS from './actionConfig.mjs';
import * as FLAGS from './flagsConfig.mjs'; import * as FLAGS from './flagsConfig.mjs';
import HOOKS from './hooksConfig.mjs'; import * as ITEMBROWSER from './itemBrowserConfig.mjs'
import * as ITEMBROWSER from './itemBrowserConfig.mjs';
export const SYSTEM_ID = 'daggerheart'; export const SYSTEM_ID = 'daggerheart';
export const SYSTEM = { export const SYSTEM = {
id: SYSTEM_ID, id: SYSTEM_ID,
ENCOUNTER,
GENERAL, GENERAL,
DOMAIN, DOMAIN,
ACTOR, ACTOR,
@ -23,6 +20,5 @@ export const SYSTEM = {
EFFECTS, EFFECTS,
ACTIONS, ACTIONS,
FLAGS, FLAGS,
HOOKS,
ITEMBROWSER ITEMBROWSER
}; };

View file

@ -1,12 +1,9 @@
export { default as DhCombat } from './combat.mjs'; export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs'; export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
export * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs'; export * as actions from './action/_module.mjs';
export * as activeEffects from './activeEffect/_module.mjs'; export * as activeEffects from './activeEffect/_module.mjs';
export * as actors from './actor/_module.mjs'; export * as actors from './actor/_module.mjs';
export * as chatMessages from './chat-message/_modules.mjs'; export * as chatMessages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs'; export * as fields from './fields/_module.mjs';
export * as items from './item/_module.mjs'; export * as items from './item/_module.mjs';
export * as scenes from './scene/_module.mjs';

View file

@ -1,7 +1,6 @@
import AttackAction from './attackAction.mjs'; import AttackAction from './attackAction.mjs';
import BaseAction from './baseAction.mjs'; import BaseAction from './baseAction.mjs';
import BeastformAction from './beastformAction.mjs'; import BeastformAction from './beastformAction.mjs';
import CountdownAction from './countdownAction.mjs';
import DamageAction from './damageAction.mjs'; import DamageAction from './damageAction.mjs';
import EffectAction from './effectAction.mjs'; import EffectAction from './effectAction.mjs';
import HealingAction from './healingAction.mjs'; import HealingAction from './healingAction.mjs';
@ -11,7 +10,6 @@ import SummonAction from './summonAction.mjs';
export const actionsTypes = { export const actionsTypes = {
base: BaseAction, base: BaseAction,
attack: AttackAction, attack: AttackAction,
countdown: CountdownAction,
damage: DamageAction, damage: DamageAction,
healing: HealingAction, healing: HealingAction,
summon: SummonAction, summon: SummonAction,

View file

@ -37,10 +37,8 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) { async use(event, options) {
const result = await super.use(event, options); const result = await super.use(event, options);
if (result.message.system.action.roll?.type === 'attack') {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id); await updateCountdowns(CONFIG.DH.GENERAL.countdownTypes.characterAttack.id);
}
return result; return result;
} }
@ -53,13 +51,11 @@ export default class DHAttackAction extends DHDamageAction {
const labels = []; const labels = [];
const { roll, range, damage } = this; const { roll, range, damage } = this;
if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`)); if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`))
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; for (const { value, type } of damage.parts) {
for (const { value, valueAlt, type } of damage.parts) { const str = Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {});
const usedValue = useAltDamage ? valueAlt : value;
const str = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
const icons = Array.from(type) const icons = Array.from(type)
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)

View file

@ -1,10 +1,14 @@
import DhpActor from '../../documents/actor.mjs'; import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { ActionMixin } from '../fields/actionField.mjs'; import { ActionMixin } from '../fields/actionField.mjs';
import { originItemField } from '../chat-message/actorRoll.mjs'; import { abilities } from '../../config/actorConfig.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
/*
!!! I'm currently refactoring the whole Action thing, it's a WIP !!!
*/
/* /*
ToDo ToDo
- Target Check / Target Picker - Target Check / Target Picker
@ -16,108 +20,48 @@ const fields = foundry.data.fields;
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) { export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['cost', 'uses', 'range']; static extraSchemas = ['cost', 'uses', 'range'];
/** @inheritDoc */
static defineSchema() { static defineSchema() {
const schemaFields = { const schemaFields = {
_id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }),
systemPath: new fields.StringField({ required: true, initial: 'actions' }), systemPath: new fields.StringField({ required: true, initial: 'actions' }),
type: new fields.StringField({ initial: undefined, readonly: true, required: true }), type: new fields.StringField({ initial: undefined, readonly: true, required: true }),
baseAction: new fields.BooleanField({ initial: false }),
name: new fields.StringField({ initial: undefined }), name: new fields.StringField({ initial: undefined }),
description: new fields.HTMLField(), description: new fields.HTMLField(),
img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }), img: new fields.FilePathField({ initial: undefined, categories: ['IMAGE'], base64: false }),
chatDisplay: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.ACTIONS.Config.displayInChat' }), chatDisplay: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.ACTIONS.Config.displayInChat' }),
originItem: originItemField(),
actionType: new fields.StringField({ actionType: new fields.StringField({
choices: CONFIG.DH.ITEM.actionTypes, choices: CONFIG.DH.ITEM.actionTypes,
initial: 'action', initial: 'action',
nullable: false, nullable: true
required: true })
}),
targetUuid: new fields.StringField({ initial: undefined })
}; };
this.extraSchemas.forEach(s => { this.extraSchemas.forEach(s => {
let clsField = this.getActionField(s); let clsField;
if (clsField) schemaFields[s] = new clsField(); if ((clsField = this.getActionField(s))) schemaFields[s] = new clsField();
}); });
return schemaFields; return schemaFields;
} }
/**
* The default values to supply to schema fields when they are created in the actionConfig. Defined by implementing classes.
*/
get defaultValues() {
return {};
}
/**
* Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property.
*
* Each step can be called individually as long as needed config is provided.
* Ex: <action>.workflow.get("damage").execute(config)
* @returns {Map}
*/
defineWorkflow() {
const workflow = new Map();
this.constructor.extraSchemas.forEach(s => {
let clsField = this.constructor.getActionField(s);
if (clsField?.execute) {
workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) });
if (s === 'damage')
workflow.set('applyDamage', { order: 75, execute: clsField.applyDamage.bind(this) });
}
});
return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order));
}
/**
* Getter returning the workflow property or creating it the first time the property is called
*/
get workflow() {
if (this.hasOwnProperty('_workflow')) return this._workflow;
const workflow = Object.freeze(this.defineWorkflow());
Object.defineProperty(this, '_workflow', { value: workflow, writable: false });
return workflow;
}
/**
* Get the Field class from ActionFields global config
* @param {string} name Field short name, equal to Action property
* @returns Action Field
*/
static getActionField(name) { static getActionField(name) {
const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`]; const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`];
return fields.DataField.isPrototypeOf(field) && field; return fields.DataField.isPrototypeOf(field) && field;
} }
/** @inheritDoc */
prepareData() { prepareData() {
this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name); this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name);
this.img = this.img ?? this.parent?.parent?.img; this.img = this.img ?? this.parent?.parent?.img;
/* Fallback to feature description */
this.description = this.description || this.parent?.description;
} }
/**
* Get Action ID
*/
get id() { get id() {
return this._id; return this._id;
} }
/**
* Return Item the action is attached too.
*/
get item() { get item() {
return this.parent.parent; return this.parent.parent;
} }
/**
* Return the first Actor parent found.
*/
get actor() { get actor() {
return this.item instanceof DhpActor return this.item instanceof DhpActor
? this.item ? this.item
@ -130,11 +74,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return 'trait'; return 'trait';
} }
/**
* Prepare base data based on Action Type & Parent Type
* @param {object} parent
* @returns {object}
*/
static getSourceConfig(parent) { static getSourceConfig(parent) {
const updateSource = {}; const updateSource = {};
if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) { if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) {
@ -157,15 +96,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return updateSource; return updateSource;
} }
/**
* Obtain a data object used to evaluate any dice rolls associated with this particular Action
* @param {object} [data ={}] Optional data object from previous configuration/rolls
* @returns {object}
*/
getRollData(data = {}) { getRollData(data = {}) {
const actorData = this.actor ? this.actor.getRollData(false) : {}; if (!this.actor) return null;
const actorData = this.actor.getRollData(false);
// Add Roll results to RollDatas
actorData.result = data.roll?.total ?? 1; actorData.result = data.roll?.total ?? 1;
actorData.scale = data.costs?.length // Right now only return the first scalable cost. actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1) ? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1; : 1;
@ -174,28 +111,19 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return actorData; return actorData;
} }
/** async use(event, options = {}) {
* Execute each part of the Action workflow in order, calling a specific event before and after each part.
* @param {object} config Config object usually created from prepareConfig method
*/
async executeWorkflow(config) {
for (const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
if ((await part.execute(config)) === false) return;
if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return;
}
}
/**
* Main method to use the Action
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
async use(event) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
let config = this.prepareConfig(event); if (this.chatDisplay) await this.toChat();
if (!config) return; let { byPassRoll } = options,
config = this.prepareConfig(event, byPassRoll);
for (let i = 0; i < this.constructor.extraSchemas.length; i++) {
let clsField = this.constructor.getActionField(this.constructor.extraSchemas[i]);
if (clsField?.prepareConfig) {
const keep = clsField.prepareConfig.call(this, config);
if (config.isFastForward && !keep) return;
}
}
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
@ -205,168 +133,282 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (!config) return; if (!config) return;
} }
// Execute the Action Worflow in order based of schema fields if (config.hasRoll) {
await this.executeWorkflow(config); const rollConfig = this.prepareRoll(config);
await config.resourceUpdates.updateResources(); config.roll = rollConfig;
config = await this.actor.diceRoll(config);
if (!config) return;
}
if (this.doFollowUp(config)) {
if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config);
else if (this.trigger) await this.trigger(event, config);
else if (this.hasSave || this.hasEffect) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
}
// Consume resources
await this.consume(config);
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay) await this.toChat();
return config; return config;
} }
/** /* */
* Create the basic config common to every action type prepareConfig(event, byPass = false) {
* @param {Event} event Event from the button used to trigger the Action const hasRoll = this.getUseHasRoll(byPass);
* @returns {object} return {
*/
prepareBaseConfig(event) {
const config = {
event, event,
title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`, title: `${this.item.name}: ${this.name}`,
source: { source: {
item: this.item._id, item: this.item._id,
originItem: this.originItem,
action: this._id, action: this._id,
actor: this.actor.uuid actor: this.actor.uuid
}, },
dialog: {}, dialog: {
actionType: this.actionType, configure: hasRoll
hasRoll: this.hasRoll, },
hasDamage: this.hasDamage, type: this.type,
hasHealing: this.hasHealing, hasRoll: hasRoll,
hasEffect: this.hasEffect, hasDamage: this.damage?.parts?.length && this.type !== 'healing',
hasHealing: this.damage?.parts?.length && this.type === 'healing',
hasEffect: !!this.effects?.length,
hasSave: this.hasSave, hasSave: this.hasSave,
isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'), selectedRollMode: game.settings.get('core', 'rollMode'),
isFastForward: event.shiftKey,
data: this.getRollData(), data: this.getRollData(),
evaluate: this.hasRoll, evaluate: hasRoll
resourceUpdates: new ResourceUpdateMap(this.actor),
targetUuid: this.targetUuid
}; };
DHBaseAction.applyKeybindings(config);
return config;
} }
/**
* Create the config for that action used for its workflow
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
prepareConfig(event) {
const config = this.prepareBaseConfig(event);
for (const clsField of Object.values(this.schema.fields)) {
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
}
return config;
}
/**
* Method used to know if a configuration dialog must be shown or not when there is no roll.
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @returns {boolean}
*/
requireConfigurationDialog(config) { requireConfigurationDialog(config) {
return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses); return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses);
} }
/** prepareRoll() {
* Consume Action configured resources & uses. const roll = {
* That method is only used when we want those resources to be consumed outside of the use method workflow. baseModifiers: this.roll.getModifier(),
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. label: 'Attack',
* @param {boolean} successCost type: this.actionType,
*/ difficulty: this.roll?.difficulty,
async consume(config, successCost = false) { formula: this.roll.getFormula(),
await this.workflow.get('cost')?.execute(config, successCost); advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
await this.workflow.get('uses')?.execute(config, successCost); };
if (this.roll?.type === 'diceSet' || !this.hasRoll) roll.lite = true;
if (config.roll && !config.roll.success && successCost) { return roll;
}
doFollowUp(config) {
return !config.hasRoll;
}
async consume(config, successCost = false) {
const usefulResources = {
...foundry.utils.deepClone(this.actor.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
}
};
for (var cost of config.costs) {
if (cost.keyIsID) {
usefulResources[cost.key] = {
value: cost.value,
target: this.parent.parent,
keyIsID: true
};
}
}
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(config.costs)
.filter(
c =>
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
(successCost && c.consumeOnSuccess)
)
.reduce((a, c) => {
const resource = usefulResources[c.key];
if (resource) {
a.push({
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
});
return a;
}
}, []);
await (this.actor.system.partner ?? this.actor).modifyResource(resources);
if (
config.uses?.enabled &&
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
(successCost && config.uses?.consumeOnSuccess))
)
this.update({ 'uses.value': this.uses.value + 1 });
if (config.roll?.success || successCost) {
setTimeout(() => { setTimeout(() => {
(config.message ?? config.parent).update({ 'system.successConsumed': true }); (config.message ?? config.parent).update({ 'system.successConsumed': true });
}, 50); }, 50);
} }
} }
/* */
/** /* ROLL */
* Set if a configuration dialog must be shown or not if a special keyboard key is pressed. getUseHasRoll(byPass = false) {
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. return this.hasRoll && !byPass;
*/
static applyKeybindings(config) {
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
} }
/**
* Getters to know which parts the action is composed of. A field can exist but configured to not be used.
* @returns {boolean} If that part is in the action.
*/
get hasRoll() { get hasRoll() {
return !!this.roll?.type; return !!this.roll?.type;
} }
get hasDamage() { get modifiers() {
return this.damage?.parts?.length && this.type !== 'healing'; if (!this.actor) return [];
} const modifiers = [];
/** Placeholder for specific bonuses **/
get hasHealing() { return modifiers;
return this.damage?.parts?.length && this.type === 'healing';
} }
/* ROLL */
/* SAVE */
get hasSave() { get hasSave() {
return !!this.save?.trait; return !!this.save?.trait;
} }
/* SAVE */
/* EFFECTS */
get hasEffect() { get hasEffect() {
return this.effects?.length > 0; return this.effects?.length > 0;
} }
async applyEffects(event, data, targets) {
targets ??= data.system.targets;
const force = true; /* Where should this come from? */
if (!this.effects?.length || !targets.length) return;
let effects = this.effects;
targets.forEach(async token => {
if (!token.hit && !force) return;
if (this.hasSave && token.saved.success === true) {
effects = this.effects.filter(e => e.onSave === true);
}
if (!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
effect = this.item.effects.get(e._id);
if (!actor || !effect) return;
await this.applyEffect(effect, actor);
});
});
}
async applyEffect(effect, actor) {
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
if (existingEffect) {
return effect.update(
foundry.utils.mergeObject({
...effect.constructor.getInitialDuration(),
disabled: false
})
);
}
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...effect.toObject(),
disabled: false,
transfer: false,
origin: effect.uuid
});
await ActiveEffect.implementation.create(effectData, { parent: actor });
}
/* EFFECTS */
/* SAVE */
async rollSave(actor, event, message) {
if (!actor) return;
const title = actor.isNPC
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
});
return actor.diceRoll({
event,
title,
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
type: 'reaction'
},
type: 'trait',
hasRoll: true,
data: actor.getRollData()
});
}
updateSaveMessage(result, message, targetId) {
if (!result) return;
const updateMsg = this.updateChatMessage.bind(this, message, targetId, {
result: result.roll.total,
success: result.roll.success
});
if (game.modules.get('dice-so-nice')?.active)
game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(() => updateMsg());
else updateMsg();
}
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);
if (!actor || !actor?.isOwner) reject();
action.rollSave(actor, event, message).then(result => resolve(result));
});
}
/* SAVE */
async updateChatMessage(message, targetId, changes, chain = true) {
setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id);
await chatMessage.update({
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: changes
}
}
}
});
}, 100);
if (chain) {
if (message.system.source.message)
this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source?.message === message._id);
relatedChatMessages.forEach(c => {
this.updateChatMessage(c, targetId, changes, false);
});
}
}
/** /**
* Generates a list of localized tags for this action. * Generates a list of localized tags for this action.
* @returns {string[]} An array of localized tag strings. * @returns {string[]} An array of localized tag strings.
*/ */
_getTags() { _getTags() {
const tags = [game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`)]; const tags = [
game.i18n.localize(`DAGGERHEART.ACTIONS.TYPES.${this.type}.name`),
game.i18n.localize(`DAGGERHEART.CONFIG.ActionType.${this.actionType}`)
];
return tags; return tags;
} }
} }
export class ResourceUpdateMap extends Map {
#actor;
constructor(actor) {
super();
this.#actor = actor;
}
addResources(resources) {
for (const resource of resources) {
if (!resource.key) continue;
const existing = this.get(resource.key);
if (existing) {
this.set(resource.key, {
...existing,
value: existing.value + (resource.value ?? 0),
total: existing.total + (resource.total ?? 0)
});
} else {
this.set(resource.key, resource);
}
}
}
#getResources() {
return Array.from(this.values());
}
async updateResources() {
if (this.#actor) {
const target = this.#actor.system.partner ?? this.#actor;
await target.modifyResource(this.#getResources());
}
}
}

View file

@ -1,9 +1,10 @@
import BeastformDialog from '../../applications/dialogs/beastformDialog.mjs';
import DHBaseAction from './baseAction.mjs'; import DHBaseAction from './baseAction.mjs';
export default class DhBeastformAction extends DHBaseAction { export default class DhBeastformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'beastform']; static extraSchemas = [...super.extraSchemas, 'beastform'];
/* async use(event, options) { async use(event, options) {
const beastformConfig = this.prepareBeastformConfig(); const beastformConfig = this.prepareBeastformConfig();
const abort = await this.handleActiveTransformations(); const abort = await this.handleActiveTransformations();
@ -81,5 +82,5 @@ export default class DhBeastformAction extends DHBaseAction {
beastformEffects.map(x => x.id) beastformEffects.map(x => x.id)
); );
return existingEffects; return existingEffects;
} */ }
} }

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