[Feature] Damage-Reroll (#753)

* Added rerolls for damage dice in chat

* Fixed multiple dice

* Added reroll icon

* Fixed new style of dialog
This commit is contained in:
WBHarry 2025-08-10 01:32:12 +02:00 committed by GitHub
parent 2aaab73699
commit 300719c116
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1094 additions and 167 deletions

View file

@ -10,11 +10,11 @@
## Overview ## Overview
This is the community repo for the Foundry VTT system *Foundryborne* 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 ## User Install
1. **recommended** Searching for *Daggerheart* or *Foundryborne* in the System Instalaltion dialgoe of the FoundryVTT admin settings. 1. **recommended** Searching for _Daggerheart_ or _Foundryborne_ in the System Instalaltion dialgoe of the FoundryVTT admin settings.
2. Pasting `https://raw.githubusercontent.com/Foundryborne/daggerheart/refs/heads/main/system.json` into the Install System dialog on the Setup menu of the application. 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. 3. Downloading one of the .zip archives from the Releases page and extracting it into your foundry Data folder, under Data/systems/daggerheart.

View file

@ -498,6 +498,11 @@
"ReactionRoll": { "ReactionRoll": {
"title": "Reaction Roll: {trait}" "title": "Reaction Roll: {trait}"
}, },
"RerollDialog": {
"title": "Reroll",
"deselectDiceNotification": "Deselect one of the selected dice first",
"acceptCurrentRolls": "Accept Current Rolls"
},
"ResourceDice": { "ResourceDice": {
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"

View file

@ -6,5 +6,6 @@ export { default as DeathMove } from './deathMove.mjs';
export { default as Downtime } from './downtime.mjs'; export { default as Downtime } from './downtime.mjs';
export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs';
export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';

View file

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

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

View file

@ -344,7 +344,7 @@ export default function DHApplicationMixin(Base) {
callback: async (target, event) => { callback: async (target, event) => {
const doc = await getDocFromElement(target), const doc = await getDocFromElement(target),
action = doc?.system?.attack ?? doc; action = doc?.system?.attack ?? doc;
return action && action.use(event, { byPassRoll: true }) return action && action.use(event, { byPassRoll: true });
} }
}); });

View file

@ -20,6 +20,40 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
classes: ['daggerheart'] 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) => { addChatListeners = async (app, html, data) => {
html.querySelectorAll('.duality-action-damage').forEach(element => html.querySelectorAll('.duality-action-damage').forEach(element =>
element.addEventListener('click', event => this.onRollDamage(event, data.message)) 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]'); 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({ const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
'system.roll': newRoll,
'rolls': [parsedRoll] await game.messages.get(message._id).update({
}); 'system.roll': newRoll,
'rolls': [parsedRoll]
});
}
} }
} }

View file

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

View file

@ -224,11 +224,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
.filter( .filter(
c => c =>
(!successCost && (!c.consumeOnSuccess || config.roll?.success)) || (!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
(successCost && c.consumeOnSuccess) (successCost && c.consumeOnSuccess)
) )
.reduce((a, c) => { .reduce((a, c) => {
const resource = usefulResources[c.key]; const resource = usefulResources[c.key];
if( resource ) { if (resource) {
a.push({ a.push({
key: c.key, key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
@ -247,9 +247,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
) )
this.update({ 'uses.value': this.uses.value + 1 }); this.update({ 'uses.value': this.uses.value + 1 });
if(config.roll?.success || successCost) { if (config.roll?.success || successCost) {
setTimeout(() => { setTimeout(() => {
(config.message ?? config.parent).update({'system.successConsumed': true}) (config.message ?? config.parent).update({ 'system.successConsumed': true });
}, 50); }, 50);
} }
} }
@ -371,11 +371,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
async updateChatMessage(message, targetId, changes, chain = true) { async updateChatMessage(message, targetId, changes, chain = true) {
setTimeout(async () => { setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id); const chatMessage = ui.chat.collection.get(message._id);
await chatMessage.update({ await chatMessage.update({
flags: { flags: {
[game.system.id]: { [game.system.id]: {
"reactionRolls": { reactionRolls: {
[targetId]: changes [targetId]: changes
} }
} }

View file

@ -18,7 +18,6 @@ const targetsField = () =>
); );
export default class DHActorRoll extends foundry.abstract.TypeDataModel { export default class DHActorRoll extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
return { return {
title: new fields.StringField(), title: new fields.StringField(),
@ -65,7 +64,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
set targetMode(mode) { set targetMode(mode) {
if(!this.parent.isAuthor) return; if (!this.parent.isAuthor) return;
this.parent.targetSelection = mode; this.parent.targetSelection = mode;
this.registerTargetHook(); this.registerTargetHook();
this.updateTargets(); this.updateTargets();
@ -76,13 +75,14 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
async updateTargets() { async updateTargets() {
if(!ui.chat.collection.get(this.parent.id)) return; if (!ui.chat.collection.get(this.parent.id)) return;
let targets; let targets;
if(this.targetMode) if (this.targetMode) targets = this.targets;
targets = this.targets;
else 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)
);
await this.parent.update({ await this.parent.update({
flags: { flags: {
[game.system.id]: { [game.system.id]: {
@ -90,16 +90,19 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
targetMode: this.targetMode targetMode: this.targetMode
} }
} }
}) });
} }
registerTargetHook() { registerTargetHook() {
if(!this.parent.isAuthor) return; if (!this.parent.isAuthor) return;
if(this.targetMode && this.parent.targetHook !== null) { if (this.targetMode && this.parent.targetHook !== null) {
Hooks.off("targetToken", this.parent.targetHook); Hooks.off('targetToken', this.parent.targetHook);
return this.parent.targetHook = null; return (this.parent.targetHook = null);
} else if (!this.targetMode && 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)); return (this.parent.targetHook = Hooks.on(
'targetToken',
foundry.utils.debounce(this.updateTargets.bind(this), 50)
));
} }
} }
@ -107,13 +110,16 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
if (this.hasTarget) { if (this.hasTarget) {
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0; this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
this.currentTargets = this.getTargetList(); this.currentTargets = this.getTargetList();
if(this.targetMode === true && this.hasRoll) { if (this.targetMode === true && this.hasRoll) {
this.targetShort = this.targets.reduce((a,c) => { this.targetShort = this.targets.reduce(
if(c.hit) a.hit += 1; (a, c) => {
else a.miss += 1; if (c.hit) a.hit += 1;
return a; else a.miss += 1;
}, {hit: 0, miss: 0}) return a;
},
{ hit: 0, miss: 0 }
);
} }
if (this.hasSave) this.setPendingSaves(); if (this.hasSave) this.setPendingSaves();
} }
@ -123,13 +129,16 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
getTargetList() { getTargetList() {
const targets = this.targetMode && this.parent.isAuthor ? this.targets : (this.parent.getFlag(game.system.id, "targets") ?? this.targets), const targets =
reactionRolls = this.parent.getFlag(game.system.id, "reactionRolls"); 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]) => { Object.entries(reactionRolls).forEach(([k, r]) => {
const target = targets.find(t => t.id === k); const target = targets.find(t => t.id === k);
if(target) target.saved = r; if (target) target.saved = r;
}); });
} }

View file

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

View file

@ -227,7 +227,7 @@ export function ActionMixin(Base) {
} else { } else {
result = await this.item.update({ [path]: updates }, options); result = await this.item.update({ [path]: updates }, options);
} }
return this.inCollection return this.inCollection
? foundry.utils.getProperty(result, basePath)?.get(this.id) ? foundry.utils.getProperty(result, basePath)?.get(this.id)
: foundry.utils.getProperty(result, basePath); : foundry.utils.getProperty(result, basePath);

View file

@ -145,10 +145,9 @@ export default class D20Roll extends DHRoll {
const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion; const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion;
target.hit = roll.isCritical || roll.total >= difficulty; target.hit = roll.isCritical || roll.total >= difficulty;
}); });
data.success = config.targets.some(target => target.hit) data.success = config.targets.some(target => target.hit);
} else if (config.roll.difficulty) } else if (config.roll.difficulty) data.success = roll.isCritical || roll.total >= config.roll.difficulty;
data.success = roll.isCritical || roll.total >= config.roll.difficulty;
data.advantage = { data.advantage = {
type: config.roll.advantage, type: config.roll.advantage,
dice: roll.dAdvantage?.denomination, dice: roll.dAdvantage?.denomination,

View file

@ -29,7 +29,9 @@ export default class DamageRoll extends DHRoll {
} }
static async buildPost(roll, config, message) { 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) { if (game.modules.get('dice-so-nice')?.active) {
const pool = foundry.dice.terms.PoolTerm.fromRolls( const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
@ -120,11 +122,10 @@ export default class DamageRoll extends DHRoll {
} }
/* To Remove When Reaction System */ /* To Remove When Reaction System */
if(index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for(const mod in config.modifiers) { for (const mod in config.modifiers) {
const modifier = config.modifiers[mod]; const modifier = config.modifiers[mod];
if(modifier.beforeCrit === true && (modifier.enabled || modifier.value)) if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
modifier.callback(part);
} }
} }
@ -142,11 +143,10 @@ export default class DamageRoll extends DHRoll {
} }
/* To Remove When Reaction System */ /* To Remove When Reaction System */
if(index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for(const mod in config.modifiers) { for (const mod in config.modifiers) {
const modifier = config.modifiers[mod]; const modifier = config.modifiers[mod];
if(!modifier.beforeCrit && (modifier.enabled || modifier.value)) if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
modifier.callback(part);
} }
} }
@ -156,11 +156,11 @@ export default class DamageRoll extends DHRoll {
/* To Remove When Reaction System */ /* To Remove When Reaction System */
static temporaryModifierBuilder(config) { static temporaryModifierBuilder(config) {
const mods = {}; const mods = {};
if(config.data?.parent) { if (config.data?.parent) {
if(config.data.parent.appliedEffects) { if (config.data.parent.appliedEffects) {
// Bardic Rally // Bardic Rally
mods.rally = { mods.rally = {
label: "DAGGERHEART.CLASS.Feature.rallyDice", label: 'DAGGERHEART.CLASS.Feature.rallyDice',
values: config.data?.parent?.appliedEffects.reduce((a, c) => { values: config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value }); if (change) a.push({ value: c.id, label: change.value });
@ -168,8 +168,10 @@ export default class DamageRoll extends DHRoll {
}, []), }, []),
value: null, value: null,
beforeCrit: true, beforeCrit: true,
callback: (part) => { callback: part => {
const rallyFaces = config.modifiers.rally.values.find(r => r.value === config.modifiers.rally.value)?.label; const rallyFaces = config.modifiers.rally.values.find(
r => r.value === config.modifiers.rally.value
)?.label;
part.roll.terms.push( part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }), new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.parse(`1${rallyFaces}`) ...this.parse(`1${rallyFaces}`)
@ -177,58 +179,58 @@ export default class DamageRoll extends DHRoll {
} }
}; };
} }
const item = config.data.parent.items?.get(config.source.item); const item = config.data.parent.items?.get(config.source.item);
if(item) { if (item) {
// Massive (Weapon Feature) // Massive (Weapon Feature)
if(item.system.itemFeatures.find(f => f.value === "massive")) if (item.system.itemFeatures.find(f => f.value === 'massive'))
mods.massive = { mods.massive = {
label: CONFIG.DH.ITEM.weaponFeatures.massive.label, label: CONFIG.DH.ITEM.weaponFeatures.massive.label,
enabled: true, enabled: true,
callback: (part) => { callback: part => {
part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`); part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`);
part.roll.terms[0].number += 1; part.roll.terms[0].number += 1;
} }
}; };
// Powerful (Weapon Feature) // Powerful (Weapon Feature)
if(item.system.itemFeatures.find(f => f.value === "powerful")) if (item.system.itemFeatures.find(f => f.value === 'powerful'))
mods.powerful = { mods.powerful = {
label: CONFIG.DH.ITEM.weaponFeatures.powerful.label, label: CONFIG.DH.ITEM.weaponFeatures.powerful.label,
enabled: true, enabled: true,
callback: (part) => { callback: part => {
part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`); part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`);
part.roll.terms[0].number += 1; part.roll.terms[0].number += 1;
} }
}; };
// Brutal (Weapon Feature) // Brutal (Weapon Feature)
if(item.system.itemFeatures.find(f => f.value === "brutal")) if (item.system.itemFeatures.find(f => f.value === 'brutal'))
mods.brutal = { mods.brutal = {
label: CONFIG.DH.ITEM.weaponFeatures.brutal.label, label: CONFIG.DH.ITEM.weaponFeatures.brutal.label,
enabled: true, enabled: true,
beforeCrit: true, beforeCrit: true,
callback: (part) => { callback: part => {
part.roll.terms[0].modifiers.push(`x${part.roll.terms[0].faces}`); part.roll.terms[0].modifiers.push(`x${part.roll.terms[0].faces}`);
} }
}; };
// Serrated (Weapon Feature) // Serrated (Weapon Feature)
if(item.system.itemFeatures.find(f => f.value === "serrated")) if (item.system.itemFeatures.find(f => f.value === 'serrated'))
mods.serrated = { mods.serrated = {
label: CONFIG.DH.ITEM.weaponFeatures.serrated.label, label: CONFIG.DH.ITEM.weaponFeatures.serrated.label,
enabled: true, enabled: true,
callback: (part) => { callback: part => {
part.roll.terms[0].modifiers.push(`sc8`); part.roll.terms[0].modifiers.push(`sc8`);
} }
}; };
// Self-Correcting (Weapon Feature) // Self-Correcting (Weapon Feature)
if(item.system.itemFeatures.find(f => f.value === "selfCorrecting")) if (item.system.itemFeatures.find(f => f.value === 'selfCorrecting'))
mods.selfCorrecting = { mods.selfCorrecting = {
label: CONFIG.DH.ITEM.weaponFeatures.selfCorrecting.label, label: CONFIG.DH.ITEM.weaponFeatures.selfCorrecting.label,
enabled: true, enabled: true,
callback: (part) => { callback: part => {
part.roll.terms[0].modifiers.push(`sc6`); part.roll.terms[0].modifiers.push(`sc6`);
} }
}; };
@ -238,4 +240,90 @@ export default class DamageRoll extends DHRoll {
config.modifiers = mods; config.modifiers = mods;
return 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() { get title() {
return game.i18n.localize( return game.i18n.localize('DAGGERHEART.GENERAL.Roll.basic');
"DAGGERHEART.GENERAL.Roll.basic"
);
} }
static messageType = 'adversaryRoll'; static messageType = 'adversaryRoll';
@ -68,8 +66,7 @@ export default class DHRoll extends Roll {
} }
// Create Chat Message // Create Chat Message
if (!config.source?.message) if (!config.source?.message) config.message = await this.toMessage(roll, config);
config.message = await this.toMessage(roll, config);
} }
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
@ -97,30 +94,30 @@ export default class DHRoll extends Roll {
rolls: [roll] rolls: [roll]
}; };
config.selectedRollMode ??= game.settings.get('core', 'rollMode'); 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; return msg;
} }
/** @inheritDoc */ /** @inheritDoc */
async render({flavor, template=this.constructor.CHAT_TEMPLATE, isPrivate=false, ...options}={}) { async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) {
if ( !this._evaluated ) return; if (!this._evaluated) return;
const chatData = await this._prepareChatRenderContext({flavor, isPrivate, ...options}); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, chatData); return foundry.applications.handlebars.renderTemplate(template, chatData);
} }
/** @inheritDoc */ /** @inheritDoc */
async _prepareChatRenderContext({flavor, isPrivate=false, ...options}={}) { async _prepareChatRenderContext({ flavor, isPrivate = false, ...options } = {}) {
if(isPrivate) { if (isPrivate) {
return { return {
user: game.user.id, user: game.user.id,
flavor: null, flavor: null,
title: "???", title: '???',
roll: { roll: {
total: "??" total: '??'
}, },
hasRoll: true, hasRoll: true,
isPrivate isPrivate
} };
} else { } else {
options.message.system.user = game.user.id; options.message.system.user = game.user.id;
return options.message.system; return options.message.system;
@ -217,7 +214,7 @@ export default class DHRoll extends Roll {
export const registerRollDiceHooks = () => { export const registerRollDiceHooks = () => {
Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => { Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => {
const hopeFearAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hopeFear; const hopeFearAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hopeFear;
if ( if (
!config.source?.actor || !config.source?.actor ||
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||

View file

@ -644,7 +644,7 @@ export default class DhpActor extends Actor {
); );
break; break;
case 'armor': case 'armor':
if(this.system.armor?.system?.marks) { if (this.system.armor?.system?.marks) {
updates.armor.resources['system.marks.value'] = Math.max( updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0 0
@ -652,9 +652,12 @@ export default class DhpActor extends Actor {
} }
break; break;
default: default:
if(this.system.resources?.[r.key]) { if (this.system.resources?.[r.key]) {
updates.actor.resources[`system.resources.${r.key}.value`] = Math.max( 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), Math.min(
this.system.resources[r.key].value + r.value,
this.system.resources[r.key].max
),
0 0
); );
} }

View file

@ -4,10 +4,13 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
async renderHTML() { async renderHTML() {
const actor = game.actors.get(this.speaker.actor); const actor = game.actors.get(this.speaker.actor);
const actorData = actor && this.isContentVisible ? actor : { const actorData =
img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', actor && this.isContentVisible
name: '' ? 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. */ /* 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 }); const html = await super.renderHTML({ actor: actorData, author: this.author });
@ -18,28 +21,26 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritDoc */ /** @inheritDoc */
prepareData() { prepareData() {
if(this.isAuthor && this.targetSelection === null) if (this.isAuthor && this.targetSelection === null) this.targetSelection = this.system.targets?.length > 0;
this.targetSelection = this.system.targets?.length > 0;
super.prepareData(); super.prepareData();
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritDoc */ /** @inheritDoc */
_onCreate(data, options, userId) { _onCreate(data, options, userId) {
super._onCreate(data, options, userId); super._onCreate(data, options, userId);
if(this.system.registerTargetHook) this.system.registerTargetHook(); if (this.system.registerTargetHook) this.system.registerTargetHook();
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritDoc */ /** @inheritDoc */
async _preDelete(options, user) { async _preDelete(options, user) {
if(this.targetHook !== null) Hooks.off("targetToken", this.targetHook); if (this.targetHook !== null) Hooks.off('targetToken', this.targetHook);
return super._preDelete(options, user); return super._preDelete(options, user);
} }
@ -84,7 +85,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
element.addEventListener('mouseleave', this.unhoverTarget); element.addEventListener('mouseleave', this.unhoverTarget);
element.addEventListener('click', this.clickTarget); element.addEventListener('click', this.clickTarget);
}); });
html.querySelectorAll('.button-target-selection').forEach(element => { html.querySelectorAll('.button-target-selection').forEach(element => {
element.addEventListener('click', this.onTargetSelection.bind(this)); element.addEventListener('click', this.onTargetSelection.bind(this));
}); });
@ -192,7 +193,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
onTargetSelection(event) { onTargetSelection(event) {
event.stopPropagation(); event.stopPropagation();
if(!event.target.classList.contains("target-selected")) if (!event.target.classList.contains('target-selected'))
this.system.targetMode = Boolean(event.target.dataset.targetHit); this.system.targetMode = Boolean(event.target.dataset.targetHit);
} }
} }

View file

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

View file

@ -27,3 +27,5 @@
@import './damage-reduction/sheets.less'; @import './damage-reduction/sheets.less';
@import './multiclass-choice/sheet.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

@ -577,9 +577,9 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 10px; gap: 10px;
text-align: center; text-align: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100% width: 100%;
} }
} }

View file

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

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

@ -29,7 +29,13 @@
{{#each results}} {{#each results}}
{{#unless discarded}} {{#unless discarded}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}"> <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> </div>
{{/unless}} {{/unless}}
{{/each}} {{/each}}