mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-12 03:31:07 +01:00
Merge branch 'main' into release
This commit is contained in:
commit
395820513b
1285 changed files with 25675 additions and 8850 deletions
|
|
@ -44,12 +44,14 @@ 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
|
||||||
|
|
@ -71,6 +73,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**!
|
||||||
|
|
||||||
🐸🛠️
|
🐸🛠️
|
||||||
|
|
|
||||||
BIN
assets/icons/arrow-dunk.png
Normal file
BIN
assets/icons/arrow-dunk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/icons/dice/duality/DualityBW.png
Normal file
BIN
assets/icons/dice/duality/DualityBW.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
59
assets/icons/dice/duality/DualityBW.svg
Normal file
59
assets/icons/dice/duality/DualityBW.svg
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
1
assets/icons/documents/actors/dark-squad.svg
Normal file
1
assets/icons/documents/actors/dark-squad.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -1,5 +1,6 @@
|
||||||
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';
|
||||||
|
|
@ -7,10 +8,8 @@ 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 { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
|
|
||||||
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
|
import { BaseRoll, 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,
|
runMigrations,
|
||||||
|
|
@ -26,6 +25,7 @@ Hooks.once('init', () => {
|
||||||
CONFIG.DH = SYSTEM;
|
CONFIG.DH = SYSTEM;
|
||||||
game.system.api = {
|
game.system.api = {
|
||||||
applications,
|
applications,
|
||||||
|
data,
|
||||||
models,
|
models,
|
||||||
documents,
|
documents,
|
||||||
dice,
|
dice,
|
||||||
|
|
@ -34,15 +34,6 @@ Hooks.once('init', () => {
|
||||||
|
|
||||||
CONFIG.TextEditor.enrichers.push(...enricherConfig);
|
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 = {
|
CONFIG.Dice.daggerheart = {
|
||||||
DHRoll: DHRoll,
|
DHRoll: DHRoll,
|
||||||
DualityRoll: DualityRoll,
|
DualityRoll: DualityRoll,
|
||||||
|
|
@ -94,6 +85,10 @@ Hooks.once('init', () => {
|
||||||
types: ['environment'],
|
types: ['environment'],
|
||||||
makeDefault: true
|
makeDefault: true
|
||||||
});
|
});
|
||||||
|
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
|
||||||
|
types: ['party'],
|
||||||
|
makeDefault: true
|
||||||
|
});
|
||||||
|
|
||||||
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
|
CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect;
|
||||||
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
|
CONFIG.ActiveEffect.dataModels = models.activeEffects.config;
|
||||||
|
|
@ -133,9 +128,12 @@ Hooks.once('init', () => {
|
||||||
CONFIG.ui.combat = applications.ui.DhCombatTracker;
|
CONFIG.ui.combat = applications.ui.DhCombatTracker;
|
||||||
CONFIG.ui.chat = applications.ui.DhChatLog;
|
CONFIG.ui.chat = applications.ui.DhChatLog;
|
||||||
CONFIG.ui.hotbar = applications.ui.DhHotbar;
|
CONFIG.ui.hotbar = applications.ui.DhHotbar;
|
||||||
|
CONFIG.ui.sidebar = applications.sidebar.DhSidebar;
|
||||||
|
CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu;
|
||||||
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
|
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
|
||||||
|
|
||||||
CONFIG.ui.resources = applications.ui.DhFearTracker;
|
CONFIG.ui.resources = applications.ui.DhFearTracker;
|
||||||
|
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
|
||||||
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
|
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
|
||||||
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
|
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
|
||||||
|
|
||||||
|
|
@ -146,18 +144,39 @@ Hooks.once('init', () => {
|
||||||
// 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.registerSheet(foundry.documents.Scene, SYSTEM.id, applications.scene.DhSceneConfigSettings, {
|
||||||
|
makeDefault: true,
|
||||||
|
label: 'Daggerheart'
|
||||||
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
ui.resources = new CONFIG.ui.resources();
|
ui.resources = new CONFIG.ui.resources();
|
||||||
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
|
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
|
||||||
ui.resources.render({ force: true });
|
ui.resources.render({ force: true });
|
||||||
|
|
||||||
registerCountdownHooks();
|
ui.countdowns = new CONFIG.ui.countdowns();
|
||||||
|
ui.countdowns.render({ force: true });
|
||||||
|
|
||||||
|
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
|
||||||
|
ui.compendiumBrowser = new applications.ui.ItemBrowser();
|
||||||
|
|
||||||
socketRegistration.registerSocketHooks();
|
socketRegistration.registerSocketHooks();
|
||||||
registerRollDiceHooks();
|
registerRollDiceHooks();
|
||||||
socketRegistration.registerUserQueries();
|
socketRegistration.registerUserQueries();
|
||||||
|
|
@ -230,29 +249,6 @@ Hooks.on('chatMessage', (_, message) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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) => {
|
Hooks.on('moveToken', async (movedToken, data) => {
|
||||||
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
|
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
|
||||||
if (!effectsAutomation.rangeDependent) return;
|
if (!effectsAutomation.rangeDependent) return;
|
||||||
|
|
@ -300,3 +296,6 @@ Hooks.on('moveToken', async (movedToken, data) => {
|
||||||
await effect.value.update({ disabled: effect.disabled });
|
await effect.value.update({ disabled: effect.disabled });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
||||||
|
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
||||||
|
|
|
||||||
336
lang/en.json
336
lang/en.json
|
|
@ -20,12 +20,21 @@
|
||||||
"character": "Character",
|
"character": "Character",
|
||||||
"companion": "Companion",
|
"companion": "Companion",
|
||||||
"adversary": "Adversary",
|
"adversary": "Adversary",
|
||||||
"environment": "Environment"
|
"environment": "Environment",
|
||||||
|
"party": "Party"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CONTROLS": {
|
"CONTROLS": {
|
||||||
"inFront": "In Front"
|
"inFront": "In Front"
|
||||||
},
|
},
|
||||||
|
"SCENE": {
|
||||||
|
"TABS": {
|
||||||
|
"SHEET": {
|
||||||
|
"dh": "Daggerheart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"DAGGERHEART": {
|
"DAGGERHEART": {
|
||||||
"ACTIONS": {
|
"ACTIONS": {
|
||||||
"TYPES": {
|
"TYPES": {
|
||||||
|
|
@ -64,6 +73,14 @@
|
||||||
"exactHint": "The Character's Tier is used if empty",
|
"exactHint": "The Character's Tier is used if empty",
|
||||||
"label": "Beastform"
|
"label": "Beastform"
|
||||||
},
|
},
|
||||||
|
"damage": {
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"flatMultiplier": "Flat Multiplier"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"customFormula": "Custom Formula",
|
||||||
|
"formula": "Formula"
|
||||||
|
},
|
||||||
"displayInChat": "Display in chat"
|
"displayInChat": "Display in chat"
|
||||||
},
|
},
|
||||||
"RollField": {
|
"RollField": {
|
||||||
|
|
@ -80,6 +97,7 @@
|
||||||
"attackName": "Attack Name",
|
"attackName": "Attack Name",
|
||||||
"includeBase": { "label": "Include Item Damage" },
|
"includeBase": { "label": "Include Item Damage" },
|
||||||
"multiplier": "Multiplier",
|
"multiplier": "Multiplier",
|
||||||
|
"saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.",
|
||||||
"resultBased": {
|
"resultBased": {
|
||||||
"label": "Formula based on Hope/Fear result."
|
"label": "Formula based on Hope/Fear result."
|
||||||
},
|
},
|
||||||
|
|
@ -153,7 +171,9 @@
|
||||||
"hint": "Add single words or short text as reminders and hints of what a character has advantage on."
|
"hint": "Add single words or short text as reminders and hints of what a character has advantage on."
|
||||||
},
|
},
|
||||||
"age": "Age",
|
"age": "Age",
|
||||||
|
"backgroundQuestions": "Backgrounds",
|
||||||
"companionFeatures": "Companion Features",
|
"companionFeatures": "Companion Features",
|
||||||
|
"connections": "Connections",
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"consume": "Consume Item",
|
"consume": "Consume Item",
|
||||||
"equip": "Equip",
|
"equip": "Equip",
|
||||||
|
|
@ -194,6 +214,7 @@
|
||||||
"confirmTitle": "Companion Levelup",
|
"confirmTitle": "Companion Levelup",
|
||||||
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
|
"confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)"
|
||||||
},
|
},
|
||||||
|
"viewLevelups": "View Levelups",
|
||||||
"InvalidOldCharacterImportTitle": "Old Character Import",
|
"InvalidOldCharacterImportTitle": "Old Character Import",
|
||||||
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?"
|
"InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?"
|
||||||
},
|
},
|
||||||
|
|
@ -238,6 +259,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"APPLICATIONS": {
|
"APPLICATIONS": {
|
||||||
|
"Attribution": {
|
||||||
|
"title": "Attribution"
|
||||||
|
},
|
||||||
"CharacterCreation": {
|
"CharacterCreation": {
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"ancestry": "Ancestry",
|
"ancestry": "Ancestry",
|
||||||
|
|
@ -246,7 +270,8 @@
|
||||||
"experience": "Experience",
|
"experience": "Experience",
|
||||||
"traits": "Traits",
|
"traits": "Traits",
|
||||||
"domainCards": "Domain Cards",
|
"domainCards": "Domain Cards",
|
||||||
"equipment": "Equipment"
|
"equipment": "Equipment",
|
||||||
|
"story": "Story"
|
||||||
},
|
},
|
||||||
"ancestryNamePlaceholder": "Your ancestry's name",
|
"ancestryNamePlaceholder": "Your ancestry's name",
|
||||||
"buttonTitle": "Character Setup",
|
"buttonTitle": "Character Setup",
|
||||||
|
|
@ -267,6 +292,7 @@
|
||||||
"selectSubclass": "Select Subclass",
|
"selectSubclass": "Select Subclass",
|
||||||
"startingItems": "Starting Items",
|
"startingItems": "Starting Items",
|
||||||
"story": "Story",
|
"story": "Story",
|
||||||
|
"storyExplanation": "Select which background and connection prompts you want to copy into your character's background.",
|
||||||
"suggestedArmor": "Suggested Armor",
|
"suggestedArmor": "Suggested Armor",
|
||||||
"suggestedPrimaryWeapon": "Suggested Primary Weapon",
|
"suggestedPrimaryWeapon": "Suggested Primary Weapon",
|
||||||
"suggestedSecondaryWeapon": "Suggested Secondary Weapon",
|
"suggestedSecondaryWeapon": "Suggested Secondary Weapon",
|
||||||
|
|
@ -290,7 +316,8 @@
|
||||||
"toLoadout": "Send to Loadout",
|
"toLoadout": "Send to Loadout",
|
||||||
"toVault": "Send to Vault",
|
"toVault": "Send to Vault",
|
||||||
"unequip": "Unequip",
|
"unequip": "Unequip",
|
||||||
"useItem": "Use Item"
|
"useItem": "Use Item",
|
||||||
|
"cancelBeastform": "Cancel Beastform"
|
||||||
},
|
},
|
||||||
"Countdown": {
|
"Countdown": {
|
||||||
"addCountdown": "Add Countdown",
|
"addCountdown": "Add Countdown",
|
||||||
|
|
@ -305,7 +332,8 @@
|
||||||
"label": { "label": "Label", "hint": "Used for custom" },
|
"label": { "label": "Label", "hint": "Used for custom" },
|
||||||
"value": { "label": "Value" }
|
"value": { "label": "Value" }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"type": { "label": "Countdown Type" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -320,6 +348,25 @@
|
||||||
"encounter": "Encounter"
|
"encounter": "Encounter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CountdownEdit": {
|
||||||
|
"title": "Countdown Edit",
|
||||||
|
"viewTitle": "Countdowns",
|
||||||
|
"editTitle": "Edit Countdowns",
|
||||||
|
"newCountdown": "New Countdown",
|
||||||
|
"removeCountdownTitle": "Remove Countdown",
|
||||||
|
"removeCountdownText": "Are you sure you want to remove the countdown: {name}?",
|
||||||
|
"current": "Current",
|
||||||
|
"max": "Max",
|
||||||
|
"currentCountdownValue": "Current: {value}",
|
||||||
|
"currentCountdownMax": "Max: {value}",
|
||||||
|
"category": "Category",
|
||||||
|
"type": "Type",
|
||||||
|
"defaultOwnershipTooltip": "The default player ownership of countdowns",
|
||||||
|
"hideNewCountdowns": "Hide New Countdowns"
|
||||||
|
},
|
||||||
|
"DaggerheartMenu": {
|
||||||
|
"title": "GM Tools"
|
||||||
|
},
|
||||||
"DeleteConfirmation": {
|
"DeleteConfirmation": {
|
||||||
"title": "Delete {type} - {name}",
|
"title": "Delete {type} - {name}",
|
||||||
"text": "Are you sure you want to delete {name}?"
|
"text": "Are you sure you want to delete {name}?"
|
||||||
|
|
@ -391,9 +438,15 @@
|
||||||
},
|
},
|
||||||
"HUD": {
|
"HUD": {
|
||||||
"tokenHUD": {
|
"tokenHUD": {
|
||||||
"genericEffects": "Foundry Effects"
|
"genericEffects": "Foundry Effects",
|
||||||
|
"depositPartyTokens": "Deposit Party Tokens",
|
||||||
|
"retrievePartyTokens": "Retrieve Party Tokens"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ImageSelect": {
|
||||||
|
"title": "Select Image",
|
||||||
|
"selectImage": "Select Image"
|
||||||
|
},
|
||||||
"Levelup": {
|
"Levelup": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"creatureComfort": {
|
"creatureComfort": {
|
||||||
|
|
@ -475,24 +528,28 @@
|
||||||
},
|
},
|
||||||
"takeLevelUp": "Finish Level Up",
|
"takeLevelUp": "Finish Level Up",
|
||||||
"tier2": {
|
"tier2": {
|
||||||
|
"name": "Tier 2",
|
||||||
"label": "Levels 2-4",
|
"label": "Levels 2-4",
|
||||||
"infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.",
|
"infoLabel": "At Level 2, gain an additional Experience at +2 and gain a +1 bonus to your Proficiency.",
|
||||||
"pretext": "Choose two options from the list below",
|
"pretext": "Choose two options from the list below",
|
||||||
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
||||||
},
|
},
|
||||||
"tier3": {
|
"tier3": {
|
||||||
|
"name": "Tier 3",
|
||||||
"label": "Levels 5-7",
|
"label": "Levels 5-7",
|
||||||
"infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
|
"infoLabel": "At Level 5, take an additional Experience and clear all marks on Character Traits.",
|
||||||
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
||||||
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
||||||
},
|
},
|
||||||
"tier4": {
|
"tier4": {
|
||||||
|
"name": "Tier 4",
|
||||||
"label": "Levels 8-10",
|
"label": "Levels 8-10",
|
||||||
"infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.",
|
"infoLabel": "At Level 8, take an additional Experience and clear all marks on Character Traits.",
|
||||||
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
"pretext": "When you level up, record it on your character sheet, then choose two from the list below or any unmarked from the previous tier.",
|
||||||
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
"posttext": "Take an additional domain card of your level or lower from a domain you have access to."
|
||||||
},
|
},
|
||||||
"title": "{actor} Level Up"
|
"title": "{actor} Level Up",
|
||||||
|
"viewModeTitle": "{actor} Level Up (View Mode)"
|
||||||
},
|
},
|
||||||
"MulticlassChoice": {
|
"MulticlassChoice": {
|
||||||
"title": "Multiclassing - {actor}",
|
"title": "Multiclassing - {actor}",
|
||||||
|
|
@ -514,6 +571,19 @@
|
||||||
"ResourceDice": {
|
"ResourceDice": {
|
||||||
"title": "{name} Resource",
|
"title": "{name} Resource",
|
||||||
"rerollDice": "Reroll Dice"
|
"rerollDice": "Reroll Dice"
|
||||||
|
},
|
||||||
|
"TagTeamSelect": {
|
||||||
|
"title": "Tag Team Roll",
|
||||||
|
"leaderTitle": "Initiating Character",
|
||||||
|
"membersTitle": "Participants",
|
||||||
|
"partyTeam": "Party Team",
|
||||||
|
"hopeCost": "Hope Cost",
|
||||||
|
"initiatingCharacter": "Initiating Character",
|
||||||
|
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
|
||||||
|
"damageNotRolled": "Damage not rolled in chat message yet",
|
||||||
|
"insufficientHope": "The initiating character doesn't have enough hope",
|
||||||
|
"createTagTeam": "Create TagTeam Roll",
|
||||||
|
"chatMessageRollTitle": "Roll"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CLASS": {
|
"CLASS": {
|
||||||
|
|
@ -857,6 +927,11 @@
|
||||||
"magical": {
|
"magical": {
|
||||||
"name": "Magical",
|
"name": "Magical",
|
||||||
"abbreviation": "Mag"
|
"abbreviation": "Mag"
|
||||||
|
},
|
||||||
|
"direct": {
|
||||||
|
"name": "Direct Damage",
|
||||||
|
"short": "Direct",
|
||||||
|
"abbreviation": "Dir"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DeathMoves": {
|
"DeathMoves": {
|
||||||
|
|
@ -906,23 +981,28 @@
|
||||||
"HealingType": {
|
"HealingType": {
|
||||||
"hitPoints": {
|
"hitPoints": {
|
||||||
"name": "Hit Points",
|
"name": "Hit Points",
|
||||||
"abbreviation": "HP"
|
"abbreviation": "HP",
|
||||||
|
"inChatRoll": "Damage"
|
||||||
},
|
},
|
||||||
"stress": {
|
"stress": {
|
||||||
"name": "Stress",
|
"name": "Stress",
|
||||||
"abbreviation": "STR"
|
"abbreviation": "STR",
|
||||||
|
"inChatRoll": "Stress"
|
||||||
},
|
},
|
||||||
"hope": {
|
"hope": {
|
||||||
"name": "Hope",
|
"name": "Hope",
|
||||||
"abbreviation": "HO"
|
"abbreviation": "HO",
|
||||||
|
"inChatRoll": "Hope"
|
||||||
},
|
},
|
||||||
"armor": {
|
"armor": {
|
||||||
"name": "Armor Slot",
|
"name": "Armor Slot",
|
||||||
"abbreviation": "AS"
|
"abbreviation": "AS",
|
||||||
|
"inChatRoll": "Armor Slot"
|
||||||
},
|
},
|
||||||
"fear": {
|
"fear": {
|
||||||
"name": "Fear",
|
"name": "Fear",
|
||||||
"abbreviation": "FR"
|
"abbreviation": "FR",
|
||||||
|
"inChatRoll": "Fear"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ItemResourceProgression": {
|
"ItemResourceProgression": {
|
||||||
|
|
@ -989,6 +1069,12 @@
|
||||||
"selectType": "Select Action Type",
|
"selectType": "Select Action Type",
|
||||||
"selectAction": "Action Selection"
|
"selectAction": "Action Selection"
|
||||||
},
|
},
|
||||||
|
"TargetTypes": {
|
||||||
|
"any": "Any",
|
||||||
|
"friendly": "Friendly",
|
||||||
|
"hostile": "Hostile",
|
||||||
|
"self": "Self"
|
||||||
|
},
|
||||||
"TemplateTypes": {
|
"TemplateTypes": {
|
||||||
"circle": "Circle",
|
"circle": "Circle",
|
||||||
"cone": "Cone",
|
"cone": "Cone",
|
||||||
|
|
@ -1838,6 +1924,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SpotlightRequests": {
|
||||||
|
"singular": "Spotlight Request",
|
||||||
|
"plural": "Spotlight Requests"
|
||||||
|
},
|
||||||
"Tabs": {
|
"Tabs": {
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"attack": "Attack",
|
"attack": "Attack",
|
||||||
|
|
@ -1862,6 +1952,7 @@
|
||||||
"story": "Story",
|
"story": "Story",
|
||||||
"biography": "Biography",
|
"biography": "Biography",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
|
"resources": "Resources",
|
||||||
"foundation": "Foundation",
|
"foundation": "Foundation",
|
||||||
"specialization": "Specialization",
|
"specialization": "Specialization",
|
||||||
"mastery": "Mastery",
|
"mastery": "Mastery",
|
||||||
|
|
@ -1877,7 +1968,15 @@
|
||||||
"tier4": "tier 4",
|
"tier4": "tier 4",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"downtime": "Downtime",
|
"downtime": "Downtime",
|
||||||
"rules": "Rules"
|
"roll": "Roll",
|
||||||
|
"rules": "Rules",
|
||||||
|
"partyMembers": "Party Members",
|
||||||
|
"projects": "Projects",
|
||||||
|
"types": "Types",
|
||||||
|
"itemFeatures": "Item Features",
|
||||||
|
"questions": "Questions",
|
||||||
|
"configuration": "Configuration",
|
||||||
|
"base": "Base"
|
||||||
},
|
},
|
||||||
"Tiers": {
|
"Tiers": {
|
||||||
"singular": "Tier",
|
"singular": "Tier",
|
||||||
|
|
@ -1894,10 +1993,12 @@
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"any": "Any",
|
"any": "Any",
|
||||||
"armor": "Armor",
|
"armor": "Armor",
|
||||||
|
"armorFeatures": "Armor Features",
|
||||||
"armors": "Armors",
|
"armors": "Armors",
|
||||||
"armorScore": "Armor Score",
|
"armorScore": "Armor Score",
|
||||||
"activeEffects": "Active Effects",
|
"activeEffects": "Active Effects",
|
||||||
"armorSlots": "Armor Slots",
|
"armorSlots": "Armor Slots",
|
||||||
|
"artistAttribution": "Artwork By: {artist}",
|
||||||
"attack": "Attack",
|
"attack": "Attack",
|
||||||
"basics": "Basics",
|
"basics": "Basics",
|
||||||
"bonus": "Bonus",
|
"bonus": "Bonus",
|
||||||
|
|
@ -1905,6 +2006,7 @@
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"criticalSuccess": "Critical Success",
|
"criticalSuccess": "Critical Success",
|
||||||
"criticalShort": "Critical",
|
"criticalShort": "Critical",
|
||||||
|
"custom": "Custom",
|
||||||
"d20Roll": "D20 Roll",
|
"d20Roll": "D20 Roll",
|
||||||
"damage": "Damage",
|
"damage": "Damage",
|
||||||
"damageRoll": "Damage Roll",
|
"damageRoll": "Damage Roll",
|
||||||
|
|
@ -1927,6 +2029,7 @@
|
||||||
"fear": "Fear",
|
"fear": "Fear",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"formula": "Formula",
|
"formula": "Formula",
|
||||||
|
"gm": "GM",
|
||||||
"healing": "Healing",
|
"healing": "Healing",
|
||||||
"healingRoll": "Healing Roll",
|
"healingRoll": "Healing Roll",
|
||||||
"hit": {
|
"hit": {
|
||||||
|
|
@ -1945,6 +2048,7 @@
|
||||||
"inactiveEffects": "Inactive Effects",
|
"inactiveEffects": "Inactive Effects",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"itemResource": "Item Resource",
|
"itemResource": "Item Resource",
|
||||||
|
"itemQuantity": "Item Quantity",
|
||||||
"items": "Items",
|
"items": "Items",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
|
|
@ -1963,11 +2067,16 @@
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"noTarget": "No current target",
|
"noTarget": "No current target",
|
||||||
"partner": "Partner",
|
"partner": "Partner",
|
||||||
|
"player": {
|
||||||
|
"single": "Player",
|
||||||
|
"plurial": "Players"
|
||||||
|
},
|
||||||
"proficiency": "Proficiency",
|
"proficiency": "Proficiency",
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
"range": "Range",
|
"range": "Range",
|
||||||
"reactionRoll": "Reaction Roll",
|
"reactionRoll": "Reaction Roll",
|
||||||
"recovery": "Recovery",
|
"recovery": "Recovery",
|
||||||
|
"refresh": "Refresh",
|
||||||
"reroll": "Reroll",
|
"reroll": "Reroll",
|
||||||
"rerollThing": "Reroll {thing}",
|
"rerollThing": "Reroll {thing}",
|
||||||
"resource": "Resource",
|
"resource": "Resource",
|
||||||
|
|
@ -1978,6 +2087,8 @@
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"scalable": "Scalable",
|
"scalable": "Scalable",
|
||||||
"situationalBonus": "Situational Bonus",
|
"situationalBonus": "Situational Bonus",
|
||||||
|
"spent": "Spent",
|
||||||
|
"step": "Step",
|
||||||
"stress": "Stress",
|
"stress": "Stress",
|
||||||
"subclasses": "Subclasses",
|
"subclasses": "Subclasses",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
|
|
@ -1988,6 +2099,7 @@
|
||||||
},
|
},
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
"traitModifier": "Trait Modifier",
|
||||||
"true": "True",
|
"true": "True",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unarmed": "Unarmed",
|
"unarmed": "Unarmed",
|
||||||
|
|
@ -1997,11 +2109,17 @@
|
||||||
"used": "Used",
|
"used": "Used",
|
||||||
"uses": "Uses",
|
"uses": "Uses",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
|
"weaponFeatures": "Weapon Features",
|
||||||
"weapons": "Weapons",
|
"weapons": "Weapons",
|
||||||
"withThing": "With {thing}"
|
"withThing": "With {thing}"
|
||||||
},
|
},
|
||||||
"ITEMS": {
|
"ITEMS": {
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
|
"attribution": {
|
||||||
|
"source": { "label": "Source" },
|
||||||
|
"page": { "label": "Page" },
|
||||||
|
"artist": { "label": "Artist" }
|
||||||
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"amount": { "label": "Amount" },
|
"amount": { "label": "Amount" },
|
||||||
"dieFaces": { "label": "Die Faces" },
|
"dieFaces": { "label": "Die Faces" },
|
||||||
|
|
@ -2076,7 +2194,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Consumable": {
|
"Consumable": {
|
||||||
"consumeOnUse": "Consume On Use"
|
"consumeOnUse": "Consume On Use",
|
||||||
|
"destroyOnEmpty": "Destroy On Empty"
|
||||||
},
|
},
|
||||||
"DomainCard": {
|
"DomainCard": {
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
|
@ -2097,14 +2216,44 @@
|
||||||
"SETTINGS": {
|
"SETTINGS": {
|
||||||
"Appearance": {
|
"Appearance": {
|
||||||
"FIELDS": {
|
"FIELDS": {
|
||||||
"displayFear": { "label": "Fear Display" },
|
"displayFear": {
|
||||||
"dualityColorScheme": { "label": "Chat Style" },
|
"label": "Display Fear"
|
||||||
"showGenericStatusEffects": { "label": "Show Foundry Status Effects" },
|
},
|
||||||
|
"showGenericStatusEffects": {
|
||||||
|
"label": "Show Foundry Status Effects"
|
||||||
|
},
|
||||||
|
"hideAttribution": {
|
||||||
|
"label": "Hide Attribution"
|
||||||
|
},
|
||||||
"expandedTitle": "Auto-expand Descriptions",
|
"expandedTitle": "Auto-expand Descriptions",
|
||||||
"extendCharacterDescriptions": { "label": "Characters" },
|
"extendCharacterDescriptions": {
|
||||||
"extendAdversaryDescriptions": { "label": "Adversaries" },
|
"label": "Characters"
|
||||||
"extendEnvironmentDescriptions": { "label": "Environments" },
|
},
|
||||||
"extendItemDescriptions": { "label": "Items" }
|
"extendAdversaryDescriptions": {
|
||||||
|
"label": "Adversaries"
|
||||||
|
},
|
||||||
|
"extendEnvironmentDescriptions": {
|
||||||
|
"label": "Environments"
|
||||||
|
},
|
||||||
|
"extendItemDescriptions": {
|
||||||
|
"label": "Items"
|
||||||
|
},
|
||||||
|
"expandRollMessage": {
|
||||||
|
"title": "Auto-expand Message Sections",
|
||||||
|
"desc": {
|
||||||
|
"label": "Description"
|
||||||
|
},
|
||||||
|
"roll": {
|
||||||
|
"label": "Formula"
|
||||||
|
},
|
||||||
|
"damage": {
|
||||||
|
"label": "Damage/Healing"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"label": "Target"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"useResourcePips": { "label": "Pip Display For Resources" }
|
||||||
},
|
},
|
||||||
"fearDisplay": {
|
"fearDisplay": {
|
||||||
"token": "Tokens",
|
"token": "Tokens",
|
||||||
|
|
@ -2159,15 +2308,45 @@
|
||||||
"playerCanEditSheet": {
|
"playerCanEditSheet": {
|
||||||
"label": "Players Can Manually Edit Character Settings",
|
"label": "Players Can Manually Edit Character Settings",
|
||||||
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
|
"hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules."
|
||||||
|
},
|
||||||
|
"roll": {
|
||||||
|
"roll": {
|
||||||
|
"label": "Roll",
|
||||||
|
"hint": "Auto behavior for rolls like Attack, Spellcast, etc."
|
||||||
|
},
|
||||||
|
"damage": {
|
||||||
|
"label": "Damage/Healing Roll",
|
||||||
|
"hint": "Auto behavior for Damage & Healing rolls after the Attack/Spellcast."
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"label": "Reaction Roll",
|
||||||
|
"hint": "Auto behavior if a Reaction Roll is needed. Targets must be selected before the action is made"
|
||||||
|
},
|
||||||
|
"damageApply": {
|
||||||
|
"label": "Apply Damage/Healing",
|
||||||
|
"hint": "Automatically apply damages & healings. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
|
||||||
|
},
|
||||||
|
"effect": {
|
||||||
|
"label": "Apply Effects",
|
||||||
|
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summaryMessages": {
|
||||||
|
"label": "Summary Messages"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defeated": {
|
"defeated": {
|
||||||
"title": "Defeated Handling"
|
"title": "Defeated Handling"
|
||||||
|
},
|
||||||
|
"roll": {
|
||||||
|
"title": "Actions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Homebrew": {
|
"Homebrew": {
|
||||||
"newDowntimeMove": "Downtime Move",
|
"newDowntimeMove": "Downtime Move",
|
||||||
|
"newFeature": "New ItemFeature",
|
||||||
"downtimeMoves": "Downtime Moves",
|
"downtimeMoves": "Downtime Moves",
|
||||||
|
"itemFeatures": "Item Features",
|
||||||
"nrChoices": "# Moves Per Rest",
|
"nrChoices": "# Moves Per Rest",
|
||||||
"resetMovesTitle": "Reset {type} Downtime Moves",
|
"resetMovesTitle": "Reset {type} Downtime Moves",
|
||||||
"resetMovesText": "Are you sure you want to reset?",
|
"resetMovesText": "Are you sure you want to reset?",
|
||||||
|
|
@ -2200,6 +2379,10 @@
|
||||||
"deleteDomain": "Delete Domain",
|
"deleteDomain": "Delete Domain",
|
||||||
"deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.",
|
"deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.",
|
||||||
"duplicateDomain": "There is already a domain with this identification."
|
"duplicateDomain": "There is already a domain with this identification."
|
||||||
|
},
|
||||||
|
"adversaryType": {
|
||||||
|
"title": "Custom Adversary Types",
|
||||||
|
"newType": "Adversary Type"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Menu": {
|
"Menu": {
|
||||||
|
|
@ -2220,10 +2403,8 @@
|
||||||
"hint": "System ruler setup for displaying ranges in Daggerheart"
|
"hint": "System ruler setup for displaying ranges in Daggerheart"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance Settings",
|
|
||||||
"label": "Appearance Settings",
|
"label": "Appearance Settings",
|
||||||
"hint": "Modify the look of various parts of the system",
|
"hint": "Modify the look of various parts of the system",
|
||||||
"name": "Appearance Settings",
|
|
||||||
"duality": "Duality Rolls",
|
"duality": "Duality Rolls",
|
||||||
"diceSoNice": {
|
"diceSoNice": {
|
||||||
"title": "Dice So Nice",
|
"title": "Dice So Nice",
|
||||||
|
|
@ -2263,6 +2444,15 @@
|
||||||
"ResetSettings": {
|
"ResetSettings": {
|
||||||
"resetConfirmationTitle": "Reset Settings",
|
"resetConfirmationTitle": "Reset Settings",
|
||||||
"resetConfirmationText": "Are you sure you want to reset the {settings}?"
|
"resetConfirmationText": "Are you sure you want to reset the {settings}?"
|
||||||
|
},
|
||||||
|
"Scene": {
|
||||||
|
"FIELDS": {
|
||||||
|
"rangeMeasurement": {
|
||||||
|
"setting": { "label": "Setting" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules",
|
||||||
|
"rangeMeasurement": "Range Measurement"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"UI": {
|
"UI": {
|
||||||
|
|
@ -2270,6 +2460,7 @@
|
||||||
"action": {
|
"action": {
|
||||||
"title": "Action"
|
"title": "Action"
|
||||||
},
|
},
|
||||||
|
"appliedTo": "Applied To",
|
||||||
"applyEffect": {
|
"applyEffect": {
|
||||||
"title": "Apply Effects - {name}"
|
"title": "Apply Effects - {name}"
|
||||||
},
|
},
|
||||||
|
|
@ -2279,6 +2470,11 @@
|
||||||
"rollHealing": "Roll Healing",
|
"rollHealing": "Roll Healing",
|
||||||
"applyEffect": "Apply Effects"
|
"applyEffect": "Apply Effects"
|
||||||
},
|
},
|
||||||
|
"clearResource": "Clear {quantity} {resource}",
|
||||||
|
"damageSummary": {
|
||||||
|
"title": "Damage Applied",
|
||||||
|
"healingTitle": "Healing Applied"
|
||||||
|
},
|
||||||
"damageRoll": {
|
"damageRoll": {
|
||||||
"title": "Damage - {damage}",
|
"title": "Damage - {damage}",
|
||||||
"dealDamageToTargets": "Damage Hit Targets",
|
"dealDamageToTargets": "Damage Hit Targets",
|
||||||
|
|
@ -2300,18 +2496,91 @@
|
||||||
"dualityRoll": {
|
"dualityRoll": {
|
||||||
"abilityCheckTitle": "{ability} Check"
|
"abilityCheckTitle": "{ability} Check"
|
||||||
},
|
},
|
||||||
|
"effectSummary": {
|
||||||
|
"title": "Effects Applied"
|
||||||
|
},
|
||||||
"featureTitle": "Class Feature",
|
"featureTitle": "Class Feature",
|
||||||
|
"groupRoll": {
|
||||||
|
"title": "Group Roll",
|
||||||
|
"leader": "Leader",
|
||||||
|
"partyTeam": "Party Team",
|
||||||
|
"team": "Team",
|
||||||
|
"selectLeader": "Select a Leader",
|
||||||
|
"selectMember": "Select a Member",
|
||||||
|
"rerollTitle": "Reroll Group Roll",
|
||||||
|
"rerollContent": "Are you sure you want to reroll your {trait} check?",
|
||||||
|
"rerollTooltip": "Reroll"
|
||||||
|
},
|
||||||
"healingRoll": {
|
"healingRoll": {
|
||||||
"title": "Heal - {damage}",
|
"title": "Heal - {damage}",
|
||||||
"heal": "Heal",
|
"heal": "Heal",
|
||||||
"applyHealing": "Apply Healing"
|
"applyHealing": "Apply Healing"
|
||||||
},
|
},
|
||||||
|
"markResource": "Mark {quantity} {resource}",
|
||||||
|
"refreshMessage": {
|
||||||
|
"title": "Feature Refresh",
|
||||||
|
"header": "Refreshed"
|
||||||
|
},
|
||||||
"reroll": {
|
"reroll": {
|
||||||
"confirmTitle": "Reroll Dice",
|
"confirmTitle": "Reroll Dice",
|
||||||
"confirmText": "Are you sure you want to reroll?"
|
"confirmText": "Are you sure you want to reroll?"
|
||||||
},
|
},
|
||||||
"resourceRoll": {
|
"resourceRoll": {
|
||||||
"playerMessage": "{user} rerolled their {name}"
|
"playerMessage": "{user} rerolled their {name}"
|
||||||
|
},
|
||||||
|
"tagTeam": {
|
||||||
|
"title": "Tag Team",
|
||||||
|
"membersTitle": "Members"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ChatLog": {
|
||||||
|
"rerollDamage": "Reroll Damage",
|
||||||
|
"assignTagRoll": "Assign as Tag Roll"
|
||||||
|
},
|
||||||
|
"Countdowns": {
|
||||||
|
"title": "Countdowns",
|
||||||
|
"toggleIconMode": "Toggle Icon Only",
|
||||||
|
"noPlayerAccess": "This countdown isn't visible to any players"
|
||||||
|
},
|
||||||
|
"ItemBrowser": {
|
||||||
|
"title": "Daggerheart Compendium Browser",
|
||||||
|
"hint": "Select a Folder in sidebar to start browsing through the compendium",
|
||||||
|
"searchPlaceholder": "Search...",
|
||||||
|
"columnName": "Name",
|
||||||
|
"tooltipFilters": "Filters",
|
||||||
|
"tooltipErase": "Erase",
|
||||||
|
"difficultyMin": "Difficulty (Min)",
|
||||||
|
"difficultyMax": "Difficulty (Max)",
|
||||||
|
"hitPointsMin": "Hit Points (Min)",
|
||||||
|
"hitPointsMax": "Hit Points (Max)",
|
||||||
|
"stressMin": "Stress (Min)",
|
||||||
|
"stressMax": "Stress (Max)",
|
||||||
|
"armorScoreMin": "Armor Score (Min)",
|
||||||
|
"armorScoreMax": "Armor Score (Max)",
|
||||||
|
"levelMin": "Level (Min)",
|
||||||
|
"levelMax": "Level (Max)",
|
||||||
|
"recallCostMin": "Recall Cost (Min)",
|
||||||
|
"recallCostMax": "Recall Cost (Max)",
|
||||||
|
"evasionMin": "Evasion (Min)",
|
||||||
|
"evasionMax": "Evasion (Max)",
|
||||||
|
"subtype": "Subtype",
|
||||||
|
"folders": {
|
||||||
|
"characters": "Characters",
|
||||||
|
"adversaries": "Adversaries",
|
||||||
|
"ancestries": "Ancestries",
|
||||||
|
"equipment": "Equipment",
|
||||||
|
"classes": "Classes",
|
||||||
|
"subclasses": "Subclasses",
|
||||||
|
"domainCards": "Domain Cards",
|
||||||
|
"communities": "Communities",
|
||||||
|
"environments": "Environments",
|
||||||
|
"beastforms": "Beastforms",
|
||||||
|
"features": "Features",
|
||||||
|
"items": "Items",
|
||||||
|
"weapons": "Weapons",
|
||||||
|
"armors": "Armors",
|
||||||
|
"consumables": "Consumables",
|
||||||
|
"loots": "Loots"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Notifications": {
|
"Notifications": {
|
||||||
|
|
@ -2319,6 +2588,7 @@
|
||||||
"beastformInapplicable": "A beastform can only be applied to a Character.",
|
"beastformInapplicable": "A beastform can only be applied to a Character.",
|
||||||
"beastformAlreadyApplied": "The character already has a beastform applied!",
|
"beastformAlreadyApplied": "The character already has a beastform applied!",
|
||||||
"noTargetsSelected": "No targets are selected.",
|
"noTargetsSelected": "No targets are selected.",
|
||||||
|
"noTargetsSelectedOrPerm": "No targets are selected or with the update permission.",
|
||||||
"attackTargetDoesNotExist": "The target token no longer exists",
|
"attackTargetDoesNotExist": "The target token no longer exists",
|
||||||
"insufficentAdvancements": "You don't have enough advancements left.",
|
"insufficentAdvancements": "You don't have enough advancements left.",
|
||||||
"noAssignedPlayerCharacter": "You have no assigned character.",
|
"noAssignedPlayerCharacter": "You have no assigned character.",
|
||||||
|
|
@ -2346,6 +2616,8 @@
|
||||||
"wrongDomain": "The card isn't from one of your class domains.",
|
"wrongDomain": "The card isn't from one of your class domains.",
|
||||||
"cardTooHighLevel": "The card is too high level!",
|
"cardTooHighLevel": "The card is too high level!",
|
||||||
"duplicateCard": "You cannot select the same card more than once.",
|
"duplicateCard": "You cannot select the same card more than once.",
|
||||||
|
"duplicateCharacter": "This actor is already registered in the party members list.",
|
||||||
|
"onlyCharactersInPartySheet": "You can drag only characters to a party sheet.",
|
||||||
"notPrimary": "The weapon is not a primary weapon!",
|
"notPrimary": "The weapon is not a primary weapon!",
|
||||||
"notSecondary": "The weapon is not a secondary weapon!",
|
"notSecondary": "The weapon is not a secondary weapon!",
|
||||||
"itemTooHighTier": "The item must be from Tier1",
|
"itemTooHighTier": "The item must be from Tier1",
|
||||||
|
|
@ -2373,11 +2645,23 @@
|
||||||
"beastformEquipWeapon": "You cannot use weapons while in a Beastform.",
|
"beastformEquipWeapon": "You cannot use weapons while in a Beastform.",
|
||||||
"loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.",
|
"loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.",
|
||||||
"domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.",
|
"domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.",
|
||||||
"insufficientResources": "You have insufficient resources",
|
"insufficientResources": "You don't have enough resources to use that action.",
|
||||||
|
"actionNoUsesRemaining": "That action doesn't have remaining uses.",
|
||||||
"multiclassAlreadyPresent": "You already have a class and multiclass",
|
"multiclassAlreadyPresent": "You already have a class and multiclass",
|
||||||
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
|
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
|
||||||
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
|
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
|
||||||
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class."
|
"gmMenuRefresh": "You refreshed all actions and resources {types}",
|
||||||
|
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
|
||||||
|
"gmRequired": "This action requires an online GM",
|
||||||
|
"gmOnly": "This can only be accessed by the GM",
|
||||||
|
"noActorOwnership": "You do not have permissions for this character"
|
||||||
|
},
|
||||||
|
"Sidebar": {
|
||||||
|
"daggerheartMenu": {
|
||||||
|
"title": "Daggerheart Menu",
|
||||||
|
"startSession": "Start Session",
|
||||||
|
"startScene": "Start Scene"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Tooltip": {
|
"Tooltip": {
|
||||||
"disableEffect": "Disable Effect",
|
"disableEffect": "Disable Effect",
|
||||||
|
|
@ -2404,7 +2688,9 @@
|
||||||
"rulesOff": "Rules Off",
|
"rulesOff": "Rules Off",
|
||||||
"remainingUses": "Uses refresh on {type}",
|
"remainingUses": "Uses refresh on {type}",
|
||||||
"rightClickExtand": "Right-Click to extand",
|
"rightClickExtand": "Right-Click to extand",
|
||||||
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up."
|
"companionPartnerLevelBlock": "The companion needs an assigned partner to level up.",
|
||||||
|
"configureAttribution": "Configure Attribution",
|
||||||
|
"deleteItem": "Delete Item"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ 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';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -46,8 +45,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
};
|
};
|
||||||
|
|
||||||
this._dragDrop = this._createDragDropHandlers();
|
this._dragDrop = this._createDragDropHandlers();
|
||||||
|
|
||||||
this.itemBrowser = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
|
|
@ -86,9 +83,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' },
|
||||||
|
|
@ -98,6 +95,13 @@ 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: '',
|
||||||
|
|
@ -112,13 +116,6 @@ 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: '',
|
||||||
|
|
@ -159,10 +156,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 'community':
|
case 'ancestry':
|
||||||
v.disabled = this.setup.visibility < 2;
|
v.disabled = this.setup.visibility < 2;
|
||||||
break;
|
break;
|
||||||
case 'class':
|
case 'community':
|
||||||
v.disabled = this.setup.visibility < 3;
|
v.disabled = this.setup.visibility < 3;
|
||||||
break;
|
break;
|
||||||
case 'traits':
|
case 'traits':
|
||||||
|
|
@ -195,7 +192,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
async _prepareContext(_options) {
|
||||||
this.tabGroups.setup = this.tabGroups.setup ?? 'ancestry';
|
this.tabGroups.setup = this.tabGroups.setup ?? 'class';
|
||||||
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);
|
||||||
|
|
@ -269,13 +266,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 'ancestry':
|
case 'class':
|
||||||
context.nextDisabled = this.setup.visibility === 1;
|
context.nextDisabled = this.setup.visibility === 1;
|
||||||
break;
|
break;
|
||||||
case 'community':
|
case 'ancestry':
|
||||||
context.nextDisabled = this.setup.visibility === 2;
|
context.nextDisabled = this.setup.visibility === 2;
|
||||||
break;
|
break;
|
||||||
case 'class':
|
case 'community':
|
||||||
context.nextDisabled = this.setup.visibility === 3;
|
context.nextDisabled = this.setup.visibility === 3;
|
||||||
break;
|
break;
|
||||||
case 'traits':
|
case 'traits':
|
||||||
|
|
@ -366,11 +363,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.class.uuid && this.setup.subclass.uuid ? 4 : 3;
|
return this.setup.community.uuid ? 4 : 3;
|
||||||
case 2:
|
case 2:
|
||||||
return this.setup.community.uuid ? 3 : 2;
|
return this.setup.primaryAncestry.uuid ? 3 : 2;
|
||||||
case 1:
|
case 1:
|
||||||
return this.setup.primaryAncestry.uuid ? 2 : 1;
|
return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,8 +422,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
equipment = ['armor', 'weapon'];
|
equipment = ['armor', 'weapon'];
|
||||||
|
|
||||||
const presets = {
|
const presets = {
|
||||||
compendium: 'daggerheart',
|
folder: equipment.includes(type) ? `equipments.folders.${type}s` : type,
|
||||||
folder: equipment.includes(type) ? 'equipments' : type,
|
|
||||||
render: {
|
render: {
|
||||||
noFolder: true
|
noFolder: true
|
||||||
}
|
}
|
||||||
|
|
@ -442,14 +438,14 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
presets.filter = {
|
presets.filter = {
|
||||||
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
|
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (equipment.includes(type))
|
if (equipment.includes(type))
|
||||||
presets.filter = {
|
presets.filter = {
|
||||||
'system.tier': { key: 'system.tier', value: 1 },
|
'system.tier': { key: 'system.tier', value: 1 },
|
||||||
'type': { key: 'type', value: type }
|
'type': { key: 'type', value: type }
|
||||||
};
|
};
|
||||||
|
|
||||||
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true }));
|
ui.compendiumBrowser.open(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async viewItem(_, target) {
|
static async viewItem(_, target) {
|
||||||
|
|
@ -477,10 +473,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 = 'community';
|
this.tabGroups.setup = 'ancestry';
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
this.tabGroups.setup = 'class';
|
this.tabGroups.setup = 'community';
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
this.tabGroups.setup = 'traits';
|
this.tabGroups.setup = 'traits';
|
||||||
|
|
@ -567,7 +563,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
|
||||||
{ overwrite: true }
|
{ overwrite: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.itemBrowser) this.itemBrowser.close();
|
if (ui.compendiumBrowser) ui.compendiumBrowser.close();
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
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 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';
|
||||||
|
|
|
||||||
93
module/applications/dialogs/attributionDialog.mjs
Normal file
93
module/applications/dialogs/attributionDialog.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -276,7 +276,22 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
|
||||||
const featureItem = item;
|
const featureItem = item;
|
||||||
app.addEventListener(
|
app.addEventListener(
|
||||||
'close',
|
'close',
|
||||||
() => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem }),
|
async () => {
|
||||||
|
const selected = app.selected.toObject();
|
||||||
|
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(
|
||||||
|
app.configData.data.parent,
|
||||||
|
app.selected
|
||||||
|
);
|
||||||
|
if (data) {
|
||||||
|
if (!data.selectedImage) selected = null;
|
||||||
|
else {
|
||||||
|
if (data.usesDynamicToken) selected.system.tokenRingImg = data.selectedImage;
|
||||||
|
else selected.system.tokenImg = data.selectedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ selected: selected, evolved: app.evolved, hybrid: app.hybrid, item: featureItem });
|
||||||
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
app.render({ force: true });
|
app.render({ force: true });
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
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) {
|
||||||
|
|
@ -7,20 +9,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.roll?.type === 'reaction';
|
this.reactionOverride = config.actionType === '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.actions.get(config.source.action);
|
: this.item.system.actionsList?.find(a => a.id === 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'
|
||||||
|
|
@ -32,6 +34,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
updateIsAdvantage: this.updateIsAdvantage,
|
updateIsAdvantage: this.updateIsAdvantage,
|
||||||
selectExperience: this.selectExperience,
|
selectExperience: this.selectExperience,
|
||||||
toggleReaction: this.toggleReaction,
|
toggleReaction: this.toggleReaction,
|
||||||
|
toggleTagTeamRoll: this.toggleTagTeamRoll,
|
||||||
submitRoll: this.submitRoll
|
submitRoll: this.submitRoll
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
|
|
@ -42,7 +45,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
};
|
};
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
return this.config.title;
|
return `${this.config.title}${this.actor ? `: ${this.actor.name}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get actor() {
|
get actor() {
|
||||||
|
|
@ -66,7 +69,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;
|
context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
|
||||||
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,
|
||||||
|
|
@ -81,7 +84,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
|
||||||
);
|
);
|
||||||
context.costs = updatedCosts.map(x => ({
|
context.costs = updatedCosts.map(x => ({
|
||||||
...x,
|
...x,
|
||||||
label: x.keyIsID
|
label: x.itemId
|
||||||
? 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)
|
||||||
}));
|
}));
|
||||||
|
|
@ -113,15 +116,31 @@ 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 = !context.rollConfig.type && context.rollType === 'DualityRoll';
|
context.showReaction = !this.config.roll?.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 (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) {
|
||||||
|
|
@ -133,6 +152,12 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
@ -151,35 +176,38 @@ 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)
|
: [
|
||||||
: [
|
...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?.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.roll.type = this.reactionOverride
|
this.config.actionType = this.reactionOverride
|
||||||
? CONFIG.DH.ITEM.actionTypes.reaction.id
|
? CONFIG.DH.ITEM.actionTypes.reaction.id
|
||||||
: this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id
|
: this.config.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id
|
||||||
? null
|
? null
|
||||||
: this.config.roll.type;
|
: this.config.actionType;
|
||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
196
module/applications/dialogs/group-roll-dialog.mjs
Normal file
196
module/applications/dialogs/group-roll-dialog.mjs
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
67
module/applications/dialogs/imageSelectDialog.mjs
Normal file
67
module/applications/dialogs/imageSelectDialog.mjs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
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: 600,
|
||||||
|
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' },
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
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(resolve, reject, name, ownership) {
|
constructor(name, ownership, defaultOwnership) {
|
||||||
super({});
|
super({});
|
||||||
|
|
||||||
this.resolve = resolve;
|
|
||||||
this.reject = reject;
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.ownership = ownership;
|
this.ownership = foundry.utils.deepClone(ownership);
|
||||||
|
this.defaultOwnership = defaultOwnership;
|
||||||
}
|
}
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
tag: 'form',
|
tag: 'form',
|
||||||
classes: ['daggerheart', 'views', 'ownership-selection'],
|
classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'],
|
||||||
|
window: {
|
||||||
|
icon: 'fa-solid fa-users'
|
||||||
|
},
|
||||||
position: {
|
position: {
|
||||||
width: 600,
|
width: 600,
|
||||||
height: 'auto'
|
height: 'auto'
|
||||||
|
|
@ -30,43 +32,48 @@ 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 = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({
|
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
|
||||||
value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level],
|
context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels;
|
||||||
label: game.i18n.localize(`OWNERSHIP.${level}`)
|
context.defaultOwnership = this.defaultOwnership;
|
||||||
}));
|
context.ownership = game.users.reduce((acc, user) => {
|
||||||
context.ownership = {
|
if (!user.isGM) {
|
||||||
default: this.ownership.default,
|
acc[user.id] = {
|
||||||
players: Object.keys(this.ownership.players).reduce((acc, x) => {
|
...user,
|
||||||
const user = game.users.get(x);
|
img: user.character?.img ?? 'icons/svg/cowled.svg',
|
||||||
if (!user.isGM) {
|
ownership: this.getOwnershipData(user.id)
|
||||||
acc[x] = {
|
};
|
||||||
img: user.character?.img ?? 'icons/svg/cowled.svg',
|
}
|
||||||
name: user.name,
|
|
||||||
ownership: this.ownership.players[x].value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {});
|
||||||
};
|
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateData(event, _, formData) {
|
static async updateData(event, _, formData) {
|
||||||
const { ownership } = foundry.utils.expandObject(formData.object);
|
const data = foundry.utils.expandObject(formData.object);
|
||||||
|
this.close(data);
|
||||||
this.resolve(ownership);
|
|
||||||
this.close(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(fromSave) {
|
async close(data) {
|
||||||
if (!fromSave) {
|
if (data) {
|
||||||
this.reject();
|
this.saveData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
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) {
|
||||||
|
|
@ -122,6 +124,15 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
315
module/applications/dialogs/tagTeamDialog.mjs
Normal file
315
module/applications/dialogs/tagTeamDialog.mjs
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
context.selectedData = Object.values(context.members).reduce(
|
||||||
|
(acc, member) => {
|
||||||
|
if (!member.roll) return acc;
|
||||||
|
if (member.selected) {
|
||||||
|
acc.result = `${member.roll.system.roll.total} ${member.roll.system.roll.result.label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.usesDamage) {
|
||||||
|
if (!acc.damageValues) acc.damageValues = {};
|
||||||
|
for (let damage of member.damageValues) {
|
||||||
|
if (acc.damageValues[damage.key]) {
|
||||||
|
acc.damageValues[damage.key].total += damage.total;
|
||||||
|
} else {
|
||||||
|
acc.damageValues[damage.key] = foundry.utils.deepClone(damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ result: null, damageValues: null }
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
for (let roll of secondaryRolls) {
|
||||||
|
if (roll.system.hasDamage) {
|
||||||
|
for (let key in roll.system.damage) {
|
||||||
|
var damage = roll.system.damage[key];
|
||||||
|
if (systemData.damage[key]) {
|
||||||
|
systemData.damage[key].total += damage.total;
|
||||||
|
systemData.damage[key].parts = [...systemData.damage[key].parts, ...damage.parts];
|
||||||
|
} else {
|
||||||
|
systemData.damage[key] = damage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = [];
|
||||||
|
if (systemData.roll.isCritical || systemData.roll.result.duality === 1) {
|
||||||
|
const value =
|
||||||
|
memberId !== this.data.initiator.id
|
||||||
|
? 1
|
||||||
|
: this.data.initiator.cost
|
||||||
|
? 1 - this.data.initiator.cost
|
||||||
|
: 1;
|
||||||
|
resourceUpdates.push({ key: 'hope', value: value, total: -value, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
|
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 */
|
||||||
|
|
@ -11,8 +17,20 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static #nonCombatTypes = ['environment', 'companion'];
|
||||||
|
|
||||||
async _prepareContext(options) {
|
async _prepareContext(options) {
|
||||||
const context = await super._prepareContext(options);
|
const context = await super._prepareContext(options);
|
||||||
|
|
||||||
|
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) acc[key] = effect;
|
if (effect.systemEffect) acc[key] = effect;
|
||||||
|
|
@ -36,6 +54,119 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onToggleCombat() {
|
||||||
|
const tokens = canvas.tokens.controlled
|
||||||
|
.filter(t => !t.actor || !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
|
||||||
|
.map(t => t.document);
|
||||||
|
if (!this.object.controlled) tokens.push(this.document);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.document.inCombat) await TokenDocument.implementation.deleteCombatants(tokens);
|
||||||
|
else await TokenDocument.implementation.createCombatants(tokens);
|
||||||
|
} 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.active);
|
||||||
|
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 = {};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
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';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -12,8 +11,6 @@ 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() {
|
||||||
|
|
@ -540,7 +537,6 @@ 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
|
||||||
|
|
@ -559,7 +555,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true }));
|
ui.compendiumBrowser.open(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async selectPreview(_, button) {
|
static async selectPreview(_, button) {
|
||||||
|
|
@ -662,7 +658,8 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
95
module/applications/levelup/levelupViewMode.mjs
Normal file
95
module/applications/levelup/levelupViewMode.mjs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
module/applications/scene/_module.mjs
Normal file
1
module/applications/scene/_module.mjs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs';
|
||||||
63
module/applications/scene/sceneConfigSettings.mjs
Normal file
63
module/applications/scene/sceneConfigSettings.mjs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
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);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
@ -3,43 +3,48 @@ 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) {
|
||||||
constructor() {
|
/**@inheritdoc */
|
||||||
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: this.reset,
|
reset: DHAppearanceSettings.#onReset,
|
||||||
save: this.save,
|
preview: DHAppearanceSettings.#onPreview
|
||||||
preview: this.preview
|
|
||||||
},
|
},
|
||||||
form: { handler: this.updateData, submitOnChange: true }
|
form: {
|
||||||
|
closeOnSubmit: true,
|
||||||
|
handler: DHAppearanceSettings.#onSubmit
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
main: {
|
header: { template: 'systems/daggerheart/templates/settings/appearance-settings/header.hbs' },
|
||||||
template: 'systems/daggerheart/templates/settings/appearance-settings.hbs'
|
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
|
||||||
}
|
main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' },
|
||||||
|
diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' },
|
||||||
|
footer: { template: 'templates/generic/form-footer.hbs' }
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @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' },
|
||||||
|
|
@ -51,79 +56,149 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
changeTab(tab, group, options) {
|
/**@type {DhAppearance}*/
|
||||||
super.changeTab(tab, group, options);
|
setting;
|
||||||
|
|
||||||
this.render();
|
static #localized = false;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
async _preFirstRender(_context, _options) {
|
||||||
|
await super._preFirstRender(_context, _options);
|
||||||
|
if (!DHAppearanceSettings.#localized) {
|
||||||
|
foundry.helpers.Localization.localizeDataModel(this.setting.constructor);
|
||||||
|
DHAppearanceSettings.#localized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
/** @inheritdoc */
|
||||||
const context = await super._prepareContext(_options);
|
_configureRenderParts(options) {
|
||||||
context.settingFields = this.settings;
|
const parts = super._configureRenderParts(options);
|
||||||
|
if (!game.modules.get('dice-so-nice')?.active) {
|
||||||
context.showDiceSoNice = game.modules.get('dice-so-nice')?.active;
|
delete parts.diceSoNice;
|
||||||
if (game.dice3d) {
|
delete parts.tabs;
|
||||||
context.diceSoNiceTextures = game.dice3d.exports.TEXTURELIST;
|
|
||||||
context.diceSoNiceColorsets = game.dice3d.exports.COLORSETS;
|
|
||||||
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).map(key => ({
|
|
||||||
key: key,
|
|
||||||
name: `DICESONICE.Material${key.capitalize()}`
|
|
||||||
}));
|
|
||||||
context.diceSoNiceSystems = [];
|
|
||||||
for (const [key, system] of game.dice3d.DiceFactory.systems.entries()) {
|
|
||||||
context.diceSoNiceSystems.push({ key, name: system.name });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
context.diceTab = {
|
/**@inheritdoc */
|
||||||
key: this.tabGroups.diceSoNice,
|
async _prepareContext(options) {
|
||||||
source: this.settings._source.diceSoNice[this.tabGroups.diceSoNice],
|
const context = await super._prepareContext(options);
|
||||||
fields: this.settings.schema.fields.diceSoNice.fields[this.tabGroups.diceSoNice].fields
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateData(event, element, formData) {
|
/**@inheritdoc */
|
||||||
const updatedSettings = foundry.utils.expandObject(formData.object);
|
async _preparePartContext(partId, context, options) {
|
||||||
|
const partContext = await super._preparePartContext(partId, context, options);
|
||||||
await this.settings.updateSource(updatedSettings);
|
if (partId in context.tabs) partContext.tab = partContext.tabs[partId];
|
||||||
this.render();
|
switch (partId) {
|
||||||
|
case 'diceSoNice':
|
||||||
|
await this.prepareDiceSoNiceContext(partContext);
|
||||||
|
break;
|
||||||
|
case 'footer':
|
||||||
|
partContext.buttons = [
|
||||||
|
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
|
||||||
|
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return partContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async preview() {
|
/**
|
||||||
const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice];
|
* Prepare render context for the DSN part.
|
||||||
let faces = 'd12';
|
* @param {ApplicationRenderContext} context
|
||||||
switch (this.tabGroups.diceSoNice) {
|
* @returns {Promise<void>}
|
||||||
case 'advantage':
|
* @protected
|
||||||
case 'disadvantage':
|
*/
|
||||||
faces = 'd6';
|
async prepareDiceSoNiceContext(context) {
|
||||||
}
|
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
|
||||||
const preset = await getDiceSoNicePreset(source, faces);
|
(acc, [k, v]) => ({
|
||||||
const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate();
|
...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])
|
||||||
|
);
|
||||||
|
|
||||||
|
foundry.utils.mergeObject(
|
||||||
|
context.dsnTabs,
|
||||||
|
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
|
||||||
|
(acc, key) => ({
|
||||||
|
...acc,
|
||||||
|
[key]: {
|
||||||
|
values: this.setting.diceSoNice[key],
|
||||||
|
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the configuration form.
|
||||||
|
* @this {DHAppearanceSettings}
|
||||||
|
* @param {SubmitEvent} event
|
||||||
|
* @param {HTMLFormElement} form
|
||||||
|
* @param {foundry.applications.ux.FormDataExtended} formData
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async #onSubmit(event, form, formData) {
|
||||||
|
const data = this.setting.schema.clean(foundry.utils.expandObject(formData.object));
|
||||||
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the configuration form.
|
||||||
|
* @this {DHAppearanceSettings}
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #onPreview(_, target) {
|
||||||
|
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
|
||||||
|
const { diceSoNice } = foundry.utils.expandObject(formData.object);
|
||||||
|
const { key } = target.dataset;
|
||||||
|
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
|
||||||
|
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
|
||||||
|
const diceSoNiceRoll = await new foundry.dice.Roll(`1${faces}`).evaluate();
|
||||||
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() {
|
/**
|
||||||
this.settings = new DhAppearance();
|
* Reset the form back to default values.
|
||||||
this.render();
|
* @this {DHAppearanceSettings}
|
||||||
}
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
static async save() {
|
static async #onReset() {
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance, this.settings.toObject());
|
this.setting = new this.setting.constructor();
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,14 @@ 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' }],
|
tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }],
|
||||||
initial: 'general',
|
initial: 'general',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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) {
|
||||||
|
|
@ -10,11 +11,14 @@ 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.selected = this.#getDefaultAdversaryType();
|
||||||
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');
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +39,9 @@ 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,
|
||||||
reset: this.reset
|
reset: this.reset
|
||||||
},
|
},
|
||||||
|
|
@ -45,6 +52,8 @@ 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' }
|
||||||
};
|
};
|
||||||
|
|
@ -52,12 +61,19 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
main: {
|
main: {
|
||||||
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }],
|
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { 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;
|
||||||
|
|
@ -79,6 +95,11 @@ 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;
|
||||||
|
|
@ -95,33 +116,53 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addItem(_, target) {
|
static async addItem(_, target) {
|
||||||
await this.settings.updateSource({
|
const { type } = target.dataset;
|
||||||
[`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: {
|
if (['shortRest', 'longRest'].includes(type)) {
|
||||||
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
|
await this.settings.updateSource({
|
||||||
img: 'icons/magic/life/cross-worn-green.webp',
|
[`restMoves.${type}.moves.${foundry.utils.randomID()}`]: {
|
||||||
description: '',
|
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'),
|
||||||
actions: []
|
img: 'icons/magic/life/cross-worn-green.webp',
|
||||||
}
|
description: '',
|
||||||
});
|
actions: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (['armorFeatures', 'weaponFeatures'].includes(type)) {
|
||||||
|
await this.settings.updateSource({
|
||||||
|
[`itemFeatures.${type}.${foundry.utils.randomID()}`]: {
|
||||||
|
name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newFeature'),
|
||||||
|
img: 'icons/magic/life/cross-worn-green.webp',
|
||||||
|
description: '',
|
||||||
|
actions: [],
|
||||||
|
effects: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async editItem(_, target) {
|
static async editItem(_, target) {
|
||||||
const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id];
|
const { type, id } = target.dataset;
|
||||||
const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`;
|
const isDowntime = ['shortRest', 'longRest'].includes(type);
|
||||||
const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure(
|
const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`;
|
||||||
move,
|
const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id];
|
||||||
path,
|
|
||||||
this.settings
|
|
||||||
);
|
|
||||||
if (!editedMove) return;
|
|
||||||
|
|
||||||
await this.updateAction.bind(this)(editedMove, target.dataset.type, target.dataset.id);
|
const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure(
|
||||||
|
featureBase,
|
||||||
|
path,
|
||||||
|
this.settings,
|
||||||
|
{ hasIcon: isDowntime, hasEffects: !isDowntime }
|
||||||
|
);
|
||||||
|
if (!editedBase) return;
|
||||||
|
|
||||||
|
await this.updateAction.bind(this)(editedBase, 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({
|
||||||
[`restMoves.${type}.moves.${id}`]: {
|
[`${path}.${id}`]: {
|
||||||
actions: data.actions,
|
actions: data.actions,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
icon: data.icon,
|
icon: data.icon,
|
||||||
|
|
@ -129,12 +170,16 @@ 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({
|
||||||
[`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null
|
[`${path}.-=${id}`]: null
|
||||||
});
|
});
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +346,32 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ export { default as ActionConfig } from './action-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 DowntimeConfig } from './downtimeConfig.mjs';
|
export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs';
|
||||||
|
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
|
||||||
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
export { default as EnvironmentSettings } from './environment-settings.mjs';
|
||||||
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
|
export { default as ActiveEffectConfig } from './activeEffectConfig.mjs';
|
||||||
export { default as DhTokenConfig } from './token-config.mjs';
|
export { default as DhTokenConfig } from './token-config.mjs';
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
group: 'primary',
|
group: 'primary',
|
||||||
id: 'base',
|
id: 'base',
|
||||||
icon: null,
|
icon: null,
|
||||||
label: 'Base'
|
label: 'DAGGERHEART.GENERAL.Tabs.base'
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
active: false,
|
active: false,
|
||||||
|
|
@ -74,7 +74,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
group: 'primary',
|
group: 'primary',
|
||||||
id: 'config',
|
id: 'config',
|
||||||
icon: null,
|
icon: null,
|
||||||
label: 'Configuration'
|
label: 'DAGGERHEART.GENERAL.Tabs.configuration'
|
||||||
},
|
},
|
||||||
effect: {
|
effect: {
|
||||||
active: false,
|
active: false,
|
||||||
|
|
@ -82,7 +82,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
group: 'primary',
|
group: 'primary',
|
||||||
id: 'effect',
|
id: 'effect',
|
||||||
icon: null,
|
icon: null,
|
||||||
label: 'Effect'
|
label: 'DAGGERHEART.GENERAL.Tabs.effects'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -132,12 +132,19 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
|
const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts);
|
||||||
const resource = this.action.parent.resource;
|
const resource = this.action.parent.resource;
|
||||||
if (resource) {
|
if (resource) {
|
||||||
options[this.action.parent.parent.id] = {
|
options.resource = {
|
||||||
label: 'DAGGERHEART.GENERAL.itemResource',
|
label: 'DAGGERHEART.GENERAL.itemResource',
|
||||||
group: 'Global'
|
group: 'Global'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.action.parent.metadata?.isQuantifiable) {
|
||||||
|
options.quantity = {
|
||||||
|
label: 'DAGGERHEART.GENERAL.itemQuantity',
|
||||||
|
group: 'Global'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,13 +171,14 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
|
||||||
|
|
||||||
_prepareSubmitData(_event, formData) {
|
_prepareSubmitData(_event, formData) {
|
||||||
const submitData = foundry.utils.expandObject(formData.object);
|
const submitData = foundry.utils.expandObject(formData.object);
|
||||||
|
|
||||||
|
const itemAbilityCostKeys = Object.keys(CONFIG.DH.GENERAL.itemAbilityCosts);
|
||||||
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
|
for (const keyPath of this.constructor.CLEAN_ARRAYS) {
|
||||||
const data = foundry.utils.getProperty(submitData, keyPath);
|
const data = foundry.utils.getProperty(submitData, keyPath);
|
||||||
const dataValues = data ? Object.values(data) : [];
|
const dataValues = data ? Object.values(data) : [];
|
||||||
if (keyPath === 'cost') {
|
if (keyPath === 'cost') {
|
||||||
for (var value of dataValues) {
|
for (var value of dataValues) {
|
||||||
const item = this.action.parent.parent.id === value.key;
|
value.itemId = itemAbilityCostKeys.includes(value.key) ? this.action.parent.parent.id : null;
|
||||||
value.keyIsID = Boolean(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,13 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
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'],
|
||||||
|
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.isActorEffect = false;
|
||||||
|
context.isItemEffect = true;
|
||||||
|
const useGeneric = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.appearance
|
||||||
|
).showGenericStatusEffects;
|
||||||
|
if (!useGeneric) {
|
||||||
|
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 submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object);
|
||||||
|
const changes = Object.values(submitData.changes ?? {});
|
||||||
|
changes.push({});
|
||||||
|
|
||||||
|
this.effect.changes = changes;
|
||||||
|
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 changes = Object.values(submitData.changes);
|
||||||
|
const row = event.target.closest('li');
|
||||||
|
const index = Number(row.dataset.index) || 0;
|
||||||
|
changes.splice(index, 1);
|
||||||
|
|
||||||
|
this.effect.changes = changes;
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ import DHActionConfig from './action-config.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) {
|
export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
constructor(move, movePath, settings, options) {
|
constructor(move, movePath, settings, optionalParts, options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.move = move;
|
this.move = move;
|
||||||
|
|
@ -12,6 +12,10 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
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() {
|
||||||
|
|
@ -30,6 +34,7 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
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
|
||||||
},
|
},
|
||||||
|
|
@ -41,13 +46,14 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
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' }],
|
tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }],
|
||||||
initial: 'main',
|
initial: 'main',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +61,9 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -130,13 +139,30 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
}
|
}
|
||||||
|
|
||||||
static async editItem(_, target) {
|
static async editItem(_, target) {
|
||||||
const actionId = target.dataset.id;
|
const { type, id } = target.dataset;
|
||||||
const action = this.move.actions.get(actionId);
|
if (type === 'effect') {
|
||||||
await new DHActionConfig(action, async updatedMove => {
|
const effectIndex = this.move.effects.findIndex(x => x.id === id);
|
||||||
await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove });
|
const effect = this.move.effects[effectIndex];
|
||||||
|
const updatedEffect =
|
||||||
|
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
|
||||||
|
if (!updatedEffect) return;
|
||||||
|
|
||||||
|
await this.settings.updateSource({
|
||||||
|
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
|
||||||
|
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
});
|
||||||
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
||||||
this.render();
|
this.render();
|
||||||
}).render(true);
|
} else {
|
||||||
|
const action = this.move.actions.get(id);
|
||||||
|
await new DHActionConfig(action, async updatedMove => {
|
||||||
|
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove });
|
||||||
|
this.move = foundry.utils.getProperty(this.settings, this.movePath);
|
||||||
|
this.render();
|
||||||
|
}).render(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeItem(_, target) {
|
static async removeItem(_, target) {
|
||||||
|
|
@ -145,16 +171,38 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati
|
||||||
this.render();
|
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.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(move, movePath, settings, options = {}) {
|
static async configure(move, movePath, settings, optionalParts, options = {}) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const app = new this(move, movePath, settings, options);
|
const app = new this(move, movePath, settings, optionalParts, 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 });
|
||||||
});
|
});
|
||||||
|
|
@ -2,3 +2,4 @@ export { default as Adversary } from './adversary.mjs';
|
||||||
export { default as Character } from './character.mjs';
|
export { default as Character } from './character.mjs';
|
||||||
export { default as Companion } from './companion.mjs';
|
export { default as Companion } from './companion.mjs';
|
||||||
export { default as Environment } from './environment.mjs';
|
export { default as Environment } from './environment.mjs';
|
||||||
|
export { default as Party } from './party.mjs';
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,51 @@ 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: {
|
||||||
reactionRoll: AdversarySheet.#reactionRoll
|
toggleHitPoints: AdversarySheet.#toggleHitPoints,
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' },
|
limited: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/limited.hbs',
|
||||||
|
scrollable: ['.limited-container']
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs',
|
||||||
|
scrollable: ['.shortcut-items-section']
|
||||||
|
},
|
||||||
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
|
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
|
||||||
features: { template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs' },
|
features: {
|
||||||
notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' },
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/features.hbs',
|
||||||
effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' }
|
scrollable: ['.feature-section']
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs'
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs',
|
||||||
|
scrollable: ['.effects-sections']
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
|
|
@ -33,10 +60,33 @@ 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;
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +95,11 @@ 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);
|
||||||
|
|
@ -113,6 +167,27 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
||||||
/* 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}
|
||||||
|
|
@ -123,9 +198,9 @@ 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: 'reaction'
|
type: 'trait'
|
||||||
},
|
},
|
||||||
type: 'trait',
|
actionType: 'reaction',
|
||||||
hasRoll: true,
|
hasRoll: true,
|
||||||
data: this.actor.getRollData()
|
data: this.actor.getRollData()
|
||||||
};
|
};
|
||||||
|
|
@ -133,6 +208,40 @@ export default class AdversarySheet extends DHBaseActorSheet {
|
||||||
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 */
|
/* Application Listener Actions */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
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 DhCharacterlevelUp from '../../levelup/characterLevelup.mjs';
|
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
|
||||||
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
|
||||||
import FilterMenu from '../../ux/filter-menu.mjs';
|
import FilterMenu from '../../ux/filter-menu.mjs';
|
||||||
import { 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 */
|
||||||
|
|
||||||
|
|
@ -15,22 +14,34 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: ['character'],
|
classes: ['character'],
|
||||||
position: { width: 850, height: 800 },
|
position: { width: 850, height: 800 },
|
||||||
|
/* Foundry adds disabled to all buttons and inputs if editPermission is missing. This is not desired. */
|
||||||
|
editPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
|
||||||
actions: {
|
actions: {
|
||||||
toggleVault: CharacterSheet.#toggleVault,
|
toggleVault: CharacterSheet.#toggleVault,
|
||||||
rollAttribute: CharacterSheet.#rollAttribute,
|
rollAttribute: CharacterSheet.#rollAttribute,
|
||||||
|
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,
|
||||||
useDowntime: this.useDowntime,
|
useDowntime: this.useDowntime
|
||||||
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: [
|
||||||
{
|
{
|
||||||
|
|
@ -68,8 +79,14 @@ 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: {
|
||||||
|
|
@ -78,22 +95,27 @@ 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'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -118,6 +140,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
});
|
});
|
||||||
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
|
htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => {
|
||||||
element.addEventListener('change', this.updateItemQuantity.bind(this));
|
element.addEventListener('change', this.updateItemQuantity.bind(this));
|
||||||
|
element.addEventListener('click', e => e.stopPropagation());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add listener for armor marks input
|
// Add listener for armor marks input
|
||||||
|
|
@ -126,16 +149,37 @@ export default class CharacterSheet 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 _onRender(context, options) {
|
async _onRender(context, options) {
|
||||||
await super._onRender(context, options);
|
await super._onRender(context, options);
|
||||||
|
|
||||||
this.element
|
if (!this.document.testUserPermission(game.user, 'LIMITED', { exact: true })) {
|
||||||
.querySelector('.level-value')
|
this.element
|
||||||
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
|
.querySelector('.level-value')
|
||||||
|
?.addEventListener('change', event => this.document.updateLevel(Number(event.currentTarget.value)));
|
||||||
|
|
||||||
this._createFilterMenus();
|
const observer = this.document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER, {
|
||||||
this._createSearchFilter();
|
exact: true
|
||||||
|
});
|
||||||
|
if (observer) {
|
||||||
|
this.element.querySelector('.window-content').classList.add('viewMode');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._createFilterMenus();
|
||||||
|
this._createSearchFilter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -155,6 +199,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => {
|
||||||
|
acc[key] = this.document.system.resources[key];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max);
|
||||||
|
context.resources.hitPoints.emptyPips =
|
||||||
|
context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0;
|
||||||
|
context.resources.stress.emptyPips =
|
||||||
|
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
|
||||||
|
|
||||||
context.inventory = {
|
context.inventory = {
|
||||||
currency: {
|
currency: {
|
||||||
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
|
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
|
||||||
|
|
@ -210,7 +264,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.displayDomainCardsAsList);
|
context.cardView = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -586,7 +640,14 @@ 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 DhCharacterlevelUp(this.document).render({ force: true });
|
new CharacterLevelup(this.document).render({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the charater level management window in viewMode.
|
||||||
|
*/
|
||||||
|
static #viewLevelups() {
|
||||||
|
new LevelupViewMode(this.document).render({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -605,7 +666,6 @@ 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:
|
filter:
|
||||||
key === 'subclasses'
|
key === 'subclasses'
|
||||||
|
|
@ -621,7 +681,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return new ItemBrowser({ presets }).render({ force: true });
|
ui.compendiumBrowser.open(presets);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -649,31 +709,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.consumeResource(result?.costs);
|
if (result) game.system.api.fields.ActionFields.CostField.execute.call(this, result);
|
||||||
}
|
|
||||||
|
|
||||||
// Remove when Action Refactor part #2 done
|
|
||||||
async consumeResource(costs) {
|
|
||||||
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
|
||||||
|
|
@ -718,11 +754,42 @@ 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.displayDomainCardsAsList, newAbilityView);
|
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.displayDomainCardsAsCard, 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}
|
||||||
|
|
@ -762,13 +829,6 @@ 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}
|
||||||
|
|
@ -828,6 +888,23 @@ export default class CharacterSheet extends DHBaseActorSheet {
|
||||||
itemData.system.inVault = true;
|
itemData.system.inVault = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.type === 'beastform') {
|
||||||
|
if (this.document.effects.find(x => x.type === 'beastform')) {
|
||||||
|
return ui.notifications.warn(
|
||||||
|
game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData);
|
||||||
|
if (data) {
|
||||||
|
if (!data.selectedImage) return;
|
||||||
|
else {
|
||||||
|
if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage;
|
||||||
|
else itemData.system.tokenImg = data.selectedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData);
|
if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData);
|
||||||
const createdItem = await this._onDropItemCreate(itemData);
|
const createdItem = await this._onDropItemCreate(itemData);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,23 @@ 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: { template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs' }
|
effects: {
|
||||||
|
template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs',
|
||||||
|
scrollable: ['.effects-sections']
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -42,6 +51,61 @@ 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}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
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 */
|
||||||
|
|
@ -8,21 +9,39 @@ export default class DhpEnvironment extends DHBaseActorSheet {
|
||||||
classes: ['environment'],
|
classes: ['environment'],
|
||||||
position: {
|
position: {
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 725
|
height: 740
|
||||||
},
|
},
|
||||||
window: {
|
window: {
|
||||||
resizable: true
|
resizable: true,
|
||||||
|
controls: [
|
||||||
|
{
|
||||||
|
icon: 'fa-solid fa-signature',
|
||||||
|
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
|
||||||
|
action: 'editAttribution'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
toggleResourceDice: DhpEnvironment.#toggleResourceDice,
|
||||||
|
handleResourceDice: DhpEnvironment.#handleResourceDice
|
||||||
},
|
},
|
||||||
actions: {},
|
|
||||||
dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }]
|
dragDrop: [{ dragSelector: '.action-section .inventory-item', 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: { template: 'systems/daggerheart/templates/sheets/actors/environment/features.hbs' },
|
features: {
|
||||||
|
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-sections']
|
||||||
},
|
},
|
||||||
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
|
notes: { template: 'systems/daggerheart/templates/sheets/actors/environment/notes.hbs' }
|
||||||
};
|
};
|
||||||
|
|
@ -36,12 +55,25 @@ 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;
|
break;
|
||||||
case 'notes':
|
case 'notes':
|
||||||
await this._prepareNotesContext(context, options);
|
await this._prepareNotesContext(context, options);
|
||||||
|
|
@ -106,4 +138,42 @@ export default class DhpEnvironment extends DHBaseActorSheet {
|
||||||
event.dataTransfer.setDragImage(item, 60, 0);
|
event.dataTransfer.setDragImage(item, 60, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* 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;
|
||||||
|
}, {})
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
512
module/applications/sheets/actors/party.mjs
Normal file
512
module/applications/sheets/actors/party.mjs
Normal file
|
|
@ -0,0 +1,512 @@
|
||||||
|
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
|
||||||
|
},
|
||||||
|
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: '.actors-section .inventory-item', 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'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async _onRender(context, options) {
|
||||||
|
await super._onRender(context, options);
|
||||||
|
this._createFilterMenus();
|
||||||
|
this._createSearchFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Prepare Context */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
async _prepareContext(_options) {
|
||||||
|
const context = await super._prepareContext(_options);
|
||||||
|
|
||||||
|
context.inventory = {
|
||||||
|
currency: {
|
||||||
|
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
|
||||||
|
coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'),
|
||||||
|
handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'),
|
||||||
|
bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'),
|
||||||
|
chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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).render({
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #groupRoll(params) {
|
||||||
|
new GroupRollDialog(this.document.system.partyMembers).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 _onDragStart(event) {
|
||||||
|
const item = event.currentTarget.closest('.inventory-item');
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
|
||||||
|
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
|
||||||
|
event.dataTransfer.setDragImage(item, 60, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onDrop(event) {
|
||||||
|
// Prevent event bubbling to avoid duplicate handling
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
|
||||||
|
const item = await foundry.utils.fromUuid(data.uuid);
|
||||||
|
|
||||||
|
if (item instanceof DhpActor) {
|
||||||
|
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, item.uuid] });
|
||||||
|
} else if (item instanceof DHItem) {
|
||||||
|
this.document.createEmbeddedDocuments('Item', [item.toObject()]);
|
||||||
|
} else {
|
||||||
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,9 +44,8 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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',
|
||||||
|
|
@ -85,6 +84,8 @@ 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}
|
||||||
|
|
@ -98,10 +99,12 @@ 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: [
|
||||||
{
|
{
|
||||||
|
|
@ -125,6 +128,43 @@ export default function DHApplicationMixin(Base) {
|
||||||
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.
|
||||||
*/
|
*/
|
||||||
|
|
@ -164,11 +204,19 @@ 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -176,8 +224,8 @@ export default function DHApplicationMixin(Base) {
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
_syncPartState(partId, newElement, priorElement, state) {
|
_preSyncPartState(partId, newElement, priorElement, state) {
|
||||||
super._syncPartState(partId, newElement, priorElement, state);
|
super._preSyncPartState(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`;
|
||||||
|
|
@ -372,6 +420,19 @@ export default function DHApplicationMixin(Base) {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (usable) {
|
if (usable) {
|
||||||
|
options.unshift({
|
||||||
|
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.cancelBeastform',
|
||||||
|
icon: 'fa-solid fa-ban',
|
||||||
|
condition: target => {
|
||||||
|
const doc = getDocFromElementSync(target);
|
||||||
|
return doc && doc.system?.actions?.some(a => a.type === 'beastform');
|
||||||
|
},
|
||||||
|
callback: async target =>
|
||||||
|
game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(
|
||||||
|
await getDocFromElement(target)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
options.unshift({
|
options.unshift({
|
||||||
name: 'DAGGERHEART.GENERAL.damage',
|
name: 'DAGGERHEART.GENERAL.damage',
|
||||||
icon: 'fa-solid fa-explosion',
|
icon: 'fa-solid fa-explosion',
|
||||||
|
|
@ -382,7 +443,9 @@ 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;
|
||||||
return action && action.use(event, { byPassRoll: true });
|
const config = action.prepareConfig(event);
|
||||||
|
config.hasRoll = false;
|
||||||
|
return action && action.workflow.get('damage').execute(config, null, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -401,7 +464,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.id)
|
callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletable)
|
if (deletable)
|
||||||
|
|
@ -442,11 +505,12 @@ 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(context) {
|
async #prepareInventoryDescription(element) {
|
||||||
// Get all inventory item elements with a data-item-uuid attribute
|
// Get all inventory item elements with a data-item-uuid attribute
|
||||||
const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]');
|
const inventoryItems = 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 || {};
|
||||||
|
|
@ -537,28 +601,27 @@ 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.compendium = 'daggerheart';
|
presets.folder = 'equipments.folders.weapons';
|
||||||
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 }
|
||||||
|
|
@ -568,7 +631,15 @@ export default function DHApplicationMixin(Base) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ItemBrowser({ presets }).render({ force: true });
|
ui.compendiumBrowser.open(presets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the attribution dialog
|
||||||
|
* @type {ApplicationClickAction}
|
||||||
|
*/
|
||||||
|
static async #editAttribution() {
|
||||||
|
new game.system.api.applications.dialogs.AttributionDialog(this.document).render({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -640,7 +711,7 @@ export default function DHApplicationMixin(Base) {
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #toChat(_event, target) {
|
static async #toChat(_event, target) {
|
||||||
let doc = await getDocFromElement(target);
|
const doc = await getDocFromElement(target);
|
||||||
return doc.toChat(doc.uuid);
|
return doc.toChat(doc.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -649,10 +720,19 @@ export default function DHApplicationMixin(Base) {
|
||||||
* @type {ApplicationClickAction}
|
* @type {ApplicationClickAction}
|
||||||
*/
|
*/
|
||||||
static async #useItem(event, target) {
|
static async #useItem(event, target) {
|
||||||
let doc = await getDocFromElement(target);
|
const 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}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,12 @@ 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 */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -55,6 +61,13 @@ 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;
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,6 +82,31 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,16 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: ['item'],
|
classes: ['item'],
|
||||||
position: { width: 600 },
|
position: { width: 600 },
|
||||||
window: { resizable: true },
|
window: {
|
||||||
|
resizable: true,
|
||||||
|
controls: [
|
||||||
|
{
|
||||||
|
icon: 'fa-solid fa-signature',
|
||||||
|
label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
|
||||||
|
action: 'editAttribution'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
form: {
|
form: {
|
||||||
submitOnChange: true
|
submitOnChange: true
|
||||||
},
|
},
|
||||||
|
|
@ -55,6 +64,15 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
||||||
tagifyConfigs: [
|
tagifyConfigs: [
|
||||||
{
|
{
|
||||||
selector: '.features-input',
|
selector: '.features-input',
|
||||||
options: () => CONFIG.DH.ITEM.armorFeatures,
|
options: () => CONFIG.DH.ITEM.orderedArmorFeatures(),
|
||||||
callback: ArmorSheet.#onFeatureSelect
|
callback: ArmorSheet.#onFeatureSelect
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ 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']
|
||||||
|
|
@ -55,7 +59,13 @@ export default class ClassSheet extends DHBaseItemSheet {
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static TABS = {
|
static TABS = {
|
||||||
primary: {
|
primary: {
|
||||||
tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }, { id: 'effects' }],
|
tabs: [
|
||||||
|
{ id: 'description' },
|
||||||
|
{ id: 'features' },
|
||||||
|
{ id: 'settings' },
|
||||||
|
{ id: 'questions' },
|
||||||
|
{ id: 'effects' }
|
||||||
|
],
|
||||||
initial: 'description',
|
initial: 'description',
|
||||||
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
|
||||||
tagifyConfigs: [
|
tagifyConfigs: [
|
||||||
{
|
{
|
||||||
selector: '.features-input',
|
selector: '.features-input',
|
||||||
options: () => CONFIG.DH.ITEM.weaponFeatures,
|
options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(),
|
||||||
callback: WeaponSheet.#onFeatureSelect
|
callback: WeaponSheet.#onFeatureSelect
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
2
module/applications/sidebar/_module.mjs
Normal file
2
module/applications/sidebar/_module.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs';
|
||||||
|
export { default as DhSidebar } from './sidebar.mjs';
|
||||||
73
module/applications/sidebar/sidebar.mjs
Normal file
73
module/applications/sidebar/sidebar.mjs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
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;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
160
module/applications/sidebar/tabs/daggerheartMenu.mjs
Normal file
160
module/applications/sidebar/tabs/daggerheartMenu.mjs
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
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 && types.includes(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 (types.includes(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
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 * 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';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
|
import { abilities } from '../../config/actorConfig.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) {
|
||||||
|
|
@ -37,7 +38,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
name: 'Reroll Damage',
|
name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
|
||||||
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,21 +56,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
}
|
}
|
||||||
|
|
||||||
addChatListeners = async (app, html, data) => {
|
addChatListeners = async (app, html, data) => {
|
||||||
html.querySelectorAll('.duality-action-damage').forEach(element =>
|
|
||||||
element.addEventListener('click', event => this.onRollDamage(event, data.message))
|
|
||||||
);
|
|
||||||
html.querySelectorAll('.target-save').forEach(element =>
|
|
||||||
element.addEventListener('click', event => this.onRollSave(event, data.message))
|
|
||||||
);
|
|
||||||
html.querySelectorAll('.roll-all-save-button').forEach(element =>
|
|
||||||
element.addEventListener('click', event => this.onRollAllSave(event, data.message))
|
|
||||||
);
|
|
||||||
html.querySelectorAll('.simple-roll-button').forEach(element =>
|
html.querySelectorAll('.simple-roll-button').forEach(element =>
|
||||||
element.addEventListener('click', event => this.onRollSimple(event, data.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, data.message))
|
element.addEventListener('click', event => this.abilityUseButton(event, data.message))
|
||||||
);
|
);
|
||||||
|
|
@ -79,6 +68,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
html.querySelectorAll('.reroll-button').forEach(element =>
|
html.querySelectorAll('.reroll-button').forEach(element =>
|
||||||
element.addEventListener('click', event => this.rerollEvent(event, data.message))
|
element.addEventListener('click', event => this.rerollEvent(event, data.message))
|
||||||
);
|
);
|
||||||
|
html.querySelectorAll('.group-roll-button').forEach(element =>
|
||||||
|
element.addEventListener('click', event => this.groupRollButton(event, data.message))
|
||||||
|
);
|
||||||
|
html.querySelectorAll('.group-roll-reroll').forEach(element =>
|
||||||
|
element.addEventListener('click', event => this.groupRollReroll(event, data.message))
|
||||||
|
);
|
||||||
|
html.querySelectorAll('.group-roll-success').forEach(element =>
|
||||||
|
element.addEventListener('click', event => this.groupRollSuccessEvent(event, data.message))
|
||||||
|
);
|
||||||
|
html.querySelectorAll('.group-roll-header-expand-section').forEach(element =>
|
||||||
|
element.addEventListener('click', this.groupRollExpandSection)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
setupHooks() {
|
setupHooks() {
|
||||||
|
|
@ -90,80 +91,6 @@ 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(!actor.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?.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),
|
||||||
|
|
@ -197,8 +124,11 @@ 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) action.use(event, { byPassRoll: true });
|
if (event.currentTarget.dataset.directDamage) {
|
||||||
else action.use(event);
|
const config = action.prepareConfig(event);
|
||||||
|
config.hasRoll = false;
|
||||||
|
action.workflow.get('damage').execute(config, null, true);
|
||||||
|
} else action.use(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async actionUseButton(event, message) {
|
async actionUseButton(event, message) {
|
||||||
|
|
@ -249,6 +179,169 @@ 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 { 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,
|
||||||
|
resources: 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);
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
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: {
|
||||||
|
|
@ -36,10 +34,21 @@ 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 spotlightRequests = characters
|
||||||
|
?.filter(x => !x.isNPC)
|
||||||
|
.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 => x.system.spotlight.requestOrderIndex == 0),
|
||||||
|
spotlightRequests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +123,8 @@ 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);
|
||||||
|
|
@ -142,11 +152,15 @@ 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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -168,8 +182,4 @@ 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
199
module/applications/ui/countdownEdit.mjs
Normal file
199
module/applications/ui/countdownEdit.mjs
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { DhCountdown } from '../../data/countdowns.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,
|
||||||
|
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.countdownTypes = CONFIG.DH.GENERAL.countdownTypes;
|
||||||
|
context.hideNewCountdowns = this.hideNewCountdowns;
|
||||||
|
context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => {
|
||||||
|
const countdown = this.data.countdowns[key];
|
||||||
|
acc[key] = {
|
||||||
|
...countdown,
|
||||||
|
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].name),
|
||||||
|
progress: {
|
||||||
|
...countdown.progress,
|
||||||
|
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.progress.type].label)
|
||||||
|
},
|
||||||
|
editing: this.editingCountdowns.has(key)
|
||||||
|
};
|
||||||
|
|
||||||
|
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];
|
||||||
|
const wasEqual = existing && existing.progress.current === existing.progress.max;
|
||||||
|
if (wasEqual && countdown.progress.max !== existing.progress.max) {
|
||||||
|
countdown.progress.current = countdown.progress.max;
|
||||||
|
} else {
|
||||||
|
countdown.progress.current = Math.min(countdown.progress.current, countdown.progress.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideNewCountdowns = hideNewCountdowns;
|
||||||
|
this.updateSetting(settingsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 #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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,355 +1,229 @@
|
||||||
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) {
|
/**
|
||||||
constructor(basePath) {
|
* A UI element which displays the countdowns in this world.
|
||||||
super({});
|
*
|
||||||
|
* @extends ApplicationV2
|
||||||
|
* @mixes HandlebarsApplication
|
||||||
|
*/
|
||||||
|
|
||||||
this.basePath = basePath;
|
export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
}
|
constructor(options = {}) {
|
||||||
|
super(options);
|
||||||
get title() {
|
|
||||||
return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
|
this.sidebarCollapsed = true;
|
||||||
type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`)
|
this.setupHooks();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: ['daggerheart', 'dh-style', 'countdown'],
|
id: 'countdowns',
|
||||||
tag: 'form',
|
tag: 'div',
|
||||||
position: { width: 740, height: 700 },
|
classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
|
||||||
window: {
|
window: {
|
||||||
|
icon: 'fa-solid fa-clock-rotate-left',
|
||||||
frame: true,
|
frame: true,
|
||||||
title: 'Countdowns',
|
title: 'DAGGERHEART.UI.Countdowns.title',
|
||||||
resizable: true,
|
positioned: false,
|
||||||
|
resizable: false,
|
||||||
minimizable: false
|
minimizable: false
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
addCountdown: this.addCountdown,
|
toggleViewMode: DhCountdowns.#toggleViewMode,
|
||||||
removeCountdown: this.removeCountdown,
|
editCountdowns: DhCountdowns.#editCountdowns,
|
||||||
editImage: this.onEditImage,
|
decreaseCountdown: (_, target) => this.editCountdown(false, target),
|
||||||
openOwnership: this.openOwnership,
|
increaseCountdown: (_, target) => this.editCountdown(true, target)
|
||||||
openCountdownOwnership: this.openCountdownOwnership,
|
|
||||||
toggleSimpleView: this.toggleSimpleView
|
|
||||||
},
|
},
|
||||||
form: { handler: this.updateData, submitOnChange: true }
|
position: {
|
||||||
};
|
width: 400,
|
||||||
|
height: 222,
|
||||||
static PARTS = {
|
top: 50
|
||||||
countdowns: {
|
|
||||||
template: 'systems/daggerheart/templates/ui/countdowns.hbs',
|
|
||||||
scrollable: ['.expanded-view']
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_attachPartListeners(partId, htmlElement, options) {
|
/** @override */
|
||||||
super._attachPartListeners(partId, htmlElement, options);
|
static PARTS = {
|
||||||
|
resources: {
|
||||||
|
root: true,
|
||||||
|
template: 'systems/daggerheart/templates/ui/countdowns.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => {
|
get element() {
|
||||||
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false));
|
return document.body.querySelector('.daggerheart.dh-style.countdowns');
|
||||||
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);
|
||||||
|
|
||||||
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) {
|
const iconOnly =
|
||||||
const button = constructHTMLButton({
|
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
|
||||||
label: '',
|
CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
|
||||||
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'],
|
if (iconOnly) frame.classList.add('icon-only');
|
||||||
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' }
|
else frame.classList.remove('icon-only');
|
||||||
});
|
|
||||||
this.window.controls.after(button);
|
const header = frame.querySelector('.window-header');
|
||||||
|
header.querySelector('button[data-action="close"]').remove();
|
||||||
|
|
||||||
|
if (game.user.isGM) {
|
||||||
|
const editTooltip = game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle');
|
||||||
|
const editButton = `<a style="margin-right: 8px;" class="header-control" data-tooltip="${editTooltip}" aria-label="${editTooltip}" data-action="editCountdowns"><i class="fa-solid fa-wrench"></i></a>`;
|
||||||
|
header.insertAdjacentHTML('beforeEnd', editButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
|
||||||
|
const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="toggleViewMode"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
|
||||||
|
header.insertAdjacentHTML('beforeEnd', minimizeButton);
|
||||||
|
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
testUserPermission(level, exact, altSettings) {
|
/** @override */
|
||||||
if (game.user.isGM) return true;
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options);
|
||||||
const settings =
|
|
||||||
altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
|
|
||||||
const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level;
|
|
||||||
const userAllowed = exact
|
|
||||||
? settings.playerOwnership[game.user.id]?.value === level
|
|
||||||
: settings.playerOwnership[game.user.id]?.value >= level;
|
|
||||||
return defaultAllowed || userAllowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _prepareContext(_options) {
|
|
||||||
const context = await super._prepareContext(_options);
|
|
||||||
const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[
|
|
||||||
this.basePath
|
|
||||||
];
|
|
||||||
|
|
||||||
context.isGM = game.user.isGM;
|
context.isGM = game.user.isGM;
|
||||||
context.base = this.basePath;
|
context.sidebarCollapsed = this.sidebarCollapsed;
|
||||||
|
context.iconOnly =
|
||||||
|
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
|
||||||
|
CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
|
||||||
|
|
||||||
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true);
|
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
||||||
context.source = {
|
context.countdowns = Object.keys(setting.countdowns).reduce((acc, key) => {
|
||||||
...countdownData,
|
const countdown = setting.countdowns[key];
|
||||||
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
|
const ownership = DhCountdowns.#getPlayerOwnership(game.user, setting, countdown);
|
||||||
const countdown = countdownData.countdowns[key];
|
if (ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) return acc;
|
||||||
|
|
||||||
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
|
const playersWithAccess = game.users.reduce((acc, user) => {
|
||||||
acc[key] = {
|
const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown);
|
||||||
...countdown,
|
if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
|
||||||
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown)
|
acc.push(user);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, []);
|
||||||
};
|
const nonGmPlayers = game.users.filter(x => !x.isGM);
|
||||||
context.systemFields = countdownData.schema.fields;
|
acc[key] = {
|
||||||
context.countdownFields = context.systemFields.countdowns.element.fields;
|
...countdown,
|
||||||
context.simple = this.simpleView;
|
editable: game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER,
|
||||||
|
playerAccess: playersWithAccess.length !== nonGmPlayers.length ? playersWithAccess : [],
|
||||||
|
noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateData(event, _, formData) {
|
static #getPlayerOwnership(user, setting, countdown) {
|
||||||
const data = foundry.utils.expandObject(formData.object);
|
const playerOwnership = countdown.ownership[user.id];
|
||||||
const newSetting = foundry.utils.mergeObject(
|
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
|
||||||
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(),
|
? setting.defaultOwnership
|
||||||
data
|
: playerOwnership;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (game.user.isGM) {
|
toggleCollapsedPosition = async (_, collapsed) => {
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting);
|
this.sidebarCollapsed = collapsed;
|
||||||
this.render();
|
if (!collapsed) this.element.classList.add('expanded');
|
||||||
} else {
|
else this.element.classList.remove('expanded');
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
};
|
||||||
action: socketEvent.GMUpdate,
|
|
||||||
data: {
|
cooldownRefresh = ({ refreshType }) => {
|
||||||
action: GMUpdateEvent.UpdateSetting,
|
if (refreshType === RefreshType.Countdown) this.render();
|
||||||
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
|
};
|
||||||
update: newSetting
|
|
||||||
}
|
static canPerformEdit() {
|
||||||
});
|
if (game.user.isGM) return true;
|
||||||
|
|
||||||
|
const noGM = !game.users.find(x => x.isGM && x.active);
|
||||||
|
if (noGM) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSetting(update) {
|
static async #toggleViewMode() {
|
||||||
if (game.user.isGM) {
|
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update);
|
const appMode = CONFIG.DH.GENERAL.countdownAppMode;
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
|
||||||
action: socketEvent.Refresh,
|
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
|
||||||
data: {
|
|
||||||
refreshType: RefreshType.Countdown,
|
|
||||||
application: `${this.basePath}-countdowns`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.render();
|
if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
|
||||||
} else {
|
else this.element.classList.remove('icon-only');
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
|
||||||
action: socketEvent.GMUpdate,
|
|
||||||
data: {
|
|
||||||
action: GMUpdateEvent.UpdateSetting,
|
|
||||||
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
|
|
||||||
update: update,
|
|
||||||
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static onEditImage(_, target) {
|
|
||||||
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
|
|
||||||
const current = setting.countdowns[target.dataset.countdown].img;
|
|
||||||
const fp = new foundry.applications.apps.FilePicker.implementation({
|
|
||||||
current,
|
|
||||||
type: 'image',
|
|
||||||
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
|
|
||||||
top: this.position.top + 40,
|
|
||||||
left: this.position.left + 10
|
|
||||||
});
|
|
||||||
return fp.browse();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateImage(path, countdown) {
|
|
||||||
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
|
||||||
await setting.updateSource({
|
|
||||||
[`${this.basePath}.countdowns.${countdown}.img`]: path
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.updateSetting(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
static openOwnership(_, target) {
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
|
|
||||||
const ownership = { default: setting.ownership.default, players: setting.playerOwnership };
|
|
||||||
new OwnershipSelection(resolve, reject, this.title, ownership).render(true);
|
|
||||||
}).then(async ownership => {
|
|
||||||
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
|
||||||
await setting.updateSource({
|
|
||||||
[`${this.basePath}.ownership`]: ownership
|
|
||||||
});
|
|
||||||
|
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting.toObject());
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static openCountdownOwnership(_, target) {
|
|
||||||
const countdownId = target.dataset.countdown;
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]
|
|
||||||
.countdowns[countdownId];
|
|
||||||
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
|
|
||||||
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
|
|
||||||
}).then(async ownership => {
|
|
||||||
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
|
||||||
await setting.updateSource({
|
|
||||||
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
|
|
||||||
});
|
|
||||||
|
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting);
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async toggleSimpleView() {
|
|
||||||
this.simpleView = !this.simpleView;
|
|
||||||
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCountdownValue(event, increase) {
|
static async #editCountdowns() {
|
||||||
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
new game.system.api.applications.ui.CountdownEdit().render(true);
|
||||||
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
|
|
||||||
|
|
||||||
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentValue = countdown.progress.current;
|
|
||||||
|
|
||||||
if (increase && currentValue === countdown.progress.max) return;
|
|
||||||
if (!increase && currentValue === 0) return;
|
|
||||||
|
|
||||||
await countdownSetting.updateSource({
|
|
||||||
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
|
|
||||||
? currentValue + 1
|
|
||||||
: currentValue - 1
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.updateSetting(countdownSetting.toObject());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addCountdown() {
|
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.max)
|
||||||
|
: 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('collapseSidebar', this.toggleCollapsedPosition.bind());
|
||||||
|
Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
|
||||||
|
}
|
||||||
|
|
||||||
|
close(options) {
|
||||||
|
Hooks.off('collapseSidebar', this.toggleCollapsedPosition);
|
||||||
|
Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
|
||||||
|
super.close(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateCountdowns(progressType) {
|
||||||
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);
|
||||||
await countdownSetting.updateSource({
|
const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => {
|
||||||
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
|
const countdown = countdownSetting.countdowns[key];
|
||||||
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
|
if (countdown.progress.type === progressType && countdown.progress.current > 0) {
|
||||||
ownership: game.user.isGM
|
acc.push(key);
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
players: {
|
|
||||||
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
await this.updateSetting(countdownSetting.toObject());
|
return acc;
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
static async removeCountdown(_, target) {
|
const countdownData = countdownSetting.toObject();
|
||||||
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
|
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, {
|
||||||
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name;
|
...countdownData,
|
||||||
|
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
|
||||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
const countdown = foundry.utils.deepClone(countdownData.countdowns[key]);
|
||||||
window: {
|
if (updatedCountdowns.includes(key)) {
|
||||||
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownTitle')
|
countdown.progress.current -= 1;
|
||||||
},
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
acc[key] = countdown;
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
);
|
});
|
||||||
}, {});
|
|
||||||
|
|
||||||
await countdownSetting.updateSource(update);
|
const data = { refreshType: RefreshType.Countdown };
|
||||||
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting);
|
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
||||||
|
action: socketEvent.Refresh,
|
||||||
const data = { refreshType: RefreshType.Countdown };
|
data
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
});
|
||||||
action: socketEvent.Refresh,
|
Hooks.callAll(socketEvent.Refresh, data);
|
||||||
data
|
}
|
||||||
});
|
|
||||||
Hooks.callAll(socketEvent.Refresh, data);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { emitAsGM, GMUpdateEvent, socketEvent } from "../../systemRegistration/socket.mjs";
|
import { emitAsGM, GMUpdateEvent } 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)
|
if (this.currentFear > this.maxFear && game.user.isGM)
|
||||||
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,19 +106,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFear(value) {
|
async updateFear(value) {
|
||||||
return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value);
|
return emitAsGM(
|
||||||
/* if(!game.user.isGM)
|
GMUpdateEvent.UpdateFear,
|
||||||
await game.socket.emit(`system.${CONFIG.DH.id}`, {
|
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
|
||||||
action: socketEvent.GMUpdate,
|
value
|
||||||
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); */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,14 @@ 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 = options.presets;
|
this.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'],
|
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser', 'loader'],
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
window: {
|
window: {
|
||||||
frame: true,
|
frame: true,
|
||||||
|
|
@ -84,17 +82,29 @@ 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) {
|
||||||
if (context.presets?.render?.noFolder || context.presets?.render?.lite)
|
this.presets = options.presets ?? {};
|
||||||
options.parts.splice(options.parts.indexOf('sidebar'), 1);
|
const noFolder = this.presets?.render?.noFolder;
|
||||||
|
if (noFolder === true) {
|
||||||
|
this.compendiumBrowserTypeKey = 'compendiumBrowserNoFolder';
|
||||||
|
}
|
||||||
|
const lite = this.presets?.render?.lite;
|
||||||
|
if (lite === true) {
|
||||||
|
this.compendiumBrowserTypeKey = 'compendiumBrowserLite';
|
||||||
|
}
|
||||||
|
const userPresetPosition = game.user.getFlag(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
|
||||||
|
);
|
||||||
|
|
||||||
|
options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
|
||||||
|
|
||||||
|
if (!userPresetPosition) {
|
||||||
|
const width = noFolder === true || lite === true ? 600 : 850;
|
||||||
|
if (this.rendered) this.setPosition({ width });
|
||||||
|
else options.position.width = width;
|
||||||
|
}
|
||||||
|
|
||||||
await super._preRender(context, options);
|
await super._preRender(context, options);
|
||||||
}
|
}
|
||||||
|
|
@ -103,32 +113,35 @@ 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._createSearchFilter();
|
this.element
|
||||||
this._createFilterInputs();
|
.querySelectorAll('[data-action="selectFolder"]')
|
||||||
this._createDragProcess();
|
.forEach(element =>
|
||||||
|
element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.'))
|
||||||
if (context.presets?.render?.lite) this.element.classList.add('lite');
|
|
||||||
|
|
||||||
if (context.presets?.render?.noFolder) this.element.classList.add('no-folder');
|
|
||||||
|
|
||||||
if (context.presets?.render?.noFilter) this.element.classList.add('no-filter');
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
this._createSearchFilter();
|
||||||
|
|
||||||
|
this.element.classList.toggle('lite', this.presets?.render?.lite === true);
|
||||||
|
this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true);
|
||||||
|
this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true);
|
||||||
|
this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => {
|
||||||
|
element.hidden =
|
||||||
|
this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_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
|
htmlElement.querySelectorAll('[data-action="selectFolder"]').forEach(element =>
|
||||||
.querySelectorAll('[data-action="selectFolder"]')
|
element.addEventListener('contextmenu', event => {
|
||||||
.forEach(element => element.addEventListener("contextmenu", (event) => {
|
|
||||||
event.target.classList.toggle('expanded');
|
event.target.classList.toggle('expanded');
|
||||||
}))
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
@ -139,22 +152,26 @@ 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: c.label,
|
label: game.i18n.localize(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
|
||||||
|
|
@ -162,47 +179,108 @@ 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, compend, folder) {
|
static async selectFolder(_, target) {
|
||||||
const config = foundry.utils.deepClone(this.config),
|
const folderId = target?.dataset?.folderId ?? this.presets.folder,
|
||||||
compendium = compend ?? target.closest('[data-compendium-id]').dataset.compendiumId,
|
folderData = foundry.utils.getProperty(this.config, folderId) ?? {};
|
||||||
folderId = folder ?? target.dataset.folderId,
|
|
||||||
folderPath = `${compendium}.folders.${folderId}`,
|
const columns = ItemBrowser.getFolderConfig(folderData).map(col => ({
|
||||||
folderData = foundry.utils.getProperty(config, folderPath);
|
...col,
|
||||||
|
label: game.i18n.localize(col.label)
|
||||||
|
}));
|
||||||
|
|
||||||
this.selectedMenu = {
|
this.selectedMenu = {
|
||||||
path: folderPath.split('.'),
|
path: folderId?.split('.') ?? [],
|
||||||
data: {
|
data: {
|
||||||
...folderData,
|
...folderData,
|
||||||
columns: ItemBrowser.getFolderConfig(folderData)
|
columns: columns
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = [];
|
await this.render({ force: true, presets: this.presets });
|
||||||
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 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items = ItemBrowser.sortBy(items, 'name');
|
if (this.selectedMenu?.data?.type?.length) this.loadItems();
|
||||||
|
|
||||||
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('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');
|
||||||
|
|
@ -237,6 +315,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
else if (typeof f.choices === 'function') {
|
else if (typeof f.choices === 'function') {
|
||||||
f.choices = f.choices(this.items);
|
f.choices = f.choices(this.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
});
|
});
|
||||||
|
|
@ -314,6 +398,7 @@ 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;
|
||||||
|
|
@ -336,7 +421,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
item = this.items.find(i => i.uuid === itemUUID);
|
item = this.items.find(i => i.uuid === itemUUID);
|
||||||
|
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
|
|
||||||
const matchesMenu =
|
const matchesMenu =
|
||||||
this.fieldFilter.length === 0 ||
|
this.fieldFilter.length === 0 ||
|
||||||
this.fieldFilter.every(
|
this.fieldFilter.every(
|
||||||
|
|
@ -405,11 +490,13 @@ 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 aProp.innerText < bProp.innerText ? 1 : -1;
|
return aValue < bValue ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
return aProp.innerText > bProp.innerText ? 1 : -1;
|
return aValue > bValue ? 1 : -1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -438,4 +525,41 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,48 @@ 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 vagueLabel = this.constructor.getDistanceLabel(rulerValue, rangeMeasurementSettings);
|
const result = DhMeasuredTemplate.getRangeLabels(rulerValue, rangeMeasurementSettings);
|
||||||
this.ruler.text = vagueLabel;
|
this.ruler.text = result.distance + (result.units ? ' ' + result.units : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDistanceLabel(distance, settings) {
|
static getRangeLabels(distanceValue, settings) {
|
||||||
if (distance <= settings.melee) {
|
let result = { distance: distanceValue, units: '' };
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
|
const sceneRangeMeasurement = canvas.scene.flags.daggerheart?.rangeMeasurement;
|
||||||
}
|
|
||||||
if (distance <= settings.veryClose) {
|
const { disable, custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
|
if (sceneRangeMeasurement.setting === disable.id) {
|
||||||
}
|
result.distance = distanceValue;
|
||||||
if (distance <= settings.close) {
|
result.units = canvas.scene?.grid?.units;
|
||||||
return game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
|
return result;
|
||||||
}
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
const melee = sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.melee : settings.melee;
|
||||||
|
const veryClose =
|
||||||
|
sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.veryClose : settings.veryClose;
|
||||||
|
const close = sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.close : settings.close;
|
||||||
|
const far = sceneRangeMeasurement.setting === custom.id ? sceneRangeMeasurement.far : settings.far;
|
||||||
|
if (distanceValue <= melee) {
|
||||||
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (distanceValue <= veryClose) {
|
||||||
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (distanceValue <= close) {
|
||||||
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (distanceValue <= far) {
|
||||||
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (distanceValue > far) {
|
||||||
|
result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
|
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
|
||||||
context.cost = { total: distance, units: null };
|
context.cost = { total: result.distance, units: result.units };
|
||||||
context.distance = { total: distance, units: null };
|
context.distance = { total: result.distance, units: result.units };
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
|
||||||
|
|
@ -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 distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range);
|
const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range);
|
||||||
context.cost = { total: distance, units: null };
|
context.cost = { total: result.distance, units: result.units };
|
||||||
context.distance = { total: distance, units: null };
|
context.distance = { total: result.distance, units: result.units };
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,11 @@ export const adversaryTypes = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export const displayDomainCardsAsList = 'displayDomainCardsAsList';
|
export const displayDomainCardsAsCard = 'displayDomainCardsAsCard';
|
||||||
export const narrativeCountdown = {
|
export const narrativeCountdown = {
|
||||||
simple: 'countdown-narrative-simple',
|
simple: 'countdown-narrative-simple',
|
||||||
position: 'countdown-narrative-position'
|
position: 'countdown-narrative-position'
|
||||||
|
|
@ -8,8 +8,21 @@ 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'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -90,22 +90,22 @@ export const rangeInclusion = {
|
||||||
export const otherTargetTypes = {
|
export const otherTargetTypes = {
|
||||||
friendly: {
|
friendly: {
|
||||||
id: 'friendly',
|
id: 'friendly',
|
||||||
label: 'Friendly'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.friendly'
|
||||||
},
|
},
|
||||||
hostile: {
|
hostile: {
|
||||||
id: 'hostile',
|
id: 'hostile',
|
||||||
label: 'Hostile'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.hostile'
|
||||||
},
|
},
|
||||||
any: {
|
any: {
|
||||||
id: 'any',
|
id: 'any',
|
||||||
label: 'Any'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.any'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const targetTypes = {
|
export const targetTypes = {
|
||||||
self: {
|
self: {
|
||||||
id: 'self',
|
id: 'self',
|
||||||
label: 'Self'
|
label: 'DAGGERHEART.CONFIG.TargetTypes.self'
|
||||||
},
|
},
|
||||||
...otherTargetTypes
|
...otherTargetTypes
|
||||||
};
|
};
|
||||||
|
|
@ -164,28 +164,36 @@ 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',
|
||||||
|
|
@ -204,8 +212,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: () => ({
|
||||||
|
|
@ -561,6 +569,19 @@ 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',
|
||||||
|
|
@ -574,19 +595,20 @@ export const abilityCosts = {
|
||||||
},
|
},
|
||||||
hope: {
|
hope: {
|
||||||
id: 'hope',
|
id: 'hope',
|
||||||
label: 'Hope',
|
label: 'DAGGERHEART.CONFIG.HealingType.hope.name',
|
||||||
group: 'TYPES.Actor.character'
|
group: 'TYPES.Actor.character'
|
||||||
},
|
},
|
||||||
armor: {
|
armor: {
|
||||||
id: 'armor',
|
id: 'armor',
|
||||||
label: 'Armor Slot',
|
label: 'DAGGERHEART.CONFIG.HealingType.armor.name',
|
||||||
group: 'TYPES.Actor.character'
|
group: 'TYPES.Actor.character'
|
||||||
},
|
},
|
||||||
fear: {
|
fear: {
|
||||||
id: 'fear',
|
id: 'fear',
|
||||||
label: 'Fear',
|
label: 'DAGGERHEART.CONFIG.HealingType.fear.name',
|
||||||
group: 'TYPES.Actor.adversary'
|
group: 'TYPES.Actor.adversary'
|
||||||
}
|
},
|
||||||
|
resource: itemAbilityCosts.resource
|
||||||
};
|
};
|
||||||
|
|
||||||
export const countdownTypes = {
|
export const countdownTypes = {
|
||||||
|
|
@ -624,8 +646,57 @@ 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',
|
||||||
|
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative'
|
||||||
|
},
|
||||||
|
encounter: {
|
||||||
|
id: 'encounter',
|
||||||
|
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,63 +3,63 @@ export const typeConfig = {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: 'system.tier',
|
key: 'system.tier',
|
||||||
label: 'Tier'
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.type',
|
key: 'system.type',
|
||||||
label: 'Type'
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'system.tier',
|
key: 'system.tier',
|
||||||
label: 'Tier',
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
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: 'Type',
|
label: 'DAGGERHEART.GENERAL.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: 'Difficulty (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMin',
|
||||||
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: 'Difficulty (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.difficultyMax',
|
||||||
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: 'Hit Points (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
|
||||||
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: 'Hit Points (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
|
||||||
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: 'Stress (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.stressMin',
|
||||||
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: 'Stress (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.stressMax',
|
||||||
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'
|
||||||
}
|
}
|
||||||
|
|
@ -69,22 +69,22 @@ export const typeConfig = {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: 'type',
|
key: 'type',
|
||||||
label: 'Type'
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.secondary',
|
key: 'system.secondary',
|
||||||
label: 'Subtype',
|
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
||||||
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
|
format: isSecondary => (isSecondary ? 'secondary' : isSecondary === false ? 'primary' : '-')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.tier',
|
key: 'system.tier',
|
||||||
label: 'Tier'
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'type',
|
key: 'type',
|
||||||
label: 'Type',
|
label: 'DAGGERHEART.GENERAL.type',
|
||||||
choices: () =>
|
choices: () =>
|
||||||
CONFIG.Item.documentClass.TYPES.filter(t =>
|
CONFIG.Item.documentClass.TYPES.filter(t =>
|
||||||
['armor', 'weapon', 'consumable', 'loot'].includes(t)
|
['armor', 'weapon', 'consumable', 'loot'].includes(t)
|
||||||
|
|
@ -92,15 +92,15 @@ export const typeConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.secondary',
|
key: 'system.secondary',
|
||||||
label: 'Subtype',
|
label: 'DAGGERHEART.UI.ItemBrowser.subtype',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: false, label: 'Primary Weapon' },
|
{ value: false, label: 'DAGGERHEART.ITEMS.Weapon.primaryWeapon' },
|
||||||
{ value: true, label: 'Secondary Weapon' }
|
{ value: true, label: 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.tier',
|
key: 'system.tier',
|
||||||
label: 'Tier',
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
choices: [
|
choices: [
|
||||||
{ value: '1', label: '1' },
|
{ value: '1', label: '1' },
|
||||||
{ value: '2', label: '2' },
|
{ value: '2', label: '2' },
|
||||||
|
|
@ -110,36 +110,36 @@ export const typeConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.burden',
|
key: 'system.burden',
|
||||||
label: 'Burden',
|
label: 'DAGGERHEART.GENERAL.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: 'Trait',
|
label: 'DAGGERHEART.GENERAL.Trait.single',
|
||||||
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: 'Range',
|
label: 'DAGGERHEART.GENERAL.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: 'Armor Score (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMin',
|
||||||
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: 'Armor Score (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.armorScoreMax',
|
||||||
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: 'Features',
|
label: 'DAGGERHEART.GENERAL.features',
|
||||||
choices: () =>
|
choices: () =>
|
||||||
[
|
[
|
||||||
...Object.entries(CONFIG.DH.ITEM.weaponFeatures),
|
...Object.entries(CONFIG.DH.ITEM.weaponFeatures),
|
||||||
|
|
@ -149,6 +149,102 @@ export const typeConfig = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
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: []
|
||||||
|
|
@ -157,54 +253,54 @@ export const typeConfig = {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: 'system.type',
|
key: 'system.type',
|
||||||
label: 'Type'
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.domain',
|
key: 'system.domain',
|
||||||
label: 'Domain'
|
label: 'DAGGERHEART.GENERAL.Domain.single'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.level',
|
key: 'system.level',
|
||||||
label: 'Level'
|
label: 'DAGGERHEART.GENERAL.level'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'system.type',
|
key: 'system.type',
|
||||||
label: 'Type',
|
label: 'DAGGERHEART.GENERAL.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: 'Domain',
|
label: 'DAGGERHEART.GENERAL.Domain.single',
|
||||||
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: 'Level (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.levelMin',
|
||||||
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: 'Level (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.levelMax',
|
||||||
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: 'Recall Cost (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.recallCostMin',
|
||||||
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: 'Recall Cost (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.recallCostMax',
|
||||||
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
|
field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost',
|
||||||
operator: 'lte'
|
operator: 'lte'
|
||||||
}
|
}
|
||||||
|
|
@ -214,50 +310,50 @@ export const typeConfig = {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: 'system.evasion',
|
key: 'system.evasion',
|
||||||
label: 'Evasion'
|
label: 'DAGGERHEART.GENERAL.evasion'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.hitPoints',
|
key: 'system.hitPoints',
|
||||||
label: 'Hit Points'
|
label: 'DAGGERHEART.GENERAL.HitPoints.plural'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.domains',
|
key: 'system.domains',
|
||||||
label: 'Domains'
|
label: 'DAGGERHEART.GENERAL.Domain.plural'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'system.evasion',
|
key: 'system.evasion',
|
||||||
name: 'evasion.min',
|
name: 'evasion.min',
|
||||||
label: 'Evasion (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.evasionMin',
|
||||||
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: 'Evasion (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.evasionMax',
|
||||||
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: 'Hit Points (Min)',
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMin',
|
||||||
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: 'Hit Points (Max)',
|
label: 'DAGGERHEART.UI.ItemBrowser.hitPointsMax',
|
||||||
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: 'Domains',
|
label: 'DAGGERHEART.GENERAL.Domain.plural',
|
||||||
choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label })),
|
choices: () => Object.values(CONFIG.DH.DOMAIN.allDomains()).map(d => ({ value: d.id, label: d.label })),
|
||||||
operator: 'contains2'
|
operator: 'contains2'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -271,17 +367,20 @@ export const typeConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.spellcastingTrait',
|
key: 'system.spellcastingTrait',
|
||||||
label: 'Spellcasting Trait'
|
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'system.linkedClass.uuid',
|
key: 'system.linkedClass.uuid',
|
||||||
label: 'Class',
|
label: 'Class',
|
||||||
choices: (items) => {
|
choices: items => {
|
||||||
const list = items.map(item => ({ value: item.system.linkedClass.uuid, label: item.system.linkedClass.name }));
|
const list = items.map(item => ({
|
||||||
return list.reduce((a,c) => {
|
value: item.system.linkedClass.uuid,
|
||||||
if(!(a.find(i => i.value === c.value))) a.push(c);
|
label: item.system.linkedClass.name
|
||||||
|
}));
|
||||||
|
return list.reduce((a, c) => {
|
||||||
|
if (!a.find(i => i.value === c.value)) a.push(c);
|
||||||
return a;
|
return a;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
@ -292,22 +391,22 @@ export const typeConfig = {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
key: 'system.tier',
|
key: 'system.tier',
|
||||||
label: 'Tier'
|
label: 'DAGGERHEART.GENERAL.Tiers.singular'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'system.mainTrait',
|
key: 'system.mainTrait',
|
||||||
label: 'Main Trait'
|
label: 'DAGGERHEART.GENERAL.Trait.single'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'system.tier',
|
key: 'system.tier',
|
||||||
label: 'Tier',
|
label: 'DAGGERHEART.GENERAL.Tiers.singular',
|
||||||
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: 'Main Trait',
|
label: 'DAGGERHEART.GENERAL.Trait.single',
|
||||||
field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait'
|
field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -315,109 +414,144 @@ export const typeConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const compendiumConfig = {
|
export const compendiumConfig = {
|
||||||
daggerheart: {
|
characters: {
|
||||||
id: 'daggerheart',
|
id: 'characters',
|
||||||
label: 'DAGGERHEART',
|
keys: ['characters'],
|
||||||
folders: {
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.characters',
|
||||||
adversaries: {
|
type: ['character']
|
||||||
id: 'adversaries',
|
// listType: 'characters'
|
||||||
keys: ['adversaries'],
|
},
|
||||||
label: 'Adversaries',
|
adversaries: {
|
||||||
type: ['adversary'],
|
id: 'adversaries',
|
||||||
listType: 'adversaries'
|
keys: ['adversaries'],
|
||||||
},
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.adversaries',
|
||||||
ancestries: {
|
type: ['adversary'],
|
||||||
id: 'ancestries',
|
listType: 'adversaries'
|
||||||
|
},
|
||||||
|
ancestries: {
|
||||||
|
id: 'ancestries',
|
||||||
|
keys: ['ancestries'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.ancestries',
|
||||||
|
type: ['ancestry']
|
||||||
|
/* folders: {
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
keys: ['ancestries'],
|
keys: ['ancestries'],
|
||||||
label: 'Ancestries',
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
type: ['ancestry'],
|
type: ['feature']
|
||||||
folders: {
|
}
|
||||||
features: {
|
} */
|
||||||
id: 'features',
|
},
|
||||||
keys: ['ancestries'],
|
equipments: {
|
||||||
label: 'Features',
|
id: 'equipments',
|
||||||
type: ['feature']
|
keys: ['armors', 'weapons', 'consumables', 'loot'],
|
||||||
}
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.equipment',
|
||||||
}
|
type: ['armor', 'weapon', 'consumable', 'loot'],
|
||||||
|
listType: 'items',
|
||||||
|
folders: {
|
||||||
|
weapons: {
|
||||||
|
id: 'weapons',
|
||||||
|
keys: ['weapons'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.weapons',
|
||||||
|
type: ['weapon'],
|
||||||
|
listType: 'weapons'
|
||||||
},
|
},
|
||||||
equipments: {
|
armors: {
|
||||||
id: 'equipments',
|
id: 'armors',
|
||||||
keys: ['armors', 'weapons', 'consumables', 'loot'],
|
keys: ['armors'],
|
||||||
label: 'Equipment',
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.armors',
|
||||||
type: ['armor', 'weapon', 'consumable', 'loot'],
|
type: ['armor'],
|
||||||
listType: 'items'
|
listType: 'armors'
|
||||||
},
|
},
|
||||||
classes: {
|
consumables: {
|
||||||
id: 'classes',
|
id: 'consumables',
|
||||||
keys: ['classes'],
|
keys: ['consumables'],
|
||||||
label: 'Classes',
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.consumables',
|
||||||
type: ['class'],
|
type: ['consumable']
|
||||||
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: {
|
loots: {
|
||||||
id: 'subclasses',
|
id: 'loots',
|
||||||
keys: ['subclasses'],
|
keys: ['loots'],
|
||||||
label: 'Subclasses',
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.loots',
|
||||||
type: ['subclass'],
|
type: ['loot']
|
||||||
listType: 'subclasses'
|
|
||||||
},
|
|
||||||
domains: {
|
|
||||||
id: 'domains',
|
|
||||||
keys: ['domains'],
|
|
||||||
label: 'Domain Cards',
|
|
||||||
type: ['domainCard'],
|
|
||||||
listType: 'cards'
|
|
||||||
},
|
|
||||||
communities: {
|
|
||||||
id: 'communities',
|
|
||||||
keys: ['communities'],
|
|
||||||
label: 'Communities',
|
|
||||||
type: ['community'],
|
|
||||||
folders: {
|
|
||||||
features: {
|
|
||||||
id: 'features',
|
|
||||||
keys: ['communities'],
|
|
||||||
label: 'Features',
|
|
||||||
type: ['feature']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
environments: {
|
|
||||||
id: 'environments',
|
|
||||||
keys: ['environments'],
|
|
||||||
label: 'Environments',
|
|
||||||
type: ['environment']
|
|
||||||
},
|
|
||||||
beastforms: {
|
|
||||||
id: 'beastforms',
|
|
||||||
keys: ['beastforms'],
|
|
||||||
label: 'Beastforms',
|
|
||||||
type: ['beastform'],
|
|
||||||
listType: 'beastforms',
|
|
||||||
folders: {
|
|
||||||
features: {
|
|
||||||
id: 'features',
|
|
||||||
keys: ['beastforms'],
|
|
||||||
label: 'Features',
|
|
||||||
type: ['feature']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
classes: {
|
||||||
|
id: 'classes',
|
||||||
|
keys: ['classes'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.classes',
|
||||||
|
type: ['class'],
|
||||||
|
/* folders: {
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['classes'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
|
type: ['feature']
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
id: 'items',
|
||||||
|
keys: ['classes'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.items',
|
||||||
|
type: ['armor', 'weapon', 'consumable', 'loot'],
|
||||||
|
listType: 'items'
|
||||||
|
}
|
||||||
|
}, */
|
||||||
|
listType: 'classes'
|
||||||
|
},
|
||||||
|
subclasses: {
|
||||||
|
id: 'subclasses',
|
||||||
|
keys: ['subclasses'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.subclasses',
|
||||||
|
type: ['subclass'],
|
||||||
|
listType: 'subclasses'
|
||||||
|
},
|
||||||
|
domains: {
|
||||||
|
id: 'domains',
|
||||||
|
keys: ['domains'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.domainCards',
|
||||||
|
type: ['domainCard'],
|
||||||
|
listType: 'cards'
|
||||||
|
},
|
||||||
|
communities: {
|
||||||
|
id: 'communities',
|
||||||
|
keys: ['communities'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.communities',
|
||||||
|
type: ['community']
|
||||||
|
/* folders: {
|
||||||
|
features: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['communities'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.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: {
|
||||||
|
id: 'features',
|
||||||
|
keys: ['features'],
|
||||||
|
label: 'DAGGERHEART.UI.ItemBrowser.folders.features',
|
||||||
|
type: ['feature']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -452,6 +452,34 @@ 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];
|
||||||
|
acc[key] = { ...feature, label: feature.name };
|
||||||
|
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',
|
||||||
|
|
@ -865,6 +893,9 @@ export const weaponFeatures = {
|
||||||
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: [
|
||||||
{
|
{
|
||||||
|
|
@ -1380,6 +1411,34 @@ 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];
|
||||||
|
acc[key] = { ...feature, label: feature.name };
|
||||||
|
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 featureTypes = {
|
export const featureTypes = {
|
||||||
ancestry: {
|
ancestry: {
|
||||||
id: 'ancestry',
|
id: 'ancestry',
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,25 @@ export const gameSettings = {
|
||||||
},
|
},
|
||||||
LevelTiers: 'LevelTiers',
|
LevelTiers: 'LevelTiers',
|
||||||
Countdowns: 'Countdowns',
|
Countdowns: 'Countdowns',
|
||||||
LastMigrationVersion: 'LastMigrationVersion'
|
LastMigrationVersion: 'LastMigrationVersion',
|
||||||
|
TagTeamRoll: 'TagTeamRoll'
|
||||||
|
};
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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 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';
|
||||||
|
|
@ -7,3 +8,4 @@ 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';
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
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 { 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
|
||||||
|
|
@ -20,6 +15,7 @@ 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() }),
|
||||||
|
|
@ -32,36 +28,82 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
actionType: new fields.StringField({
|
actionType: new fields.StringField({
|
||||||
choices: CONFIG.DH.ITEM.actionTypes,
|
choices: CONFIG.DH.ITEM.actionTypes,
|
||||||
initial: 'action',
|
initial: 'action',
|
||||||
nullable: true
|
nullable: false,
|
||||||
|
required: true
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
this.extraSchemas.forEach(s => {
|
this.extraSchemas.forEach(s => {
|
||||||
let clsField;
|
let clsField = this.getActionField(s);
|
||||||
if ((clsField = this.getActionField(s))) schemaFields[s] = new clsField();
|
if (clsField) schemaFields[s] = new clsField();
|
||||||
});
|
});
|
||||||
|
|
||||||
return schemaFields;
|
return schemaFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
@ -74,6 +116,11 @@ 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) {
|
||||||
|
|
@ -96,6 +143,11 @@ 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 = {}) {
|
||||||
if (!this.actor) return null;
|
if (!this.actor) return null;
|
||||||
const actorData = this.actor.getRollData(false);
|
const actorData = this.actor.getRollData(false);
|
||||||
|
|
@ -111,19 +163,30 @@ 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.");
|
||||||
|
|
||||||
if (this.chatDisplay) await this.toChat();
|
if (this.chatDisplay) await this.toChat();
|
||||||
let { byPassRoll } = options,
|
|
||||||
config = this.prepareConfig(event, byPassRoll);
|
let config = this.prepareConfig(event);
|
||||||
for (let i = 0; i < this.constructor.extraSchemas.length; i++) {
|
if (!config) return;
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -133,273 +196,116 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.hasRoll) {
|
// Execute the Action Worflow in order based of schema fields
|
||||||
const rollConfig = this.prepareRoll(config);
|
await this.executeWorkflow(config);
|
||||||
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;
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* */
|
/**
|
||||||
prepareConfig(event, byPass = false) {
|
* Create the basic config common to every action type
|
||||||
const hasRoll = this.getUseHasRoll(byPass);
|
* @param {Event} event Event from the button used to trigger the Action
|
||||||
return {
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
prepareBaseConfig(event) {
|
||||||
|
const config = {
|
||||||
event,
|
event,
|
||||||
title: `${this.item.name}: ${game.i18n.localize(this.name)}`,
|
title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`,
|
||||||
source: {
|
source: {
|
||||||
item: this.item._id,
|
item: this.item._id,
|
||||||
action: this._id,
|
action: this._id,
|
||||||
actor: this.actor.uuid
|
actor: this.actor.uuid
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {},
|
||||||
configure: hasRoll
|
actionType: this.actionType,
|
||||||
},
|
hasRoll: this.hasRoll,
|
||||||
type: this.roll?.type ?? this.type,
|
hasDamage: this.hasDamage,
|
||||||
hasRoll: hasRoll,
|
hasHealing: this.hasHealing,
|
||||||
hasDamage: this.damage?.parts?.length && this.type !== 'healing',
|
hasEffect: this.hasEffect,
|
||||||
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: hasRoll
|
evaluate: this.hasRoll
|
||||||
};
|
};
|
||||||
|
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() {
|
/**
|
||||||
const roll = {
|
* Consume Action configured resources & uses.
|
||||||
baseModifiers: this.roll.getModifier(),
|
* That method is only used when we want those resources to be consumed outside of the use method workflow.
|
||||||
label: 'Attack',
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
type: this.actionType,
|
* @param {boolean} successCost
|
||||||
difficulty: this.roll?.difficulty,
|
*/
|
||||||
formula: this.roll.getFormula(),
|
|
||||||
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
|
|
||||||
};
|
|
||||||
if (this.roll?.type === 'diceSet' || !this.hasRoll) roll.lite = true;
|
|
||||||
|
|
||||||
return roll;
|
|
||||||
}
|
|
||||||
|
|
||||||
doFollowUp(config) {
|
|
||||||
return !config.hasRoll;
|
|
||||||
}
|
|
||||||
|
|
||||||
async consume(config, successCost = false) {
|
async consume(config, successCost = false) {
|
||||||
const actor = this.actor.system.partner ?? this.actor,
|
await this.workflow.get('cost')?.execute(config, successCost);
|
||||||
usefulResources = {
|
await this.workflow.get('uses')?.execute(config, successCost);
|
||||||
...foundry.utils.deepClone(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 (config.roll && !config.roll.success && successCost) {
|
||||||
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 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 */
|
/**
|
||||||
getUseHasRoll(byPass = false) {
|
* Set if a configuration dialog must be shown or not if a special keyboard key is pressed.
|
||||||
return this.hasRoll && !byPass;
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
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 modifiers() {
|
get hasDamage() {
|
||||||
if (!this.actor) return [];
|
return this.damage?.parts?.length && this.type !== 'healing';
|
||||||
const modifiers = [];
|
}
|
||||||
/** Placeholder for specific bonuses **/
|
|
||||||
return modifiers;
|
get hasHealing() {
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
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();
|
||||||
|
|
@ -82,5 +81,5 @@ export default class DhBeastformAction extends DHBaseAction {
|
||||||
beastformEffects.map(x => x.id)
|
beastformEffects.map(x => x.id)
|
||||||
);
|
);
|
||||||
return existingEffects;
|
return existingEffects;
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,5 @@
|
||||||
import { setsEqual } from '../../helpers/utils.mjs';
|
|
||||||
import DHBaseAction from './baseAction.mjs';
|
import DHBaseAction from './baseAction.mjs';
|
||||||
|
|
||||||
export default class DHDamageAction extends DHBaseAction {
|
export default class DHDamageAction extends DHBaseAction {
|
||||||
static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects'];
|
static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects'];
|
||||||
|
|
||||||
getFormulaValue(part, data) {
|
|
||||||
let formulaValue = part.value;
|
|
||||||
|
|
||||||
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
|
|
||||||
|
|
||||||
const isAdversary = this.actor.type === 'adversary';
|
|
||||||
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
|
|
||||||
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
|
|
||||||
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formulaValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatFormulas(formulas, systemData) {
|
|
||||||
const formattedFormulas = [];
|
|
||||||
formulas.forEach(formula => {
|
|
||||||
if (isNaN(formula.formula))
|
|
||||||
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData));
|
|
||||||
const same = formattedFormulas.find(
|
|
||||||
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
|
|
||||||
);
|
|
||||||
if (same) same.formula += ` + ${formula.formula}`;
|
|
||||||
else formattedFormulas.push(formula);
|
|
||||||
});
|
|
||||||
return formattedFormulas;
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollDamage(event, data) {
|
|
||||||
const systemData = data.system ?? data;
|
|
||||||
|
|
||||||
let formulas = this.damage.parts.map(p => ({
|
|
||||||
formula: this.getFormulaValue(p, systemData).getFormula(this.actor),
|
|
||||||
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
|
|
||||||
applyTo: p.applyTo
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!formulas.length) return;
|
|
||||||
|
|
||||||
formulas = this.formatFormulas(formulas, systemData);
|
|
||||||
|
|
||||||
delete systemData.evaluate;
|
|
||||||
const config = {
|
|
||||||
...systemData,
|
|
||||||
roll: formulas,
|
|
||||||
dialog: {},
|
|
||||||
data: this.getRollData()
|
|
||||||
};
|
|
||||||
if (this.hasSave) config.onSave = this.save.damageMod;
|
|
||||||
if (data.system) {
|
|
||||||
config.source.message = data._id;
|
|
||||||
config.directDamage = false;
|
|
||||||
} else {
|
|
||||||
config.directDamage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CONFIG.Dice.daggerheart.DamageRoll.build(config);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,4 @@ import DHBaseAction from './baseAction.mjs';
|
||||||
|
|
||||||
export default class DHMacroAction extends DHBaseAction {
|
export default class DHMacroAction extends DHBaseAction {
|
||||||
static extraSchemas = [...super.extraSchemas, 'macro'];
|
static extraSchemas = [...super.extraSchemas, 'macro'];
|
||||||
|
|
||||||
async trigger(event, ...args) {
|
|
||||||
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
|
|
||||||
macro = await fromUuid(fixUUID);
|
|
||||||
try {
|
|
||||||
if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`);
|
|
||||||
macro.execute();
|
|
||||||
} catch (error) {
|
|
||||||
ui.notifications.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,24 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDefaultObject() {
|
||||||
|
return {
|
||||||
|
name: 'New Effect',
|
||||||
|
id: foundry.utils.randomID(),
|
||||||
|
disabled: false,
|
||||||
|
img: 'icons/magic/life/heart-cross-blue.webp',
|
||||||
|
description: '',
|
||||||
|
statuses: [],
|
||||||
|
changes: [],
|
||||||
|
system: {
|
||||||
|
rangeDependence: {
|
||||||
|
enabled: false,
|
||||||
|
type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id,
|
||||||
|
target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id,
|
||||||
|
range: CONFIG.DH.GENERAL.range.melee.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ import DhCharacter from './character.mjs';
|
||||||
import DhCompanion from './companion.mjs';
|
import DhCompanion from './companion.mjs';
|
||||||
import DhAdversary from './adversary.mjs';
|
import DhAdversary from './adversary.mjs';
|
||||||
import DhEnvironment from './environment.mjs';
|
import DhEnvironment from './environment.mjs';
|
||||||
|
import DhParty from './party.mjs';
|
||||||
|
|
||||||
export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment };
|
export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty };
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
character: DhCharacter,
|
character: DhCharacter,
|
||||||
companion: DhCompanion,
|
companion: DhCompanion,
|
||||||
adversary: DhAdversary,
|
adversary: DhAdversary,
|
||||||
environment: DhEnvironment
|
environment: DhEnvironment,
|
||||||
|
party: DhParty
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
return foundry.utils.mergeObject(super.metadata, {
|
return foundry.utils.mergeObject(super.metadata, {
|
||||||
label: 'TYPES.Actor.adversary',
|
label: 'TYPES.Actor.adversary',
|
||||||
type: 'adversary',
|
type: 'adversary',
|
||||||
settingSheet: DHAdversarySettings
|
settingSheet: DHAdversarySettings,
|
||||||
|
hasAttribution: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
}),
|
}),
|
||||||
type: new fields.StringField({
|
type: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: CONFIG.DH.ACTOR.adversaryTypes,
|
choices: CONFIG.DH.ACTOR.allAdversaryTypes,
|
||||||
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
|
initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
|
||||||
}),
|
}),
|
||||||
motivesAndTactics: new fields.StringField(),
|
motivesAndTactics: new fields.StringField(),
|
||||||
|
|
@ -169,4 +170,13 @@ export default class DhpAdversary extends BaseDataActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getTags() {
|
||||||
|
const tags = [
|
||||||
|
game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
|
||||||
|
`${game.i18n.localize(`DAGGERHEART.CONFIG.AdversaryType.${this.type}.label`)}`,
|
||||||
|
`${game.i18n.localize('DAGGERHEART.GENERAL.difficulty')}: ${this.difficulty}`
|
||||||
|
];
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
|
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
|
||||||
import { createScrollText, getScrollTextData } from '../../helpers/utils.mjs';
|
import { getScrollTextData } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
|
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
|
||||||
new foundry.data.fields.SchemaField({
|
new foundry.data.fields.SchemaField({
|
||||||
|
|
@ -39,7 +39,9 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||||
type: 'base',
|
type: 'base',
|
||||||
isNPC: true,
|
isNPC: true,
|
||||||
settingSheet: null,
|
settingSheet: null,
|
||||||
hasResistances: true
|
hasResistances: true,
|
||||||
|
hasAttribution: false,
|
||||||
|
hasLimitedView: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +55,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
const schema = {};
|
const schema = {};
|
||||||
|
|
||||||
|
if (this.metadata.hasAttribution) {
|
||||||
|
schema.attribution = new fields.SchemaField({
|
||||||
|
source: new fields.StringField(),
|
||||||
|
page: new fields.NumberField(),
|
||||||
|
artist: new fields.StringField()
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
|
if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
|
||||||
if (this.metadata.hasResistances)
|
if (this.metadata.hasResistances)
|
||||||
schema.resistance = new fields.SchemaField({
|
schema.resistance = new fields.SchemaField({
|
||||||
|
|
@ -78,6 +87,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||||
*/
|
*/
|
||||||
static DEFAULT_ICON = null;
|
static DEFAULT_ICON = null;
|
||||||
|
|
||||||
|
get attributionLabel() {
|
||||||
|
if (!this.attribution) return;
|
||||||
|
|
||||||
|
const { source, page } = this.attribution;
|
||||||
|
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -138,6 +154,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
|
||||||
_onUpdate(changes, options, userId) {
|
_onUpdate(changes, options, userId) {
|
||||||
super._onUpdate(changes, options, userId);
|
super._onUpdate(changes, options, userId);
|
||||||
|
|
||||||
createScrollText(this.parent, options.scrollingTextData);
|
if (options.scrollingTextData) this.parent.queueScrollText(options.scrollingTextData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -673,4 +673,8 @@ export default class DhCharacter extends BaseDataActor {
|
||||||
this.companion.updateLevel(1);
|
this.companion.updateLevel(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getTags() {
|
||||||
|
return [this.class.value?.name, this.class.subclass?.name, this.community?.name, this.ancestry?.name].filter((t) => !!t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ export default class DhEnvironment extends BaseDataActor {
|
||||||
label: 'TYPES.Actor.environment',
|
label: 'TYPES.Actor.environment',
|
||||||
type: 'environment',
|
type: 'environment',
|
||||||
settingSheet: DHEnvironmentSettings,
|
settingSheet: DHEnvironmentSettings,
|
||||||
hasResistances: false
|
hasResistances: false,
|
||||||
|
hasAttribution: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
48
module/data/actor/party.mjs
Normal file
48
module/data/actor/party.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import BaseDataActor from './base.mjs';
|
||||||
|
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
|
||||||
|
|
||||||
|
export default class DhParty extends BaseDataActor {
|
||||||
|
/**@inheritdoc */
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
return {
|
||||||
|
...super.defineSchema(),
|
||||||
|
partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }),
|
||||||
|
notes: new fields.HTMLField(),
|
||||||
|
gold: new fields.SchemaField({
|
||||||
|
coins: new fields.NumberField({ initial: 0, integer: true }),
|
||||||
|
handfuls: new fields.NumberField({ initial: 1, integer: true }),
|
||||||
|
bags: new fields.NumberField({ initial: 0, integer: true }),
|
||||||
|
chests: new fields.NumberField({ initial: 0, integer: true })
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/dark-squad.svg';
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
prepareBaseData() {
|
||||||
|
super.prepareBaseData();
|
||||||
|
this.partyMembers = this.partyMembers.filter(p => !!p);
|
||||||
|
|
||||||
|
// Register this party to all members
|
||||||
|
if (game.actors.get(this.parent.id) === this.parent) {
|
||||||
|
for (const member of this.partyMembers) {
|
||||||
|
member.parties?.add(this.parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDelete(options, userId) {
|
||||||
|
super._onDelete(options, userId);
|
||||||
|
|
||||||
|
// Clear this party from all members that aren't deleted
|
||||||
|
for (const member of this.partyMembers) {
|
||||||
|
member.parties?.delete(this.parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import DHAbilityUse from "./abilityUse.mjs";
|
import DHAbilityUse from './abilityUse.mjs';
|
||||||
import DHActorRoll from "./adversaryRoll.mjs";
|
import DHActorRoll from './actorRoll.mjs';
|
||||||
|
import DHGroupRoll from './groupRoll.mjs';
|
||||||
|
import DHSystemMessage from './systemMessage.mjs';
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
abilityUse: DHAbilityUse,
|
abilityUse: DHAbilityUse,
|
||||||
adversaryRoll: DHActorRoll,
|
adversaryRoll: DHActorRoll,
|
||||||
damageRoll: DHActorRoll,
|
damageRoll: DHActorRoll,
|
||||||
dualityRoll: DHActorRoll
|
dualityRoll: DHActorRoll,
|
||||||
|
groupRoll: DHGroupRoll,
|
||||||
|
systemMessage: DHSystemMessage
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
hasEffect: new fields.BooleanField({ initial: false }),
|
hasEffect: new fields.BooleanField({ initial: false }),
|
||||||
hasSave: new fields.BooleanField({ initial: false }),
|
hasSave: new fields.BooleanField({ initial: false }),
|
||||||
hasTarget: new fields.BooleanField({ initial: false }),
|
hasTarget: new fields.BooleanField({ initial: false }),
|
||||||
|
isDirect: new fields.BooleanField({ initial: false }),
|
||||||
isCritical: new fields.BooleanField({ initial: false }),
|
isCritical: new fields.BooleanField({ initial: false }),
|
||||||
onSave: new fields.StringField(),
|
onSave: new fields.StringField(),
|
||||||
source: new fields.SchemaField({
|
source: new fields.SchemaField({
|
||||||
|
|
@ -54,9 +55,12 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get action() {
|
get action() {
|
||||||
const actionItem = this.actionItem;
|
const actionActor = this.actionActor,
|
||||||
if (!actionItem || !this.source.action) return null;
|
actionItem = this.actionItem;
|
||||||
return actionItem.system.actionsList?.find(a => a.id === this.source.action);
|
if (!this.source.action) return null;
|
||||||
|
if (actionItem) return actionItem.system.actionsList?.find(a => a.id === this.source.action);
|
||||||
|
else if (actionActor?.system.attack?._id === this.source.action) return actionActor.system.attack;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get targetMode() {
|
get targetMode() {
|
||||||
|
|
@ -94,7 +98,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerTargetHook() {
|
registerTargetHook() {
|
||||||
if (!this.parent.isAuthor) return;
|
if (!this.parent.isAuthor || !this.hasTarget) return;
|
||||||
if (this.targetMode && this.parent.targetHook !== null) {
|
if (this.targetMode && this.parent.targetHook !== null) {
|
||||||
Hooks.off('targetToken', this.parent.targetHook);
|
Hooks.off('targetToken', this.parent.targetHook);
|
||||||
return (this.parent.targetHook = null);
|
return (this.parent.targetHook = null);
|
||||||
|
|
@ -126,7 +130,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER');
|
this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER');
|
||||||
this.canButtonApply = game.user.isGM; //temp
|
this.canButtonApply = game.user.isGM; //temp
|
||||||
this.isGM = game.user.isGM; //temp
|
this.isGM = game.user.isGM; //temp
|
||||||
}
|
}
|
||||||
|
|
||||||
39
module/data/chat-message/groupRoll.mjs
Normal file
39
module/data/chat-message/groupRoll.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { abilities } from '../../config/actorConfig.mjs';
|
||||||
|
|
||||||
|
export default class DHGroupRoll extends foundry.abstract.TypeDataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
leader: new fields.EmbeddedDataField(GroupRollMemberField),
|
||||||
|
members: new fields.ArrayField(new fields.EmbeddedDataField(GroupRollMemberField))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalModifier() {
|
||||||
|
return this.members.reduce((acc, m) => {
|
||||||
|
if (m.manualSuccess === null) return acc;
|
||||||
|
|
||||||
|
return acc + (m.manualSuccess ? 1 : -1);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupRollMemberField extends foundry.abstract.DataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
actor: new fields.ObjectField(),
|
||||||
|
trait: new fields.StringField({ choices: abilities }),
|
||||||
|
difficulty: new fields.StringField(),
|
||||||
|
result: new fields.ObjectField({ nullable: true, initial: null }),
|
||||||
|
manualSuccess: new fields.BooleanField({ nullable: true, initial: null })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Can be expanded if we handle automation of success/failure */
|
||||||
|
get success() {
|
||||||
|
return manualSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
module/data/chat-message/systemMessage.mjs
Normal file
9
module/data/chat-message/systemMessage.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default class DHSystemMessage extends foundry.abstract.TypeDataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
useTitle: new fields.BooleanField({ initial: true })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,8 @@ export default class DhCombatant extends foundry.abstract.TypeDataModel {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
spotlight: new fields.SchemaField({
|
spotlight: new fields.SchemaField({
|
||||||
requesting: new fields.BooleanField({ required: true, initial: false })
|
requesting: new fields.BooleanField({ required: true, initial: false }),
|
||||||
|
requestOrderIndex: new fields.NumberField({ required: true, integer: true, initial: 0 })
|
||||||
}),
|
}),
|
||||||
actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
|
actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
|
|
||||||
|
|
||||||
export default class DhCountdowns extends foundry.abstract.DataModel {
|
export default class DhCountdowns extends foundry.abstract.DataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
|
||||||
narrative: new fields.EmbeddedDataField(DhCountdownData),
|
narrative: new fields.EmbeddedDataField(DhCountdownData),
|
||||||
encounter: new fields.EmbeddedDataField(DhCountdownData)
|
encounter: new fields.EmbeddedDataField(DhCountdownData),
|
||||||
|
/**/
|
||||||
|
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
|
||||||
|
defaultOwnership: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
choices: CONFIG.DH.GENERAL.basicOwnershiplevels,
|
||||||
|
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static CountdownCategories = { narrative: 'narrative', combat: 'combat' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
|
||||||
class DhCountdownData extends foundry.abstract.DataModel {
|
class DhCountdownData extends foundry.abstract.DataModel {
|
||||||
static LOCALIZATION_PREFIXES = ['DAGGERHEART.APPLICATIONS.Countdown']; // Nots ure why this won't work. Setting labels manually for now
|
|
||||||
|
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
|
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhOldCountdown)),
|
||||||
ownership: new fields.SchemaField({
|
ownership: new fields.SchemaField({
|
||||||
default: new fields.NumberField({
|
default: new fields.NumberField({
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -56,7 +59,8 @@ class DhCountdownData extends foundry.abstract.DataModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DhCountdown extends foundry.abstract.DataModel {
|
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
|
||||||
|
class DhOldCountdown extends foundry.abstract.DataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
|
|
@ -129,17 +133,88 @@ class DhCountdown extends foundry.abstract.DataModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const registerCountdownHooks = () => {
|
export class DhCountdown extends foundry.abstract.DataModel {
|
||||||
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
|
static defineSchema() {
|
||||||
if (refreshType === RefreshType.Countdown) {
|
const fields = foundry.data.fields;
|
||||||
if (application) {
|
return {
|
||||||
foundry.applications.instances.get(application)?.render();
|
type: new fields.StringField({
|
||||||
} else {
|
required: true,
|
||||||
foundry.applications.instances.get('narrative-countdowns')?.render();
|
choices: CONFIG.DH.GENERAL.countdownBaseTypes,
|
||||||
foundry.applications.instances.get('encounter-countdowns')?.render();
|
label: 'DAGGERHEART.GENERAL.type'
|
||||||
}
|
}),
|
||||||
|
name: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.name.label'
|
||||||
|
}),
|
||||||
|
img: new fields.FilePathField({
|
||||||
|
categories: ['IMAGE'],
|
||||||
|
base64: false,
|
||||||
|
initial: 'icons/magic/time/hourglass-yellow-green.webp'
|
||||||
|
}),
|
||||||
|
ownership: new fields.TypedObjectField(
|
||||||
|
new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
choices: CONFIG.DH.GENERAL.simpleOwnershiplevels,
|
||||||
|
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
|
||||||
|
})
|
||||||
|
),
|
||||||
|
progress: new fields.SchemaField({
|
||||||
|
current: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.current.label'
|
||||||
|
}),
|
||||||
|
max: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.label'
|
||||||
|
}),
|
||||||
|
type: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
choices: CONFIG.DH.GENERAL.countdownTypes,
|
||||||
|
initial: CONFIG.DH.GENERAL.countdownTypes.custom.id,
|
||||||
|
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.type.label'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
static defaultCountdown(type, playerHidden) {
|
||||||
}
|
const ownership = playerHidden
|
||||||
});
|
? game.users.reduce((acc, user) => {
|
||||||
};
|
if (!user.isGM) {
|
||||||
|
acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id,
|
||||||
|
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
|
||||||
|
img: 'icons/magic/time/hourglass-yellow-green.webp',
|
||||||
|
ownership: ownership,
|
||||||
|
progress: {
|
||||||
|
current: 1,
|
||||||
|
max: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get playerOwnership() {
|
||||||
|
return Array.from(game.users).reduce((acc, user) => {
|
||||||
|
acc[user.id] = {
|
||||||
|
value: user.isGM
|
||||||
|
? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER
|
||||||
|
: this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1
|
||||||
|
? this.ownership.players[user.id].type
|
||||||
|
: this.ownership.default,
|
||||||
|
isGM: user.isGM
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,5 @@ export { default as EffectsField } from './effectsField.mjs';
|
||||||
export { default as SaveField } from './saveField.mjs';
|
export { default as SaveField } from './saveField.mjs';
|
||||||
export { default as BeastformField } from './beastformField.mjs';
|
export { default as BeastformField } from './beastformField.mjs';
|
||||||
export { default as DamageField } from './damageField.mjs';
|
export { default as DamageField } from './damageField.mjs';
|
||||||
export { default as HealingField } from './healingField.mjs';
|
|
||||||
export { default as RollField } from './rollField.mjs';
|
export { default as RollField } from './rollField.mjs';
|
||||||
export { default as MacroField } from './macroField.mjs';
|
export { default as MacroField } from './macroField.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
|
import BeastformDialog from '../../../applications/dialogs/beastformDialog.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class BeastformField extends fields.SchemaField {
|
export default class BeastformField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 90;
|
||||||
|
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const beastformFields = {
|
const beastformFields = {
|
||||||
tierAccess: new fields.SchemaField({
|
tierAccess: new fields.SchemaField({
|
||||||
|
|
@ -27,4 +34,96 @@ export default class BeastformField extends fields.SchemaField {
|
||||||
};
|
};
|
||||||
super(beastformFields, options, context);
|
super(beastformFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beastform Transformation Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
static async execute(config) {
|
||||||
|
// Should not be useful anymore here
|
||||||
|
await BeastformField.handleActiveTransformations.call(this);
|
||||||
|
|
||||||
|
const { selected, evolved, hybrid } = await BeastformDialog.configure(config, this.item);
|
||||||
|
if (!selected) return false;
|
||||||
|
|
||||||
|
return await BeastformField.transform.call(this, selected, evolved, hybrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
if (this.actor.effects.find(x => x.type === 'beastform')) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
|
||||||
|
const actorLevel = this.actor.system.levelData.level.current;
|
||||||
|
const actorTier =
|
||||||
|
Object.values(settingsTiers).find(
|
||||||
|
tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end
|
||||||
|
) ?? 1;
|
||||||
|
|
||||||
|
config.tierLimit = this.beastform.tierAccess.exact ?? actorTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO by Harry
|
||||||
|
* @param {*} selectedForm
|
||||||
|
* @param {*} evolvedData
|
||||||
|
* @param {*} hybridData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static async transform(selectedForm, evolvedData, hybridData) {
|
||||||
|
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm;
|
||||||
|
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
|
||||||
|
if (!beastformEffect) {
|
||||||
|
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evolvedData?.form) {
|
||||||
|
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
|
||||||
|
if (!evolvedForm) {
|
||||||
|
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
|
||||||
|
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) {
|
||||||
|
formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => {
|
||||||
|
Object.keys(formCategory).forEach(advantageKey => {
|
||||||
|
advantages[advantageKey] = formCategory[advantageKey];
|
||||||
|
});
|
||||||
|
return advantages;
|
||||||
|
}, {});
|
||||||
|
formData.system.features = [
|
||||||
|
...formData.system.features,
|
||||||
|
...Object.values(hybridData.features).flatMap(x => Object.keys(x))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actor.createEmbeddedDocuments('Item', [formData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove existing beastform effect and return true if there was one
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static async handleActiveTransformations() {
|
||||||
|
const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform');
|
||||||
|
const existingEffects = beastformEffects.length > 0;
|
||||||
|
await this.actor.deleteEmbeddedDocuments(
|
||||||
|
'ActiveEffect',
|
||||||
|
beastformEffects.map(x => x.id)
|
||||||
|
);
|
||||||
|
return existingEffects;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class CostField extends fields.ArrayField {
|
export default class CostField extends fields.ArrayField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 150;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const element = new fields.SchemaField({
|
const element = new fields.SchemaField({
|
||||||
key: new fields.StringField({
|
key: new fields.StringField({
|
||||||
|
|
@ -8,7 +14,7 @@ export default class CostField extends fields.ArrayField {
|
||||||
required: true,
|
required: true,
|
||||||
initial: 'hope'
|
initial: 'hope'
|
||||||
}),
|
}),
|
||||||
keyIsID: new fields.BooleanField(),
|
itemId: new fields.StringField({ nullable: true, initial: null }),
|
||||||
value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }),
|
value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }),
|
||||||
scalable: new fields.BooleanField({ initial: false }),
|
scalable: new fields.BooleanField({ initial: false }),
|
||||||
step: new fields.NumberField({ nullable: true, initial: null }),
|
step: new fields.NumberField({ nullable: true, initial: null }),
|
||||||
|
|
@ -20,18 +26,88 @@ export default class CostField extends fields.ArrayField {
|
||||||
super(element, options, context);
|
super(element, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
|
* Cost Consumption Action Workflow part.
|
||||||
|
* Consume configured action resources.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
|
||||||
|
*/
|
||||||
|
static async execute(config, successCost = false) {
|
||||||
|
const actor = this.actor.system.partner ?? this.actor,
|
||||||
|
usefulResources = {
|
||||||
|
...foundry.utils.deepClone(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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.parent?.parent) {
|
||||||
|
for (var cost of config.costs) {
|
||||||
|
if (cost.itemId) {
|
||||||
|
usefulResources[cost.key] = {
|
||||||
|
value: cost.value,
|
||||||
|
target: this.parent.parent,
|
||||||
|
itemId: cost.itemId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = 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,
|
||||||
|
itemId: resource.itemId
|
||||||
|
});
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
await actor.modifyResource(resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @returns {boolean} Return false if fast-forwarded and no more uses.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
|
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
|
||||||
config.costs = CostField.calcCosts.call(this, costs);
|
config.costs = CostField.calcCosts.call(this, costs);
|
||||||
const hasCost = CostField.hasCost.call(this, config.costs);
|
const hasCost = CostField.hasCost.call(this, config.costs);
|
||||||
if (config.isFastForward && !hasCost)
|
if (config.dialog.configure === false && !hasCost) {
|
||||||
return ui.notifications.warn("You don't have the resources to use that action.");
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources'));
|
||||||
return hasCost;
|
return hasCost;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static calcCosts(costs) {
|
static calcCosts(costs) {
|
||||||
const resources = CostField.getResources.call(this, costs);
|
const resources = CostField.getResources.call(this, costs);
|
||||||
return costs.map(c => {
|
let filteredCosts = costs;
|
||||||
|
if (this.parent?.metadata.isQuantifiable && this.parent.consumeOnUse === false) {
|
||||||
|
filteredCosts = filteredCosts.filter(c => c.key !== 'quantity');
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredCosts.map(c => {
|
||||||
c.scale = c.scale ?? 0;
|
c.scale = c.scale ?? 0;
|
||||||
c.step = c.step ?? 1;
|
c.step = c.step ?? 1;
|
||||||
c.total = c.value + c.scale * c.step;
|
c.total = c.value + c.scale * c.step;
|
||||||
|
|
@ -40,13 +116,19 @@ export default class CostField extends fields.ArrayField {
|
||||||
c.key === 'fear'
|
c.key === 'fear'
|
||||||
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
|
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
|
||||||
: resources[c.key].isReversed
|
: resources[c.key].isReversed
|
||||||
? resources[c.key].max
|
? resources[c.key].max - resources[c.key].value
|
||||||
: resources[c.key].value;
|
: resources[c.key].value;
|
||||||
if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step);
|
if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step);
|
||||||
return c;
|
return c;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current Actor currently has all needed resources.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
static hasCost(costs) {
|
static hasCost(costs) {
|
||||||
const realCosts = CostField.getRealCosts.call(this, costs),
|
const realCosts = CostField.getRealCosts.call(this, costs),
|
||||||
hasFearCost = realCosts.findIndex(c => c.key === 'fear');
|
hasFearCost = realCosts.findIndex(c => c.key === 'fear');
|
||||||
|
|
@ -73,17 +155,20 @@ export default class CostField extends fields.ArrayField {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all Actor resources + parent Item potential one.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static getResources(costs) {
|
static getResources(costs) {
|
||||||
const actorResources = foundry.utils.deepClone(this.actor.system.resources);
|
const actorResources = foundry.utils.deepClone(this.actor.system.resources);
|
||||||
if (this.actor.system.partner)
|
if (this.actor.system.partner)
|
||||||
actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope);
|
actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope);
|
||||||
const itemResources = {};
|
const itemResources = {};
|
||||||
for (let itemResource of costs) {
|
for (let itemResource of costs) {
|
||||||
if (itemResource.keyIsID) {
|
if (itemResource.itemId) {
|
||||||
itemResources[itemResource.key] = {
|
itemResources[itemResource.key] = CostField.getItemIdCostResource.bind(this)(itemResource);
|
||||||
value: this.parent.resource.value ?? 0,
|
|
||||||
max: CostField.formatMax.call(this, this.parent?.resource?.max)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,8 +178,48 @@ export default class CostField extends fields.ArrayField {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getItemIdCostResource(itemResource) {
|
||||||
|
switch (itemResource.key) {
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.resource.id:
|
||||||
|
return {
|
||||||
|
value: this.parent.resource.value ?? 0,
|
||||||
|
max: CostField.formatMax.call(this, this.parent?.resource?.max)
|
||||||
|
};
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.quantity.id:
|
||||||
|
return {
|
||||||
|
value: this.parent.quantity ?? 0,
|
||||||
|
max: this.parent.quantity ?? 0
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { value: 0, max: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getItemIdCostUpdate(r) {
|
||||||
|
switch (r.key) {
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.resource.id:
|
||||||
|
return {
|
||||||
|
path: 'system.resource.value',
|
||||||
|
value: r.target.system.resource.value + r.value
|
||||||
|
};
|
||||||
|
case CONFIG.DH.GENERAL.itemAbilityCosts.quantity.id:
|
||||||
|
return {
|
||||||
|
path: 'system.quantity',
|
||||||
|
value: r.target.system.quantity + r.value
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { path: '', value: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} costs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static getRealCosts(costs) {
|
static getRealCosts(costs) {
|
||||||
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
|
const cloneCosts = foundry.utils.deepClone(costs),
|
||||||
|
realCosts = cloneCosts?.length ? cloneCosts.filter(c => c.enabled) : [];
|
||||||
let mergedCosts = [];
|
let mergedCosts = [];
|
||||||
realCosts.forEach(c => {
|
realCosts.forEach(c => {
|
||||||
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);
|
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);
|
||||||
|
|
@ -104,6 +229,12 @@ export default class CostField extends fields.ArrayField {
|
||||||
return mergedCosts;
|
return mergedCosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format scalable max cost, inject Action datas if it's a formula.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {number|string} max Configured maximum for that resource.
|
||||||
|
* @returns {number} The max cost value.
|
||||||
|
*/
|
||||||
static formatMax(max) {
|
static formatMax(max) {
|
||||||
max ??= 0;
|
max ??= 0;
|
||||||
if (isNaN(max)) {
|
if (isNaN(max)) {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,210 @@
|
||||||
import FormulaField from '../formulaField.mjs';
|
import FormulaField from '../formulaField.mjs';
|
||||||
|
import { setsEqual } from '../../../helpers/utils.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class DamageField extends fields.SchemaField {
|
export default class DamageField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 20;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options, context = {}) {
|
constructor(options, context = {}) {
|
||||||
const damageFields = {
|
const damageFields = {
|
||||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
|
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)),
|
||||||
includeBase: new fields.BooleanField({
|
includeBase: new fields.BooleanField({
|
||||||
initial: false,
|
initial: false,
|
||||||
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
|
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
|
||||||
})
|
}),
|
||||||
|
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' })
|
||||||
};
|
};
|
||||||
super(damageFields, options, context);
|
super(damageFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll Damage/Healing Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {string} [messageId=null] ChatMessage Id where the clicked button belong.
|
||||||
|
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async execute(config, messageId = null, force = false) {
|
||||||
|
if (!this.hasDamage && !this.hasHealing) return;
|
||||||
|
if (
|
||||||
|
this.hasRoll &&
|
||||||
|
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
|
||||||
|
!force
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let formulas = this.damage.parts.map(p => ({
|
||||||
|
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
|
||||||
|
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
|
||||||
|
applyTo: p.applyTo
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!formulas.length) return false;
|
||||||
|
|
||||||
|
formulas = DamageField.formatFormulas.call(this, formulas, config);
|
||||||
|
|
||||||
|
const damageConfig = {
|
||||||
|
...config,
|
||||||
|
roll: formulas,
|
||||||
|
dialog: {},
|
||||||
|
data: this.getRollData()
|
||||||
|
};
|
||||||
|
delete damageConfig.evaluate;
|
||||||
|
|
||||||
|
if (DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
|
||||||
|
damageConfig.dialog.configure = false;
|
||||||
|
if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod;
|
||||||
|
|
||||||
|
damageConfig.source.message = config.message?._id ?? messageId;
|
||||||
|
damageConfig.directDamage = !!damageConfig.source?.message;
|
||||||
|
|
||||||
|
// if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active)
|
||||||
|
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
|
||||||
|
|
||||||
|
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
|
||||||
|
if (!damageResult) return false;
|
||||||
|
config.damage = damageResult.damage;
|
||||||
|
config.message ??= damageConfig.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Damage/Healing Action Worflow part.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {*[]} targets Arrays of targets to bypass pre-selected ones.
|
||||||
|
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async applyDamage(config, targets = null, force = false) {
|
||||||
|
targets ??= config.targets.filter(target => target.hit);
|
||||||
|
if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
|
||||||
|
|
||||||
|
const targetDamage = [];
|
||||||
|
const damagePromises = [];
|
||||||
|
for (let target of targets) {
|
||||||
|
const actor = foundry.utils.fromUuidSync(target.actorId);
|
||||||
|
if (!actor) continue;
|
||||||
|
if (!config.hasHealing && config.onSave && target.saved?.success === true) {
|
||||||
|
const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1;
|
||||||
|
Object.entries(config.damage).forEach(([k, v]) => {
|
||||||
|
v.total = 0;
|
||||||
|
v.parts.forEach(part => {
|
||||||
|
part.total = Math.ceil(part.total * mod);
|
||||||
|
v.total += part.total;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = game.scenes.find(x => x.active).tokens.find(x => x.id === target.id);
|
||||||
|
if (config.hasHealing)
|
||||||
|
damagePromises.push(
|
||||||
|
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
|
||||||
|
);
|
||||||
|
else
|
||||||
|
damagePromises.push(
|
||||||
|
actor
|
||||||
|
.takeDamage(config.damage, config.isDirect)
|
||||||
|
.then(updates => targetDamage.push({ token, updates }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(damagePromises).then(async _ => {
|
||||||
|
const summaryMessageSettings = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||||
|
).summaryMessages;
|
||||||
|
if (!summaryMessageSettings.damage) return;
|
||||||
|
|
||||||
|
const cls = getDocumentClass('ChatMessage');
|
||||||
|
const msg = {
|
||||||
|
type: 'systemMessage',
|
||||||
|
user: game.user.id,
|
||||||
|
speaker: cls.getSpeaker(),
|
||||||
|
title: game.i18n.localize(
|
||||||
|
`DAGGERHEART.UI.Chat.damageSummary.${config.hasHealing ? 'healingTitle' : 'title'}`
|
||||||
|
),
|
||||||
|
content: await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/ui/chat/damageSummary.hbs',
|
||||||
|
{
|
||||||
|
targets: targetDamage
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
cls.create(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return value or valueAlt from damage part
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} part Damage Part
|
||||||
|
* @param {object} data Action getRollData
|
||||||
|
* @returns Formula value object
|
||||||
|
*/
|
||||||
|
static getFormulaValue(part, data) {
|
||||||
|
let formulaValue = part.value;
|
||||||
|
|
||||||
|
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
|
||||||
|
|
||||||
|
const isAdversary = this.actor.type === 'adversary';
|
||||||
|
if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
|
||||||
|
const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde');
|
||||||
|
if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formulaValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare formulas for Damage Roll
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object[]} formulas Array of formatted formulas object
|
||||||
|
* @param {object} data Action getRollData
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static formatFormulas(formulas, data) {
|
||||||
|
const formattedFormulas = [];
|
||||||
|
formulas.forEach(formula => {
|
||||||
|
if (isNaN(formula.formula))
|
||||||
|
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data));
|
||||||
|
const same = formattedFormulas.find(
|
||||||
|
f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo
|
||||||
|
);
|
||||||
|
if (same) same.formula += ` + ${formula.formula}`;
|
||||||
|
else formattedFormulas.push(formula);
|
||||||
|
});
|
||||||
|
return formattedFormulas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for applyDamage method for current user role
|
||||||
|
* @returns {boolean} If applyDamage should be triggered automatically
|
||||||
|
*/
|
||||||
|
static getApplyAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DHActionDiceData extends foundry.abstract.DataModel {
|
export class DHActionDiceData extends foundry.abstract.DataModel {
|
||||||
|
|
@ -22,14 +214,26 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
|
||||||
multiplier: new fields.StringField({
|
multiplier: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.multiplierTypes,
|
choices: CONFIG.DH.GENERAL.multiplierTypes,
|
||||||
initial: 'prof',
|
initial: 'prof',
|
||||||
label: 'Multiplier'
|
label: 'DAGGERHEART.ACTIONS.Config.damage.multiplier',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
|
flatMultiplier: new fields.NumberField({
|
||||||
dice: new fields.StringField({ choices: CONFIG.DH.GENERAL.diceTypes, initial: 'd6', label: 'Dice' }),
|
nullable: true,
|
||||||
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'Bonus' }),
|
initial: 1,
|
||||||
|
label: 'DAGGERHEART.ACTIONS.Config.damage.flatMultiplier'
|
||||||
|
}),
|
||||||
|
dice: new fields.StringField({
|
||||||
|
choices: CONFIG.DH.GENERAL.diceTypes,
|
||||||
|
initial: 'd6',
|
||||||
|
label: 'DAGGERHEART.GENERAL.Dice.single',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
|
}),
|
||||||
|
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'DAGGERHEART.GENERAL.bonus' }),
|
||||||
custom: new fields.SchemaField({
|
custom: new fields.SchemaField({
|
||||||
enabled: new fields.BooleanField({ label: 'Custom Formula' }),
|
enabled: new fields.BooleanField({ label: 'DAGGERHEART.ACTIONS.Config.general.customFormula' }),
|
||||||
formula: new FormulaField({ label: 'Formula', initial: "" })
|
formula: new FormulaField({ label: 'DAGGERHEART.ACTIONS.Config.general.formula', initial: '' })
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
|
import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class EffectsField extends fields.ArrayField {
|
export default class EffectsField extends fields.ArrayField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 100;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const element = new fields.SchemaField({
|
const element = new fields.SchemaField({
|
||||||
_id: new fields.DocumentIdField(),
|
_id: new fields.DocumentIdField(),
|
||||||
|
|
@ -8,4 +16,116 @@ export default class EffectsField extends fields.ArrayField {
|
||||||
});
|
});
|
||||||
super(element, options, context);
|
super(element, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Effects Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {object[]} [targets=null] Array of targets to override pre-selected ones.
|
||||||
|
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async execute(config, targets = null, force = false) {
|
||||||
|
if (!config.hasEffect) return;
|
||||||
|
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||||
|
if (!message) {
|
||||||
|
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||||
|
roll._evaluated = true;
|
||||||
|
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||||
|
}
|
||||||
|
if (EffectsField.getAutomation() || force) {
|
||||||
|
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
|
||||||
|
await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid);
|
||||||
|
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Action Effects to a list of Targets
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object[]} targets Array of formatted targets
|
||||||
|
*/
|
||||||
|
static async applyEffects(targets) {
|
||||||
|
if (!this.effects?.length || !targets?.length) return;
|
||||||
|
|
||||||
|
let effects = this.effects;
|
||||||
|
const messageTargets = [];
|
||||||
|
targets.forEach(async baseToken => {
|
||||||
|
if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true);
|
||||||
|
if (!effects.length) return;
|
||||||
|
|
||||||
|
const token = canvas.tokens.get(baseToken.id);
|
||||||
|
if (!token) return;
|
||||||
|
messageTargets.push(token.document);
|
||||||
|
|
||||||
|
effects.forEach(async e => {
|
||||||
|
const effect = this.item.effects.get(e._id);
|
||||||
|
if (!token.actor || !effect) return;
|
||||||
|
await EffectsField.applyEffect(effect, token.actor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (messageTargets.length === 0) return;
|
||||||
|
|
||||||
|
const summaryMessageSettings = game.settings.get(
|
||||||
|
CONFIG.DH.id,
|
||||||
|
CONFIG.DH.SETTINGS.gameSettings.Automation
|
||||||
|
).summaryMessages;
|
||||||
|
if (!summaryMessageSettings.effects) return;
|
||||||
|
|
||||||
|
const cls = getDocumentClass('ChatMessage');
|
||||||
|
const msg = {
|
||||||
|
type: 'systemMessage',
|
||||||
|
user: game.user.id,
|
||||||
|
speaker: cls.getSpeaker(),
|
||||||
|
title: game.i18n.localize('DAGGERHEART.UI.Chat.effectSummary.title'),
|
||||||
|
content: await foundry.applications.handlebars.renderTemplate(
|
||||||
|
'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
|
||||||
|
{
|
||||||
|
effects: this.effects.map(e => this.item.effects.get(e._id)),
|
||||||
|
targets: messageTargets
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
cls.create(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an Effect to a target or enable it if already on it
|
||||||
|
* @param {object} effect Effect object containing ActiveEffect UUID
|
||||||
|
* @param {object} actor Actor Document
|
||||||
|
*/
|
||||||
|
static 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {boolean} If execute should be triggered automatically
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { DHDamageData } from './damageField.mjs';
|
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
|
||||||
|
|
||||||
export default class HealingField extends fields.SchemaField {
|
|
||||||
constructor(options, context = {}) {
|
|
||||||
const healingFields = {
|
|
||||||
parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData))
|
|
||||||
};
|
|
||||||
super(healingFields, options, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,29 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class MacroField extends fields.DocumentUUIDField {
|
export default class MacroField extends fields.DocumentUUIDField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 70;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(context = {}) {
|
constructor(context = {}) {
|
||||||
super({ type: "Macro" }, context);
|
super({ type: 'Macro' }, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macro Action Workflow part.
|
||||||
|
* Must be called within Action context or similar or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. Currently not used.
|
||||||
|
*/
|
||||||
|
static async execute(config) {
|
||||||
|
const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro,
|
||||||
|
macro = await fromUuid(fixUUID);
|
||||||
|
try {
|
||||||
|
if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`);
|
||||||
|
macro.execute();
|
||||||
|
} catch (error) {
|
||||||
|
ui.notifications.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class RangeField extends fields.StringField {
|
export default class RangeField extends fields.StringField {
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(context = {}) {
|
constructor(context = {}) {
|
||||||
const options = {
|
const options = {
|
||||||
choices: CONFIG.DH.GENERAL.range,
|
choices: CONFIG.DH.GENERAL.range,
|
||||||
required: false,
|
required: false,
|
||||||
blank: true,
|
blank: true,
|
||||||
label: "DAGGERHEART.GENERAL.range"
|
label: 'DAGGERHEART.GENERAL.range'
|
||||||
};
|
};
|
||||||
super(options, context);
|
super(options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
return true;
|
* Update Action Workflow config object.
|
||||||
|
* NOT YET IMPLEMENTED.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,27 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
return {
|
return {
|
||||||
type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }),
|
type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }),
|
||||||
trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities, label: "DAGGERHEART.GENERAL.Trait.single" }),
|
trait: new fields.StringField({
|
||||||
|
nullable: true,
|
||||||
|
initial: null,
|
||||||
|
choices: CONFIG.DH.ACTOR.abilities,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Trait.single'
|
||||||
|
}),
|
||||||
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
||||||
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
|
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
|
||||||
advState: new fields.StringField({
|
advState: new fields.StringField({
|
||||||
choices: CONFIG.DH.ACTIONS.advantageState,
|
choices: CONFIG.DH.ACTIONS.advantageState,
|
||||||
initial: 'neutral'
|
initial: 'neutral',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
diceRolling: new fields.SchemaField({
|
diceRolling: new fields.SchemaField({
|
||||||
multiplier: new fields.StringField({
|
multiplier: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.diceSetNumbers,
|
choices: CONFIG.DH.GENERAL.diceSetNumbers,
|
||||||
initial: 'prof',
|
initial: 'prof',
|
||||||
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier'
|
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
flatMultiplier: new fields.NumberField({
|
flatMultiplier: new fields.NumberField({
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
@ -26,7 +35,9 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
dice: new fields.StringField({
|
dice: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.diceTypes,
|
choices: CONFIG.DH.GENERAL.diceTypes,
|
||||||
initial: CONFIG.DH.GENERAL.diceTypes.d6,
|
initial: CONFIG.DH.GENERAL.diceTypes.d6,
|
||||||
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice'
|
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice',
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
}),
|
}),
|
||||||
compare: new fields.StringField({
|
compare: new fields.StringField({
|
||||||
choices: CONFIG.DH.ACTIONS.diceCompare,
|
choices: CONFIG.DH.ACTIONS.diceCompare,
|
||||||
|
|
@ -71,29 +82,6 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
const modifiers = [];
|
const modifiers = [];
|
||||||
if (!this.parent?.actor) return modifiers;
|
if (!this.parent?.actor) return modifiers;
|
||||||
switch (this.parent.actor.type) {
|
switch (this.parent.actor.type) {
|
||||||
case 'character':
|
|
||||||
const spellcastingTrait =
|
|
||||||
this.type === 'spellcast'
|
|
||||||
? (this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility')
|
|
||||||
: null;
|
|
||||||
const trait =
|
|
||||||
this.useDefault || !this.trait
|
|
||||||
? (spellcastingTrait ?? this.parent.item.system.attack?.roll?.trait ?? 'agility')
|
|
||||||
: this.trait;
|
|
||||||
if (
|
|
||||||
this.type === CONFIG.DH.GENERAL.rollTypes.attack.id ||
|
|
||||||
this.type === CONFIG.DH.GENERAL.rollTypes.trait.id
|
|
||||||
)
|
|
||||||
modifiers.push({
|
|
||||||
label: `DAGGERHEART.CONFIG.Traits.${trait}.name`,
|
|
||||||
value: this.parent.actor.system.traits[trait].value
|
|
||||||
});
|
|
||||||
else if (this.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id)
|
|
||||||
modifiers.push({
|
|
||||||
label: `DAGGERHEART.CONFIG.RollTypes.spellcast.name`,
|
|
||||||
value: this.parent.actor.system.spellcastModifier
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'companion':
|
case 'companion':
|
||||||
case 'adversary':
|
case 'adversary':
|
||||||
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
|
if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
|
||||||
|
|
@ -107,10 +95,79 @@ export class DHActionRollData extends foundry.abstract.DataModel {
|
||||||
}
|
}
|
||||||
return modifiers;
|
return modifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get rollTrait() {
|
||||||
|
if (this.parent?.actor?.type !== 'character') return null;
|
||||||
|
switch (this.type) {
|
||||||
|
case CONFIG.DH.GENERAL.rollTypes.spellcast.id:
|
||||||
|
return this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility';
|
||||||
|
case CONFIG.DH.GENERAL.rollTypes.attack.id:
|
||||||
|
case CONFIG.DH.GENERAL.rollTypes.trait.id:
|
||||||
|
return this.useDefault || !this.trait
|
||||||
|
? (this.parent.item.system.attack?.roll?.trait ?? 'agility')
|
||||||
|
: this.trait;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RollField extends fields.EmbeddedDataField {
|
export default class RollField extends fields.EmbeddedDataField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 10;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options, context = {}) {
|
constructor(options, context = {}) {
|
||||||
super(DHActionRollData, options, context);
|
super(DHActionRollData, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
static async execute(config) {
|
||||||
|
if (!config.hasRoll) return;
|
||||||
|
config = await this.actor.diceRoll(config);
|
||||||
|
if (!config) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
if (!config.hasRoll) return;
|
||||||
|
|
||||||
|
config.dialog.configure = RollField.getAutomation() ? !config.dialog.configure : config.dialog.configure;
|
||||||
|
|
||||||
|
const roll = {
|
||||||
|
baseModifiers: this.roll.getModifier(),
|
||||||
|
label: 'Attack',
|
||||||
|
type: this.roll?.type,
|
||||||
|
trait: this.roll?.rollTrait,
|
||||||
|
difficulty: this.roll?.difficulty,
|
||||||
|
formula: this.roll.getFormula(),
|
||||||
|
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
|
||||||
|
};
|
||||||
|
if (this.roll.type === 'diceSet' || !this.hasRoll) roll.lite = true;
|
||||||
|
|
||||||
|
config.roll = roll;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {boolean} If execute should be triggered automatically
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
|
import { abilities } from '../../../config/actorConfig.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class SaveField extends fields.SchemaField {
|
export default class SaveField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 50;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const saveFields = {
|
const saveFields = {
|
||||||
trait: new fields.StringField({
|
trait: new fields.StringField({
|
||||||
|
|
@ -11,9 +19,164 @@ export default class SaveField extends fields.SchemaField {
|
||||||
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
|
||||||
damageMod: new fields.StringField({
|
damageMod: new fields.StringField({
|
||||||
initial: CONFIG.DH.ACTIONS.damageOnSave.none.id,
|
initial: CONFIG.DH.ACTIONS.damageOnSave.none.id,
|
||||||
choices: CONFIG.DH.ACTIONS.damageOnSave
|
choices: CONFIG.DH.ACTIONS.damageOnSave,
|
||||||
|
nullable: false,
|
||||||
|
required: true
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
super(saveFields, options, context);
|
super(saveFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reaction Roll Action Workflow part.
|
||||||
|
* Must be called within Action context or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {object[]} [targets=null] Array of targets to override pre-selected ones.
|
||||||
|
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
|
||||||
|
*/
|
||||||
|
static async execute(config, targets = null, force = false) {
|
||||||
|
if (!config.hasSave) return;
|
||||||
|
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
|
||||||
|
roll._evaluated = true;
|
||||||
|
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
|
||||||
|
}
|
||||||
|
if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
|
||||||
|
targets ??= config.targets.filter(t => !config.hasRoll || t.hit);
|
||||||
|
await SaveField.rollAllSave.call(this, targets, config.event, message);
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll a Reaction Roll for all targets. Send a query to the owner if the User is not.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object[]} targets Array of formatted targets.
|
||||||
|
* @param {Event} event Triggering event
|
||||||
|
* @param {ChatMessage} message The ChatMessage the triggered button comes from.
|
||||||
|
*/
|
||||||
|
static async rollAllSave(targets, event, message) {
|
||||||
|
if (!targets) return;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const aPromise = [];
|
||||||
|
targets.forEach(target => {
|
||||||
|
aPromise.push(
|
||||||
|
new Promise(async subResolve => {
|
||||||
|
const actor = fromUuidSync(target.actorId);
|
||||||
|
if (actor) {
|
||||||
|
const rollSave =
|
||||||
|
game.user === actor.owner
|
||||||
|
? SaveField.rollSave.call(this, actor, event)
|
||||||
|
: actor.owner.query('reactionRoll', {
|
||||||
|
actionId: this.uuid,
|
||||||
|
actorId: actor.uuid,
|
||||||
|
event,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
const result = await rollSave;
|
||||||
|
await SaveField.updateSaveMessage.call(this, result, message, target.id);
|
||||||
|
subResolve();
|
||||||
|
} else subResolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Promise.all(aPromise).then(result => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll a Reaction Roll for the specified Actor against the Action difficulty.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} actor Actor document
|
||||||
|
* @param {Event} event Triggering event
|
||||||
|
* @returns {object} Actor diceRoll config result.
|
||||||
|
*/
|
||||||
|
static async rollSave(actor, event) {
|
||||||
|
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)
|
||||||
|
}),
|
||||||
|
rollConfig = {
|
||||||
|
event,
|
||||||
|
title,
|
||||||
|
roll: {
|
||||||
|
trait: this.save.trait,
|
||||||
|
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
|
||||||
|
type: 'trait'
|
||||||
|
},
|
||||||
|
actionType: 'reaction',
|
||||||
|
hasRoll: true,
|
||||||
|
data: actor.getRollData()
|
||||||
|
};
|
||||||
|
if (SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
|
||||||
|
rollConfig.dialog = { configure: false };
|
||||||
|
return actor.diceRoll(rollConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a Roll ChatMessage for a token according to his Reaction Roll result.
|
||||||
|
* @param {object} result Result from the Reaction Roll
|
||||||
|
* @param {object} message ChatMessage to update
|
||||||
|
* @param {string} targetId Token ID
|
||||||
|
*/
|
||||||
|
static async updateSaveMessage(result, message, targetId) {
|
||||||
|
if (!result) return;
|
||||||
|
const updateMsg = async function (message, targetId, result) {
|
||||||
|
// setTimeout(async () => {
|
||||||
|
const chatMessage = ui.chat.collection.get(message._id),
|
||||||
|
changes = {
|
||||||
|
flags: {
|
||||||
|
[game.system.id]: {
|
||||||
|
reactionRolls: {
|
||||||
|
[targetId]: {
|
||||||
|
result: result.roll.total,
|
||||||
|
success: result.roll.success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await chatMessage.update(changes);
|
||||||
|
// }, 100);
|
||||||
|
};
|
||||||
|
if (game.modules.get('dice-so-nice')?.active)
|
||||||
|
game.dice3d
|
||||||
|
.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id)
|
||||||
|
.then(async () => await updateMsg(message, targetId, result));
|
||||||
|
else await updateMsg(message, targetId, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the automation setting for execute method for current user role
|
||||||
|
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
|
||||||
|
*/
|
||||||
|
static getAutomation() {
|
||||||
|
return (
|
||||||
|
(game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) ||
|
||||||
|
(!game.user.isGM &&
|
||||||
|
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a query to an Actor owner to roll a Reaction Roll then send back the result.
|
||||||
|
* @param {object} param0
|
||||||
|
* @param {string} param0.actionId Action ID
|
||||||
|
* @param {string} param0.actorId Actor ID
|
||||||
|
* @param {Event} param0.event Triggering event
|
||||||
|
* @param {ChatMessage} param0.message Chat Message to update
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
SaveField.rollSave.call(action, actor, event, message).then(result => resolve(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,80 @@
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class TargetField extends fields.SchemaField {
|
export default class TargetField extends fields.SchemaField {
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const targetFields = {
|
const targetFields = {
|
||||||
type: new fields.StringField({
|
type: new fields.StringField({
|
||||||
choices: CONFIG.DH.GENERAL.targetTypes,
|
choices: CONFIG.DH.GENERAL.targetTypes,
|
||||||
initial: CONFIG.DH.GENERAL.targetTypes.any.id,
|
initial: CONFIG.DH.GENERAL.targetTypes.any.id,
|
||||||
nullable: true
|
nullable: true,
|
||||||
|
blank: true
|
||||||
}),
|
}),
|
||||||
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
|
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
|
||||||
};
|
};
|
||||||
super(targetFields, options, context);
|
super(targetFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
if (!this.target?.type) return [];
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
|
if (!this.target?.type) return (config.targets = []);
|
||||||
config.hasTarget = true;
|
config.hasTarget = true;
|
||||||
let targets;
|
let targets;
|
||||||
|
// If the Action is configured as self-targeted, set targets as the owner.
|
||||||
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
|
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
|
||||||
targets = [this.actor.token ?? this.actor.prototypeToken];
|
targets = [this.actor.token ?? this.actor.prototypeToken];
|
||||||
else {
|
else {
|
||||||
targets = Array.from(game.user.targets);
|
targets = Array.from(game.user.targets);
|
||||||
if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) {
|
if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) {
|
||||||
targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t));
|
targets = targets.filter(target => TargetField.isTargetFriendly(this.actor, target, this.target.type));
|
||||||
if (this.target.amount && targets.length > this.target.amount) targets = [];
|
if (this.target.amount && targets.length > this.target.amount) targets = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.targets = targets.map(t => TargetField.formatTarget.call(this, t));
|
config.targets = targets.map(t => TargetField.formatTarget.call(this, t));
|
||||||
const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets);
|
const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets);
|
||||||
if (config.isFastForward && !hasTargets)
|
if (config.dialog.configure === false && !hasTargets) {
|
||||||
return ui.notifications.warn('Too many targets selected for that actions.');
|
ui.notifications.warn('Too many targets selected for that actions.');
|
||||||
return hasTargets;
|
return hasTargets;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the number of selected targets respect the amount set in the Action.
|
||||||
|
* NOT YET IMPLEMENTED. Will be with Target Picker.
|
||||||
|
* @param {number} amount Max amount of targets configured in the action.
|
||||||
|
* @param {*[]} targets Array of targeted tokens.
|
||||||
|
* @returns {boolean} If the amount of targeted tokens does not exceed action configured one.
|
||||||
|
*/
|
||||||
static checkTargets(amount, targets) {
|
static checkTargets(amount, targets) {
|
||||||
return true;
|
return true;
|
||||||
// return !amount || (targets.length > amount);
|
// return !amount || (targets.length > amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
static isTargetFriendly(target) {
|
/**
|
||||||
const actorDisposition = this.actor.token
|
* Compare 2 Actors disposition between each other
|
||||||
? this.actor.token.disposition
|
* @param {*} actor First actor document.
|
||||||
: this.actor.prototypeToken.disposition,
|
* @param {*} target Second actor document.
|
||||||
|
* @param {string} type Disposition id to compare (friendly/hostile).
|
||||||
|
* @returns {boolean} If both actors respect the provided type.
|
||||||
|
*/
|
||||||
|
static isTargetFriendly(actor, target, type) {
|
||||||
|
const actorDisposition = actor.token ? actor.token.disposition : actor.prototypeToken.disposition,
|
||||||
targetDisposition = target.document.disposition;
|
targetDisposition = target.document.disposition;
|
||||||
return (
|
return (
|
||||||
(this.target.type === CONFIG.DH.GENERAL.targetTypes.friendly.id &&
|
(type === CONFIG.DH.GENERAL.targetTypes.friendly.id && actorDisposition === targetDisposition) ||
|
||||||
actorDisposition === targetDisposition) ||
|
(type === CONFIG.DH.GENERAL.targetTypes.hostile.id && actorDisposition + targetDisposition === 0)
|
||||||
(this.target.type === CONFIG.DH.GENERAL.targetTypes.hostile.id &&
|
|
||||||
actorDisposition + targetDisposition === 0)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format actor to useful datas for Action roll workflow.
|
||||||
|
* @param {*} actor Actor object to format.
|
||||||
|
* @returns {*} Formatted Actor.
|
||||||
|
*/
|
||||||
static formatTarget(actor) {
|
static formatTarget(actor) {
|
||||||
return {
|
return {
|
||||||
id: actor.id,
|
id: actor.id,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ import FormulaField from '../formulaField.mjs';
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
export default class UsesField extends fields.SchemaField {
|
export default class UsesField extends fields.SchemaField {
|
||||||
|
/**
|
||||||
|
* Action Workflow order
|
||||||
|
*/
|
||||||
|
static order = 160;
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
constructor(options = {}, context = {}) {
|
constructor(options = {}, context = {}) {
|
||||||
const usesFields = {
|
const usesFields = {
|
||||||
value: new fields.NumberField({ nullable: true, initial: null }),
|
value: new fields.NumberField({ nullable: true, initial: null }),
|
||||||
|
|
@ -20,15 +26,45 @@ export default class UsesField extends fields.SchemaField {
|
||||||
super(usesFields, options, context);
|
super(usesFields, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static prepareConfig(config) {
|
/**
|
||||||
|
* Uses Consumption Action Workflow part.
|
||||||
|
* Increment Action spent uses by 1.
|
||||||
|
* Must be called within Action context or similar or similar.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed.
|
||||||
|
*/
|
||||||
|
static async execute(config, successCost = false) {
|
||||||
|
if (
|
||||||
|
config.uses?.enabled &&
|
||||||
|
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
|
||||||
|
(successCost && config.uses?.consumeOnSuccess))
|
||||||
|
)
|
||||||
|
this.update({ 'uses.value': this.uses.value + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Action Workflow config object.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
|
||||||
|
* @returns {boolean} Return false if fast-forwarded and no more uses.
|
||||||
|
*/
|
||||||
|
prepareConfig(config) {
|
||||||
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
|
const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null;
|
||||||
if (uses && !uses.value) uses.value = 0;
|
if (uses && !uses.value) uses.value = 0;
|
||||||
config.uses = uses;
|
config.uses = uses;
|
||||||
const hasUses = UsesField.hasUses.call(this, config.uses);
|
const hasUses = UsesField.hasUses.call(this, config.uses);
|
||||||
if (config.isFastForward && !hasUses) return ui.notifications.warn("That action doesn't have remaining uses.");
|
if (config.dialog.configure === false && !hasUses) {
|
||||||
return hasUses;
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionNoUsesRemaining'));
|
||||||
|
return hasUses;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare Uses object for Action Workflow
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {object} uses
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
static calcUses(uses) {
|
static calcUses(uses) {
|
||||||
if (!uses) return null;
|
if (!uses) return null;
|
||||||
return {
|
return {
|
||||||
|
|
@ -38,6 +74,12 @@ export default class UsesField extends fields.SchemaField {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the Action still get atleast one unspent uses.
|
||||||
|
* Must be called within Action context.
|
||||||
|
* @param {*} uses
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
static hasUses(uses) {
|
static hasUses(uses) {
|
||||||
if (!uses) return true;
|
if (!uses) return true;
|
||||||
let max = uses.max ?? 0;
|
let max = uses.max ?? 0;
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,13 @@ export default class ForeignDocumentUUIDField extends foundry.data.fields.Docume
|
||||||
toObject(value) {
|
toObject(value) {
|
||||||
return value?.uuid ?? value;
|
return value?.uuid ?? value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
_cast(value) {
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (value instanceof foundry.abstract.Document) return value.uuid;
|
||||||
|
throw new Error(
|
||||||
|
`The value provided to a ForeignDocumentUUIDField must be a ${foundry.abstract.Document.name} instance or a UUID string.`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import AttachableItem from './attachableItem.mjs';
|
import AttachableItem from './attachableItem.mjs';
|
||||||
import { armorFeatures } from '../../config/itemConfig.mjs';
|
|
||||||
|
|
||||||
export default class DHArmor extends AttachableItem {
|
export default class DHArmor extends AttachableItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -25,7 +24,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
new fields.SchemaField({
|
new fields.SchemaField({
|
||||||
value: new fields.StringField({
|
value: new fields.StringField({
|
||||||
required: true,
|
required: true,
|
||||||
choices: CONFIG.DH.ITEM.armorFeatures,
|
choices: CONFIG.DH.ITEM.allArmorFeatures,
|
||||||
blank: true
|
blank: true
|
||||||
}),
|
}),
|
||||||
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
|
||||||
|
|
@ -60,13 +59,14 @@ export default class DHArmor extends AttachableItem {
|
||||||
const allowed = await super._preUpdate(changes, options, user);
|
const allowed = await super._preUpdate(changes, options, user);
|
||||||
if (allowed === false) return false;
|
if (allowed === false) return false;
|
||||||
|
|
||||||
|
const changedArmorFeatures = changes.system?.armorFeatures ?? [];
|
||||||
|
const removedFeatures = this.armorFeatures.filter(x => changedArmorFeatures.every(y => y.value !== x.value));
|
||||||
if (changes.system?.armorFeatures) {
|
if (changes.system?.armorFeatures) {
|
||||||
const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x));
|
const added = changedArmorFeatures.filter(x => this.armorFeatures.every(y => y.value !== x.value));
|
||||||
const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x));
|
|
||||||
|
|
||||||
const effectIds = [];
|
const effectIds = [];
|
||||||
const actionIds = [];
|
const actionIds = [];
|
||||||
for (var feature of removed) {
|
for (var feature of removedFeatures) {
|
||||||
effectIds.push(...feature.effectIds);
|
effectIds.push(...feature.effectIds);
|
||||||
actionIds.push(...feature.actionIds);
|
actionIds.push(...feature.actionIds);
|
||||||
}
|
}
|
||||||
|
|
@ -76,8 +76,9 @@ export default class DHArmor extends AttachableItem {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
|
||||||
for (const feature of added) {
|
for (const feature of added) {
|
||||||
const featureData = armorFeatures[feature.value];
|
const featureData = allFeatures[feature.value];
|
||||||
if (featureData.effects?.length > 0) {
|
if (featureData.effects?.length > 0) {
|
||||||
const embeddedItems = await this.parent.createEmbeddedDocuments(
|
const embeddedItems = await this.parent.createEmbeddedDocuments(
|
||||||
'ActiveEffect',
|
'ActiveEffect',
|
||||||
|
|
@ -91,7 +92,7 @@ export default class DHArmor extends AttachableItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newActions = {};
|
const newActions = {};
|
||||||
if (featureData.actions?.length > 0) {
|
if (featureData.actions?.length > 0 || featureData.actions?.size > 0) {
|
||||||
for (let action of featureData.actions) {
|
for (let action of featureData.actions) {
|
||||||
const embeddedEffects = await this.parent.createEmbeddedDocuments(
|
const embeddedEffects = await this.parent.createEmbeddedDocuments(
|
||||||
'ActiveEffect',
|
'ActiveEffect',
|
||||||
|
|
@ -110,10 +111,12 @@ export default class DHArmor extends AttachableItem {
|
||||||
{
|
{
|
||||||
...cls.getSourceConfig(this),
|
...cls.getSourceConfig(this),
|
||||||
...action,
|
...action,
|
||||||
|
type: action.type,
|
||||||
_id: actionId,
|
_id: actionId,
|
||||||
name: game.i18n.localize(action.name),
|
name: game.i18n.localize(action.name),
|
||||||
description: game.i18n.localize(action.description),
|
description: game.i18n.localize(action.description),
|
||||||
effects: embeddedEffects.map(x => ({ _id: x.id }))
|
effects: embeddedEffects.map(x => ({ _id: x.id })),
|
||||||
|
systemPath: 'actions'
|
||||||
},
|
},
|
||||||
{ parent: this }
|
{ parent: this }
|
||||||
);
|
);
|
||||||
|
|
@ -126,6 +129,16 @@ export default class DHArmor extends AttachableItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onUpdate(a, b, c) {
|
||||||
|
super._onUpdate(a, b, c);
|
||||||
|
|
||||||
|
if (this.actor?.type === 'character') {
|
||||||
|
for (const party of this.actor.parties) {
|
||||||
|
party.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a list of localized tags based on this item's type-specific properties.
|
* Generates a list of localized tags based on this item's type-specific properties.
|
||||||
* @returns {string[]} An array of localized tag strings.
|
* @returns {string[]} An array of localized tag strings.
|
||||||
|
|
@ -145,7 +158,8 @@ export default class DHArmor extends AttachableItem {
|
||||||
*/
|
*/
|
||||||
_getLabels() {
|
_getLabels() {
|
||||||
const labels = [];
|
const labels = [];
|
||||||
if(this.baseScore) labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`)
|
if (this.baseScore)
|
||||||
|
labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`);
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
hasResource: false,
|
hasResource: false,
|
||||||
isQuantifiable: false,
|
isQuantifiable: false,
|
||||||
isInventoryItem: false,
|
isInventoryItem: false,
|
||||||
hasActions: false
|
hasActions: false,
|
||||||
|
hasAttribution: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +38,13 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
static defineSchema() {
|
static defineSchema() {
|
||||||
const schema = {};
|
const schema = {
|
||||||
|
attribution: new fields.SchemaField({
|
||||||
|
source: new fields.StringField(),
|
||||||
|
page: new fields.NumberField(),
|
||||||
|
artist: new fields.StringField()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true });
|
if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true });
|
||||||
|
|
||||||
|
|
@ -110,6 +117,13 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get attributionLabel() {
|
||||||
|
if (!this.attribution) return;
|
||||||
|
|
||||||
|
const { source, page } = this.attribution;
|
||||||
|
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
|
* Obtain a data object used to evaluate any dice rolls associated with this Item Type
|
||||||
* @param {object} [options] - Options which modify the getRollData method.
|
* @param {object} [options] - Options which modify the getRollData method.
|
||||||
|
|
@ -187,6 +201,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
super._onUpdate(changed, options, userId);
|
super._onUpdate(changed, options, userId);
|
||||||
|
|
||||||
updateLinkedItemApps(options, this.parent.sheet);
|
updateLinkedItemApps(options, this.parent.sheet);
|
||||||
createScrollText(this.parent?.parent, options.scrollingTextData);
|
|
||||||
|
if (this.parent?.parent && options.scrollingTextData)
|
||||||
|
this.parent.parent.queueScrollText(options.scrollingTextData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,13 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
tokenImg: new fields.FilePathField({
|
tokenImg: new fields.FilePathField({
|
||||||
initial: 'icons/svg/mystery-man.svg',
|
initial: 'icons/svg/mystery-man.svg',
|
||||||
categories: ['IMAGE'],
|
categories: ['IMAGE'],
|
||||||
|
wildcard: true,
|
||||||
base64: false
|
base64: false
|
||||||
}),
|
}),
|
||||||
tokenRingImg: new fields.FilePathField({
|
tokenRingImg: new fields.FilePathField({
|
||||||
initial: 'icons/svg/mystery-man.svg',
|
initial: 'icons/svg/mystery-man.svg',
|
||||||
categories: ['IMAGE'],
|
categories: ['IMAGE'],
|
||||||
|
wildcard: true,
|
||||||
base64: false
|
base64: false
|
||||||
}),
|
}),
|
||||||
tokenSize: new fields.SchemaField({
|
tokenSize: new fields.SchemaField({
|
||||||
|
|
@ -108,6 +110,30 @@ export default class DHBeastform extends BaseDataItem {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getWildcardImage(actor, beastform) {
|
||||||
|
const usesDynamicToken = actor.prototypeToken.ring.enabled && beastform.system.tokenRingImg;
|
||||||
|
const tokenPath = usesDynamicToken ? beastform.system.tokenRingImg : beastform.system.tokenImg;
|
||||||
|
const usesWildcard = tokenPath.includes('*');
|
||||||
|
if (usesWildcard) {
|
||||||
|
const filePicker = new foundry.applications.apps.FilePicker.implementation(tokenPath);
|
||||||
|
const { files } = await foundry.applications.apps.FilePicker.implementation.browse(
|
||||||
|
filePicker.activeSource,
|
||||||
|
tokenPath,
|
||||||
|
{
|
||||||
|
wildcard: true,
|
||||||
|
type: 'image'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const selectedImage = await game.system.api.applications.dialogs.ImageSelectDialog.configure(
|
||||||
|
game.i18n.localize('DAGGERHEART.APPLICATIONS.ImageSelect.title'),
|
||||||
|
files
|
||||||
|
);
|
||||||
|
return { usesDynamicToken, selectedImage };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async _preCreate() {
|
async _preCreate() {
|
||||||
if (!this.actor) return;
|
if (!this.actor) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ export default class DHClass extends BaseDataItem {
|
||||||
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
|
suggestedSecondaryWeapon: new ForeignDocumentUUIDField({ type: 'Item' }),
|
||||||
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
|
suggestedArmor: new ForeignDocumentUUIDField({ type: 'Item' })
|
||||||
}),
|
}),
|
||||||
|
backgroundQuestions: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
|
||||||
|
connections: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }),
|
||||||
isMulticlass: new fields.BooleanField({ initial: false })
|
isMulticlass: new fields.BooleanField({ initial: false })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +98,20 @@ export default class DHClass extends BaseDataItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.system.isMulticlass) {
|
||||||
|
const addQuestions = (base, questions) => {
|
||||||
|
return `${base}${questions.map(q => `<p><strong>${q}</strong></p>`).join('<br/>')}`;
|
||||||
|
};
|
||||||
|
const backgroundQuestions = data.system.backgroundQuestions.filter(x => x);
|
||||||
|
const connections = data.system.connections.filter(x => x);
|
||||||
|
await this.actor.update({
|
||||||
|
'system.biography': {
|
||||||
|
background: addQuestions(this.actor.system.biography.background, backgroundQuestions),
|
||||||
|
connections: addQuestions(this.actor.system.biography.connections, connections)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowed = await super._preCreate(data, options, user);
|
const allowed = await super._preCreate(data, options, user);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import BaseDataItem from './base.mjs';
|
import BaseDataItem from './base.mjs';
|
||||||
import { ActionField } from '../fields/actionField.mjs';
|
|
||||||
|
|
||||||
export default class DHConsumable extends BaseDataItem {
|
export default class DHConsumable extends BaseDataItem {
|
||||||
/** @inheritDoc */
|
/** @inheritDoc */
|
||||||
|
|
@ -19,7 +18,8 @@ export default class DHConsumable extends BaseDataItem {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
return {
|
return {
|
||||||
...super.defineSchema(),
|
...super.defineSchema(),
|
||||||
consumeOnUse: new fields.BooleanField({ initial: false })
|
consumeOnUse: new fields.BooleanField({ initial: true }),
|
||||||
|
destroyOnEmpty: new fields.BooleanField({ initial: true })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,5 +27,4 @@ export default class DHConsumable extends BaseDataItem {
|
||||||
|
|
||||||
/**@override */
|
/**@override */
|
||||||
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/round-potion.svg';
|
static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/items/round-potion.svg';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue