From f1a530f57f71f18c40b34e3b345cad40b1cda5c0 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Fri, 29 May 2026 12:19:08 +0200
Subject: [PATCH 01/14] [Feature] Full Rerolls (#1928)
* Initial
* Removed damage dialogs
* Fixed DamageReroll
* Fixed d20 modifiers
* Fixed
* Fixed DiceSoNice multiple damageType reroll
* Added triggerChatRollFx
* Fixed dice.denomination being lost on damage reroll
---
lang/en.json | 7 +-
module/applications/dialogs/_module.mjs | 1 -
.../applications/dialogs/groupRollDialog.mjs | 2 -
.../dialogs/rerollDamageDialog.mjs | 280 ------------------
module/applications/dialogs/rerollDialog.mjs | 279 -----------------
.../dialogs/resourceDiceDialog.mjs | 4 +-
module/applications/dialogs/tagTeamDialog.mjs | 2 -
module/applications/ui/chatLog.mjs | 18 +-
module/data/chat-message/actorRoll.mjs | 31 ++
module/data/fields/action/damageField.mjs | 3 -
module/data/fields/action/summonField.mjs | 6 +-
module/dice/d20Roll.mjs | 12 +
module/dice/damageRoll.mjs | 42 +--
module/dice/dhRoll.mjs | 6 +-
module/dice/die/dualityDie.mjs | 22 +-
module/dice/dualityRoll.mjs | 46 ++-
module/dice/helpers.mjs | 17 ++
module/helpers/utils.mjs | 15 +
styles/less/dialog/index.less | 2 -
styles/less/dialog/reroll-dialog/sheet.less | 125 --------
.../dialogs/rerollDialog/damage/main.hbs | 35 ---
templates/dialogs/rerollDialog/footer.hbs | 4 -
templates/dialogs/rerollDialog/main.hbs | 35 ---
23 files changed, 164 insertions(+), 830 deletions(-)
delete mode 100644 module/applications/dialogs/rerollDamageDialog.mjs
delete mode 100644 module/applications/dialogs/rerollDialog.mjs
create mode 100644 module/dice/helpers.mjs
delete mode 100644 styles/less/dialog/reroll-dialog/sheet.less
delete mode 100644 templates/dialogs/rerollDialog/damage/main.hbs
delete mode 100644 templates/dialogs/rerollDialog/footer.hbs
delete mode 100644 templates/dialogs/rerollDialog/main.hbs
diff --git a/lang/en.json b/lang/en.json
index a06c46c2..f1841e09 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -712,12 +712,6 @@
"ReactionRoll": {
"title": "Reaction Roll: {trait}"
},
- "RerollDialog": {
- "title": "Reroll",
- "damageTitle": "Reroll Damage",
- "deselectDiceNotification": "Deselect one of the selected dice first",
- "acceptCurrentRolls": "Accept Current Rolls"
- },
"ResourceDice": {
"title": "{name} Resource",
"rerollDice": "Reroll Dice"
@@ -3097,6 +3091,7 @@
}
},
"ChatLog": {
+ "rerollActionRoll": "Reroll Action",
"rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll"
},
diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs
index c866f1cd..fc5169b2 100644
--- a/module/applications/dialogs/_module.mjs
+++ b/module/applications/dialogs/_module.mjs
@@ -10,7 +10,6 @@ export { default as ImageSelectDialog } from './imageSelectDialog.mjs';
export { default as ItemTransferDialog } from './itemTransfer.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs';
-export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs';
diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs
index bd45fe91..52baf537 100644
--- a/module/applications/dialogs/groupRollDialog.mjs
+++ b/module/applications/dialogs/groupRollDialog.mjs
@@ -358,8 +358,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
});
if (!result) return;
- // todo: move logic to actor.rollTrait() or actor.diceRoll()
- if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll;
diff --git a/module/applications/dialogs/rerollDamageDialog.mjs b/module/applications/dialogs/rerollDamageDialog.mjs
deleted file mode 100644
index b821bd24..00000000
--- a/module/applications/dialogs/rerollDamageDialog.mjs
+++ /dev/null
@@ -1,280 +0,0 @@
-const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
-
-export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
- constructor(message, options = {}) {
- super(options);
-
- this.message = message;
- this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
- const type = message.system.damage[typeKey];
- acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
- const part = type.parts[partKey];
- acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
- const dice = part.dice[diceKey];
- const activeResults = dice.results.filter(x => x.active);
- acc[diceKey] = {
- dice: dice.dice,
- selectedResults: activeResults.length,
- maxSelected: activeResults.length,
- results: activeResults.map(x => ({ ...x, selected: true }))
- };
-
- return acc;
- }, {});
-
- return acc;
- }, {});
-
- return acc;
- }, {});
- }
-
- static DEFAULT_OPTIONS = {
- id: 'reroll-dialog',
- classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
- window: {
- icon: 'fa-solid fa-dice'
- },
- actions: {
- toggleResult: RerollDamageDialog.#toggleResult,
- selectRoll: RerollDamageDialog.#selectRoll,
- doReroll: RerollDamageDialog.#doReroll,
- save: RerollDamageDialog.#save
- }
- };
-
- /** @override */
- static PARTS = {
- main: {
- id: 'main',
- template: 'systems/daggerheart/templates/dialogs/rerollDialog/damage/main.hbs'
- },
- footer: {
- id: 'footer',
- template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
- }
- };
-
- get title() {
- return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.damageTitle');
- }
-
- _attachPartListeners(partId, htmlElement, options) {
- super._attachPartListeners(partId, htmlElement, options);
-
- htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
- element.addEventListener('change', this.toggleDice.bind(this));
- });
- }
-
- async _prepareContext(_options) {
- const context = await super._prepareContext(_options);
- context.damage = this.damage;
- context.disabledReroll = !this.getRerollDice().length;
- context.saveDisabled = !this.isSelectionDone();
-
- return context;
- }
-
- static async #save() {
- const update = {
- 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
- const type = this.damage[typeKey];
- let typeTotal = 0;
- const messageType = this.message.system.damage[typeKey];
- const parts = Object.keys(type).map(partKey => {
- const part = type[partKey];
- const messagePart = messageType.parts[partKey];
- let partTotal = messagePart.modifierTotal;
- const dice = Object.keys(part).map(diceKey => {
- const dice = part[diceKey];
- const total = dice.results.reduce((acc, result) => {
- if (result.active) acc += result.result;
- return acc;
- }, 0);
- partTotal += total;
- const messageDice = messagePart.dice[diceKey];
- return {
- ...messageDice,
- total: total,
- results: dice.results.map(x => ({
- ...x,
- hasRerolls: dice.results.length > 1
- }))
- };
- });
-
- typeTotal += partTotal;
- return {
- ...messagePart,
- total: partTotal,
- dice: dice
- };
- });
-
- acc[typeKey] = {
- ...messageType,
- total: typeTotal,
- parts: parts
- };
-
- return acc;
- }, {})
- };
-
- await this.message.update(update);
- await this.close();
- }
-
- getRerollDice() {
- const rerollDice = [];
- Object.keys(this.damage).forEach(typeKey => {
- const type = this.damage[typeKey];
- Object.keys(type).forEach(partKey => {
- const part = type[partKey];
- Object.keys(part).forEach(diceKey => {
- const dice = part[diceKey];
- Object.keys(dice.results).forEach(resultKey => {
- const result = dice.results[resultKey];
- if (result.toReroll) {
- rerollDice.push({
- ...result,
- dice: dice.dice,
- type: typeKey,
- part: partKey,
- dice: diceKey,
- result: resultKey
- });
- }
- });
- });
- });
- });
-
- return rerollDice;
- }
-
- isSelectionDone() {
- const diceFinishedData = [];
- Object.keys(this.damage).forEach(typeKey => {
- const type = this.damage[typeKey];
- Object.keys(type).forEach(partKey => {
- const part = type[partKey];
- Object.keys(part).forEach(diceKey => {
- const dice = part[diceKey];
- const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
- diceFinishedData.push(selected === dice.maxSelected);
- });
- });
- });
-
- return diceFinishedData.every(x => x);
- }
-
- toggleDice(event) {
- const target = event.target;
- const { type, part, dice } = target.dataset;
- const toggleDice = this.damage[type][part][dice];
-
- const existingDiceRerolls = this.getRerollDice().filter(
- x => x.type === type && x.part === part && x.dice === dice
- );
-
- const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
-
- toggleDice.toReroll = !allRerolled;
- toggleDice.results.forEach(result => {
- if (result.active) {
- result.toReroll = !allRerolled;
- }
- });
-
- this.render();
- }
-
- static #toggleResult(event) {
- event.stopPropagation();
-
- const target = event.target.closest('.to-reroll-result');
- const { type, part, dice, result } = target.dataset;
- const toggleDice = this.damage[type][part][dice];
- const toggleResult = toggleDice.results[result];
- toggleResult.toReroll = !toggleResult.toReroll;
-
- const existingDiceRerolls = this.getRerollDice().filter(
- x => x.type === type && x.part === part && x.dice === dice
- );
-
- const allToReroll = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
- toggleDice.toReroll = allToReroll;
-
- this.render();
- }
-
- static async #selectRoll(_, button) {
- const { type, part, dice, result } = button.dataset;
-
- const diceVal = this.damage[type][part][dice];
- const diceResult = diceVal.results[result];
- if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
- return ui.notifications.warn(
- game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
- );
- }
-
- if (diceResult.active) {
- diceVal.toReroll = false;
- diceResult.toReroll = false;
- }
-
- diceVal.selectedResults += diceResult.active ? -1 : 1;
- diceResult.active = !diceResult.active;
-
- this.render();
- }
-
- static async #doReroll() {
- const toReroll = this.getRerollDice().map(x => {
- const { type, part, dice, result } = x;
- const diceData = this.damage[type][part][dice].results[result];
- return {
- ...diceData,
- dice: this.damage[type][part][dice].dice,
- typeKey: type,
- partKey: part,
- diceKey: dice,
- resultsIndex: result
- };
- });
-
- const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
-
- if (game.modules.get('dice-so-nice')?.active) {
- const diceSoNiceRoll = {
- _evaluated: true,
- dice: roll.dice,
- options: { appearance: {} }
- };
-
- await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
- }
-
- toReroll.forEach((data, index) => {
- const { typeKey, partKey, diceKey, resultsIndex } = data;
- const rerolledDice = roll.dice[index];
-
- const dice = this.damage[typeKey][partKey][diceKey];
- dice.toReroll = false;
- dice.results[resultsIndex].active = false;
- dice.results[resultsIndex].discarded = true;
- dice.results[resultsIndex].toReroll = false;
- dice.results.splice(dice.results.length, 0, {
- ...rerolledDice.results[0],
- toReroll: false,
- selected: true
- });
- });
-
- this.render();
- }
-}
diff --git a/module/applications/dialogs/rerollDialog.mjs b/module/applications/dialogs/rerollDialog.mjs
deleted file mode 100644
index cae4e53a..00000000
--- a/module/applications/dialogs/rerollDialog.mjs
+++ /dev/null
@@ -1,279 +0,0 @@
-const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
-
-export default class RerollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
- constructor(message, options = {}) {
- super(options);
-
- this.message = message;
- this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => {
- const type = message.system.damage[typeKey];
- acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => {
- const part = type.parts[partKey];
- acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => {
- const dice = part.dice[diceKey];
- const activeResults = dice.results.filter(x => x.active);
- acc[diceKey] = {
- dice: dice.dice,
- selectedResults: activeResults.length,
- maxSelected: activeResults.length,
- results: activeResults.map(x => ({ ...x, selected: true }))
- };
-
- return acc;
- }, {});
-
- return acc;
- }, {});
-
- return acc;
- }, {});
- }
-
- static DEFAULT_OPTIONS = {
- id: 'reroll-dialog',
- classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'],
- window: {
- icon: 'fa-solid fa-dice'
- },
- actions: {
- toggleResult: RerollDialog.#toggleResult,
- selectRoll: RerollDialog.#selectRoll,
- doReroll: RerollDialog.#doReroll,
- save: RerollDialog.#save
- }
- };
-
- /** @override */
- static PARTS = {
- main: {
- id: 'main',
- template: 'systems/daggerheart/templates/dialogs/rerollDialog/main.hbs'
- },
- footer: {
- id: 'footer',
- template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs'
- }
- };
-
- get title() {
- return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.title');
- }
-
- _attachPartListeners(partId, htmlElement, options) {
- super._attachPartListeners(partId, htmlElement, options);
-
- htmlElement.querySelectorAll('.to-reroll-input').forEach(element => {
- element.addEventListener('change', this.toggleDice.bind(this));
- });
- }
-
- async _prepareContext(_options) {
- const context = await super._prepareContext(_options);
- context.damage = this.damage;
- context.disabledReroll = !this.getRerollDice().length;
- context.saveDisabled = !this.isSelectionDone();
-
- return context;
- }
-
- static async #save() {
- const update = {
- 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => {
- const type = this.damage[typeKey];
- let typeTotal = 0;
- const messageType = this.message.system.damage[typeKey];
- const parts = Object.keys(type).map(partKey => {
- const part = type[partKey];
- const messagePart = messageType.parts[partKey];
- let partTotal = messagePart.modifierTotal;
- const dice = Object.keys(part).map(diceKey => {
- const dice = part[diceKey];
- const total = dice.results.reduce((acc, result) => {
- if (result.active) acc += result.result;
- return acc;
- }, 0);
- partTotal += total;
- const messageDice = messagePart.dice[diceKey];
- return {
- ...messageDice,
- total: total,
- results: dice.results.map(x => ({
- ...x,
- hasRerolls: dice.results.length > 1
- }))
- };
- });
-
- typeTotal += partTotal;
- return {
- ...messagePart,
- total: partTotal,
- dice: dice
- };
- });
-
- acc[typeKey] = {
- ...messageType,
- total: typeTotal,
- parts: parts
- };
-
- return acc;
- }, {})
- };
- await this.message.update(update);
- await this.close();
- }
-
- getRerollDice() {
- const rerollDice = [];
- Object.keys(this.damage).forEach(typeKey => {
- const type = this.damage[typeKey];
- Object.keys(type).forEach(partKey => {
- const part = type[partKey];
- Object.keys(part).forEach(diceKey => {
- const dice = part[diceKey];
- Object.keys(dice.results).forEach(resultKey => {
- const result = dice.results[resultKey];
- if (result.toReroll) {
- rerollDice.push({
- ...result,
- dice: dice.dice,
- type: typeKey,
- part: partKey,
- dice: diceKey,
- result: resultKey
- });
- }
- });
- });
- });
- });
-
- return rerollDice;
- }
-
- isSelectionDone() {
- const diceFinishedData = [];
- Object.keys(this.damage).forEach(typeKey => {
- const type = this.damage[typeKey];
- Object.keys(type).forEach(partKey => {
- const part = type[partKey];
- Object.keys(part).forEach(diceKey => {
- const dice = part[diceKey];
- const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0);
- diceFinishedData.push(selected === dice.maxSelected);
- });
- });
- });
-
- return diceFinishedData.every(x => x);
- }
-
- toggleDice(event) {
- const target = event.target;
- const { type, part, dice } = target.dataset;
- const toggleDice = this.damage[type][part][dice];
-
- const existingDiceRerolls = this.getRerollDice().filter(
- x => x.type === type && x.part === part && x.dice === dice
- );
-
- const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length;
-
- toggleDice.toReroll = !allRerolled;
- toggleDice.results.forEach(result => {
- if (result.active) {
- result.toReroll = !allRerolled;
- }
- });
-
- this.render();
- }
-
- static #toggleResult(event) {
- event.stopPropagation();
-
- const target = event.target.closest('.to-reroll-result');
- const { type, part, dice, result } = target.dataset;
- const toggleDice = this.damage[type][part][dice];
- const toggleResult = toggleDice.results[result];
- toggleResult.toReroll = !toggleResult.toReroll;
-
- const existingDiceRerolls = this.getRerollDice().filter(
- x => x.type === type && x.part === part && x.dice === dice
- );
-
- const allToReroll = existingDiceRerolls.length === toggleDice.results.length;
- toggleDice.toReroll = allToReroll;
-
- this.render();
- }
-
- static async #selectRoll(_, button) {
- const { type, part, dice, result } = button.dataset;
-
- const diceVal = this.damage[type][part][dice];
- const diceResult = diceVal.results[result];
- if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) {
- return ui.notifications.warn(
- game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification')
- );
- }
-
- if (diceResult.active) {
- diceVal.toReroll = false;
- diceResult.toReroll = false;
- }
-
- diceVal.selectedResults += diceResult.active ? -1 : 1;
- diceResult.active = !diceResult.active;
-
- this.render();
- }
-
- static async #doReroll() {
- const toReroll = this.getRerollDice().map(x => {
- const { type, part, dice, result } = x;
- const diceData = this.damage[type][part][dice].results[result];
- return {
- ...diceData,
- dice: this.damage[type][part][dice].dice,
- typeKey: type,
- partKey: part,
- diceKey: dice,
- resultsIndex: result
- };
- });
-
- const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate();
-
- if (game.modules.get('dice-so-nice')?.active) {
- const diceSoNiceRoll = {
- _evaluated: true,
- dice: roll.dice,
- options: { appearance: {} }
- };
-
- await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
- }
-
- toReroll.forEach((data, index) => {
- const { typeKey, partKey, diceKey, resultsIndex } = data;
- const rerolledDice = roll.dice[index];
-
- const dice = this.damage[typeKey][partKey][diceKey];
- dice.toReroll = false;
- dice.results[resultsIndex].active = false;
- dice.results[resultsIndex].discarded = true;
- dice.results[resultsIndex].toReroll = false;
- dice.results.splice(dice.results.length, 0, {
- ...rerolledDice.results[0],
- toReroll: false,
- selected: true
- });
- });
-
- this.render();
- }
-}
diff --git a/module/applications/dialogs/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs
index 32e1e5d8..8394538c 100644
--- a/module/applications/dialogs/resourceDiceDialog.mjs
+++ b/module/applications/dialogs/resourceDiceDialog.mjs
@@ -1,4 +1,4 @@
-import { itemAbleRollParse } from '../../helpers/utils.mjs';
+import { itemAbleRollParse, triggerChatRollFx } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@@ -69,7 +69,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate();
- if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
+ await triggerChatRollFx([roll]);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
this.resetUsed = true;
diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs
index ba76831f..4e63d93b 100644
--- a/module/applications/dialogs/tagTeamDialog.mjs
+++ b/module/applications/dialogs/tagTeamDialog.mjs
@@ -434,8 +434,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
if (!result) return;
- if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
-
const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll;
this.updatePartyData(
diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs
index 34b25591..7036a5df 100644
--- a/module/applications/ui/chatLog.mjs
+++ b/module/applications/ui/chatLog.mjs
@@ -103,6 +103,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
+ {
+ label: 'DAGGERHEART.UI.ChatLog.rerollActionRoll',
+ icon: ' ',
+ visible: li => {
+ const message = game.messages.get(li.dataset.messageId);
+ return message.system.hasRoll && (game.user.isGM || message.isAuthor);
+ },
+ callback: async li => {
+ const message = game.messages.get(li.dataset.messageId);
+ const reroll = await message.rolls[0].reroll({ liveRoll: true });
+ message.update({ rolls: [reroll] });
+ }
+ },
{
label: 'DAGGERHEART.UI.ChatLog.rerollDamage',
icon: ' ',
@@ -113,9 +126,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
: false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage;
},
- callback: li => {
+ callback: async li => {
const message = game.messages.get(li.dataset.messageId);
- new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true });
+ const update = await message.system.getRerolledDamage();
+ message.update(update);
}
}
];
diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs
index eaa1cdc2..ccfe25ea 100644
--- a/module/data/chat-message/actorRoll.mjs
+++ b/module/data/chat-message/actorRoll.mjs
@@ -1,3 +1,5 @@
+import { triggerChatRollFx } from '../../helpers/utils.mjs';
+
const fields = foundry.data.fields;
const targetsField = () =>
@@ -130,6 +132,35 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
});
}
+ /* TODO: Change how damage data is stored somehow to enable better rerolling */
+ async getRerolledDamage() {
+ if (!this.damage) return;
+
+ const rerolls = [];
+ const update = { system: { damage: {} } };
+ for (const partKey in this.damage) {
+ const part = this.damage[partKey];
+ const testRoll = Roll.fromData(part.parts[0].roll);
+ const rerolled = await testRoll.reroll();
+ rerolls.push(rerolled);
+
+ if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
+ const partData = update.system.damage[partKey].parts[0];
+ update.system.damage[partKey].total = rerolled.total;
+ partData.modifierTotal = rerolled.terms.reduce((acc, x) => {
+ if (x.isDeterministic && !x.operator) acc += x.total;
+ return acc;
+ }, 0);
+ partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination }));
+ partData.total = rerolled.total;
+ partData.roll = rerolled.toJSON();
+ }
+
+ await triggerChatRollFx(rerolls);
+
+ return update;
+ }
+
registerTargetHook() {
if (!this.parent.isAuthor || !this.hasTarget) return;
if (this.targetMode && this.parent.targetHook !== null) {
diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs
index 30a5ad7c..9b21d3ba 100644
--- a/module/data/fields/action/damageField.mjs
+++ b/module/data/fields/action/damageField.mjs
@@ -72,9 +72,6 @@ export default class DamageField extends fields.SchemaField {
damageConfig.source.message = 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;
if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true;
diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs
index ec7881f7..a2275fa5 100644
--- a/module/data/fields/action/summonField.mjs
+++ b/module/data/fields/action/summonField.mjs
@@ -1,4 +1,4 @@
-import { itemAbleRollParse } from '../../../helpers/utils.mjs';
+import { itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs';
import FormulaField from '../formulaField.mjs';
const fields = foundry.data.fields;
@@ -40,7 +40,7 @@ export default class DHSummonField extends fields.ArrayField {
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
await roll.evaluate();
const count = roll.total;
- if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll);
+ if (!roll.isDeterministic) rolls.push(roll);
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
@@ -56,7 +56,7 @@ export default class DHSummonField extends fields.ArrayField {
}
}
- if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
+ if (rolls.length) await triggerChatRollFx(rolls);
this.actor.sheet?.minimize();
DHSummonField.handleSummon(summonData, this.actor);
diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs
index 509f5d69..b1d3bd0b 100644
--- a/module/dice/d20Roll.mjs
+++ b/module/dice/d20Roll.mjs
@@ -1,4 +1,5 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
+import { triggerChatRollFx } from '../helpers/utils.mjs';
import DHRoll from './dhRoll.mjs';
export default class D20Roll extends DHRoll {
@@ -224,4 +225,15 @@ export default class D20Roll extends DHRoll {
resetFormula() {
return (this._formula = this.constructor.getFormula(this.terms));
}
+
+ async reroll(options) {
+ const result = await super.reroll(options);
+ if (this instanceof game.system.api.dice.DualityRoll) return result;
+
+ if (options?.liveRoll) {
+ await triggerChatRollFx([result]);
+ }
+
+ return result;
+ }
}
diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs
index 98fd8401..ef810ed7 100644
--- a/module/dice/damageRoll.mjs
+++ b/module/dice/damageRoll.mjs
@@ -1,5 +1,5 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
-import { parseRallyDice } from '../helpers/utils.mjs';
+import { parseRallyDice, triggerChatRollFx } from '../helpers/utils.mjs';
import DHRoll from './dhRoll.mjs';
export default class DamageRoll extends DHRoll {
@@ -18,7 +18,12 @@ export default class DamageRoll extends DHRoll {
if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate();
roll._evaluated = true;
- const parts = config.roll.map(r => this.postEvaluate(r));
+
+ const parts = [];
+ for (const roll of config.roll) {
+ parts.push(this.postEvaluate(roll));
+ roll.roll = JSON.stringify(roll.roll.toJSON());
+ }
config.damage = this.unifyDamageRoll(parts);
}
@@ -38,25 +43,24 @@ export default class DamageRoll extends DHRoll {
const chatMessage = config.source?.message
? ui.chat.collection.get(config.source.message)
: getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public');
+
+ const diceRolls = [];
if (game.modules.get('dice-so-nice')?.active) {
- const pool = foundry.dice.terms.PoolTerm.fromRolls(
- Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
- ),
- diceRoll = Roll.fromTerms([pool]);
- await game.dice3d.showForRoll(
- diceRoll,
- game.user,
- true,
- chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
- chatMessage.blind
- );
config.mute = true;
+ const pool = foundry.dice.terms.PoolTerm.fromRolls(
+ Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
+ );
+ diceRolls.push(Roll.fromTerms([pool]));
}
+
+ await triggerChatRollFx(diceRolls, {
+ whisper: chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
+ blind: chatMessage.blind
+ });
await super.buildPost(roll, config, message);
+
if (config.source?.message) {
chatMessage.update({ 'system.damage': config.damage });
-
- if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
}
@@ -319,9 +323,10 @@ export default class DamageRoll extends DHRoll {
const newIndex = parsedDiceTerms[dice].results.length;
await term.reroll(`/r1=${termResult.result}`);
+ const diceRolls = [];
if (game.modules.get('dice-so-nice')?.active) {
const newResult = parsedDiceTerms[dice].results[newIndex];
- const diceSoNiceRoll = {
+ diceRolls.push({
_evaluated: true,
dice: [
new foundry.dice.terms.Die({
@@ -332,11 +337,10 @@ export default class DamageRoll extends DHRoll {
})
],
options: { appearance: {} }
- };
-
- await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
+ });
}
+ await triggerChatRollFx(diceRolls);
await parsedRoll.evaluate();
const results = parsedRoll.dice[dice].results.map(result => ({
diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs
index d6975f71..02c4ab24 100644
--- a/module/dice/dhRoll.mjs
+++ b/module/dice/dhRoll.mjs
@@ -1,4 +1,5 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
+import { triggerChatRollFx } from '../helpers/utils.mjs';
export default class DHRoll extends Roll {
baseTerms = [];
@@ -75,9 +76,7 @@ export default class DHRoll extends Roll {
}
if (config.skips?.createMessage) {
- if (game.modules.get('dice-so-nice')?.active) {
- await game.dice3d.showForRoll(roll, game.user, true);
- }
+ await triggerChatRollFx([roll]);
} else if (!config.source?.message) {
config.message = await this.toMessage(roll, config);
}
@@ -85,6 +84,7 @@ export default class DHRoll extends Roll {
static postEvaluate(roll, config = {}) {
return {
+ ...roll.options.roll,
total: roll.total,
formula: roll.formula,
dice: roll.dice.map(d => ({
diff --git a/module/dice/die/dualityDie.mjs b/module/dice/die/dualityDie.mjs
index 83229425..cc7ee75e 100644
--- a/module/dice/die/dualityDie.mjs
+++ b/module/dice/die/dualityDie.mjs
@@ -1,4 +1,4 @@
-import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
+import { updateResourcesForDualityReroll } from '../helpers.mjs';
export default class DualityDie extends foundry.dice.terms.Die {
constructor(options) {
@@ -12,24 +12,6 @@ export default class DualityDie extends foundry.dice.terms.Die {
return roll.withHope ? 1 : roll.withFear ? -1 : 0;
}
- #updateResources(oldDuality, newDuality, actor) {
- const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
- if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
-
- const updates = [];
- const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
- const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
- const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
-
- if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
- if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
- if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
-
- const resourceUpdates = new ResourceUpdateMap(actor);
- resourceUpdates.addResources(updates);
- resourceUpdates.updateResources();
- }
-
async reroll(modifier, options) {
const oldDuality = this.#getDualityState(options.liveRoll.roll);
await super.reroll(modifier, options);
@@ -57,7 +39,7 @@ export default class DualityDie extends foundry.dice.terms.Die {
if (options.liveRoll.isReaction) return;
const newDuality = this.#getDualityState(options.liveRoll.roll);
- this.#updateResources(oldDuality, newDuality, options.liveRoll.actor);
+ updateResourcesForDualityReroll(oldDuality, newDuality, options.liveRoll.actor);
}
}
diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs
index d58811fe..f40e9781 100644
--- a/module/dice/dualityRoll.mjs
+++ b/module/dice/dualityRoll.mjs
@@ -1,6 +1,8 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.mjs';
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
+import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
+import { updateResourcesForDualityReroll } from './helpers.mjs';
export default class DualityRoll extends D20Roll {
_advantageNumber = 1;
@@ -130,13 +132,7 @@ export default class DualityRoll extends D20Roll {
}
createBaseDice() {
- if (
- this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie &&
- this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie
- ) {
- this.terms = [this.terms[0], this.terms[1], this.terms[2]];
- return;
- }
+ this.terms = [this.terms[0], this.terms[1], this.terms[2]];
this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
@@ -388,4 +384,40 @@ export default class DualityRoll extends D20Roll {
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
}
}
+
+ async reroll(options) {
+ const oldDuality = this.withHope ? 1 : this.withFear ? -1 : 0;
+ const rerolled = DualityRoll.fromData((await super.reroll(options)).toJSON());
+
+ if (options?.liveRoll) {
+ if (game.modules.get('dice-so-nice')?.active) {
+ const diceAppearance = await getDiceSoNicePresets(
+ rerolled,
+ rerolled.dHope.denomination,
+ rerolled.dFear.denomination
+ );
+ rerolled.dHope.options.appearance = diceAppearance.hope.appearance;
+ rerolled.dFear.options.appearance = diceAppearance.fear.appearance;
+ if (rerolled.dAdvantage) rerolled.dAdvantage.options.appearance = diceAppearance.advantage.appearance;
+ if (rerolled.dDisadvantage)
+ rerolled.dDisadvantage.options.appearance = diceAppearance.disadvantage.appearance;
+
+ await game.dice3d.showForRoll(rerolled, game.user, true);
+ } else {
+ foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
+ }
+
+ if (this.options.actionType === 'reaction') return;
+
+ const newDuality = rerolled.withHope ? 1 : rerolled.withFear ? -1 : 0;
+ const actor = await foundry.utils.fromUuid(this.options.source.actor);
+ updateResourcesForDualityReroll(oldDuality, newDuality, actor);
+ }
+
+ return rerolled;
+ }
+
+ fromJSON(json) {
+ return super.fromJSON(json);
+ }
}
diff --git a/module/dice/helpers.mjs b/module/dice/helpers.mjs
new file mode 100644
index 00000000..33519949
--- /dev/null
+++ b/module/dice/helpers.mjs
@@ -0,0 +1,17 @@
+export function updateResourcesForDualityReroll(oldDuality, newDuality, actor) {
+ const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
+ if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
+
+ const updates = [];
+ const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
+ const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
+ const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
+
+ if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
+ if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
+ if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
+
+ const resourceUpdates = new ResourceUpdateMap(actor);
+ resourceUpdates.addResources(updates);
+ resourceUpdates.updateResources();
+}
diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs
index 7bc5fa25..8bc95aa0 100644
--- a/module/helpers/utils.mjs
+++ b/module/helpers/utils.mjs
@@ -864,3 +864,18 @@ export function camelize(str) {
})
.replace(/\s+/g, '');
}
+
+/**
+ * Triggers DiceSoNice rolls or dice roll audio for rolls. Not used for duality rolls.
+ * @param { Roll[] } rolls
+ * @return { void }
+ */
+export async function triggerChatRollFx(rolls, options = { whisper: false, blind: false }) {
+ const { whisper, blind } = options;
+ if (game.modules.get('dice-so-nice')?.active) {
+ const rerollPromises = rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true, whisper, blind));
+ await Promise.allSettled(rerollPromises);
+ } else {
+ foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
+ }
+}
diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less
index 11d9635e..e8f61318 100644
--- a/styles/less/dialog/index.less
+++ b/styles/less/dialog/index.less
@@ -24,8 +24,6 @@
@import './multiclass-choice/sheet.less';
-@import './reroll-dialog/sheet.less';
-
@import './tag-team-dialog/initialization.less';
@import './tag-team-dialog/sheet.less';
diff --git a/styles/less/dialog/reroll-dialog/sheet.less b/styles/less/dialog/reroll-dialog/sheet.less
deleted file mode 100644
index 71c94d80..00000000
--- a/styles/less/dialog/reroll-dialog/sheet.less
+++ /dev/null
@@ -1,125 +0,0 @@
-.daggerheart.dialog.dh-style.views.reroll-dialog {
- .window-content {
- max-width: 648px;
- }
-
- .reroll-outer-container {
- h2 {
- margin: 0;
- }
-
- .dices-container {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
-
- .dice-outer-container {
- width: 300px;
-
- legend {
- display: flex;
- align-items: center;
- gap: 4px;
-
- i {
- margin-right: 4px;
- }
- }
-
- .dice-container {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
-
- .result-container {
- position: relative;
- aspect-ratio: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.375rem;
- opacity: 0.8;
-
- &.selected {
- opacity: 1;
- border: 1px solid;
- border-radius: 6px;
- border-color: light-dark(@dark-blue, @golden);
- filter: drop-shadow(0 0 3px @golden);
- }
-
- &:before {
- content: ' ';
- position: absolute;
- width: 100%;
- height: 100%;
- z-index: -1;
- mask: var(--svg-die) no-repeat center;
- mask-size: contain;
- background: linear-gradient(139.01deg, #efe6d8 3.51%, #372e1f 96.49%);
- }
-
- &.d4:before {
- --svg-die: url(../assets/icons/dice/default/d4.svg);
- }
- &.d6:before {
- --svg-die: url(../assets/icons/dice/default/d6.svg);
- }
- &.d8:before {
- --svg-die: url(../assets/icons/dice/default/d8.svg);
- }
- &.d10:before {
- --svg-die: url(../assets/icons/dice/default/d10.svg);
- }
- &.d12:before {
- --svg-die: url('../assets/icons/dice/default/d12.svg');
- }
- &.d20:before {
- --svg-die: url(../assets/icons/dice/default/d20.svg);
- }
-
- .to-reroll-result {
- position: absolute;
- bottom: -7px;
- gap: 2px;
- border: 1px solid;
- border-radius: 6px;
- background-image: url(../assets/parchments/dh-parchment-dark.png);
- display: flex;
- align-items: center;
- padding: 2px 6px;
-
- input {
- margin: 0;
- height: 12px;
- line-height: 0px;
- position: relative;
- top: 1px;
-
- &:before,
- &:after {
- line-height: 12px;
- font-size: var(--font-size-12);
- }
- }
-
- i {
- font-size: var(--font-size-10);
- }
- }
- }
- }
- }
- }
-
- footer {
- margin-top: 8px;
- display: flex;
- justify-content: space-between;
-
- .controls {
- display: flex;
- gap: 8px;
- }
- }
-}
diff --git a/templates/dialogs/rerollDialog/damage/main.hbs b/templates/dialogs/rerollDialog/damage/main.hbs
deleted file mode 100644
index 5b994bf6..00000000
--- a/templates/dialogs/rerollDialog/damage/main.hbs
+++ /dev/null
@@ -1,35 +0,0 @@
-
- {{#each damage}}
-
{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}
- {{#each this}}
-
- {{#each this}}
-
-
-
-
- {{this.selectedResults}}/{{this.maxSelected}} Selected
-
-
-
- {{#each this.results}}
-
- {{/each}}
-
-
- {{/each}}
-
- {{/each}}
- {{/each}}
-
\ No newline at end of file
diff --git a/templates/dialogs/rerollDialog/footer.hbs b/templates/dialogs/rerollDialog/footer.hbs
deleted file mode 100644
index 5d4ae2b2..00000000
--- a/templates/dialogs/rerollDialog/footer.hbs
+++ /dev/null
@@ -1,4 +0,0 @@
-
- {{localize "DAGGERHEART.GENERAL.reroll"}}
- {{localize "DAGGERHEART.APPLICATIONS.RerollDialog.acceptCurrentRolls"}}
-
\ No newline at end of file
diff --git a/templates/dialogs/rerollDialog/main.hbs b/templates/dialogs/rerollDialog/main.hbs
deleted file mode 100644
index 6f10ce33..00000000
--- a/templates/dialogs/rerollDialog/main.hbs
+++ /dev/null
@@ -1,35 +0,0 @@
-
- {{#each damage}}
-
{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}
- {{#each this}}
-
- {{#each this}}
-
-
-
-
- {{this.selectedResults}}/{{this.results.length}} Selected
-
-
-
- {{#each this.results}}
-
- {{/each}}
-
-
- {{/each}}
-
- {{/each}}
- {{/each}}
-
\ No newline at end of file
From 9487b07e434ba1449b7ebab9998fc32313b4985a Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sat, 30 May 2026 06:47:06 -0400
Subject: [PATCH 02/14] Fix tier adjustment on actions that use standard attack
damage (#1942)
---
.gitattributes | 2 +
module/data/actor/adversary.mjs | 204 +------------------------
module/data/actor/tierAdjustment.mjs | 218 +++++++++++++++++++++++++++
3 files changed, 222 insertions(+), 202 deletions(-)
create mode 100644 .gitattributes
create mode 100644 module/data/actor/tierAdjustment.mjs
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..56ce8818
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto eol=lf
+*.json text eol=lf
diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs
index d69e17ad..d6d0dcdf 100644
--- a/module/data/actor/adversary.mjs
+++ b/module/data/actor/adversary.mjs
@@ -3,8 +3,7 @@ import { ActionField } from '../fields/actionField.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { bonusField } from '../fields/actorField.mjs';
-import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
-import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
+import { getTierAdjustedAdversary } from './tierAdjustment.mjs';
export default class DhpAdversary extends DhCreature {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];
@@ -206,205 +205,6 @@ export default class DhpAdversary extends DhCreature {
/** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
adjustForTier(tier) {
const source = this.parent.toObject(true);
-
- /** @type {(2 | 3 | 4)[]} */
- const tiers = new Array(Math.abs(tier - this.tier))
- .fill(0)
- .map((_, idx) => idx + Math.min(tier, this.tier) + 1);
- if (tier < this.tier) tiers.reverse();
- const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
- const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
-
- // Apply simple tier changes
- const scale = tier > this.tier ? 1 : -1;
- for (const entry of tierEntries) {
- source.system.difficulty += scale * entry.difficulty;
- source.system.damageThresholds.major += scale * entry.majorThreshold;
- source.system.damageThresholds.severe += scale * entry.severeThreshold;
- source.system.resources.hitPoints.max += scale * entry.hp;
- source.system.resources.stress.max += scale * entry.stress;
- source.system.attack.roll.bonus += scale * entry.attack;
- }
-
- // Get the mean and standard deviation of expected damage in the previous and new tier
- // The data we have is for attack scaling, but we reuse this for action scaling later
- const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
- const damageMeta = {
- currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
- newDamageRange: { tier, ...expectedDamageData[tier] },
- type: 'attack'
- };
-
- // Update damage of base attack
- try {
- this.#adjustActionDamage(source.system.attack, damageMeta);
- } catch (err) {
- ui.notifications.warn('Failed to convert attack damage of adversary');
- console.error(err);
- }
-
- // Update damage of each item action, making sure to also update the description if possible
- const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
- for (const item of source.items) {
- // Replace damage inlines with new formulas
- for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
- withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
- const { value: formula } = parseInlineParams(inner);
- if (!formula || !type) return match;
-
- try {
- const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' });
- const newFormula = [
- adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null,
- adjusted.bonus
- ]
- .filter(p => !!p)
- .join('+');
- return match.replace(formula, newFormula);
- } catch {
- return match;
- }
- });
- }
-
- // Update damage in item actions
- // Parse damage, and convert all formula matches in the descriptions to the new damage
- for (const action of Object.values(item.system.actions)) {
- try {
- const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
- if (!result) continue;
-
- for (const { previousFormula, formula } of Object.values(result)) {
- const oldFormulaRegexp = new RegExp(
- previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
- );
- item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
- action.description = action.description.replace(oldFormulaRegexp, formula);
- }
- } catch (err) {
- ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
- console.error(err);
- }
- }
- }
-
- // Finally set the tier of the source data, now that everything is complete
- source.system.tier = tier;
- return source;
- }
-
- /**
- * Converts a damage object to a new damage range
- * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
- * @throws error if the formula is the wrong type
- */
- #calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) {
- const terms = parseTermsFromSimpleFormula(formula);
- const flatTerms = terms.filter(t => t.diceQuantity === 0);
- const diceTerms = terms.filter(t => t.diceQuantity > 0);
- if (flatTerms.length > 1 || diceTerms.length > 1) {
- throw new Error('invalid formula for conversion');
- }
- const value = {
- ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
- bonus: flatTerms[0]?.bonus ?? 0
- };
- const previousExpected = calculateExpectedValue(value);
- if (previousExpected === 0) return value; // nothing to do
-
- const dieSizes = [4, 6, 8, 10, 12, 20];
- const steps = newDamageRange.tier - currentDamageRange.tier;
- const increasing = steps > 0;
- const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
- const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
-
- // If this was just a flat number, convert to the expected damage and exit
- if (value.diceQuantity === 0) {
- value.bonus = Math.round(expected);
- return value;
- }
-
- const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
- const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
-
- // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
- const baseOverages = Math.floor(value.bonus / getExpectedDie());
-
- // Prestep. Change number of dice for attacks, bump up/down for actions
- // We never bump up to d20, though we might bump down from it
- if (type === 'attack') {
- const minimum = increasing ? value.diceQuantity : 0;
- value.diceQuantity = Math.max(minimum, newDamageRange.tier);
- } else {
- const currentIdx = dieSizes.indexOf(value.faces);
- value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
- }
-
- value.bonus = Math.round(expected - getBaseAverage());
-
- // Attempt to handle negative values.
- // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
- if (value.bonus < 0) {
- let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
- const currentIdx = dieSizes.indexOf(value.faces);
-
- // If step downs alone don't suffice, change the flat modifier, then calculate steps required again
- // If this isn't sufficient, the result will be slightly off. This is unlikely to happen
- if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
- value.diceQuantity -= increasing ? 1 : Math.abs(steps);
- value.bonus = Math.round(expected - getBaseAverage());
- if (value.bonus >= 0) return value; // complete
- }
-
- stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
- value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
- value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
- }
-
- // If value is really high, we add a number of dice based on the number of overages
- // This attempts to preserve a similar amount of variance when increasing an action
- const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
- if (type !== 'attack' && increasing && overagesToRemove > 0) {
- value.diceQuantity += overagesToRemove;
- value.bonus = Math.round(expected - getBaseAverage());
- }
-
- return value;
- }
-
- /**
- * Updates damage to reflect a specific value.
- * @throws if damage structure is invalid for conversion
- * @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
- */
- #adjustActionDamage(action, damageMeta) {
- if (!action.damage?.parts.hitPoints) return null;
-
- const result = {};
- for (const property of ['value', 'valueAlt']) {
- const data = action.damage.parts.hitPoints[property];
- const previousFormula = data.custom.enabled
- ? data.custom.formula
- : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
- .filter(p => !!p)
- .join('+');
- const value = this.#calculateAdjustedDamage(previousFormula, damageMeta);
- const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
- .filter(p => !!p)
- .join('+');
- if (value.diceQuantity) {
- data.custom.enabled = false;
- data.bonus = value.bonus;
- data.dice = `d${value.faces}`;
- data.flatMultiplier = value.diceQuantity;
- } else if (!value.diceQuantity) {
- data.custom.enabled = true;
- data.custom.formula = formula;
- }
-
- result[property] = { previousFormula, formula, value };
- }
-
- return result;
+ return getTierAdjustedAdversary(source, tier);
}
}
diff --git a/module/data/actor/tierAdjustment.mjs b/module/data/actor/tierAdjustment.mjs
new file mode 100644
index 00000000..785eec2b
--- /dev/null
+++ b/module/data/actor/tierAdjustment.mjs
@@ -0,0 +1,218 @@
+import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
+import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
+
+export function getTierAdjustedAdversary(source, tier) {
+ const currentTier = source.tier ?? 1;
+
+ /** @type {(2 | 3 | 4)[]} */
+ const tiers = new Array(Math.abs(tier - currentTier))
+ .fill(0)
+ .map((_, idx) => idx + Math.min(tier, currentTier) + 1);
+ if (tier < currentTier) tiers.reverse();
+ const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
+ const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));
+
+ // Apply simple tier changes
+ const scale = tier > currentTier ? 1 : -1;
+ for (const entry of tierEntries) {
+ source.system.difficulty += scale * entry.difficulty;
+ source.system.damageThresholds.major += scale * entry.majorThreshold;
+ source.system.damageThresholds.severe += scale * entry.severeThreshold;
+ source.system.resources.hitPoints.max += scale * entry.hp;
+ source.system.resources.stress.max += scale * entry.stress;
+ source.system.attack.roll.bonus += scale * entry.attack;
+ }
+
+ // Get the mean and standard deviation of expected damage in the previous and new tier
+ // The data we have is for attack scaling, but we reuse this for action scaling later
+ const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
+ const damageMeta = {
+ currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
+ newDamageRange: { tier, ...expectedDamageData[tier] }
+ };
+
+ // Store initial attack damage for abilities that have you deal a "standard attack"
+ const initialAttack = {
+ type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(),
+ value: getDamagePartsFormula(source.system.attack.damage?.parts.hitPoints?.value)
+ };
+
+ // Update damage of base attack.
+ try {
+ const damage = source.system.attack.damage;
+ if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary');
+
+ for (const property of ['value', 'valueAlt']) {
+ const data = damage.parts.hitPoints[property];
+ const previousFormula = getDamagePartsFormula(data);
+ const { value, formula } = calculateAdjustedDamage(previousFormula, 'attack', damageMeta);
+ applyAdjustedDamage(data, value, formula);
+ }
+ } catch (err) {
+ ui.notifications.warn('Failed to convert attack damage of adversary');
+ console.error(err);
+ }
+
+ // Update damage of each item action, making sure to also update the description if possible
+ const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
+ for (const item of source.items) {
+ // Replace damage inlines with new formulas. Keep a record for a specific check later
+ const descriptionFormulas = [];
+ for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
+ withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
+ const { value: formula } = parseInlineParams(inner);
+ if (!formula || !type) return match;
+
+ try {
+ const newFormula = calculateAdjustedDamage(formula, 'action', damageMeta)?.formula;
+ descriptionFormulas.push(formula);
+ return match.replace(formula, newFormula);
+ } catch {
+ return match;
+ }
+ });
+ }
+
+ // Update damage in item actions and convert all formula matches in the descriptions to the new damage
+ for (const action of Object.values(item.system.actions)) {
+ if (!action.damage?.parts.hitPoints) continue;
+ try {
+ // Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead
+ const result = [];
+ for (const property of ['value', 'valueAlt']) {
+ const { [property]: data, type: damageType } = action.damage.parts.hitPoints;
+ const previousFormula = getDamagePartsFormula(data);
+ const isActuallyAttack =
+ previousFormula === initialAttack.value &&
+ foundry.utils.equals(damageType.toSorted(), initialAttack.type) &&
+ !descriptionFormulas.includes(previousFormula);
+ const type = isActuallyAttack ? 'attack' : 'action';
+ const { value, formula } = calculateAdjustedDamage(previousFormula, type, damageMeta);
+ applyAdjustedDamage(data, value, formula);
+ result.push({ previousFormula, formula });
+ }
+
+ // Override text in the description with those values
+ for (const { previousFormula, formula } of Object.values(result)) {
+ const oldFormulaRegexp = new RegExp(
+ previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
+ );
+ item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
+ action.description = action.description.replace(oldFormulaRegexp, formula);
+ }
+ } catch (err) {
+ ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
+ console.error(err);
+ }
+ }
+ }
+
+ // Finally set the tier of the source data, now that everything is complete
+ source.system.tier = tier;
+ return source;
+}
+
+/**
+ * Converts a damage object to a new damage range
+ * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
+ * @throws error if the formula is the wrong type
+ */
+function calculateAdjustedDamage(formula, type, { currentDamageRange, newDamageRange }) {
+ const terms = parseTermsFromSimpleFormula(formula);
+ const flatTerms = terms.filter(t => t.diceQuantity === 0);
+ const diceTerms = terms.filter(t => t.diceQuantity > 0);
+ if (flatTerms.length > 1 || diceTerms.length > 1) {
+ throw new Error('invalid formula for conversion');
+ }
+ const value = {
+ ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
+ bonus: flatTerms[0]?.bonus ?? 0
+ };
+ const previousExpected = calculateExpectedValue(value);
+ if (previousExpected === 0) return value; // nothing to do
+
+ const dieSizes = [4, 6, 8, 10, 12, 20];
+ const steps = newDamageRange.tier - currentDamageRange.tier;
+ const increasing = steps > 0;
+ const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
+ const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);
+
+ // If this was just a flat number, convert to the expected damage and exit
+ if (value.diceQuantity === 0) {
+ value.bonus = Math.round(expected);
+ return value;
+ }
+
+ const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
+ const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });
+
+ // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
+ const baseOverages = Math.floor(value.bonus / getExpectedDie());
+
+ // Prestep. Change number of dice for attacks, bump up/down for actions
+ // We never bump up to d20, though we might bump down from it
+ if (type === 'attack') {
+ const minimum = increasing ? value.diceQuantity : 0;
+ value.diceQuantity = Math.max(minimum, newDamageRange.tier);
+ } else {
+ const currentIdx = dieSizes.indexOf(value.faces);
+ value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
+ }
+
+ value.bonus = Math.round(expected - getBaseAverage());
+
+ // Attempt to handle negative values.
+ // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
+ if (value.bonus < 0) {
+ let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
+ const currentIdx = dieSizes.indexOf(value.faces);
+
+ // If step downs alone don't suffice, change the flat modifier, then calculate steps required again
+ // If this isn't sufficient, the result will be slightly off. This is unlikely to happen
+ if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
+ value.diceQuantity -= increasing ? 1 : Math.abs(steps);
+ value.bonus = Math.round(expected - getBaseAverage());
+ if (value.bonus >= 0) return value; // complete
+ }
+
+ stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
+ value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
+ value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
+ }
+
+ // If value is really high, we add a number of dice based on the number of overages
+ // This attempts to preserve a similar amount of variance when increasing an action
+ const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
+ if (type !== 'attack' && increasing && overagesToRemove > 0) {
+ value.diceQuantity += overagesToRemove;
+ value.bonus = Math.round(expected - getBaseAverage());
+ }
+
+ const newFormula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
+ .filter(p => !!p)
+ .join('+');
+ return { value, formula: newFormula };
+}
+
+function getDamagePartsFormula(data) {
+ return data.custom.enabled
+ ? data.custom.formula
+ : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0].filter(p => !!p).join('+');
+}
+
+/**
+ * Updates damage to reflect a specific value.
+ * @throws if damage structure is invalid for conversion
+ * @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
+ */
+function applyAdjustedDamage(diceData, value, formula) {
+ if (value.diceQuantity) {
+ diceData.custom.enabled = false;
+ diceData.bonus = value.bonus;
+ diceData.dice = `d${value.faces}`;
+ diceData.flatMultiplier = value.diceQuantity;
+ } else if (!value.diceQuantity) {
+ diceData.custom.enabled = true;
+ diceData.custom.formula = formula;
+ }
+}
From a209b035c8d863ed8788cb82278e096248d12824 Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sat, 30 May 2026 06:48:20 -0400
Subject: [PATCH 03/14] Make prosemirror button nicer (#1946)
---
styles/less/global/prose-mirror.less | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/styles/less/global/prose-mirror.less b/styles/less/global/prose-mirror.less
index 8412235d..e4b1249f 100644
--- a/styles/less/global/prose-mirror.less
+++ b/styles/less/global/prose-mirror.less
@@ -40,6 +40,11 @@
ul {
list-style: disc;
}
+ }
+ // Fixes centering and makes it not render over scrollbar
+ &:hover button.toggle:enabled {
+ display: flex;
+ right: 12px;
}
}
}
From 251d7e4e13cd172245f577b0adac9de633dd8013 Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sat, 30 May 2026 06:49:06 -0400
Subject: [PATCH 04/14] Swap order of thresholds and resources in actor editor
(#1943)
---
.../sheets-settings/adversary-settings/sheet.less | 2 +-
.../sheets-settings/adversary-settings/details.hbs | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/styles/less/sheets-settings/adversary-settings/sheet.less b/styles/less/sheets-settings/adversary-settings/sheet.less
index b4b0683b..e6eb8d0b 100644
--- a/styles/less/sheets-settings/adversary-settings/sheet.less
+++ b/styles/less/sheets-settings/adversary-settings/sheet.less
@@ -7,7 +7,7 @@
&.attack.active {
display: flex;
flex-direction: column;
- gap: 16px;
+ gap: 12px;
}
.fieldsets-section {
diff --git a/templates/sheets-settings/adversary-settings/details.hbs b/templates/sheets-settings/adversary-settings/details.hbs
index dc2fd386..3160fbb9 100644
--- a/templates/sheets-settings/adversary-settings/details.hbs
+++ b/templates/sheets-settings/adversary-settings/details.hbs
@@ -18,6 +18,12 @@
{{formField systemFields.motivesAndTactics value=document._source.system.motivesAndTactics label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.motivesAndTactics.label")}}
+
+ {{localize "DAGGERHEART.GENERAL.DamageThresholds.title"}}
+ {{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}}
+ {{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}}
+
+
{{localize "DAGGERHEART.GENERAL.Resource.plural"}}
@@ -26,10 +32,4 @@
{{/each}}
-
-
- {{localize "DAGGERHEART.GENERAL.DamageThresholds.title"}}
- {{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}}
- {{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}}
-
From 493998cc957a1c5f0a5153c28138aa8ff4db1a22 Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sat, 30 May 2026 06:51:39 -0400
Subject: [PATCH 05/14] Preload class and subclass features for description
(#1940)
Co-authored-by: WBHarry
---
module/data/item/class.mjs | 13 ++++++++----
module/data/item/subclass.mjs | 8 +++++--
module/helpers/utils.mjs | 39 +++++++++++++++++++++++++++++++++++
3 files changed, 54 insertions(+), 6 deletions(-)
diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs
index 7014e011..470a1e3c 100644
--- a/module/data/item/class.mjs
+++ b/module/data/item/class.mjs
@@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import ItemLinkFields from '../fields/itemLinkFields.mjs';
-import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
+import { addLinkedItemsDiff, fromUuids, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs';
export default class DHClass extends BaseDataItem {
/** @inheritDoc */
@@ -73,15 +73,16 @@ export default class DHClass extends BaseDataItem {
const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u);
const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass));
for (const pack of game.packs) {
+ const packIds = [];
const indexes = await pack.getIndex({ fields: ['system.linkedClass'] });
for (const index of indexes) {
if (index.type !== 'subclass') continue;
if (!uuids.includes(index.system?.linkedClass)) continue;
if (subclasses.find(x => x.uuid === index.uuid)) continue;
-
- const subclass = await foundry.utils.fromUuid(index.uuid);
- subclasses.push(subclass);
+ packIds.push(index._id);
}
+
+ if (packIds.length > 0) subclasses.push(...(await pack.getDocuments({ _id__in: packIds })));
}
return subclasses;
@@ -216,6 +217,10 @@ export default class DHClass extends BaseDataItem {
classItems.push(contentLink.outerHTML);
}
+ // Preload all class features for acquisition from the cache
+ // todo: make feature acquisition async and replace feature helpers for methods
+ await fromUuids(this._source.features.map(f => f.item));
+
const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures);
const classFeatures = await getFeaturesHTMLData(this.classFeatures);
diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs
index ecf72de3..55b078c2 100644
--- a/module/data/item/subclass.mjs
+++ b/module/data/item/subclass.mjs
@@ -1,5 +1,4 @@
-import { getFeaturesHTMLData } from '../../helpers/utils.mjs';
-import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
+import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs';
import ItemLinkFields from '../fields/itemLinkFields.mjs';
import BaseDataItem from './base.mjs';
@@ -91,6 +90,11 @@ export default class DHSubclass extends BaseDataItem {
const spellcastTrait = this.spellcastingTrait
? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label)
: null;
+
+ // Preload all class features for acquisition from the cache
+ // todo: make feature acquisition async and replace feature helpers for methods
+ await fromUuids(this._source.features.map(f => f.item));
+
const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures);
const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures);
const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures);
diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs
index 8bc95aa0..2f20175b 100644
--- a/module/helpers/utils.mjs
+++ b/module/helpers/utils.mjs
@@ -865,6 +865,45 @@ export function camelize(str) {
.replace(/\s+/g, '');
}
+/** Bulk load a list of documents using uuids. Returns the documents in the same order */
+export async function fromUuids(uuids) {
+ // Set up base entries. Each step works on a sublist of these objects
+ const entries = uuids.map(uuid => ({
+ uuid,
+ parsed: foundry.utils.parseUuid(uuid),
+ value: foundry.utils.fromUuidSync(uuid)
+ }));
+
+ // Handle missing uuids for embedded documents first
+ // A value may be index data, so we check if its a document
+ const packEmbeddedEntries = entries.filter(
+ e =>
+ !(e.value instanceof Document) &&
+ e.parsed.collection instanceof foundry.documents.collections.CompendiumCollection &&
+ e.parsed.embedded.length > 0
+ );
+ await Promise.all(
+ packEmbeddedEntries.map(async e => {
+ e.value = await fromUuid(e.uuid);
+ return true;
+ })
+ );
+
+ // Handle missing top level pack stuff, by batching per pack
+ const missingTopLevel = entries.filter(e => !(e.value instanceof Document) && e.value?.pack);
+ for (const packGroup of Object.values(Object.groupBy(missingTopLevel, e => e.value.pack))) {
+ const pack = game.packs.get(packGroup[0].value.pack);
+ if (!pack) continue;
+
+ const ids = packGroup.map(p => p.parsed.id);
+ const documents = await pack.getDocuments({ _id__in: ids });
+ for (const p of packGroup) {
+ p.value = documents.find(d => d.id === p.parsed.id) ?? p.value;
+ }
+ }
+
+ return entries.map(e => e.value);
+}
/**
* Triggers DiceSoNice rolls or dice roll audio for rolls. Not used for duality rolls.
* @param { Roll[] } rolls
From 2bc1c04c932d91f9d66bc65321813a3756270c23 Mon Sep 17 00:00:00 2001
From: WBHarry
Date: Sat, 30 May 2026 12:56:42 +0200
Subject: [PATCH 06/14] Fixed an issue where hope/fear dice size could no
longer be changed in the roll dialog
---
module/dice/dualityRoll.mjs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs
index f40e9781..1d2d556a 100644
--- a/module/dice/dualityRoll.mjs
+++ b/module/dice/dualityRoll.mjs
@@ -135,11 +135,11 @@ export default class DualityRoll extends D20Roll {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({
- faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
+ faces: this.terms[0]?.faces ?? this.data.rules.dualityRoll?.defaultHopeDice ?? 12
});
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new game.system.api.dice.diceTypes.FearDie({
- faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
+ faces: this.terms[2]?.faces ?? this.data.rules.dualityRoll?.defaultFearDice ?? 12
});
}
From 61db7ca37151fb95feec4df406c19a2b8f2aedf2 Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sat, 30 May 2026 19:00:12 -0400
Subject: [PATCH 07/14] Fix tag team roll results where one of them has stress
(#1948)
---
.../applications/dialogs/groupRollDialog.mjs | 14 +-
module/applications/dialogs/tagTeamDialog.mjs | 173 +++++++++---------
2 files changed, 95 insertions(+), 92 deletions(-)
diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs
index 52baf537..dd504b4b 100644
--- a/module/applications/dialogs/groupRollDialog.mjs
+++ b/module/applications/dialogs/groupRollDialog.mjs
@@ -106,7 +106,12 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
const context = await super._prepareContext(_options);
context.isGM = game.user.isGM;
- context.isEditable = this.getIsEditable();
+ context.isEditable =
+ game.user.isGM ||
+ this.party.system.partyMembers.some(actor => {
+ const selected = Boolean(this.party.system.groupRoll.participants[actor.id]);
+ return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
+ });
context.fields = this.party.system.schema.fields.groupRoll.fields;
context.data = this.party.system.groupRoll;
context.traitOptions = CONFIG.DH.ACTOR.abilities;
@@ -265,13 +270,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
];
}
- getIsEditable() {
- return this.party.system.partyMembers.some(actor => {
- const selected = Boolean(this.party.system.groupRoll.participants[actor.id]);
- return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
- });
- }
-
groupRollRefresh = ({ refreshType, action, parts }) => {
if (refreshType !== RefreshType.GroupRoll) return;
diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs
index 4e63d93b..3dc6b0fc 100644
--- a/module/applications/dialogs/tagTeamDialog.mjs
+++ b/module/applications/dialogs/tagTeamDialog.mjs
@@ -116,7 +116,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
- context.isEditable = this.getIsEditable();
+ context.isEditable =
+ game.user.isGM ||
+ this.party.system.partyMembers.some(actor => {
+ const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
+ return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
+ });
context.fields = this.party.system.schema.fields.tagTeam.fields;
context.data = this.party.system.tagTeam;
context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
@@ -179,57 +184,56 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
}
if (Object.keys(this.party.system.tagTeam.members).includes(partId)) {
- const data = this.party.system.tagTeam.members[partId];
- const actor = game.actors.get(partId);
-
- const rollOptions = [];
- const damageRollOptions = [];
- for (const item of actor.items) {
- if (item.system.metadata.hasActions) {
- const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
- for (const action of actions) {
- if (action.hasRoll) {
- const actionItem = {
- value: action.uuid,
- label: action.name,
- group: item.name,
- baseAction: action.baseAction
- };
-
- if (action.hasDamage) damageRollOptions.push(actionItem);
- else rollOptions.push(actionItem);
- }
- }
- }
- }
-
- const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
- const critSelected = !selectedRoll
- ? undefined
- : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
-
- const damage = data.rollData?.options?.damage;
- partContext.hasDamage |= Boolean(damage);
- const critHitPointsDamage = await this.getCriticalDamage(damage);
-
- partContext.members[partId] = {
- ...data,
- roll: data.roll,
- isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
- key: partId,
- readyToRoll: Boolean(data.rollChoice),
- hasRolled: Boolean(data.rollData),
- rollOptions,
- damageRollOptions,
- damage: damage,
- critDamage: critHitPointsDamage,
- useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
- };
+ const data = await this.#prepareMemberContext(partId);
+ partContext.hasDamage |= Boolean(data?.damage);
+ partContext.members[partId] = data;
}
return partContext;
}
+ async #prepareMemberContext(partId) {
+ const data = this.party.system.tagTeam.members[partId] ?? {};
+ const actor = game.actors.get(partId);
+ if (!actor) console.error(`Failed to get actor ${partId}`);
+
+ const rollOptions = [];
+ const damageRollOptions = [];
+ for (const item of actor?.items ?? []) {
+ if (!item.system.metadata.hasActions) continue;
+ const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
+ for (const action of actions) {
+ if (action.hasRoll) {
+ const collection = action.hasDamage ? damageRollOptions : rollOptions;
+ collection.push({
+ value: action.uuid,
+ label: action.name,
+ group: item.name,
+ baseAction: action.baseAction
+ });
+ }
+ }
+ }
+
+ const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
+ const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
+ const damage = data.rollData?.options?.damage;
+
+ return {
+ ...data,
+ roll: data.roll,
+ isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
+ key: partId,
+ readyToRoll: Boolean(data.rollChoice),
+ hasRolled: Boolean(data.rollData),
+ rollOptions,
+ damageRollOptions,
+ damage: damage,
+ critDamage: await this.getCriticalDamage(damage),
+ useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
+ };
+ }
+
getUpdatingParts(target) {
const { initialization, rollSelection, result } = this.constructor.PARTS;
const isInitialization = this.tabGroups.application === initialization.id;
@@ -273,13 +277,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
);
}
- getIsEditable() {
- return this.party.system.partyMembers.some(actor => {
- const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
- return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
- });
- }
-
tagTeamRefresh = ({ refreshType, action, parts }) => {
if (refreshType !== RefreshType.TagTeamRoll) return;
@@ -649,42 +646,50 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
}
async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) {
- const memberValues = Object.values(this.party.system.tagTeam.members);
- const selectedRoll = memberValues.find(x => x.selected);
- let baseMainRoll = selectedRoll ?? memberValues[0];
- let baseSecondaryRoll = selectedRoll
- ? memberValues.find(x => !x.selected)
- : memberValues.length > 1
- ? memberValues[1]
- : null;
+ try {
+ const memberValues = Object.values(this.party.system.tagTeam.members);
+ const selectedRoll = memberValues.find(x => x.selected);
+ const baseMainRoll = selectedRoll ?? memberValues[0];
+ const baseSecondaryRoll = selectedRoll
+ ? memberValues.find(x => !x.selected)
+ : memberValues.length > 1
+ ? memberValues[1]
+ : null;
- if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null;
+ if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null;
- const mainRoll = new MemberData(baseMainRoll.toObject());
- const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData;
- const systemData = mainRoll.rollData.options;
- const isCritical = overrideIsCritical ?? systemData.roll.isCritical;
- if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage);
+ const mainRoll = new MemberData(baseMainRoll.toObject());
+ const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData;
+ const systemData = mainRoll.rollData.options;
+ const isCritical = overrideIsCritical ?? systemData.roll.isCritical;
+ if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage);
- if (secondaryRollData?.options.hasDamage) {
- const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical)
- ? await this.getCriticalDamage(secondaryRollData.options.damage)
- : secondaryRollData.options.damage;
- if (systemData.damage) {
- for (const key in secondaryDamage) {
- const damage = secondaryDamage[key];
- systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula]
- .filter(x => x)
- .join(' + ');
- systemData.damage[key].total += damage.total;
- systemData.damage[key].parts.push(...damage.parts);
+ if (secondaryRollData?.options.hasDamage) {
+ const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical)
+ ? await this.getCriticalDamage(secondaryRollData.options.damage)
+ : secondaryRollData.options.damage;
+ if (systemData.damage) {
+ for (const [key, damage] of Object.entries(secondaryDamage ?? {})) {
+ if (key in systemData.damage) {
+ systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula]
+ .filter(x => x)
+ .join(' + ');
+ systemData.damage[key].total += damage.total;
+ systemData.damage[key].parts.push(...damage.parts);
+ } else {
+ systemData.damage[key] = damage;
+ }
+ }
+ } else {
+ systemData.damage = secondaryDamage;
}
- } else {
- systemData.damage = secondaryDamage;
}
- }
- return mainRoll;
+ return mainRoll;
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
}
static async #onCancelRoll(_event, _button, options = { confirm: true }) {
From d3141059acfef63db29b7f22060977a5f362a551 Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sat, 30 May 2026 19:02:51 -0400
Subject: [PATCH 08/14] Create index files for actor sheet styles (#1945)
---
.../adversary/{actions.less => features.less} | 0
.../less/sheets/actors/adversary/index.less | 5 +++
.../less/sheets/actors/character/index.less | 8 +++++
.../less/sheets/actors/companion/index.less | 4 +++
.../{actions.less => features.less} | 0
.../less/sheets/actors/environment/index.less | 4 +++
styles/less/sheets/actors/party/index.less | 4 +++
styles/less/sheets/index.less | 34 +++----------------
8 files changed, 30 insertions(+), 29 deletions(-)
rename styles/less/sheets/actors/adversary/{actions.less => features.less} (100%)
create mode 100644 styles/less/sheets/actors/adversary/index.less
create mode 100644 styles/less/sheets/actors/character/index.less
create mode 100644 styles/less/sheets/actors/companion/index.less
rename styles/less/sheets/actors/environment/{actions.less => features.less} (100%)
create mode 100644 styles/less/sheets/actors/environment/index.less
create mode 100644 styles/less/sheets/actors/party/index.less
diff --git a/styles/less/sheets/actors/adversary/actions.less b/styles/less/sheets/actors/adversary/features.less
similarity index 100%
rename from styles/less/sheets/actors/adversary/actions.less
rename to styles/less/sheets/actors/adversary/features.less
diff --git a/styles/less/sheets/actors/adversary/index.less b/styles/less/sheets/actors/adversary/index.less
new file mode 100644
index 00000000..a1ab41c7
--- /dev/null
+++ b/styles/less/sheets/actors/adversary/index.less
@@ -0,0 +1,5 @@
+@import './features.less';
+@import './header.less';
+@import './sheet.less';
+@import './sidebar.less';
+@import './effects.less';
diff --git a/styles/less/sheets/actors/character/index.less b/styles/less/sheets/actors/character/index.less
new file mode 100644
index 00000000..edefe0a1
--- /dev/null
+++ b/styles/less/sheets/actors/character/index.less
@@ -0,0 +1,8 @@
+@import './biography.less';
+@import './effects.less';
+@import './features.less';
+@import './header.less';
+@import './inventory.less';
+@import './loadout.less';
+@import './sheet.less';
+@import './sidebar.less';
diff --git a/styles/less/sheets/actors/companion/index.less b/styles/less/sheets/actors/companion/index.less
new file mode 100644
index 00000000..c4931814
--- /dev/null
+++ b/styles/less/sheets/actors/companion/index.less
@@ -0,0 +1,4 @@
+@import './details.less';
+@import './header.less';
+@import './sheet.less';
+@import './effects.less';
diff --git a/styles/less/sheets/actors/environment/actions.less b/styles/less/sheets/actors/environment/features.less
similarity index 100%
rename from styles/less/sheets/actors/environment/actions.less
rename to styles/less/sheets/actors/environment/features.less
diff --git a/styles/less/sheets/actors/environment/index.less b/styles/less/sheets/actors/environment/index.less
new file mode 100644
index 00000000..211c8e60
--- /dev/null
+++ b/styles/less/sheets/actors/environment/index.less
@@ -0,0 +1,4 @@
+@import './features.less';
+@import './header.less';
+@import './potentialAdversaries.less';
+@import './sheet.less';
diff --git a/styles/less/sheets/actors/party/index.less b/styles/less/sheets/actors/party/index.less
new file mode 100644
index 00000000..56f7a457
--- /dev/null
+++ b/styles/less/sheets/actors/party/index.less
@@ -0,0 +1,4 @@
+@import './header.less';
+@import './party-members.less';
+@import './sheet.less';
+@import './inventory.less';
diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less
index 7d595614..ca1bc840 100644
--- a/styles/less/sheets/index.less
+++ b/styles/less/sheets/index.less
@@ -2,35 +2,11 @@
@import './actors/actor-sheet-shared.less';
-@import './actors/adversary/actions.less';
-@import './actors/adversary/header.less';
-@import './actors/adversary/sheet.less';
-@import './actors/adversary/sidebar.less';
-@import './actors/adversary/effects.less';
-
-@import './actors/character/biography.less';
-@import './actors/character/effects.less';
-@import './actors/character/features.less';
-@import './actors/character/header.less';
-@import './actors/character/inventory.less';
-@import './actors/character/loadout.less';
-@import './actors/character/sheet.less';
-@import './actors/character/sidebar.less';
-
-@import './actors/companion/details.less';
-@import './actors/companion/header.less';
-@import './actors/companion/sheet.less';
-@import './actors/companion/effects.less';
-
-@import './actors/environment/actions.less';
-@import './actors/environment/header.less';
-@import './actors/environment/potentialAdversaries.less';
-@import './actors/environment/sheet.less';
-
-@import './actors/party/header.less';
-@import './actors/party/party-members.less';
-@import './actors/party/sheet.less';
-@import './actors/party/inventory.less';
+@import './actors/adversary/index.less';
+@import './actors/character/index.less';
+@import './actors/companion/index.less';
+@import './actors/environment/index.less';
+@import './actors/party/index.less';
@import './items/beastform.less';
@import './items/class.less';
From c23ac61ee53994b268a48db1c9172bf821684e75 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Sun, 31 May 2026 03:05:13 +0200
Subject: [PATCH 09/14] Corrected the data path for showing the difficulty
marker in roll chat messages (#1950)
---
templates/ui/chat/parts/roll-part.hbs | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/templates/ui/chat/parts/roll-part.hbs b/templates/ui/chat/parts/roll-part.hbs
index 14e3eaa6..cfee735f 100644
--- a/templates/ui/chat/parts/roll-part.hbs
+++ b/templates/ui/chat/parts/roll-part.hbs
@@ -12,13 +12,9 @@
{{/if}}
- {{#if roll.difficulty}}
-
- {{!-- {{#if canViewSecret}} --}}
- difficulty {{roll.difficulty}}
- {{!-- {{else}}
- {{localize (ifThen roll.success "DAGGERHEART.GENERAL.success" "DAGGERHEART.GENERAL.failure")}}
- {{/if}} --}}
+ {{#if roll.options.roll.difficulty}}
+
+ {{localize "DAGGERHEART.GENERAL.difficulty"}} {{roll.options.roll.difficulty}}
{{/if}}
From 53f15a7fdec1edb6de4a54c29029c33e0890e100 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Sun, 31 May 2026 03:11:43 +0200
Subject: [PATCH 10/14] [Feature] NPC Actors (#1949)
---
assets/icons/documents/actors/drama-masks.svg | 1 +
daggerheart.mjs | 5 +
lang/en.json | 6 +
.../applications/sheets-configs/_module.mjs | 1 +
.../sheets-configs/npc-settings.mjs | 85 +++++++++++
module/applications/sheets/actors/_module.mjs | 1 +
module/applications/sheets/actors/npc.mjs | 136 ++++++++++++++++++
module/data/actor/_module.mjs | 4 +-
module/data/actor/npc.mjs | 43 ++++++
module/documents/actor.mjs | 8 ++
styles/less/global/tab-navigation.less | 7 +-
.../less/sheets/actors/adversary/header.less | 8 +-
.../less/sheets/actors/companion/header.less | 4 +-
.../sheets/actors/environment/header.less | 4 +-
styles/less/sheets/actors/npc/features.less | 18 +++
styles/less/sheets/actors/npc/header.less | 83 +++++++++++
styles/less/sheets/actors/npc/index.less | 3 +
styles/less/sheets/actors/npc/sheet.less | 10 ++
styles/less/sheets/index.less | 1 +
system.json | 5 +-
.../sheets-settings/npc-settings/details.hbs | 13 ++
.../sheets-settings/npc-settings/features.hbs | 29 ++++
.../sheets-settings/npc-settings/header.hbs | 3 +
templates/sheets/actors/adversary/header.hbs | 5 +-
templates/sheets/actors/companion/header.hbs | 9 +-
.../sheets/actors/environment/header.hbs | 9 +-
templates/sheets/actors/npc/features.hbs | 14 ++
templates/sheets/actors/npc/header.hbs | 40 ++++++
templates/sheets/actors/npc/navigation.hbs | 7 +
templates/sheets/actors/npc/notes.hbs | 11 ++
.../sheets/global/tabs/tab-navigation.hbs | 2 +-
.../ui/sidebar/actor-document-partial.hbs | 2 +
32 files changed, 548 insertions(+), 29 deletions(-)
create mode 100644 assets/icons/documents/actors/drama-masks.svg
create mode 100644 module/applications/sheets-configs/npc-settings.mjs
create mode 100644 module/applications/sheets/actors/npc.mjs
create mode 100644 module/data/actor/npc.mjs
create mode 100644 styles/less/sheets/actors/npc/features.less
create mode 100644 styles/less/sheets/actors/npc/header.less
create mode 100644 styles/less/sheets/actors/npc/index.less
create mode 100644 styles/less/sheets/actors/npc/sheet.less
create mode 100644 templates/sheets-settings/npc-settings/details.hbs
create mode 100644 templates/sheets-settings/npc-settings/features.hbs
create mode 100644 templates/sheets-settings/npc-settings/header.hbs
create mode 100644 templates/sheets/actors/npc/features.hbs
create mode 100644 templates/sheets/actors/npc/header.hbs
create mode 100644 templates/sheets/actors/npc/navigation.hbs
create mode 100644 templates/sheets/actors/npc/notes.hbs
diff --git a/assets/icons/documents/actors/drama-masks.svg b/assets/icons/documents/actors/drama-masks.svg
new file mode 100644
index 00000000..84307da0
--- /dev/null
+++ b/assets/icons/documents/actors/drama-masks.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/daggerheart.mjs b/daggerheart.mjs
index 363430be..23977628 100644
--- a/daggerheart.mjs
+++ b/daggerheart.mjs
@@ -196,6 +196,11 @@ Hooks.once('init', () => {
makeDefault: true,
label: sheetLabel('TYPES.Actor.environment')
});
+ Actors.registerSheet(SYSTEM.id, applications.sheets.actors.NPC, {
+ types: ['npc'],
+ makeDefault: true,
+ label: sheetLabel('TYPES.Actor.npc')
+ });
Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, {
types: ['party'],
makeDefault: true,
diff --git a/lang/en.json b/lang/en.json
index f1841e09..9ce515d9 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -23,6 +23,7 @@
"companion": "Companion",
"adversary": "Adversary",
"environment": "Environment",
+ "npc": "NPC",
"party": "Party"
}
},
@@ -333,6 +334,11 @@
},
"newAdversary": "New Adversary"
},
+ "NPC": {
+ "FIELDS": {
+ "motives": { "label": "Motives" }
+ }
+ },
"Party": {
"Subtitle": {
"character": "{community} {ancestry} | {subclass} {class}",
diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs
index 4b83a042..9528a424 100644
--- a/module/applications/sheets-configs/_module.mjs
+++ b/module/applications/sheets-configs/_module.mjs
@@ -2,6 +2,7 @@ export { default as ActionConfig } from './action-config.mjs';
export { default as ActionSettingsConfig } from './action-settings-config.mjs';
export { default as CharacterSettings } from './character-settings.mjs';
export { default as AdversarySettings } from './adversary-settings.mjs';
+export { default as NPCSettings } from './npc-settings.mjs';
export { default as CompanionSettings } from './companion-settings.mjs';
export { default as SettingFeatureConfig } from './setting-feature-config.mjs';
export { default as EnvironmentSettings } from './environment-settings.mjs';
diff --git a/module/applications/sheets-configs/npc-settings.mjs b/module/applications/sheets-configs/npc-settings.mjs
new file mode 100644
index 00000000..c187877c
--- /dev/null
+++ b/module/applications/sheets-configs/npc-settings.mjs
@@ -0,0 +1,85 @@
+import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
+
+/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
+
+export default class DHNPCSettings extends DHBaseActorSettings {
+ /**@inheritdoc */
+ static DEFAULT_OPTIONS = {
+ classes: ['npc-settings'],
+ position: { width: 455, height: 'auto' },
+ actions: {},
+ dragDrop: [
+ { dragSelector: null, dropSelector: '.tab.features' },
+ { dragSelector: '.feature-item', dropSelector: null }
+ ]
+ };
+
+ /**@override */
+ static PARTS = {
+ header: {
+ id: 'header',
+ template: 'systems/daggerheart/templates/sheets-settings/npc-settings/header.hbs'
+ },
+ tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
+ details: {
+ id: 'details',
+ template: 'systems/daggerheart/templates/sheets-settings/npc-settings/details.hbs'
+ },
+ features: {
+ id: 'features',
+ template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs'
+ }
+ };
+
+ /** @override */
+ static TABS = {
+ primary: {
+ tabs: [{ id: 'details' }, { id: 'features' }],
+ initial: 'details',
+ labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
+ }
+ };
+
+ async _prepareContext(options) {
+ const context = await super._prepareContext(options);
+
+ const featureForms = ['passive', 'action', 'reaction'];
+ context.features = context.document.system.features.sort((a, b) =>
+ a.system.featureForm !== b.system.featureForm
+ ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
+ : a.sort - b.sort
+ );
+
+ return context;
+ }
+
+ /* -------------------------------------------- */
+
+ async _onDragStart(event) {
+ const featureItem = event.currentTarget.closest('.feature-item');
+
+ if (featureItem) {
+ const feature = this.actor.items.get(featureItem.id);
+ const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true };
+ event.dataTransfer.setData('text/plain', JSON.stringify(featureData));
+ event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0);
+ }
+ }
+
+ async _onDrop(event) {
+ event.stopPropagation();
+ const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
+
+ const item = await fromUuid(data.uuid);
+ if (item?.type === 'feature') {
+ if (data.fromInternal && item.parent?.uuid === this.actor.uuid) {
+ return;
+ }
+
+ const itemData = item.toObject();
+ delete itemData._id;
+
+ await this.actor.createEmbeddedDocuments('Item', [itemData]);
+ }
+ }
+}
diff --git a/module/applications/sheets/actors/_module.mjs b/module/applications/sheets/actors/_module.mjs
index c4ea2d94..1a2bebfb 100644
--- a/module/applications/sheets/actors/_module.mjs
+++ b/module/applications/sheets/actors/_module.mjs
@@ -2,4 +2,5 @@ export { default as Adversary } from './adversary.mjs';
export { default as Character } from './character.mjs';
export { default as Companion } from './companion.mjs';
export { default as Environment } from './environment.mjs';
+export { default as NPC } from './npc.mjs';
export { default as Party } from './party.mjs';
diff --git a/module/applications/sheets/actors/npc.mjs b/module/applications/sheets/actors/npc.mjs
new file mode 100644
index 00000000..8c9048c2
--- /dev/null
+++ b/module/applications/sheets/actors/npc.mjs
@@ -0,0 +1,136 @@
+import DHBaseActorSheet from '../api/base-actor.mjs';
+
+export default class NPCSheet extends DHBaseActorSheet {
+ /** @inheritDoc */
+ static DEFAULT_OPTIONS = {
+ classes: ['npc'],
+ position: { width: 660, height: 600 },
+ window: { resizable: true },
+ actions: {},
+ window: {
+ resizable: true,
+ controls: [
+ {
+ icon: 'fa-solid fa-signature',
+ label: 'DAGGERHEART.UI.Tooltip.configureAttribution',
+ action: 'editAttribution'
+ }
+ ]
+ },
+ dragDrop: [
+ {
+ dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]',
+ dropSelector: null
+ }
+ ]
+ };
+
+ static PARTS = {
+ header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' },
+ tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' },
+ features: {
+ template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs',
+ scrollable: ['.feature-section']
+ },
+ notes: {
+ template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs'
+ }
+ };
+
+ /** @inheritdoc */
+ static TABS = {
+ primary: {
+ tabs: [{ id: 'notes' }, { id: 'features' }],
+ initial: 'notes',
+ labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
+ }
+ };
+
+ /** @inheritdoc */
+ _prepareTabs(group) {
+ const result = super._prepareTabs(group);
+ if (group === 'primary') {
+ result.features.empty = this.document.system.features.length === 0;
+ }
+ return result;
+ }
+
+ /** @inheritdoc */
+ async _preparePartContext(partId, context, options) {
+ context = await super._preparePartContext(partId, context, options);
+ switch (partId) {
+ case 'header':
+ await this._prepareHeaderContext(context, options);
+ break;
+ case 'features':
+ await this._prepareFeaturesContext(context, options);
+ break;
+ case 'notes':
+ await this._prepareNotesContext(context, options);
+ break;
+ }
+
+ return context;
+ }
+
+ /**
+ * Prepare render context for the Header part.
+ * @param {ApplicationRenderContext} context
+ * @param {ApplicationRenderOptions} options
+ * @returns {Promise}
+ * @protected
+ */
+ async _prepareHeaderContext(context, _options) {
+ const { system } = this.document;
+ const { TextEditor } = foundry.applications.ux;
+
+ context.description = await TextEditor.implementation.enrichHTML(system.description, {
+ secrets: this.document.isOwner,
+ relativeTo: this.document
+ });
+ }
+
+ /**
+ * Prepare render context for the Features part.
+ * @param {ApplicationRenderContext} context
+ * @param {ApplicationRenderOptions} options
+ * @returns {Promise}
+ * @protected
+ */
+ async _prepareFeaturesContext(context, _options) {
+ const featureForms = ['passive', 'action', 'reaction'];
+ context.features = this.document.system.features.sort((a, b) =>
+ a.system.featureForm !== b.system.featureForm
+ ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm)
+ : a.sort - b.sort
+ );
+ }
+
+ /**
+ * Prepare render context for the Biography part.
+ * @param {ApplicationRenderContext} context
+ * @param {ApplicationRenderOptions} options
+ * @returns {Promise}
+ * @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
+ })
+ };
+ }
+ }
+}
diff --git a/module/data/actor/_module.mjs b/module/data/actor/_module.mjs
index 99577620..1fe1ef3f 100644
--- a/module/data/actor/_module.mjs
+++ b/module/data/actor/_module.mjs
@@ -1,15 +1,17 @@
import DhCharacter from './character.mjs';
import DhCompanion from './companion.mjs';
import DhAdversary from './adversary.mjs';
+import DhNPC from './npc.mjs';
import DhEnvironment from './environment.mjs';
import DhParty from './party.mjs';
-export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty };
+export { DhCharacter, DhCompanion, DhAdversary, DhNPC, DhEnvironment, DhParty };
export const config = {
character: DhCharacter,
companion: DhCompanion,
adversary: DhAdversary,
+ npc: DhNPC,
environment: DhEnvironment,
party: DhParty
};
diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs
new file mode 100644
index 00000000..2ccaf926
--- /dev/null
+++ b/module/data/actor/npc.mjs
@@ -0,0 +1,43 @@
+import DHNPCSettings from '../../applications/sheets-configs/npc-settings.mjs';
+import BaseDataActor from './base.mjs';
+
+export default class DhpNPC extends BaseDataActor {
+ static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.NPC'];
+
+ static get metadata() {
+ return foundry.utils.mergeObject(super.metadata, {
+ label: 'TYPES.Actor.npc',
+ type: 'npc',
+ settingSheet: DHNPCSettings,
+ hasResistances: false,
+ hasAttribution: true
+ });
+ }
+
+ static defineSchema() {
+ const fields = foundry.data.fields;
+ return {
+ ...super.defineSchema(),
+ difficulty: new fields.NumberField({
+ nullable: true,
+ initial: null,
+ integer: true,
+ label: 'DAGGERHEART.GENERAL.difficulty'
+ }),
+ description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' }),
+ motives: new fields.StringField(),
+ notes: new fields.HTMLField()
+ };
+ }
+
+ /**@inheritdoc */
+ static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/drama-masks.svg';
+
+ get features() {
+ return this.parent.items.filter(x => x.type === 'feature');
+ }
+
+ isItemValid(source) {
+ return super.isItemValid(source) || source.type === 'feature';
+ }
+}
diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs
index e4c11a5c..6c462d98 100644
--- a/module/documents/actor.mjs
+++ b/module/documents/actor.mjs
@@ -109,6 +109,14 @@ export default class DhpActor extends Actor {
});
}
+ if (this.type === 'npc') {
+ Object.assign(update, {
+ prototypeToken: {
+ disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
+ }
+ });
+ }
+
this.updateSource(update);
}
diff --git a/styles/less/global/tab-navigation.less b/styles/less/global/tab-navigation.less
index 038a9749..3d143b4c 100755
--- a/styles/less/global/tab-navigation.less
+++ b/styles/less/global/tab-navigation.less
@@ -3,8 +3,7 @@
.daggerheart.dh-style {
.tab-navigation {
- margin: 5px 0;
- height: 40px;
+ margin: 5px 0 10px 0;
width: 100%;
.navigation-container {
@@ -21,6 +20,10 @@
a {
color: @color-text-emphatic;
+
+ &.empty:not(.active) {
+ opacity: 0.4;
+ }
}
}
}
diff --git a/styles/less/sheets/actors/adversary/header.less b/styles/less/sheets/actors/adversary/header.less
index 8bd3fcee..1e5e4fa5 100644
--- a/styles/less/sheets/actors/adversary/header.less
+++ b/styles/less/sheets/actors/adversary/header.less
@@ -35,7 +35,7 @@
.tags {
display: flex;
gap: 10px;
- padding-bottom: 16px;
+ padding-bottom: 8px;
.tag {
display: flex;
@@ -67,11 +67,5 @@
gap: 12px;
padding: 16px 0;
}
-
- .adversary-navigation {
- display: flex;
- gap: 8px;
- align-items: center;
- }
}
}
diff --git a/styles/less/sheets/actors/companion/header.less b/styles/less/sheets/actors/companion/header.less
index b4df96bf..aca789a6 100644
--- a/styles/less/sheets/actors/companion/header.less
+++ b/styles/less/sheets/actors/companion/header.less
@@ -148,10 +148,8 @@
}
.companion-navigation {
- display: flex;
- gap: 8px;
- align-items: baseline;
width: 100%;
+ padding: 0 10px;
}
}
}
diff --git a/styles/less/sheets/actors/environment/header.less b/styles/less/sheets/actors/environment/header.less
index 85471af4..da6954e0 100644
--- a/styles/less/sheets/actors/environment/header.less
+++ b/styles/less/sheets/actors/environment/header.less
@@ -138,10 +138,8 @@
}
.environment-navigation {
- display: flex;
- gap: 20px;
- align-items: baseline;
padding: 0 20px;
+
.tab-navigation {
margin-top: 0;
}
diff --git a/styles/less/sheets/actors/npc/features.less b/styles/less/sheets/actors/npc/features.less
new file mode 100644
index 00000000..107b5a06
--- /dev/null
+++ b/styles/less/sheets/actors/npc/features.less
@@ -0,0 +1,18 @@
+.application.sheet.daggerheart.actor.dh-style.npc {
+ .tab.features {
+ &.active {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .feature-section {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow-y: auto;
+ mask-image: linear-gradient(0deg, transparent 0%, black 5%);
+ padding-bottom: 20px;
+ }
+ }
+}
diff --git a/styles/less/sheets/actors/npc/header.less b/styles/less/sheets/actors/npc/header.less
new file mode 100644
index 00000000..d49d763c
--- /dev/null
+++ b/styles/less/sheets/actors/npc/header.less
@@ -0,0 +1,83 @@
+.application.sheet.daggerheart.actor.dh-style.npc {
+ .npc-header-sheet {
+ width: 100%;
+ display: flex;
+
+ .portrait {
+ cursor: pointer;
+ width: 275px;
+
+ img {
+ height: 275px;
+ }
+ }
+
+ .tags {
+ display: flex;
+ gap: 10px;
+ padding-bottom: 8px;
+
+ .tag {
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+ justify-content: center;
+ align-items: center;
+ padding: 3px 5px;
+ font-size: var(--font-size-12);
+ font: @font-body;
+
+ background: light-dark(@dark-15, @beige-15);
+ border: 1px solid light-dark(@dark, @beige);
+ border-radius: 3px;
+ }
+
+ .label {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ font-size: var(--font-size-12);
+ }
+ }
+
+ .info-section {
+ flex: 1;
+ padding: 0 15px;
+ padding-top: var(--header-height);
+ display: flex;
+ flex-direction: column;
+
+ .name-row {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 0;
+
+ h1 {
+ display: flex;
+ flex: 1;
+ padding: 6px 0 0 0;
+ font-size: var(--font-size-32);
+ text-align: start;
+ border: 1px solid transparent;
+ outline: 2px solid transparent;
+ transition: all 0.3s ease;
+ word-break: break-word;
+
+ &:hover {
+ outline: 2px solid light-dark(@dark, @golden);
+ }
+ }
+ }
+
+ .npc-info {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 16px 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/styles/less/sheets/actors/npc/index.less b/styles/less/sheets/actors/npc/index.less
new file mode 100644
index 00000000..2d7d54e3
--- /dev/null
+++ b/styles/less/sheets/actors/npc/index.less
@@ -0,0 +1,3 @@
+@import './sheet.less';
+@import './header.less';
+@import './features.less';
\ No newline at end of file
diff --git a/styles/less/sheets/actors/npc/sheet.less b/styles/less/sheets/actors/npc/sheet.less
new file mode 100644
index 00000000..8ba3b7a9
--- /dev/null
+++ b/styles/less/sheets/actors/npc/sheet.less
@@ -0,0 +1,10 @@
+.application.sheet.daggerheart.actor.dh-style.npc {
+ .window-content {
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ }
+
+ .npc-navigation {
+ padding: 0 15px;
+ }
+}
\ No newline at end of file
diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less
index ca1bc840..4312f755 100644
--- a/styles/less/sheets/index.less
+++ b/styles/less/sheets/index.less
@@ -6,6 +6,7 @@
@import './actors/character/index.less';
@import './actors/companion/index.less';
@import './actors/environment/index.less';
+@import './actors/npc/index.less';
@import './actors/party/index.less';
@import './items/beastform.less';
diff --git a/system.json b/system.json
index 2acd7570..89320768 100644
--- a/system.json
+++ b/system.json
@@ -244,11 +244,14 @@
"adversary": {
"htmlFields": ["notes", "description"]
},
+ "npc": {
+ "htmlFields": ["notes"]
+ },
"environment": {
"htmlFields": ["notes", "description"]
},
"party": {
- "htmlFields": ["notes"]
+ "htmlFields": ["notes", "description"]
}
},
"Item": {
diff --git a/templates/sheets-settings/npc-settings/details.hbs b/templates/sheets-settings/npc-settings/details.hbs
new file mode 100644
index 00000000..0e18b488
--- /dev/null
+++ b/templates/sheets-settings/npc-settings/details.hbs
@@ -0,0 +1,13 @@
+
+
+ {{localize "DAGGERHEART.GENERAL.description"}}
+ {{formInput systemFields.description value=document._source.system.description}}
+
+
+ {{formGroup systemFields.motives value=document._source.system.motives}}
+ {{formGroup systemFields.difficulty value=document._source.system.difficulty localize=true}}
+
diff --git a/templates/sheets-settings/npc-settings/features.hbs b/templates/sheets-settings/npc-settings/features.hbs
new file mode 100644
index 00000000..2f2f5f47
--- /dev/null
+++ b/templates/sheets-settings/npc-settings/features.hbs
@@ -0,0 +1,29 @@
+
+
+ {{localize "DOCUMENT.New" type=(localize "TYPES.Item.feature")}}
+
+
+ {{localize tabs.features.label}}
+
+ {{#each @root.features as |feature|}}
+
+
+
+ {{feature.name}}
+
+
+
+ {{/each}}
+
+
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}}
+
+
+
\ No newline at end of file
diff --git a/templates/sheets-settings/npc-settings/header.hbs b/templates/sheets-settings/npc-settings/header.hbs
new file mode 100644
index 00000000..c9cb60fe
--- /dev/null
+++ b/templates/sheets-settings/npc-settings/header.hbs
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/templates/sheets/actors/adversary/header.hbs b/templates/sheets/actors/adversary/header.hbs
index fba96980..5adc235a 100644
--- a/templates/sheets/actors/adversary/header.hbs
+++ b/templates/sheets/actors/adversary/header.hbs
@@ -44,10 +44,9 @@
-
- {{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
+ {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
-
+ {{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
\ No newline at end of file
diff --git a/templates/sheets/actors/companion/header.hbs b/templates/sheets/actors/companion/header.hbs
index d10c0640..9c324709 100644
--- a/templates/sheets/actors/companion/header.hbs
+++ b/templates/sheets/actors/companion/header.hbs
@@ -50,9 +50,10 @@
- {{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
-
-
-
+ {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
+
+
+
+ {{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
\ No newline at end of file
diff --git a/templates/sheets/actors/environment/header.hbs b/templates/sheets/actors/environment/header.hbs
index 2c6bbb5a..1b4073c7 100644
--- a/templates/sheets/actors/environment/header.hbs
+++ b/templates/sheets/actors/environment/header.hbs
@@ -44,9 +44,10 @@
- {{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
-
-
-
+ {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
+
+
+
+ {{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
\ No newline at end of file
diff --git a/templates/sheets/actors/npc/features.hbs b/templates/sheets/actors/npc/features.hbs
new file mode 100644
index 00000000..3b495e74
--- /dev/null
+++ b/templates/sheets/actors/npc/features.hbs
@@ -0,0 +1,14 @@
+
+
+ {{> 'daggerheart.inventory-items'
+ title=tabs.features.label
+ type='feature'
+ collection=@root.features
+ hideContextMenu=true
+ hideModifyControls=true
+ canCreate=@root.editable
+ showActions=@root.editable
+ }}
+
+
\ No newline at end of file
diff --git a/templates/sheets/actors/npc/header.hbs b/templates/sheets/actors/npc/header.hbs
new file mode 100644
index 00000000..8dc345dc
--- /dev/null
+++ b/templates/sheets/actors/npc/header.hbs
@@ -0,0 +1,40 @@
+
\ No newline at end of file
diff --git a/templates/sheets/actors/npc/navigation.hbs b/templates/sheets/actors/npc/navigation.hbs
new file mode 100644
index 00000000..ae684f0d
--- /dev/null
+++ b/templates/sheets/actors/npc/navigation.hbs
@@ -0,0 +1,7 @@
+
+ {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
+
+
+
+ {{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
+
\ No newline at end of file
diff --git a/templates/sheets/actors/npc/notes.hbs b/templates/sheets/actors/npc/notes.hbs
new file mode 100644
index 00000000..bc9ac3cf
--- /dev/null
+++ b/templates/sheets/actors/npc/notes.hbs
@@ -0,0 +1,11 @@
+
+ {{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}}
+
+ {{#if (and showAttribution document.system.attribution.artist)}}
+ {{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/templates/sheets/global/tabs/tab-navigation.hbs b/templates/sheets/global/tabs/tab-navigation.hbs
index f9a31d3e..8af1f140 100755
--- a/templates/sheets/global/tabs/tab-navigation.hbs
+++ b/templates/sheets/global/tabs/tab-navigation.hbs
@@ -4,7 +4,7 @@
{{#each tabs as |tab|}}
-
+
{{localize tab.label}}
{{/each}}
diff --git a/templates/ui/sidebar/actor-document-partial.hbs b/templates/ui/sidebar/actor-document-partial.hbs
index 2a9f47fa..1bd3ff9a 100644
--- a/templates/ui/sidebar/actor-document-partial.hbs
+++ b/templates/ui/sidebar/actor-document-partial.hbs
@@ -6,6 +6,8 @@
{{name}}
{{#if (or (eq type "adversary") (eq type "environment"))}}
{{localize "DAGGERHEART.UI.Sidebar.actorDirectory.tier" tier=system.tier type=(@root.getTypeLabel this)}}
+ {{else if (eq type "npc")}}
+ {{localize "TYPES.Actor.npc"}}
{{else if (eq type "character")}}
{{localize "DAGGERHEART.UI.Sidebar.actorDirectory.character" level=system.levelData.level.current}}
{{else if (eq type "companion")}}
From 729e8bca422496a465e66281577659d85238ada4 Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sun, 31 May 2026 05:00:28 -0400
Subject: [PATCH 11/14] Fix companion effects not being scrollable when
overflowing (#1952)
---
styles/less/sheets/actors/companion/sheet.less | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/styles/less/sheets/actors/companion/sheet.less b/styles/less/sheets/actors/companion/sheet.less
index f31679ba..8bf8a0b9 100644
--- a/styles/less/sheets/actors/companion/sheet.less
+++ b/styles/less/sheets/actors/companion/sheet.less
@@ -10,3 +10,16 @@
background: url('../assets/parchments/dh-parchment-light.png');
}
});
+
+.application.sheet.daggerheart.actor.dh-style.companion {
+ .window-content {
+ display: flex;
+ }
+
+ .tab.active {
+ flex: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+}
From 3fbc1e97c6f458b2b6f3f83d33d33f663680820d Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Sun, 31 May 2026 06:29:54 -0400
Subject: [PATCH 12/14] Replace scroll shadows with scroll animation timeline
(#1951)
---
styles/less/dialog/level-up/sheet.less | 3 +-
.../dialog/level-up/summary-container.less | 2 +-
styles/less/global/elements.less | 2 +-
.../sheets/actors/actor-sheet-shared.less | 31 ++++++-------
.../less/sheets/actors/adversary/effects.less | 3 +-
.../sheets/actors/adversary/features.less | 3 +-
.../less/sheets/actors/adversary/index.less | 2 +
.../less/sheets/actors/adversary/notes.less | 3 ++
.../less/sheets/actors/adversary/sidebar.less | 3 +-
.../sheets/actors/character/biography.less | 3 +-
.../less/sheets/actors/character/effects.less | 3 +-
.../sheets/actors/character/features.less | 3 +-
.../sheets/actors/character/inventory.less | 6 ++-
.../less/sheets/actors/character/loadout.less | 45 ++-----------------
.../less/sheets/actors/character/sidebar.less | 2 +-
.../less/sheets/actors/companion/effects.less | 2 +-
.../sheets/actors/environment/features.less | 5 ++-
.../environment/potentialAdversaries.less | 4 +-
styles/less/sheets/actors/npc/features.less | 4 +-
.../less/sheets/actors/party/inventory.less | 6 ++-
styles/less/ui/item-browser/item-browser.less | 2 +
styles/less/utils/mixin.less | 44 ++++++++++++++++++
22 files changed, 102 insertions(+), 79 deletions(-)
create mode 100644 styles/less/sheets/actors/adversary/notes.less
diff --git a/styles/less/dialog/level-up/sheet.less b/styles/less/dialog/level-up/sheet.less
index c663f304..9ebd9331 100644
--- a/styles/less/dialog/level-up/sheet.less
+++ b/styles/less/dialog/level-up/sheet.less
@@ -14,8 +14,7 @@
.tab.active {
flex: 1;
overflow: auto;
- scrollbar-width: thin;
- scrollbar-color: light-dark(@dark-blue, @golden) transparent;
+ .with-scroll-shadows();
}
div[data-application-part='form'] {
diff --git a/styles/less/dialog/level-up/summary-container.less b/styles/less/dialog/level-up/summary-container.less
index 97353ba7..de7c9f4a 100644
--- a/styles/less/dialog/level-up/summary-container.less
+++ b/styles/less/dialog/level-up/summary-container.less
@@ -18,7 +18,7 @@
overflow: auto;
padding: 10px 0;
max-height: 700px;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
+ .with-scroll-shadows();
.level-achievements-container,
.level-advancements-container {
diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less
index 7af8becd..e57ba50d 100755
--- a/styles/less/global/elements.less
+++ b/styles/less/global/elements.less
@@ -283,7 +283,7 @@
}
&.fit-height {
- height: 95%;
+ flex: 1;
}
&.flex {
diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less
index 470067ca..b3eb0469 100644
--- a/styles/less/sheets/actors/actor-sheet-shared.less
+++ b/styles/less/sheets/actors/actor-sheet-shared.less
@@ -38,11 +38,23 @@
}
.tab.inventory {
- .search-section {
- display: flex;
+ .gold-section {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 10px;
- align-items: center;
+ padding: 10px 10px 0;
+
+ .input {
+ color: light-dark(@dark, @beige);
+ }
}
+ }
+
+ .search-section {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ justify-content: space-between;
.search-bar {
position: relative;
color: light-dark(@dark-blue-50, @beige-50);
@@ -72,22 +84,11 @@
height: 32px;
position: absolute;
right: 20px;
- font-size: 16px;
+ font-size: var(--font-size-16);
z-index: 1;
color: light-dark(@dark-blue-50, @beige-50);
}
}
-
- .gold-section {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr 1fr;
- gap: 10px;
- padding: 10px 10px 0;
-
- .input {
- color: light-dark(@dark, @beige);
- }
- }
}
&.limited {
diff --git a/styles/less/sheets/actors/adversary/effects.less b/styles/less/sheets/actors/adversary/effects.less
index fbf74249..4aa44e51 100644
--- a/styles/less/sheets/actors/adversary/effects.less
+++ b/styles/less/sheets/actors/adversary/effects.less
@@ -1,4 +1,5 @@
@import '../../../utils/colors.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.adversary {
.tab.effects {
@@ -7,8 +8,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
padding-bottom: 20px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/adversary/features.less b/styles/less/sheets/actors/adversary/features.less
index af870d9b..447d050e 100644
--- a/styles/less/sheets/actors/adversary/features.less
+++ b/styles/less/sheets/actors/adversary/features.less
@@ -1,5 +1,6 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.adversary {
.tab.features {
@@ -8,8 +9,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
padding-bottom: 20px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/adversary/index.less b/styles/less/sheets/actors/adversary/index.less
index a1ab41c7..28ff9d22 100644
--- a/styles/less/sheets/actors/adversary/index.less
+++ b/styles/less/sheets/actors/adversary/index.less
@@ -3,3 +3,5 @@
@import './sheet.less';
@import './sidebar.less';
@import './effects.less';
+@import './notes.less';
+
diff --git a/styles/less/sheets/actors/adversary/notes.less b/styles/less/sheets/actors/adversary/notes.less
new file mode 100644
index 00000000..a95d070e
--- /dev/null
+++ b/styles/less/sheets/actors/adversary/notes.less
@@ -0,0 +1,3 @@
+.application.sheet.daggerheart.actor.dh-style.adversary .tab.notes.active {
+ padding-bottom: 20px;
+}
diff --git a/styles/less/sheets/actors/adversary/sidebar.less b/styles/less/sheets/actors/adversary/sidebar.less
index b1bb51db..5db9f5e9 100644
--- a/styles/less/sheets/actors/adversary/sidebar.less
+++ b/styles/less/sheets/actors/adversary/sidebar.less
@@ -286,9 +286,8 @@
overflow-y: hidden;
padding-top: 10px;
padding-bottom: 20px;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
-
scrollbar-gutter: stable;
+ .with-scroll-shadows();
&:hover {
overflow-y: auto;
diff --git a/styles/less/sheets/actors/character/biography.less b/styles/less/sheets/actors/character/biography.less
index f8d56735..8548a2fb 100644
--- a/styles/less/sheets/actors/character/biography.less
+++ b/styles/less/sheets/actors/character/biography.less
@@ -1,5 +1,6 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.character {
.tab.biography {
@@ -9,10 +10,10 @@
gap: 10px;
height: 100%;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 98%, transparent 100%);
padding-top: 8px;
padding-bottom: 20px;
height: 100%;
+ .with-scroll-shadows();
}
.characteristics-section {
diff --git a/styles/less/sheets/actors/character/effects.less b/styles/less/sheets/actors/character/effects.less
index ae49fa2d..0ab1007d 100644
--- a/styles/less/sheets/actors/character/effects.less
+++ b/styles/less/sheets/actors/character/effects.less
@@ -1,5 +1,6 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.character {
.tab.effects {
@@ -8,8 +9,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
padding-bottom: 20px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/character/features.less b/styles/less/sheets/actors/character/features.less
index 017254a3..52b41826 100644
--- a/styles/less/sheets/actors/character/features.less
+++ b/styles/less/sheets/actors/character/features.less
@@ -1,5 +1,6 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.character {
.tab.features {
@@ -8,8 +9,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
padding-bottom: 20px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/character/inventory.less b/styles/less/sheets/actors/character/inventory.less
index c8d2b584..fcfbbee9 100644
--- a/styles/less/sheets/actors/character/inventory.less
+++ b/styles/less/sheets/actors/character/inventory.less
@@ -1,5 +1,6 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.character {
.tab.inventory {
@@ -8,8 +9,9 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
- padding: 20px 0;
+ margin-top: 20px;
+ padding-bottom: 20px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/character/loadout.less b/styles/less/sheets/actors/character/loadout.less
index a896b92e..fa3e0176 100644
--- a/styles/less/sheets/actors/character/loadout.less
+++ b/styles/less/sheets/actors/character/loadout.less
@@ -1,48 +1,10 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.character {
.tab.loadout {
.search-section {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .search-bar {
- position: relative;
- color: light-dark(@dark-blue-50, @beige-50);
- width: 80%;
- padding-top: 5px;
-
- input {
- border-radius: 50px;
- background: light-dark(@dark-blue-10, @golden-10);
- border: none;
- outline: 2px solid transparent;
- transition: all 0.3s ease;
- padding: 0 20px;
-
- &:hover {
- outline: 2px solid light-dark(@dark, @golden);
- }
-
- &::-webkit-search-cancel-button {
- -webkit-appearance: none;
- display: none;
- }
- }
-
- .icon {
- align-content: center;
- height: 32px;
- position: absolute;
- right: 20px;
- font-size: var(--font-size-16);
- z-index: 1;
- color: light-dark(@dark-blue-50, @beige-50);
- }
- }
-
.btn-toggle-view {
background: light-dark(@dark-blue-10, @dark-blue);
border: 1px solid @color-border;
@@ -90,8 +52,9 @@
gap: 10px;
height: 100%;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%);
- padding: 20px 0;
+ margin-top: 20px;
+ padding-bottom: 20px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less
index 3c358d8f..c76ee9ff 100644
--- a/styles/less/sheets/actors/character/sidebar.less
+++ b/styles/less/sheets/actors/character/sidebar.less
@@ -549,8 +549,8 @@
overflow-y: hidden;
padding-top: 10px;
padding-bottom: 20px;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
scrollbar-gutter: stable;
+ .with-scroll-shadows();
&:hover {
overflow-y: auto;
diff --git a/styles/less/sheets/actors/companion/effects.less b/styles/less/sheets/actors/companion/effects.less
index 6d7fe061..c0cac669 100644
--- a/styles/less/sheets/actors/companion/effects.less
+++ b/styles/less/sheets/actors/companion/effects.less
@@ -7,8 +7,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
padding-bottom: 20px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/environment/features.less b/styles/less/sheets/actors/environment/features.less
index cc8a345a..84cf26f8 100644
--- a/styles/less/sheets/actors/environment/features.less
+++ b/styles/less/sheets/actors/environment/features.less
@@ -1,5 +1,6 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.environment {
.tab.features {
@@ -8,8 +9,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
- padding-bottom: 20px;
+ padding-bottom: 4px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/environment/potentialAdversaries.less b/styles/less/sheets/actors/environment/potentialAdversaries.less
index f3c5776a..f112c0d2 100644
--- a/styles/less/sheets/actors/environment/potentialAdversaries.less
+++ b/styles/less/sheets/actors/environment/potentialAdversaries.less
@@ -7,8 +7,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
- padding-bottom: 20px;
+ padding-bottom: 4px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/npc/features.less b/styles/less/sheets/actors/npc/features.less
index 107b5a06..a579d9f8 100644
--- a/styles/less/sheets/actors/npc/features.less
+++ b/styles/less/sheets/actors/npc/features.less
@@ -11,8 +11,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%);
- padding-bottom: 20px;
+ padding-bottom: 4px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/sheets/actors/party/inventory.less b/styles/less/sheets/actors/party/inventory.less
index 8af37a79..444c6a57 100644
--- a/styles/less/sheets/actors/party/inventory.less
+++ b/styles/less/sheets/actors/party/inventory.less
@@ -1,5 +1,6 @@
@import '../../../utils/colors.less';
@import '../../../utils/fonts.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.party {
.tab.inventory {
@@ -8,8 +9,9 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%);
- padding: 20px 0;
+ margin-top: 20px;
+ padding-bottom: 4px;
+ .with-scroll-shadows();
}
}
}
diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less
index 1387f444..aac63d7a 100644
--- a/styles/less/ui/item-browser/item-browser.less
+++ b/styles/less/ui/item-browser/item-browser.less
@@ -1,5 +1,6 @@
@import '../../utils/colors.less';
@import '../../utils/fonts.less';
+@import '../../utils/mixin.less';
.application.daggerheart.dh-style.compendium-browser {
border: initial;
@@ -242,6 +243,7 @@
.compendium-sidebar > .folder-list {
overflow-y: auto;
scrollbar-gutter: stable;
+ .with-scroll-shadows();
}
.item-list-header,
diff --git a/styles/less/utils/mixin.less b/styles/less/utils/mixin.less
index 237a5acb..18b1f9a6 100644
--- a/styles/less/utils/mixin.less
+++ b/styles/less/utils/mixin.less
@@ -160,3 +160,47 @@
@destination @length
);
}
+
+// Scroll shadows, but only if the browser supports. At the time of writing, this doesn't work on firefox
+@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
+ @property --fade-start {
+ syntax: "";
+ inherits: false;
+ initial-value: 0;
+ }
+
+ @property --fade-end {
+ syntax: "";
+ inherits: false;
+ initial-value: 0;
+ }
+
+ @keyframes scrollfade {
+ 0% {
+ --fade-start: 0;
+ }
+ 10%, 100% {
+ --fade-start: 12px;
+ }
+ 0%, 90% {
+ --fade-end: 12px;
+ }
+ 100% {
+ --fade-end: 0;
+ }
+ }
+}
+
+.with-scroll-shadows() {
+ animation: scrollfade;
+ animation-timeline: --scrollfade;
+ animation-range: entry 0% exit 100%;
+ scroll-timeline: --scrollfade y;
+ mask-image: linear-gradient(
+ 0deg,
+ transparent 0%,
+ black var(--fade-end),
+ black calc(100% - var(--fade-start)),
+ transparent 100%
+ );
+}
From 3eb33a71af6868c4dc2a6eaeb2e000fa49f2a634 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Sun, 31 May 2026 19:54:42 +0200
Subject: [PATCH 13/14] Fixed the centering of the Select-Roll circles in
TagTeamDialog. Fixed so that damage formulas that are just a modifier don't
get a prefix '+' rendered (#1955)
---
styles/less/dialog/tag-team-dialog/sheet.less | 1 +
templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/styles/less/dialog/tag-team-dialog/sheet.less b/styles/less/dialog/tag-team-dialog/sheet.less
index 3a112146..82bc0270 100644
--- a/styles/less/dialog/tag-team-dialog/sheet.less
+++ b/styles/less/dialog/tag-team-dialog/sheet.less
@@ -194,6 +194,7 @@
.roll-selection-container {
display: flex;
+ gap: 16px;
.select-roll-button {
margin-top: 8px;
diff --git a/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs
index 49fc8f4f..2a366269 100644
--- a/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs
+++ b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs
@@ -16,7 +16,7 @@
{{/unless}}
{{/each}}
{{#if part.modifierTotal}}
- {{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}}
+ {{#if part.dice.length}}{{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}} {{/if}}
{{positive part.modifierTotal}}
{{/if}}
From 7dec188167217474e17436881f0e24a2f1e9472f Mon Sep 17 00:00:00 2001
From: Carlos Fernandez
Date: Fri, 29 May 2026 05:41:22 -0400
Subject: [PATCH 14/14] Styling pass for scrollbars, fieldsets, and scroll
shadows
---
.../applications/sheets/actors/adversary.mjs | 2 +-
styles/less/global/sheet.less | 2 +-
.../sheets/actors/actor-sheet-shared.less | 2 +-
.../less/sheets/actors/adversary/effects.less | 3 +-
.../sheets/actors/adversary/features.less | 17 +++++++-
.../less/sheets/actors/adversary/index.less | 5 +--
.../less/sheets/actors/adversary/notes.less | 6 ++-
.../less/sheets/actors/adversary/sheet.less | 39 ++++++++++--------
.../sheets/actors/character/biography.less | 3 +-
.../sheets/actors/character/features.less | 1 +
.../less/sheets/actors/character/index.less | 2 +-
.../sheets/actors/character/inventory.less | 7 +++-
.../less/sheets/actors/character/loadout.less | 4 +-
.../less/sheets/actors/character/sheet.less | 41 ++++++++++---------
.../less/sheets/actors/companion/effects.less | 6 ++-
.../sheets/actors/environment/features.less | 14 ++++++-
.../less/sheets/actors/environment/index.less | 3 +-
.../less/sheets/actors/environment/notes.less | 11 +++++
.../environment/potentialAdversaries.less | 4 +-
.../less/sheets/actors/environment/sheet.less | 2 +
styles/less/sheets/actors/party/index.less | 3 +-
.../less/sheets/actors/party/inventory.less | 6 +++
styles/less/sheets/actors/party/notes.less | 12 ++++++
.../sheets/actors/party/party-members.less | 1 +
styles/less/sheets/actors/party/sheet.less | 2 +
.../sheets/actors/adversary/features.hbs | 26 +++++++-----
templates/sheets/actors/adversary/notes.hbs | 5 +--
.../sheets/actors/environment/features.hbs | 26 +++++++-----
templates/sheets/actors/environment/notes.hbs | 5 +--
templates/sheets/actors/party/notes.hbs | 5 +--
30 files changed, 177 insertions(+), 88 deletions(-)
create mode 100644 styles/less/sheets/actors/environment/notes.less
create mode 100644 styles/less/sheets/actors/party/notes.less
diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs
index 04be3efb..caaa193a 100644
--- a/module/applications/sheets/actors/adversary.mjs
+++ b/module/applications/sheets/actors/adversary.mjs
@@ -7,7 +7,7 @@ export default class AdversarySheet extends DHBaseActorSheet {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['adversary'],
- position: { width: 660, height: 766 },
+ position: { width: 670, height: 766 },
window: { resizable: true },
actions: {
toggleHitPoints: AdversarySheet.#toggleHitPoints,
diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less
index e3072da1..8381c7c3 100755
--- a/styles/less/global/sheet.less
+++ b/styles/less/global/sheet.less
@@ -54,7 +54,7 @@ body.game:is(.performance-low, .noblur) {
position: relative;
min-height: -webkit-fill-available;
transition: opacity 0.3s ease;
- padding-bottom: 20px;
+ padding-bottom: 16px;
.tab {
padding: 0 10px;
diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less
index b3eb0469..c90e5d88 100644
--- a/styles/less/sheets/actors/actor-sheet-shared.less
+++ b/styles/less/sheets/actors/actor-sheet-shared.less
@@ -42,7 +42,7 @@
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 10px;
- padding: 10px 10px 0;
+ padding: 12px 12px 0 10px;
.input {
color: light-dark(@dark, @beige);
diff --git a/styles/less/sheets/actors/adversary/effects.less b/styles/less/sheets/actors/adversary/effects.less
index 4aa44e51..da71b52b 100644
--- a/styles/less/sheets/actors/adversary/effects.less
+++ b/styles/less/sheets/actors/adversary/effects.less
@@ -8,7 +8,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- padding-bottom: 20px;
+ padding-bottom: 4px;
+ scrollbar-gutter: stable;
.with-scroll-shadows();
}
}
diff --git a/styles/less/sheets/actors/adversary/features.less b/styles/less/sheets/actors/adversary/features.less
index 447d050e..19803300 100644
--- a/styles/less/sheets/actors/adversary/features.less
+++ b/styles/less/sheets/actors/adversary/features.less
@@ -3,14 +3,27 @@
@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.adversary {
- .tab.features {
+ .tab.features.active {
+ position: relative;
+ padding-left: 15px;
.feature-section {
display: flex;
flex-direction: column;
gap: 10px;
+ padding: 6px 2px 4px 0;
overflow-y: auto;
- padding-bottom: 20px;
+ scrollbar-gutter: stable;
.with-scroll-shadows();
}
+ > button {
+ --button-size: 1.75rem;
+ width: 1.75rem;
+ position: absolute;
+ inset: auto 16px 0 auto;
+ box-shadow: 0 0 5px @light-black;
+ }
+ &:has(> button) .feature-section {
+ padding-bottom: calc(1px + 1.75rem);
+ }
}
}
diff --git a/styles/less/sheets/actors/adversary/index.less b/styles/less/sheets/actors/adversary/index.less
index 28ff9d22..a05af854 100644
--- a/styles/less/sheets/actors/adversary/index.less
+++ b/styles/less/sheets/actors/adversary/index.less
@@ -1,7 +1,6 @@
-@import './features.less';
-@import './header.less';
@import './sheet.less';
+@import './header.less';
+@import './features.less';
@import './sidebar.less';
@import './effects.less';
@import './notes.less';
-
diff --git a/styles/less/sheets/actors/adversary/notes.less b/styles/less/sheets/actors/adversary/notes.less
index a95d070e..80feb77d 100644
--- a/styles/less/sheets/actors/adversary/notes.less
+++ b/styles/less/sheets/actors/adversary/notes.less
@@ -1,3 +1,7 @@
.application.sheet.daggerheart.actor.dh-style.adversary .tab.notes.active {
- padding-bottom: 20px;
+ padding: 6px 0 4px 15px;
+ .editor-content {
+ scrollbar-gutter: stable;
+ .with-scroll-shadows();
+ }
}
diff --git a/styles/less/sheets/actors/adversary/sheet.less b/styles/less/sheets/actors/adversary/sheet.less
index 0bd845fa..509a2d63 100644
--- a/styles/less/sheets/actors/adversary/sheet.less
+++ b/styles/less/sheets/actors/adversary/sheet.less
@@ -9,28 +9,31 @@
height: 100%;
width: 100%;
padding-bottom: 0;
+ }
- .adversary-sidebar-sheet {
- grid-row: 1 / span 2;
- grid-column: 1;
+ .adversary-sidebar-sheet {
+ grid-row: 1 / span 2;
+ grid-column: 1;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .adversary-header-sheet {
+ grid-row: 1;
+ grid-column: 2;
+ }
+
+ .tab {
+ grid-row: 2;
+ grid-column: 2;
+ &.active {
overflow: hidden;
display: flex;
flex-direction: column;
- }
-
- .adversary-header-sheet {
- grid-row: 1;
- grid-column: 2;
- }
-
- .tab {
- grid-row: 2;
- grid-column: 2;
- &.active {
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
+ padding-right: 0;
+ margin-right: 2px;
+ margin-bottom: 16px;
}
}
}
diff --git a/styles/less/sheets/actors/character/biography.less b/styles/less/sheets/actors/character/biography.less
index 8548a2fb..ce52c5af 100644
--- a/styles/less/sheets/actors/character/biography.less
+++ b/styles/less/sheets/actors/character/biography.less
@@ -11,8 +11,9 @@
height: 100%;
overflow-y: auto;
padding-top: 8px;
- padding-bottom: 20px;
+ padding-bottom: 4px;
height: 100%;
+ scrollbar-gutter: stable;
.with-scroll-shadows();
}
diff --git a/styles/less/sheets/actors/character/features.less b/styles/less/sheets/actors/character/features.less
index 52b41826..f81f0e32 100644
--- a/styles/less/sheets/actors/character/features.less
+++ b/styles/less/sheets/actors/character/features.less
@@ -10,6 +10,7 @@
gap: 10px;
overflow-y: auto;
padding-bottom: 20px;
+ scrollbar-gutter: stable;
.with-scroll-shadows();
}
}
diff --git a/styles/less/sheets/actors/character/index.less b/styles/less/sheets/actors/character/index.less
index edefe0a1..f196d5bf 100644
--- a/styles/less/sheets/actors/character/index.less
+++ b/styles/less/sheets/actors/character/index.less
@@ -1,8 +1,8 @@
+@import './sheet.less';
@import './biography.less';
@import './effects.less';
@import './features.less';
@import './header.less';
@import './inventory.less';
@import './loadout.less';
-@import './sheet.less';
@import './sidebar.less';
diff --git a/styles/less/sheets/actors/character/inventory.less b/styles/less/sheets/actors/character/inventory.less
index fcfbbee9..b07bb6fe 100644
--- a/styles/less/sheets/actors/character/inventory.less
+++ b/styles/less/sheets/actors/character/inventory.less
@@ -4,13 +4,18 @@
.application.sheet.daggerheart.actor.dh-style.character {
.tab.inventory {
+ padding-right: 0;
+ .search-section {
+ padding-right: 14px;
+ }
.items-section {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
+ scrollbar-gutter: stable;
margin-top: 20px;
- padding-bottom: 20px;
+ padding-bottom: 4px;
.with-scroll-shadows();
}
}
diff --git a/styles/less/sheets/actors/character/loadout.less b/styles/less/sheets/actors/character/loadout.less
index fa3e0176..67b2914b 100644
--- a/styles/less/sheets/actors/character/loadout.less
+++ b/styles/less/sheets/actors/character/loadout.less
@@ -5,6 +5,7 @@
.application.sheet.daggerheart.actor.dh-style.character {
.tab.loadout {
.search-section {
+ padding-right: 14px;
.btn-toggle-view {
background: light-dark(@dark-blue-10, @dark-blue);
border: 1px solid @color-border;
@@ -52,8 +53,9 @@
gap: 10px;
height: 100%;
overflow-y: auto;
+ scrollbar-gutter: stable;
margin-top: 20px;
- padding-bottom: 20px;
+ padding-bottom: 4px;
.with-scroll-shadows();
}
}
diff --git a/styles/less/sheets/actors/character/sheet.less b/styles/less/sheets/actors/character/sheet.less
index 68792c99..aebcb9b9 100644
--- a/styles/less/sheets/actors/character/sheet.less
+++ b/styles/less/sheets/actors/character/sheet.less
@@ -10,28 +10,31 @@
width: 100%;
padding-bottom: 0;
overflow-x: auto;
+ }
+
+ .character-sidebar-sheet {
+ grid-row: 1 / span 2;
+ grid-column: 1;
+ display: flex;
+ flex-direction: column;
+ }
- .character-sidebar-sheet {
- grid-row: 1 / span 2;
- grid-column: 1;
+ .character-header-sheet {
+ position: relative;
+ grid-row: 1;
+ grid-column: 2;
+ }
+
+ .tab {
+ grid-row: 2;
+ grid-column: 2;
+ padding-right: 0;
+ margin-right: 2px;
+ margin-bottom: 12px;
+ &.active {
display: flex;
flex-direction: column;
- }
-
- .character-header-sheet {
- position: relative;
- grid-row: 1;
- grid-column: 2;
- }
-
- .tab {
- grid-row: 2;
- grid-column: 2;
- &.active {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
+ overflow: hidden;
}
}
}
diff --git a/styles/less/sheets/actors/companion/effects.less b/styles/less/sheets/actors/companion/effects.less
index c0cac669..40973196 100644
--- a/styles/less/sheets/actors/companion/effects.less
+++ b/styles/less/sheets/actors/companion/effects.less
@@ -1,13 +1,17 @@
@import '../../../utils/colors.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.companion {
.tab.effects {
+ margin-right: 2px;
+ padding-right: 0;
.effects-sections {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
- padding-bottom: 20px;
+ padding-bottom: 4px;
+ scrollbar-gutter: stable;
.with-scroll-shadows();
}
}
diff --git a/styles/less/sheets/actors/environment/features.less b/styles/less/sheets/actors/environment/features.less
index 84cf26f8..a8fff527 100644
--- a/styles/less/sheets/actors/environment/features.less
+++ b/styles/less/sheets/actors/environment/features.less
@@ -4,13 +4,25 @@
.application.sheet.daggerheart.actor.dh-style.environment {
.tab.features {
+ position: relative;
.feature-section {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
- padding-bottom: 4px;
+ padding: 4px 8px;
+ scrollbar-gutter: stable;
.with-scroll-shadows();
}
+ > button {
+ --button-size: 1.75rem;
+ width: 1.75rem;
+ position: absolute;
+ inset: auto 16px 0 auto;
+ box-shadow: 0 0 5px @light-black;
+ }
+ &:has(> button) .feature-section {
+ padding-bottom: calc(1px + 1.75rem);
+ }
}
}
diff --git a/styles/less/sheets/actors/environment/index.less b/styles/less/sheets/actors/environment/index.less
index 211c8e60..db35eabc 100644
--- a/styles/less/sheets/actors/environment/index.less
+++ b/styles/less/sheets/actors/environment/index.less
@@ -1,4 +1,5 @@
+@import './sheet.less';
@import './features.less';
@import './header.less';
@import './potentialAdversaries.less';
-@import './sheet.less';
+@import './notes.less';
diff --git a/styles/less/sheets/actors/environment/notes.less b/styles/less/sheets/actors/environment/notes.less
new file mode 100644
index 00000000..58447c88
--- /dev/null
+++ b/styles/less/sheets/actors/environment/notes.less
@@ -0,0 +1,11 @@
+@import '../../../utils/mixin.less';
+
+.application.sheet.daggerheart.actor.dh-style.environment {
+ .tab.notes {
+ padding: 6px 0 4px 15px;
+ .editor-content {
+ scrollbar-gutter: stable;
+ .with-scroll-shadows();
+ }
+ }
+}
\ No newline at end of file
diff --git a/styles/less/sheets/actors/environment/potentialAdversaries.less b/styles/less/sheets/actors/environment/potentialAdversaries.less
index f112c0d2..e66ff34d 100644
--- a/styles/less/sheets/actors/environment/potentialAdversaries.less
+++ b/styles/less/sheets/actors/environment/potentialAdversaries.less
@@ -1,4 +1,5 @@
@import '../../../utils/colors.less';
+@import '../../../utils/mixin.less';
.application.sheet.daggerheart.actor.dh-style.environment {
.tab.potentialAdversaries {
@@ -7,7 +8,8 @@
flex-direction: column;
gap: 10px;
overflow-y: auto;
- padding-bottom: 4px;
+ padding: 0 4px 4px 4px;
+ scrollbar-gutter: stable;
.with-scroll-shadows();
}
}
diff --git a/styles/less/sheets/actors/environment/sheet.less b/styles/less/sheets/actors/environment/sheet.less
index 2d9cc188..02489cac 100644
--- a/styles/less/sheets/actors/environment/sheet.less
+++ b/styles/less/sheets/actors/environment/sheet.less
@@ -16,6 +16,8 @@
.tab {
flex: 1;
overflow-y: auto;
+ padding-right: 0;
+ margin-right: 2px;
&.active {
overflow: hidden;
diff --git a/styles/less/sheets/actors/party/index.less b/styles/less/sheets/actors/party/index.less
index 56f7a457..279c9db2 100644
--- a/styles/less/sheets/actors/party/index.less
+++ b/styles/less/sheets/actors/party/index.less
@@ -1,4 +1,5 @@
+@import './sheet.less';
@import './header.less';
@import './party-members.less';
-@import './sheet.less';
@import './inventory.less';
+@import './notes.less';
\ No newline at end of file
diff --git a/styles/less/sheets/actors/party/inventory.less b/styles/less/sheets/actors/party/inventory.less
index 444c6a57..4b14e112 100644
--- a/styles/less/sheets/actors/party/inventory.less
+++ b/styles/less/sheets/actors/party/inventory.less
@@ -4,11 +4,17 @@
.application.sheet.daggerheart.actor.dh-style.party {
.tab.inventory {
+ padding-right: 0;
+ .search-section {
+ padding-right: 14px;
+ }
+
.items-section {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
+ scrollbar-gutter: stable;
margin-top: 20px;
padding-bottom: 4px;
.with-scroll-shadows();
diff --git a/styles/less/sheets/actors/party/notes.less b/styles/less/sheets/actors/party/notes.less
new file mode 100644
index 00000000..4ef51caa
--- /dev/null
+++ b/styles/less/sheets/actors/party/notes.less
@@ -0,0 +1,12 @@
+@import '../../../utils/mixin.less';
+
+.application.sheet.daggerheart.actor.dh-style.party {
+ .tab.notes {
+ padding: 16px 0 4px 15px;
+ .editor-content {
+ scrollbar-gutter: stable;
+ padding-left: 8px;
+ .with-scroll-shadows();
+ }
+ }
+}
\ No newline at end of file
diff --git a/styles/less/sheets/actors/party/party-members.less b/styles/less/sheets/actors/party/party-members.less
index dc464291..7b85eb35 100644
--- a/styles/less/sheets/actors/party/party-members.less
+++ b/styles/less/sheets/actors/party/party-members.less
@@ -4,6 +4,7 @@
.application.sheet.daggerheart.actor.dh-style.party .tab.partyMembers {
overflow: auto;
+ .with-scroll-shadows();
.actors-list {
display: flex;
diff --git a/styles/less/sheets/actors/party/sheet.less b/styles/less/sheets/actors/party/sheet.less
index 852b6cfc..33aa64a3 100644
--- a/styles/less/sheets/actors/party/sheet.less
+++ b/styles/less/sheets/actors/party/sheet.less
@@ -21,6 +21,8 @@
flex: 1;
overflow-y: auto;
scrollbar-gutter: stable;
+ margin-right: 2px;
+ padding-right: 8px;
&.active {
overflow: auto;
diff --git a/templates/sheets/actors/adversary/features.hbs b/templates/sheets/actors/adversary/features.hbs
index 3b495e74..685540e7 100644
--- a/templates/sheets/actors/adversary/features.hbs
+++ b/templates/sheets/actors/adversary/features.hbs
@@ -1,14 +1,20 @@
- {{> 'daggerheart.inventory-items'
- title=tabs.features.label
- type='feature'
- collection=@root.features
- hideContextMenu=true
- hideModifyControls=true
- canCreate=@root.editable
- showActions=@root.editable
- }}
+ {{#each @root.features as |item|}}
+ {{> 'daggerheart.inventory-item'
+ item=item
+ type='feature'
+ actorType=@root.document.type
+ hideContextMenu=true
+ hideModifyControls=true
+ showActions=@root.editable
+ }}
+ {{/each}}
-
\ No newline at end of file
+ {{#if @root.editable}}
+
+
+
+ {{/if}}
+
diff --git a/templates/sheets/actors/adversary/notes.hbs b/templates/sheets/actors/adversary/notes.hbs
index a5c3f706..28df7f8e 100644
--- a/templates/sheets/actors/adversary/notes.hbs
+++ b/templates/sheets/actors/adversary/notes.hbs
@@ -3,10 +3,7 @@
data-tab='{{tabs.notes.id}}'
data-group='{{tabs.notes.group}}'
>
-
- {{localize tabs.notes.label}}
- {{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}}
-
+ {{formInput notes.field value=notes.value enriched=notes.enriched class="aaa" toggled=true}}
{{#if (and showAttribution document.system.attribution.artist)}}
{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}
diff --git a/templates/sheets/actors/environment/features.hbs b/templates/sheets/actors/environment/features.hbs
index 35fcb038..1dbd4b94 100644
--- a/templates/sheets/actors/environment/features.hbs
+++ b/templates/sheets/actors/environment/features.hbs
@@ -4,14 +4,20 @@
data-group='{{tabs.features.group}}'
>
- {{> 'daggerheart.inventory-items'
- title=tabs.features.label
- type='feature'
- collection=@root.features
- hideContextMenu=true
- hideModifyControls=true
- canCreate=@root.editable
- showActions=@root.editable
- }}
-
+ {{#each @root.features as |item|}}
+ {{> 'daggerheart.inventory-item'
+ item=item
+ type='feature'
+ actorType=@root.document.type
+ hideContextMenu=true
+ hideModifyControls=true
+ showActions=@root.editable
+ }}
+ {{/each}}
+
+ {{#if @root.editable}}
+
+
+
+ {{/if}}
\ No newline at end of file
diff --git a/templates/sheets/actors/environment/notes.hbs b/templates/sheets/actors/environment/notes.hbs
index 4f6b131e..1acf0e93 100644
--- a/templates/sheets/actors/environment/notes.hbs
+++ b/templates/sheets/actors/environment/notes.hbs
@@ -3,10 +3,7 @@
data-tab='{{tabs.notes.id}}'
data-group='{{tabs.notes.group}}'
>
-
- {{localize tabs.notes.label}}
- {{formInput notes.field value=notes.value enriched=notes.value toggled=true}}
-
+ {{formInput notes.field value=notes.value enriched=notes.value toggled=true}}
{{#if (and showAttribution document.system.attribution.artist)}}
{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}
diff --git a/templates/sheets/actors/party/notes.hbs b/templates/sheets/actors/party/notes.hbs
index 663a484a..0972bee9 100644
--- a/templates/sheets/actors/party/notes.hbs
+++ b/templates/sheets/actors/party/notes.hbs
@@ -3,8 +3,5 @@
data-tab='{{tabs.notes.id}}'
data-group='{{tabs.notes.group}}'
>
-
- {{localize tabs.notes.label}}
- {{formInput notes.field value=notes.value enriched=notes.value toggled=true}}
-
+ {{formInput notes.field value=notes.value enriched=notes.value toggled=true}}
\ No newline at end of file