Merge branch 'main' into fix/filter-subclasses-creation

This commit is contained in:
Miguel Molina 2025-08-10 11:17:56 +02:00 committed by GitHub
commit 419dd70833
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1392 additions and 333 deletions

View file

@ -1,21 +1,26 @@
# Daggerheart
# Foundryborne Daggerheart
## Table of Contents
- [Overview](#overview)
- [User Install Guide](#user-install)
- [Documentation](#documentation)
- [Developer Setup](#development-setup)
- [Contribution Info](#contributing)
## Overview
This is a community repo for a Foundry VTT implementation of Daggerheart. It is not associated with Critical Role or Darrington Press.
This is the community repo for the Foundry VTT system _Foundryborne_ Daggerheart. It is not associated with Critical Role or Darrington Press.
## User Install
1. **(Not Yet Supported - No Releases Yet)** Pasting `https://raw.githubusercontent.com/Foundryborne/daggerheart/refs/heads/main/system.json` into the Install System dialog on the Setup menu of the application.
2. **(Not Yet Supported - No Releases Yet)** Browsing the repository's Releases page, where you can copy any system.json link for use in the Install System dialog.
3. **(Not Yet Supported - No Releases Yet)** Downloading one of the .zip archives from the Releases page and extracting it into your foundry Data folder, under Data/systems/daggerheart.
1. **recommended** Searching for _Daggerheart_ or _Foundryborne_ in the System Instalaltion dialgoe of the FoundryVTT admin settings.
2. Pasting `https://raw.githubusercontent.com/Foundryborne/daggerheart/refs/heads/main/system.json` into the Install System dialog on the Setup menu of the application.
3. Downloading one of the .zip archives from the Releases page and extracting it into your foundry Data folder, under Data/systems/daggerheart.
## Documentation
You can find the documentation here: https://github.com/Foundryborne/daggerheart/wiki
## Development Setup

View file

@ -498,6 +498,11 @@
"ReactionRoll": {
"title": "Reaction Roll: {trait}"
},
"RerollDialog": {
"title": "Reroll",
"deselectDiceNotification": "Deselect one of the selected dice first",
"acceptCurrentRolls": "Accept Current Rolls"
},
"ResourceDice": {
"title": "{name} Resource",
"rerollDice": "Reroll Dice"
@ -505,7 +510,8 @@
},
"CLASS": {
"Feature": {
"rallyDice": "Bardic Rally Dice"
"rallyDice": "Bardic Rally Dice",
"short": "Rally"
}
},
"CONFIG": {

View file

@ -6,5 +6,6 @@ export { default as DeathMove } from './deathMove.mjs';
export { default as Downtime } from './downtime.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';

View file

@ -151,11 +151,19 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.experiences.indexOf(button.dataset.key) > -1
? this.config.experiences.filter(x => x !== button.dataset.key)
: [...this.config.experiences, button.dataset.key];
if(this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') {
if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') {
this.config.costs =
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
? this.config.costs.filter(x => x.extKey !== button.dataset.key)
: [...this.config.costs, { extKey: button.dataset.key, key: 'hope', value: 1, name: this.config.data?.experiences?.[button.dataset.key]?.name }];
: [
...this.config.costs,
{
extKey: button.dataset.key,
key: 'hope',
value: 1,
name: this.config.data?.experiences?.[button.dataset.key]?.name
}
];
}
this.render();
}
@ -166,8 +174,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config.roll.type = this.reactionOverride
? CONFIG.DH.ITEM.actionTypes.reaction.id
: this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id
? null
: this.config.roll.type;
? null
: this.config.roll.type;
this.render();
}
}

View file

@ -56,12 +56,14 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
label,
icon
}));
context.modifiers = this.config.modifiers;
return context;
}
static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, rest.roll);
foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
this.config.selectedRollMode = rest.selectedRollMode;
this.render();

View file

@ -0,0 +1,279 @@
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();
}
}

View file

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

View file

@ -1,4 +1,5 @@
import DhAppearance from '../../data/settings/Appearance.mjs';
import { getDiceSoNicePreset } from '../../config/generalConfig.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -25,7 +26,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
},
actions: {
reset: this.reset,
save: this.save
save: this.save,
preview: this.preview
},
form: { handler: this.updateData, submitOnChange: true }
};
@ -89,6 +91,22 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
this.render();
}
static async preview() {
const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice];
let faces = 'd12';
switch (this.tabGroups.diceSoNice) {
case 'advantage':
case 'disadvantage':
faces = 'd6';
}
const preset = await getDiceSoNicePreset(source, faces);
const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate();
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false);
}
static async reset() {
this.settings = new DhAppearance();
this.render();

View file

@ -645,18 +645,17 @@ export default class CharacterSheet extends DHBaseActorSheet {
}
async consumeResource(costs) {
if(!costs?.length) return;
if (!costs?.length) return;
const usefulResources = foundry.utils.deepClone(this.actor.system.resources);
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs)
.map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
await this.actor.modifyResource(resources);
}

View file

@ -333,7 +333,7 @@ export default function DHApplicationMixin(Base) {
}
];
if (usable)
if (usable) {
options.unshift({
name: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
@ -341,20 +341,6 @@ export default function DHApplicationMixin(Base) {
const doc = getDocFromElementSync(target);
return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length;
},
callback: async (target, event) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
return action && action.use(event, { byPassRoll: true })
}
});
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
condition: target => {
const doc = getDocFromElementSync(target);
return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length;
},
callback: async (target, event) => {
const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc;
@ -362,15 +348,16 @@ export default function DHApplicationMixin(Base) {
}
});
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
condition: target => {
const doc = getDocFromElementSync(target);
return doc && !(doc.type === 'domainCard' && doc.system.inVault);
},
callback: async (target, event) => (await getDocFromElement(target)).use(event)
});
options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst',
condition: target => {
const doc = getDocFromElementSync(target);
return doc && !(doc.type === 'domainCard' && doc.system.inVault);
},
callback: async (target, event) => (await getDocFromElement(target)).use(event)
});
}
if (toChat)
options.push({

View file

@ -20,6 +20,40 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
classes: ['daggerheart']
};
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
// {
// name: 'Reroll',
// icon: '<i class="fa-solid fa-dice"></i>',
// condition: li => {
// const message = game.messages.get(li.dataset.messageId);
// return (game.user.isGM || message.isAuthor) && message.rolls.length > 0;
// },
// callback: li => {
// const message = game.messages.get(li.dataset.messageId);
// new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true });
// }
// },
{
name: 'Reroll Damage',
icon: '<i class="fa-solid fa-dice"></i>',
condition: li => {
const message = game.messages.get(li.dataset.messageId);
const hasRolledDamage = message.system.hasDamage
? Object.keys(message.system.damage).length > 0
: false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage;
},
callback: li => {
const message = game.messages.get(li.dataset.messageId);
new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true });
}
}
];
}
addChatListeners = async (app, html, data) => {
html.querySelectorAll('.duality-action-damage').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message))
@ -193,19 +227,28 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
const target = event.target.closest('[data-die-index]');
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
const rollClass =
game.system.api.dice[
message.type === 'dualityRoll' ? 'DualityRoll' : target.dataset.type === 'damage' ? 'DHRoll' : 'D20Roll'
];
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
if (target.dataset.type === 'damage') {
game.system.api.dice.DamageRoll.reroll(target, message);
} else {
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
const rollClass =
game.system.api.dice[
message.type === 'dualityRoll'
? 'DualityRoll'
: target.dataset.type === 'damage'
? 'DHRoll'
: 'D20Roll'
];
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
});
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
});
}
}
}

View file

@ -17,7 +17,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = options.presets;
if(this.presets?.compendium && this.presets?.folder)
if (this.presets?.compendium && this.presets?.folder)
ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder);
}
@ -26,7 +26,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
id: 'itemBrowser',
classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'],
tag: 'div',
// title: 'Item Browser',
window: {
frame: true,
title: 'Compendium Browser',
@ -41,9 +40,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
sortList: this.sortList
},
position: {
top: 330,
left: 120,
width: 800,
left: 100,
width: 850,
height: 600
}
};
@ -88,16 +86,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/** @inheritDoc */
async _preFirstRender(context, options) {
if(context.presets?.render?.noFolder || context.presets?.render?.lite)
options.position.width = 600;
if (context.presets?.render?.noFolder || context.presets?.render?.lite) options.position.width = 600;
await super._preFirstRender(context, options);
}
/** @inheritDoc */
async _preRender(context, options) {
if(context.presets?.render?.noFolder || context.presets?.render?.lite)
if (context.presets?.render?.noFolder || context.presets?.render?.lite)
options.parts.splice(options.parts.indexOf('sidebar'), 1);
await super._preRender(context, options);
@ -111,17 +107,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this._createFilterInputs();
this._createDragProcess();
if(context.presets?.render?.lite)
this.element.classList.add('lite');
if (context.presets?.render?.lite) this.element.classList.add('lite');
if(context.presets?.render?.noFolder)
this.element.classList.add('no-folder');
if (context.presets?.render?.noFolder) this.element.classList.add('no-folder');
if(context.presets?.render?.noFilter)
this.element.classList.add('no-filter');
if (context.presets?.render?.noFilter) this.element.classList.add('no-filter');
if(this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k,v]) => this.fieldFilter.find(c => c.name === k).value = v.value);
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(
([k, v]) => (this.fieldFilter.find(c => c.name === k).value = v.value)
);
await this._onInputFilterBrowser();
}
}
@ -203,6 +198,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
formatLabel(item, field) {
const property = foundry.utils.getProperty(item, field.key);
if (Array.isArray(property)) property.join(', ');
if (typeof field.format !== 'function') return property ?? '-';
return field.format(property);
}
@ -320,19 +316,18 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
async _onInputFilterBrowser(event) {
this.#filteredItems.browser.input.clear();
if(event) this.fieldFilter.find(f => f.name === event.target.name).value = event.target.value;
if (event) this.fieldFilter.find(f => f.name === event.target.name).value = event.target.value;
for (const li of this.element.querySelectorAll('.item-container')) {
const itemUUID = li.dataset.itemUuid,
item = this.items.find(i => i.uuid === itemUUID);
if(!item) continue;
if (!item) continue;
const matchesMenu =
this.fieldFilter.length === 0 ||
this.fieldFilter.every(f => (
!f.value && f.value !== false) ||
ItemBrowser.evaluateFilter(item, this.createFilterData(f))
this.fieldFilter.every(
f => (!f.value && f.value !== false) || ItemBrowser.evaluateFilter(item, this.createFilterData(f))
);
if (matchesMenu) this.#filteredItems.browser.input.add(item.id);
@ -350,11 +345,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
let docValue = foundry.utils.getProperty(obj, filter.field);
let filterValue = filter.value;
switch (filter.operator) {
case "contains2":
case 'contains2':
filterValue = Array.isArray(filterValue) ? filterValue : [filterValue];
docValue = Array.isArray(docValue) ? docValue : [docValue];
return docValue.some(dv => filterValue.includes(dv));
case "contains3":
case 'contains3':
return docValue.some(f => f.value === filterValue);
default:
return foundry.applications.ux.SearchFilter.evaluateFilter(obj, filter);
@ -378,24 +373,27 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.render({ force: true });
}
static getFolderConfig(folder, property = "columns") {
if(!folder) return [];
static getFolderConfig(folder, property = 'columns') {
if (!folder) return [];
return folder[property] ?? CONFIG.DH.ITEMBROWSER.typeConfig[folder.listType]?.[property] ?? [];
}
static sortList(_, target) {
const key = target.dataset.sortKey,
type = !target.dataset.sortType || target.dataset.sortType === "DESC" ? "ASC" : "DESC",
itemListContainer = target.closest(".compendium-results").querySelector(".item-list"),
itemList = itemListContainer.querySelectorAll(".item-container");
type = !target.dataset.sortType || target.dataset.sortType === 'DESC' ? 'ASC' : 'DESC',
itemListContainer = target.closest('.compendium-results').querySelector('.item-list'),
itemList = itemListContainer.querySelectorAll('.item-container');
target.closest(".item-list-header").querySelectorAll('[data-sort-key]').forEach(b => b.dataset.sortType = "");
target
.closest('.item-list-header')
.querySelectorAll('[data-sort-key]')
.forEach(b => (b.dataset.sortType = ''));
target.dataset.sortType = type;
const newOrder = [...itemList].reverse().sort((a, b) => {
const aProp = a.querySelector(`[data-item-key="${key}"]`),
bProp = b.querySelector(`[data-item-key="${key}"]`)
if(type === "DESC") {
bProp = b.querySelector(`[data-item-key="${key}"]`);
if (type === 'DESC') {
return aProp.innerText < bProp.innerText ? 1 : -1;
} else {
return aProp.innerText > bProp.innerText ? 1 : -1;

View file

@ -494,40 +494,41 @@ export const diceSetNumbers = {
flat: 'Flat'
};
export const getDiceSoNicePresets = async (hopeFaces, fearFaces, advantageFaces = 'd6', disadvantageFaces = 'd6') => {
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
const getPreset = async (type, faces) => {
const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces);
if (!system) {
ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.noDiceSystem', {
system: game.dice3d.DiceFactory.systems.get(type.system).name,
faces: faces
})
);
return;
}
export const getDiceSoNicePreset = async (type, faces) => {
const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces);
if (!system) {
ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.noDiceSystem', {
system: game.dice3d.DiceFactory.systems.get(type.system).name,
faces: faces
})
);
return;
}
if (system.modelFile && !system.modelLoaded) {
await system.loadModel(game.dice3d.DiceFactory.loaderGLTF);
} else {
await system.loadTextures();
}
return {
modelFile: system.modelFile,
appearance: {
...system.appearance,
...type
}
};
};
if (system.modelFile && !system.modelLoaded) {
await system.loadModel(game.dice3d.DiceFactory.loaderGLTF);
} else {
await system.loadTextures();
}
return {
hope: await getPreset(diceSoNice.hope, hopeFaces),
fear: await getPreset(diceSoNice.fear, fearFaces),
advantage: await getPreset(diceSoNice.advantage, advantageFaces),
disadvantage: await getPreset(diceSoNice.disadvantage, disadvantageFaces)
modelFile: system.modelFile,
appearance: {
...system.appearance,
...type
}
};
};
export const getDiceSoNicePresets = async (hopeFaces, fearFaces, advantageFaces = 'd6', disadvantageFaces = 'd6') => {
const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
return {
hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces),
fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces),
advantage: await getDiceSoNicePreset(diceSoNice.advantage, advantageFaces),
disadvantage: await getDiceSoNicePreset(diceSoNice.disadvantage, disadvantageFaces)
};
};

View file

@ -1040,16 +1040,6 @@ export const weaponFeatures = {
key: 'system.evasion',
mode: 2,
value: '-1'
},
{
key: 'system.bonuses.damage.primaryWeapon.extraDice',
mode: 2,
value: '1'
},
{
key: 'system.rules.weapon.dropLowestDamageDice',
mode: 5,
value: '1'
}
]
}
@ -1166,18 +1156,7 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.powerful.effects.powerful.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.powerful.effects.powerful.description',
img: 'icons/magic/control/buff-flight-wings-runes-red-yellow.webp',
changes: [
{
key: 'system.bonuses.damage.primaryWeapon.extraDice',
mode: 2,
value: '1'
},
{
key: 'system.rules.weapon.dropLowestDamageDice',
mode: 5,
value: '1'
}
]
changes: []
}
]
},
@ -1301,13 +1280,7 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.selfCorrecting.effects.selfCorrecting.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.selfCorrecting.effects.selfCorrecting.description',
img: 'icons/weapons/ammunition/arrow-broadhead-glowing-orange.webp',
changes: [
{
key: 'system.rules.damage.flipMinDiceValue',
mode: 5,
value: 1
}
]
changes: []
}
]
},
@ -1319,13 +1292,7 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.serrated.effects.serrated.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.serrated.effects.serrated.description',
img: 'icons/weapons/ammunition/arrow-broadhead-glowing-orange.webp',
changes: [
{
key: 'system.rules.damage.flipMinDiceValue',
mode: 5,
value: 1
}
]
changes: []
}
]
},

View file

@ -146,7 +146,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
else if (this.hasSave || this.hasEffect) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
if(config.hasTarget) config.targetSelection = config.targets.length > 0;
await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
}
@ -225,17 +224,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
.filter(
c =>
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
(successCost && c.consumeOnSuccess)
(successCost && c.consumeOnSuccess)
)
.map(c => {
.reduce((a, c) => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
if (resource) {
a.push({
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
});
return a;
}
}, []);
await (this.actor.system.partner ?? this.actor).modifyResource(resources);
if (
@ -245,9 +247,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
)
this.update({ 'uses.value': this.uses.value + 1 });
if(config.roll?.success || successCost) {
if (config.roll?.success || successCost) {
setTimeout(() => {
(config.message ?? config.parent).update({'system.successConsumed': true})
(config.message ?? config.parent).update({ 'system.successConsumed': true });
}, 50);
}
}
@ -373,7 +375,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
await chatMessage.update({
flags: {
[game.system.id]: {
"reactionRolls": {
reactionRolls: {
[targetId]: changes
}
}

View file

@ -49,8 +49,7 @@ export default class DHDamageAction extends DHBaseAction {
...systemData,
roll: formulas,
dialog: {},
data: this.getRollData(),
targetSelection: systemData.targets.length > 0
data: this.getRollData()
};
if (this.hasSave) config.onSave = this.save.damageMod;
if (data.system) {

View file

@ -287,18 +287,6 @@ export default class DhCharacter extends BaseDataActor {
})
})
}),
weapon: new fields.SchemaField({
/* Unimplemented
-> Should remove the lowest damage dice from weapon damage
-> Reflect this in the chat message somehow so players get feedback that their choice is helping them.
*/
dropLowestDamageDice: new fields.BooleanField({ initial: false }),
/* Unimplemented
-> Should flip any lowest possible dice rolls for weapon damage to highest
-> Reflect this in the chat message somehow so players get feedback that their choice is helping them.
*/
flipMinDiceValue: new fields.BooleanField({ intial: false })
}),
runeWard: new fields.BooleanField({ initial: false }),
burden: new fields.SchemaField({
ignore: new fields.BooleanField()

View file

@ -18,14 +18,11 @@ const targetsField = () =>
);
export default class DHActorRoll extends foundry.abstract.TypeDataModel {
targetHook = null;
static defineSchema() {
return {
title: new fields.StringField(),
roll: new fields.ObjectField(),
targets: targetsField(),
targetSelection: new fields.BooleanField({ initial: false }),
hasRoll: new fields.BooleanField({ initial: false }),
hasDamage: new fields.BooleanField({ initial: false }),
hasHealing: new fields.BooleanField({ initial: false }),
@ -63,42 +60,49 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
}
get targetMode() {
return this.targetSelection;
return this.parent.targetSelection;
}
set targetMode(mode) {
this.targetSelection = mode;
if (!this.parent.isAuthor) return;
this.parent.targetSelection = mode;
this.registerTargetHook();
this.updateTargets();
}
get hitTargets() {
return this.currentTargets.filter(t => t.hit || !this.hasRoll || !this.targetSelection);
return this.currentTargets.filter(t => t.hit || !this.hasRoll || !this.targetMode);
}
async updateTargets() {
if(!ui.chat.collection.get(this.parent.id)) return;
if (!ui.chat.collection.get(this.parent.id)) return;
let targets;
if(this.targetSelection)
targets = this.targets;
if (this.targetMode) targets = this.targets;
else
targets = Array.from(game.user.targets).map(t => game.system.api.fields.ActionFields.TargetField.formatTarget(t));
targets = Array.from(game.user.targets).map(t =>
game.system.api.fields.ActionFields.TargetField.formatTarget(t)
);
this.parent.setFlag(game.system.id, "targets", targets);
await this.parent.updateSource({
system: {
targetSelection: this.targetSelection
await this.parent.update({
flags: {
[game.system.id]: {
targets: targets,
targetMode: this.targetMode
}
}
});
}
registerTargetHook() {
if(!this.parent.isAuthor) return;
if(this.targetSelection && this.targetHook !== null) {
Hooks.off("targetToken", this.targetHook);
this.targetHook = null;
} else if (!this.targetSelection && this.targetHook === null) {
this.targetHook = Hooks.on('targetToken', foundry.utils.debounce(this.updateTargets.bind(this), 50));
if (!this.parent.isAuthor) return;
if (this.targetMode && this.parent.targetHook !== null) {
Hooks.off('targetToken', this.parent.targetHook);
return (this.parent.targetHook = null);
} else if (!this.targetMode && this.parent.targetHook === null) {
return (this.parent.targetHook = Hooks.on(
'targetToken',
foundry.utils.debounce(this.updateTargets.bind(this), 50)
));
}
}
@ -106,14 +110,16 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
if (this.hasTarget) {
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
this.currentTargets = this.getTargetList();
this. registerTargetHook();
if(this.targetSelection === true && this.hasRoll) {
this.targetShort = this.targets.reduce((a,c) => {
if(c.hit) a.hit += 1;
else a.miss += 1;
return a;
}, {hit: 0, miss: 0})
if (this.targetMode === true && this.hasRoll) {
this.targetShort = this.targets.reduce(
(a, c) => {
if (c.hit) a.hit += 1;
else a.miss += 1;
return a;
},
{ hit: 0, miss: 0 }
);
}
if (this.hasSave) this.setPendingSaves();
}
@ -123,13 +129,16 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
}
getTargetList() {
const targets = this.targetSelection && this.parent.isAuthor ? this.targets : (this.parent.getFlag(game.system.id, "targets") ?? this.targets),
reactionRolls = this.parent.getFlag(game.system.id, "reactionRolls");
const targets =
this.targetMode && this.parent.isAuthor
? this.targets
: (this.parent.getFlag(game.system.id, 'targets') ?? this.targets),
reactionRolls = this.parent.getFlag(game.system.id, 'reactionRolls');
if(reactionRolls) {
if (reactionRolls) {
Object.entries(reactionRolls).forEach(([k, r]) => {
const target = targets.find(t => t.id === k);
if(target) target.saved = r;
if (target) target.saved = r;
});
}
@ -137,7 +146,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
}
setPendingSaves() {
this.pendingSaves = this.targetSelection
this.pendingSaves = this.targetMode
? this.targets.filter(target => target.hit && target.saved.success === null).length > 0
: this.currentTargets.filter(target => target.saved.success === null).length > 0;
}

View file

@ -12,7 +12,10 @@ export default class CostField extends fields.ArrayField {
value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }),
scalable: new fields.BooleanField({ initial: false }),
step: new fields.NumberField({ nullable: true, initial: null }),
consumeOnSuccess: new fields.BooleanField({ initial: false, label: "DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.label" })
consumeOnSuccess: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.label'
})
});
super(element, options, context);
}
@ -72,7 +75,8 @@ export default class CostField extends fields.ArrayField {
static getResources(costs) {
const actorResources = foundry.utils.deepClone(this.actor.system.resources);
if(this.actor.system.partner) actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope);
if (this.actor.system.partner)
actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope);
const itemResources = {};
for (let itemResource of costs) {
if (itemResource.keyIsID) {
@ -92,9 +96,9 @@ export default class CostField extends fields.ArrayField {
static getRealCosts(costs) {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
let mergedCosts = [];
realCosts.forEach(c => {
realCosts.forEach(c => {
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);
if(getCost) getCost.total += c.total;
if (getCost) getCost.total += c.total;
else mergedCosts.push(c);
});
return mergedCosts;

View file

@ -229,7 +229,7 @@ export function ActionMixin(Base) {
}
return this.inCollection
? foundry.utils.getProperty(result, basePath).get(this.id)
? foundry.utils.getProperty(result, basePath)?.get(this.id)
: foundry.utils.getProperty(result, basePath);
}

View file

@ -141,14 +141,12 @@ export default class D20Roll extends DHRoll {
data.type = config.roll?.type;
data.difficulty = config.roll.difficulty;
if (config.targets?.length) {
config.targetSelection = true;
config.targets.forEach(target => {
const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion;
target.hit = roll.isCritical || roll.total >= difficulty;
});
data.success = config.targets.some(target => target.hit)
} else if (config.roll.difficulty)
data.success = roll.isCritical || roll.total >= config.roll.difficulty;
data.success = config.targets.some(target => target.hit);
} else if (config.roll.difficulty) data.success = roll.isCritical || roll.total >= config.roll.difficulty;
data.advantage = {
type: config.roll.advantage,

View file

@ -15,7 +15,6 @@ export default class DamageRoll extends DHRoll {
const parts = config.roll.map(r => this.postEvaluate(r));
config.damage = this.unifyDamageRoll(parts);
// config.targetSelection = config.targets?.length
}
static postEvaluate(roll, config = {}) {
@ -30,7 +29,9 @@ export default class DamageRoll extends DHRoll {
}
static async buildPost(roll, config, message) {
const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode);
const chatMessage = config.source?.message
? ui.chat.collection.get(config.source.message)
: getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode);
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))
@ -102,14 +103,14 @@ export default class DamageRoll extends DHRoll {
}
constructFormula(config) {
this.options.roll.forEach(part => {
this.options.roll.forEach((part, index) => {
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
this.constructFormulaPart(config, part);
this.constructFormulaPart(config, part, index);
});
return this.options.roll;
}
constructFormulaPart(config, part) {
constructFormulaPart(config, part, index) {
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
@ -120,6 +121,14 @@ export default class DamageRoll extends DHRoll {
});
}
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
if (part.extraFormula) {
part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
@ -132,6 +141,189 @@ export default class DamageRoll extends DHRoll {
criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll);
part.roll.terms.push(...this.formatModifier(criticalBonus));
}
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
return (part.roll._formula = this.constructor.getFormula(part.roll.terms));
}
/* To Remove When Reaction System */
static temporaryModifierBuilder(config) {
const mods = {};
if (config.data?.parent) {
if (config.data.parent.appliedEffects) {
// Bardic Rally
mods.rally = {
label: 'DAGGERHEART.CLASS.Feature.rallyDice',
values: config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value });
return a;
}, []),
value: null,
beforeCrit: true,
callback: part => {
const rallyFaces = config.modifiers.rally.values.find(
r => r.value === config.modifiers.rally.value
)?.label;
part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.parse(`1${rallyFaces}`)
);
}
};
}
const item = config.data.parent.items?.get(config.source.item);
if (item) {
// Massive (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'massive'))
mods.massive = {
label: CONFIG.DH.ITEM.weaponFeatures.massive.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`);
part.roll.terms[0].number += 1;
}
};
// Powerful (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'powerful'))
mods.powerful = {
label: CONFIG.DH.ITEM.weaponFeatures.powerful.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`);
part.roll.terms[0].number += 1;
}
};
// Brutal (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'brutal'))
mods.brutal = {
label: CONFIG.DH.ITEM.weaponFeatures.brutal.label,
enabled: true,
beforeCrit: true,
callback: part => {
part.roll.terms[0].modifiers.push(`x${part.roll.terms[0].faces}`);
}
};
// Serrated (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'serrated'))
mods.serrated = {
label: CONFIG.DH.ITEM.weaponFeatures.serrated.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`sc8`);
}
};
// Self-Correcting (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'selfCorrecting'))
mods.selfCorrecting = {
label: CONFIG.DH.ITEM.weaponFeatures.selfCorrecting.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`sc6`);
}
};
}
}
config.modifiers = mods;
return mods;
}
static async reroll(target, message) {
const { damageType, part, dice, result } = target.dataset;
const rollPart = message.system.damage[damageType].parts[part];
let diceIndex = 0;
let parsedRoll = game.system.api.dice.DamageRoll.fromData({
...rollPart.roll,
terms: rollPart.roll.terms.map(term => {
const isDie = term.class === 'Die';
const fixedTerm = {
...term,
...(isDie ? { results: rollPart.dice[diceIndex].results } : {})
};
if (isDie) diceIndex++;
return fixedTerm;
}),
class: 'DamageRoll',
evaluated: false
});
const parsedDiceTerms = Object.keys(parsedRoll.terms).reduce((acc, key) => {
const term = parsedRoll.terms[key];
if (term instanceof CONFIG.Dice.termTypes.DiceTerm) acc[Object.keys(acc).length] = term;
return acc;
}, {});
const term = parsedDiceTerms[dice];
const termResult = parsedDiceTerms[dice].results[result];
const newIndex = parsedDiceTerms[dice].results.length;
await term.reroll(`/r1=${termResult.result}`);
if (game.modules.get('dice-so-nice')?.active) {
const newResult = parsedDiceTerms[dice].results[newIndex];
const diceSoNiceRoll = {
_evaluated: true,
dice: [
new foundry.dice.terms.Die({
...term,
total: newResult.result,
faces: term._faces,
results: [newResult]
})
],
options: { appearance: {} }
};
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
}
await parsedRoll.evaluate();
const results = parsedRoll.dice[dice].results.map(result => ({
...result,
discarded: !result.active
}));
const newResult = results.splice(results.length - 1, 1);
results.splice(Number(result) + 1, 0, newResult[0]);
const rerolledDice = parsedRoll.dice.map((x, index) => {
const isRerollDice = index === Number(dice);
if (!isRerollDice) return { ...x, dice: x.denomination };
return {
dice: parsedRoll.dice[dice].denomination,
total: parsedRoll.dice[dice].total,
results: results.map(result => ({
...result,
hasRerolls: result.hasRerolls || isRerollDice
}))
};
});
const updateMessage = game.messages.get(message._id);
await updateMessage.update({
[`system.damage.${damageType}`]: {
...updateMessage,
total: parsedRoll.total,
[`parts.${part}`]: {
...rollPart,
total: parsedRoll.total,
dice: rerolledDice
}
}
});
}
}

View file

@ -8,9 +8,7 @@ export default class DHRoll extends Roll {
}
get title() {
return game.i18n.localize(
"DAGGERHEART.GENERAL.Roll.basic"
);
return game.i18n.localize('DAGGERHEART.GENERAL.Roll.basic');
}
static messageType = 'adversaryRoll';
@ -36,6 +34,8 @@ export default class DHRoll extends Roll {
this.applyKeybindings(config);
this.temporaryModifierBuilder(config);
let roll = new this(config.roll.formula, config.data, config);
if (config.dialog.configure !== false) {
// Open Roll Dialog
@ -66,8 +66,7 @@ export default class DHRoll extends Roll {
}
// Create Chat Message
if (!config.source?.message)
config.message = await this.toMessage(roll, config);
if (!config.source?.message) config.message = await this.toMessage(roll, config);
}
static postEvaluate(roll, config = {}) {
@ -95,30 +94,30 @@ export default class DHRoll extends Roll {
rolls: [roll]
};
config.selectedRollMode ??= game.settings.get('core', 'rollMode');
if(roll._evaluated) return await cls.create(msg, { rollMode: config.selectedRollMode });
if (roll._evaluated) return await cls.create(msg, { rollMode: config.selectedRollMode });
return msg;
}
/** @inheritDoc */
async render({flavor, template=this.constructor.CHAT_TEMPLATE, isPrivate=false, ...options}={}) {
if ( !this._evaluated ) return;
const chatData = await this._prepareChatRenderContext({flavor, isPrivate, ...options});
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) {
if (!this._evaluated) return;
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, chatData);
}
/** @inheritDoc */
async _prepareChatRenderContext({flavor, isPrivate=false, ...options}={}) {
if(isPrivate) {
async _prepareChatRenderContext({ flavor, isPrivate = false, ...options } = {}) {
if (isPrivate) {
return {
user: game.user.id,
flavor: null,
title: "???",
title: '???',
roll: {
total: "??"
total: '??'
},
hasRoll: true,
isPrivate
}
};
} else {
options.message.system.user = game.user.id;
return options.message.system;
@ -207,6 +206,10 @@ export default class DHRoll extends Roll {
}
return modifierTotal;
}
static temporaryModifierBuilder(config) {
return {};
}
}
export const registerRollDiceHooks = () => {

View file

@ -149,7 +149,7 @@ export default class DualityRoll extends D20Roll {
}
if (this.rallyFaces)
this.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
new foundry.dice.terms.Die({ faces: this.rallyFaces })
);
}

View file

@ -644,16 +644,23 @@ export default class DhpActor extends Actor {
);
break;
case 'armor':
updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0
);
if (this.system.armor?.system?.marks) {
updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0
);
}
break;
default:
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max(
Math.min(this.system.resources[r.key].value + r.value, this.system.resources[r.key].max),
0
);
if (this.system.resources?.[r.key]) {
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max(
Math.min(
this.system.resources[r.key].value + r.value,
this.system.resources[r.key].max
),
0
);
}
break;
}
}

View file

@ -1,10 +1,16 @@
export default class DhpChatMessage extends foundry.documents.ChatMessage {
targetHook = null;
targetSelection = null;
async renderHTML() {
const actor = game.actors.get(this.speaker.actor);
const actorData = actor && this.isContentVisible ? actor : {
img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg',
name: ''
};
const actorData =
actor && this.isContentVisible
? actor
: {
img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg',
name: ''
};
/* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */
const html = await super.renderHTML({ actor: actorData, author: this.author });
@ -14,6 +20,30 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
return html;
}
/* -------------------------------------------- */
/** @inheritDoc */
prepareData() {
if (this.isAuthor && this.targetSelection === null) this.targetSelection = this.system.targets?.length > 0;
super.prepareData();
}
/* -------------------------------------------- */
/** @inheritDoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
if (this.system.registerTargetHook) this.system.registerTargetHook();
}
/* -------------------------------------------- */
/** @inheritDoc */
async _preDelete(options, user) {
if (this.targetHook !== null) Hooks.off('targetToken', this.targetHook);
return super._preDelete(options, user);
}
enrichChatMessage(html) {
const elements = html.querySelectorAll('[data-perm-id]');
elements.forEach(e => {
@ -62,7 +92,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
}
getTargetList() {
const targets = this.system.hitTargets;
const targets = this.system.hitTargets ?? [];
return targets.map(target => game.canvas.tokens.documentCollection.find(t => t.actor?.uuid === target.actorId));
}
@ -134,7 +164,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
}
consumeOnSuccess() {
if (!this.system.successConsumed && !this.system.targetSelection) {
if (!this.system.successConsumed && !this.targetSelection) {
const action = this.system.action;
if (action) action.consume(this.system, true);
}
@ -143,12 +173,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
hoverTarget(event) {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (!token?.controlled) token._onHoverIn(event, { hoverOutOthers: true });
if (token && !token?.controlled) token._onHoverIn(event, { hoverOutOthers: true });
}
unhoverTarget(event) {
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (!token?.controlled) token._onHoverOut(event);
if (token && !token?.controlled) token._onHoverOut(event);
}
clickTarget(event) {
@ -163,6 +193,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
onTargetSelection(event) {
event.stopPropagation();
this.system.targetMode = Boolean(event.target.dataset.targetHit);
if (!event.target.classList.contains('target-selected'))
this.system.targetMode = Boolean(event.target.dataset.targetHit);
}
}

View file

@ -172,6 +172,26 @@ Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false
return nativeReplaceFormulaData(formula, data, { missing, warn });
};
foundry.dice.terms.Die.MODIFIERS.sc = 'selfCorrecting';
/**
* Return the configured value as result if 1 is rolled
* Example: 6d6sc6 Roll 6d6, each result of 1 will be changed into 6
* @param {string} modifier The matched modifier query
*/
foundry.dice.terms.Die.prototype.selfCorrecting = function (modifier) {
const rgx = /(?:sc)([0-9]+)/i;
const match = modifier.match(rgx);
if (!match) return false;
let [target] = match.slice(1);
target = parseInt(target);
for (const r of this.results) {
if (r.result === 1) {
r.result = target;
}
}
};
export const getDamageKey = damage => {
return ['none', 'minor', 'major', 'severe'][damage];
};

View file

@ -43,7 +43,7 @@
"parts": [
{
"value": {
"dice": "d8",
"dice": "d10",
"bonus": 9,
"multiplier": "prof",
"flatMultiplier": 1,

View file

@ -12,15 +12,7 @@
"equipped": false,
"secondary": false,
"burden": "twoHanded",
"weaponFeatures": [
{
"value": "cumbersome",
"effectIds": [
"hl0S2LrBY5Mg69q6"
],
"actionIds": []
}
],
"weaponFeatures": [],
"attack": {
"name": "Attack",
"img": "icons/skills/melee/blood-slash-foam-red.webp",
@ -51,8 +43,8 @@
"parts": [
{
"value": {
"dice": "d10",
"bonus": 8,
"dice": "d8",
"bonus": 9,
"multiplier": "prof",
"flatMultiplier": 1,
"custom": {

View file

@ -43,7 +43,7 @@
"parts": [
{
"value": {
"dice": "d8",
"dice": "d10",
"bonus": 6,
"multiplier": "prof",
"flatMultiplier": 1,

View file

@ -12,15 +12,7 @@
"equipped": false,
"secondary": false,
"burden": "twoHanded",
"weaponFeatures": [
{
"value": "cumbersome",
"effectIds": [
"8twXPJELZpvFWA5K"
],
"actionIds": []
}
],
"weaponFeatures": [],
"attack": {
"name": "Attack",
"img": "icons/skills/melee/blood-slash-foam-red.webp",
@ -51,8 +43,8 @@
"parts": [
{
"value": {
"dice": "d10",
"bonus": 5,
"dice": "d8",
"bonus": 6,
"multiplier": "prof",
"flatMultiplier": 1,
"custom": {

View file

@ -43,7 +43,7 @@
"parts": [
{
"value": {
"dice": "d8",
"dice": "d10",
"bonus": 12,
"multiplier": "prof",
"flatMultiplier": 1,

View file

@ -12,15 +12,7 @@
"equipped": false,
"secondary": false,
"burden": "twoHanded",
"weaponFeatures": [
{
"value": "cumbersome",
"effectIds": [
"f44KWDgCQeKYfccr"
],
"actionIds": []
}
],
"weaponFeatures": [],
"attack": {
"name": "Attack",
"img": "icons/skills/melee/blood-slash-foam-red.webp",
@ -51,8 +43,8 @@
"parts": [
{
"value": {
"dice": "d10",
"bonus": 11,
"dice": "d8",
"bonus": 12,
"multiplier": "prof",
"flatMultiplier": 1,
"custom": {

View file

@ -43,7 +43,7 @@
"parts": [
{
"value": {
"dice": "d8",
"dice": "d10",
"bonus": 3,
"multiplier": "prof",
"flatMultiplier": 1,

View file

@ -12,15 +12,7 @@
"equipped": false,
"secondary": false,
"burden": "twoHanded",
"weaponFeatures": [
{
"value": "cumbersome",
"effectIds": [
"Z5MnVI8EOOgzRdXC"
],
"actionIds": []
}
],
"weaponFeatures": [],
"attack": {
"name": "Attack",
"img": "icons/skills/melee/blood-slash-foam-red.webp",
@ -51,8 +43,8 @@
"parts": [
{
"value": {
"dice": "d10",
"bonus": 2,
"dice": "d8",
"bonus": 3,
"multiplier": "prof",
"flatMultiplier": 1,
"custom": {

View file

@ -27,3 +27,5 @@
@import './damage-reduction/sheets.less';
@import './multiclass-choice/sheet.less';
@import './reroll-dialog/sheet.less';

View file

@ -0,0 +1,125 @@
.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: 22px;
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: 12px;
}
}
i {
font-size: 10px;
}
}
}
}
}
}
footer {
margin-top: 8px;
display: flex;
justify-content: space-between;
.controls {
display: flex;
gap: 8px;
}
}
}

View file

@ -53,7 +53,7 @@
font-weight: 500;
font-size: 14px;
line-height: 17px;
white-space: nowrap;
color: light-dark(@dark, @beige);
}

View file

@ -571,6 +571,16 @@
white-space: nowrap;
}
}
.button-container {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
text-align: center;
display: flex;
justify-content: center;
width: 100%;
}
}
footer {

View file

@ -35,7 +35,8 @@
border-color: transparent;
}
[data-view-perm='false'] {
&[data-perm-hidden='true'], > * {
&[data-perm-hidden='true'],
> * {
display: none;
}
&::after {
@ -161,7 +162,7 @@
color: var(--text-color);
font-weight: 700;
font-family: 'Cinzel', sans-serif;
line-height: .75;
line-height: 0.75;
.roll-result-value {
font-size: var(--font-size-24);
@ -191,10 +192,14 @@
.roll-die {
display: grid;
grid-template-areas:
". a a"
"c b b";
'. a a'
'c b b';
gap: 3px;
.reroll-button:hover {
filter: drop-shadow(0 0 3px @golden);
}
label {
text-align: center;
height: var(--font-size-12);
@ -272,7 +277,7 @@
border-radius: 3px;
&:hover {
background-color: rgba(255,255,255,.1);
background-color: rgba(255, 255, 255, 0.1);
}
.target-img {

View file

@ -227,14 +227,16 @@
display: flex;
> * {
flex: 1;
flex: 2.5;
text-align: center;
}
.item-list-img {
width: 40px;
flex: unset;
}
.item-list-name {
flex-grow: 3 !important;
flex-grow: 3;
text-align: start;
}
}

View file

@ -24,6 +24,22 @@
<input type="text" value="{{extraFormula}}" name="roll.{{ @index }}.extraFormula" placeholder="Situational Bonus">
</div>
{{/each}}
{{#if @root.modifiers}}
<fieldset class="modifier-container two-columns">
<legend>{{localize "DAGGERHEART.GENERAL.Modifier.plural"}}</legend>
{{#each @root.modifiers}}
<span class="formula-label">{{ localize label }}</span>
{{#if (hasProperty this "values")}}
<select name="modifiers.{{@key}}.value">
{{selectOptions values blank="" selected=value}}
</select>
{{/if}}
{{#if (hasProperty this "enabled")}}
<input type="checkbox" name="modifiers.{{@key}}.enabled" {{ checked enabled }}>
{{/if}}
{{/each}}
</fieldset>
{{/if}}
<div class="damage-section-controls">
{{#if directDamage}}
<select class="roll-mode-select" name="selectedRollMode">

View file

@ -0,0 +1,35 @@
<div class="reroll-outer-container">
{{#each damage}}
<h2>{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}</h2>
{{#each this}}
<div class="dices-container">
{{#each this}}
<fieldset class="dice-outer-container">
<legend>
<input class="to-reroll-input" type="checkbox" data-type="{{@../../key}}" data-part="{{@../key}}" data-dice="{{@key}}" {{checked this.toReroll}} />
<i class="fa-solid fa-dice"></i>
<span>{{this.selectedResults}}/{{this.maxSelected}} Selected</span>
</legend>
<div class="dice-container">
{{#each this.results}}
<div
class="result-container {{../dice}} {{#if this.active}}selected{{/if}}"
data-action="selectRoll" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}"
>
{{this.result}}
{{#if this.active}}
<a class="to-reroll-result" data-action="toggleResult" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}">
<input class="to-reroll-result-input" type="checkbox" {{checked this.toReroll}} />
<i class="fa-solid fa-dice"></i>
</a>
{{/if}}
</div>
{{/each}}
</div>
</fieldset>
{{/each}}
</div>
{{/each}}
{{/each}}
</div>

View file

@ -0,0 +1,4 @@
<footer>
<button type="button" data-action="doReroll" {{disabled disabledReroll}}>{{localize "Reroll"}} <i class="fa-solid fa-dice"></i></button>
<button type="button" data-action="save" {{disabled saveDisabled}}>{{localize "DAGGERHEART.APPLICATIONS.RerollDialog.acceptCurrentRolls"}}</button>
</footer>

View file

@ -0,0 +1,35 @@
<div class="reroll-outer-container">
{{#each damage}}
<h2>{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}</h2>
{{#each this}}
<div class="dices-container">
{{#each this}}
<fieldset class="dice-outer-container">
<legend>
<input class="to-reroll-input" type="checkbox" data-type="{{@../../key}}" data-part="{{@../key}}" data-dice="{{@key}}" {{checked this.toReroll}} />
<i class="fa-solid fa-dice"></i>
<span>{{this.selectedResults}}/{{this.results.length}} Selected</span>
</legend>
<div class="dice-container">
{{#each this.results}}
<div
class="result-container {{../dice}} {{#if this.active}}selected{{/if}}"
data-action="selectRoll" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}"
>
{{this.result}}
{{#if this.active}}
<a class="to-reroll-result" data-action="toggleResult" data-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}">
<input class="to-reroll-result-input" type="checkbox" {{checked this.toReroll}} />
<i class="fa-solid fa-dice"></i>
</a>
{{/if}}
</div>
{{/each}}
</div>
</fieldset>
{{/each}}
</div>
{{/each}}
{{/each}}
</div>

View file

@ -69,7 +69,13 @@
{{selectOptions diceSoNiceMaterials selected=diceTab.source.material valueAttr="key" labelAttr="name" localize=true}}
</select>
</div>
</div>
<div class="button-container">
<button data-action="preview">
<i class="fa-solid fa-dice"></i>
<span>{{localize "Preview"}}</span>
</button>
</div>
</div>
</div>
</fieldset>
{{/if}}

View file

@ -11,7 +11,6 @@
{{formGroup settingFields.schema.fields.rangeMeasurement.fields.veryClose value=settingFields._source.rangeMeasurement.veryClose localize=true}}
{{formGroup settingFields.schema.fields.rangeMeasurement.fields.close value=settingFields._source.rangeMeasurement.close localize=true}}
{{formGroup settingFields.schema.fields.rangeMeasurement.fields.far value=settingFields._source.rangeMeasurement.far localize=true}}
{{formGroup settingFields.schema.fields.rangeMeasurement.fields.veryFar value=settingFields._source.rangeMeasurement.veryFar localize=true}}
</fieldset>
<div class="form-group">

View file

@ -29,7 +29,13 @@
{{#each results}}
{{#unless discarded}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
<div class="dice {{../dice}}">{{result}}</div>
<div
class="dice reroll-button {{../dice}}"
data-die-index="0" data-type="damage" data-damage-type="{{@../../../key}}" data-part="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}"
>
{{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "Rerolled"}}"></i>{{/if}}
{{result}}
</div>
</div>
{{/unless}}
{{/each}}

View file

@ -57,7 +57,7 @@
{{/if}}
{{#if roll.rally.dice}}
<div class="roll-die has-plus">
<label>{{localize "DAGGERHEART.GENERAL.fear"}}</label>
<label>{{localize "DAGGERHEART.CLASS.Feature.short"}}</label>
<div class="dice {{roll.rally.dice}}">{{roll.rally.value}}</div>
</div>
{{/if}}

View file

@ -18,8 +18,8 @@
<div class="target-selector">
<div class="roll-part-header"><div></div></div>
<div class="target-choice">
<div class="button-target-selection{{#if targetSelection}} target-selected{{/if}}" data-target-hit="true">{{localize "DAGGERHEART.UI.Chat.damageRoll.hitTarget"}}</div>
<div class="button-target-selection{{#unless targetSelection}} target-selected{{/unless}}">{{localize "DAGGERHEART.UI.Chat.damageRoll.currentTarget"}}</div>
<div class="button-target-selection{{#if targetMode}} target-selected{{/if}}" data-target-hit="true">{{localize "DAGGERHEART.UI.Chat.damageRoll.hitTarget"}}</div>
<div class="button-target-selection{{#unless targetMode}} target-selected{{/unless}}">{{localize "DAGGERHEART.UI.Chat.damageRoll.currentTarget"}}</div>
</div>
<div class="roll-part-header"><div></div></div>
</div>
@ -30,7 +30,7 @@
<img class="target-img" src="{{img}}">
<div class="target-data">
<div class="target-name" data-perm-id="{{actorId}}"><span>{{name}}</span></div>
{{#if (and ../targetSelection ../hasRoll)}}
{{#if (and ../targetMode ../hasRoll)}}
<div class="target-hit-status {{#if hit}}is-hit{{else}}is-miss{{/if}}">
{{#if hit}}
{{localize "DAGGERHEART.GENERAL.hit.single"}}