Compare commits

...

4 commits

Author SHA1 Message Date
WBHarry
25264c26e9 Fixed Adversary roll failing 2026-03-31 18:01:12 +02:00
Nikhil Nagarajan
d284bd7398
[Fix] Fix CSS trait consistency in Character Creator and Sheet (#1737)
* Fixed on CC and sheet

* SVG fixes

* Revert "SVG fixes"

This reverts commit 72c5075f3f.

* SVG repaired and CSS padding revert

* Remove comments

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-03-31 17:31:21 +02:00
Carlos Fernandez
259b66236c
[V14] Update duality and fate chat commands (#1759)
* Update duality and fate chat commands for v14

* FateRoll withfear/withHope wasn't working after merging with v14-Dev. Fixed

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
2026-03-31 17:26:45 +02:00
WBHarry
f156b12d79
[V14] Message Rolls Rework (#1757)
* Basic rework to the roll data in messages

* .

* Fixed advantage/disadvantage

* .

* .

* Fixed TagTeamDialog

* Reuse getter in faces setter

* Simplify fate roll type css class

* Add more caution to the dualityRoll fromData function

* Apply suggestion from @CarlosFdez

* Compute modifiers using deterministic terms (#1758)

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
2026-03-31 17:20:22 +02:00
24 changed files with 368 additions and 336 deletions

View file

@ -1,3 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="transparent" stroke="#18162e"/>
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="transparent" stroke="#18162e"/>
</svg>

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 397 B

Before After
Before After

View file

@ -1,3 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.12012 0.5H51.8799C55.2901 0.500041 57.8779 3.57175 57.2998 6.93262L50.4639 46.6777C50.1604 48.4411 49.0179 49.9467 47.4014 50.7139L31.3584 58.3271C29.8661 59.0354 28.1339 59.0354 26.6416 58.3271L10.5986 50.7139C8.98214 49.9467 7.83959 48.4411 7.53613 46.6777L0.700195 6.93262C0.122088 3.57175 2.7099 0.500042 6.12012 0.5Z" fill="#18152E" stroke="#F3C267"/>
<path d="M 7.12 0.5 H 52.88 C 56.29 0.5 58.88 3.57 58.3 6.93 L 51.46 46.68 C 51.16 48.44 50.02 49.95 48.4 50.71 L 32.36 58.33 C 30.87 59.04 29.13 59.04 27.64 58.33 L 11.6 50.71 C 9.98 49.95 8.84 48.44 8.54 46.68 L 1.7 6.93 C 1.12 3.57 3.71 0.5 7.12 0.5 Z" fill="#18152E" stroke="#F3C267"/>
</svg>

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 393 B

Before After
Before After

View file

@ -9,10 +9,7 @@ import * as dice from './module/dice/_module.mjs';
import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs';
import {
handlebarsRegistration,
runMigrations,
@ -35,6 +32,8 @@ CONFIG.Dice.daggerheart = {
FateRoll: FateRoll
};
Object.assign(CONFIG.Dice.termTypes, dice.diceTypes);
CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config;
CONFIG.Actor.collection = collections.DhActorCollection;
@ -333,78 +332,6 @@ Hooks.on('renderHandlebarsApplication', (_, element) => {
enricherRenderSetup(element);
});
Hooks.on('chatMessage', (_, message) => {
if (message.startsWith('/dr')) {
const result =
message.trim().toLowerCase() === '/dr' ? { result: {} } : rollCommandToJSON(message.replace(/\/dr\s?/, ''));
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const reaction = rollCommand.reaction;
const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true });
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage,
grantResources
});
return false;
}
if (message.startsWith('/fr')) {
const result =
message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, ''));
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
}
});
Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => {
if (data.openForAllPlayers && data.partyId) {
const party = game.actors.get(data.partyId);

View file

@ -200,6 +200,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
partContext.members[partId] = {
...data,
roll: data.roll,
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: partId,
readyToRoll: Boolean(data.rollChoice),
@ -448,24 +449,19 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const { member, diceType } = button.dataset;
const memberData = this.party.system.tagTeam.members[member];
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 2 : 4;
const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll(
memberData.rollData,
dieIndex,
diceType,
false
);
const rollData = parsedRoll.toJSON();
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2;
const newRoll = game.system.api.dice.DualityRoll.fromData(memberData.rollData);
const dice = newRoll.dice[dieIndex];
await dice.reroll(`/r1=${dice.total}`, {
liveRoll: {
roll: newRoll,
isReaction: true
}
});
const rollData = newRoll.toJSON();
this.updatePartyData(
{
[`system.tagTeam.members.${member}.rollData`]: {
...rollData,
options: {
...rollData.options,
roll: newRoll
}
}
[`system.tagTeam.members.${member}.rollData`]: rollData
},
this.getUpdatingParts(button)
);
@ -700,7 +696,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const error = this.checkInitiatorHopeError(this.party.system.tagTeam.initiator);
if (error) return error;
const mainRoll = (await this.getJoinedRoll()).rollData;
const joinedRoll = await this.getJoinedRoll();
const mainRoll = joinedRoll.rollData;
const finalRoll = foundry.utils.deepClone(joinedRoll.roll);
const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor);
mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
@ -711,7 +709,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: mainActor }),
system: mainRoll.options,
rolls: [mainRoll],
rolls: [JSON.stringify(joinedRoll.roll)],
sound: null,
flags: { core: { RollTable: true } }
};
@ -723,7 +721,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId in tagTeamData.members) {
const resourceUpdates = [];
const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1;
const rollGivesHope = finalRoll.isCritical || finalRoll.withHope;
if (memberId === tagTeamData.initiator.memberId) {
const value = tagTeamData.initiator.cost
? rollGivesHope
@ -734,9 +732,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
} else if (rollGivesHope) {
resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true });
}
if (mainRoll.options.roll.isCritical)
resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (mainRoll.options.roll.result.duality === -1) {
if (finalRoll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (finalRoll.withFear) {
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
}

View file

@ -1,5 +1,8 @@
import { abilities } from '../../config/actorConfig.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs';
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) {
@ -21,6 +24,84 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
classes: ['daggerheart']
};
static CHAT_COMMANDS = {
...super.CHAT_COMMANDS,
dr: {
rgx: /^(?:\/dr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const reaction = rollCommand.reaction;
const traitValue = rollCommand.trait?.toLowerCase();
const advantage = rollCommand.advantage
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: rollCommand.disadvantage
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const difficulty = rollCommand.difficulty;
const grantResources = rollCommand.grantResources;
const target = getCommandTarget({ allowNull: true });
const title =
(flavor ?? traitValue)
? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label)
})
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
enrichedDualityRoll({
reaction,
traitValue,
target,
difficulty,
title,
label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'),
actionType: null,
advantage,
grantResources
});
return false;
}
},
fr: {
rgx: /^(?:\/fr)((?:\s)[^]*)?/,
fn: (_, match) => {
const argString = match[1]?.trim();
const result = argString ? rollCommandToJSON(argString) : { result: {} };
if (!result) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing'));
return false;
}
const { result: rollCommand, flavor } = result;
const fateTypeData = getFateTypeData(rollCommand?.type);
if (!fateTypeData)
return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing'));
const { value: fateType, label: fateTypeLabel } = fateTypeData;
const target = getCommandTarget({ allowNull: true });
const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll');
enrichedFateRoll({
target,
title,
label: fateTypeLabel,
fateType
});
return false;
}
}
};
_getEntryContextOptions() {
return [
...super._getEntryContextOptions(),
@ -175,7 +256,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
action.use(event);
}
async rerollEvent(event, message) {
async rerollEvent(event, messageData) {
event.stopPropagation();
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
@ -187,6 +268,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (!confirmed) return;
}
const message = game.messages.get(messageData._id);
const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') {
@ -209,27 +291,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
});
} 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'
];
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const { newRoll, parsedRoll } = await rollClass.reroll(
originalRoll_parsed,
target.dataset.dieIndex,
target.dataset.type
);
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
const rerollDice = message.system.roll.dice[target.dataset.dieIndex];
await rerollDice.reroll(`/r1=${rerollDice.total}`, {
liveRoll: {
roll: message.system.roll,
actor: message.system.actionActor,
isReaction: message.system.roll.options.actionType === 'reaction'
}
});
await message.update({
rolls: [message.system.roll.toJSON()]
});
}
}

View file

@ -32,7 +32,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
return {
title: new fields.StringField(),
actionDescription: new fields.HTMLField(),
roll: new fields.ObjectField(),
targets: targetsField(),
hasRoll: new fields.BooleanField({ initial: false }),
hasDamage: new fields.BooleanField({ initial: false }),
@ -55,6 +54,16 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
};
}
get roll() {
if (this.parent.type === 'dualityRoll')
return this.parent.rolls.find(x => x instanceof game.system.api.dice.DualityRoll);
if (this.parent.type === 'fateRoll')
return this.parent.rolls.find(x => x instanceof game.system.api.dice.FateRoll);
return null;
}
get actionActor() {
if (!this.source.actor) return null;
return fromUuidSync(this.source.actor);

View file

@ -4,3 +4,4 @@ export { default as DamageRoll } from './damageRoll.mjs';
export { default as DHRoll } from './dhRoll.mjs';
export { default as DualityRoll } from './dualityRoll.mjs';
export { default as FateRoll } from './fateRoll.mjs';
export { diceTypes } from './die/_module.mjs';

View file

@ -36,7 +36,7 @@ export default class D20Roll extends DHRoll {
get isCritical() {
if (!this.d20._evaluated) return;
const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold;
const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.criticalThreshold;
return this.d20.total >= criticalThreshold;
}
@ -217,49 +217,11 @@ export default class D20Roll extends DHRoll {
results: d.results
};
});
data.modifierTotal = this.calculateTotalModifiers(roll);
data.modifierTotal = roll.modifierTotal;
return data;
}
resetFormula() {
return (this._formula = this.constructor.getFormula(this.terms));
}
static async reroll(rollString, _target, message) {
let parsedRoll = game.system.api.dice.D20Roll.fromData(rollString);
parsedRoll = await parsedRoll.reroll();
const newRoll = game.system.api.dice.D20Roll.postEvaluate(parsedRoll, {
targets: message.system.targets,
roll: {
advantage: message.system.roll.advantage?.type,
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
}
});
if (game.modules.get('dice-so-nice')?.active) {
await game.dice3d.showForRoll(parsedRoll, game.user, true);
}
const rerolled = {
any: true,
rerolls: [
...(message.system.roll.dice[0].rerolled?.rerolls?.length > 0
? [message.system.roll.dice[0].rerolled?.rerolls]
: []),
rollString.terms[0].results
]
};
return {
newRoll: {
...newRoll,
dice: [
{
...newRoll.dice[0],
rerolled: rerolled
}
]
},
parsedRoll
};
}
}

View file

@ -12,6 +12,10 @@ export default class DHRoll extends Roll {
return game.i18n.localize('DAGGERHEART.GENERAL.Roll.basic');
}
get modifierTotal() {
return this.constructor.calculateTotalModifiers(this);
}
static messageType = 'adversaryRoll';
static CHAT_TEMPLATE = 'systems/daggerheart/templates/ui/chat/roll.hbs';
@ -138,6 +142,7 @@ export default class DHRoll extends Roll {
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, {
...chatData,
roll: this,
parent: chatData.parent,
targetMode: chatData.targetMode,
metagamingSettings
@ -241,16 +246,21 @@ export default class DHRoll extends Roll {
return (this._formula = this.constructor.getFormula(this.terms));
}
/**
* Calculate total modifiers of any rolls, including non-dh rolls.
* This exists because damage rolls still may receive base roll classes
*/
static calculateTotalModifiers(roll) {
let modifierTotal = 0;
for (let i = 0; i < roll.terms.length; i++) {
if (
roll.terms[i] instanceof foundry.dice.terms.NumericTerm &&
!!roll.terms[i - 1] &&
roll.terms[i - 1] instanceof foundry.dice.terms.OperatorTerm
)
modifierTotal += Number(`${roll.terms[i - 1].operator}${roll.terms[i].total}`);
if (!roll.terms[i].isDeterministic) continue;
const termTotal = roll.terms[i].total;
if (typeof termTotal === 'number') {
const multiplier = roll.terms[i - 1]?.operator === " - " ? -1 : 1;
modifierTotal += multiplier * termTotal;
}
}
return modifierTotal;
}

View file

@ -0,0 +1,9 @@
import DualityDie from './dualityDie.mjs';
import AdvantageDie from './advantageDie.mjs';
import DisadvantageDie from './disadvantageDie.mjs';
export const diceTypes = {
DualityDie,
AdvantageDie,
DisadvantageDie
};

View file

@ -0,0 +1,7 @@
export default class AdvantageDie extends foundry.dice.terms.Die {
constructor(options) {
super(options);
this.modifiers = [];
}
}

View file

@ -0,0 +1,7 @@
export default class DisadvantageDie extends foundry.dice.terms.Die {
constructor(options) {
super(options);
this.modifiers = [];
}
}

View file

@ -0,0 +1,62 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
export default class DualityDie extends foundry.dice.terms.Die {
constructor(options) {
super(options);
this.modifiers = [];
}
#getDualityState(roll) {
if (!roll) return null;
return roll.withHope ? 1 : roll.withFear ? -1 : 0;
}
#updateResources(oldDuality, newDuality, actor) {
const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return;
const updates = [];
const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0);
const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0);
const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0);
if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
const resourceUpdates = new ResourceUpdateMap(actor);
resourceUpdates.addResources(updates);
resourceUpdates.updateResources();
}
async reroll(modifier, options) {
const oldDuality = this.#getDualityState(options.liveRoll.roll);
await super.reroll(modifier, options);
if (options?.liveRoll) {
/* Can't currently test since DiceSoNice is not v14. Might need to set the appearance earlier if a roll is triggered by super.reroll */
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
dice: [this],
options: { appearance: {} }
};
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
} else {
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
await options.liveRoll.roll._evaluate();
if (options.liveRoll.isReaction) return;
const newDuality = this.#getDualityState(options.liveRoll.roll);
this.#updateResources(oldDuality, newDuality, options.liveRoll.actor);
}
}
}

View file

@ -1,8 +1,6 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.mjs';
import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
export default class DualityRoll extends D20Roll {
_advantageFaces = 6;
@ -26,27 +24,31 @@ export default class DualityRoll extends D20Roll {
}
get dHope() {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
if (!(this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
return this.dice[0];
}
set dHope(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces);
// TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dHope.faces
this.dHope.faces = this.getFaces(faces);
}
get dFear() {
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
if (!(this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
return this.dice[1];
}
set dFear(faces) {
if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[1].faces = this.getFaces(faces);
// TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dFear.faces
this.dFear.faces = this.getFaces(faces);
}
get dAdvantage() {
return this.dice[2];
return this.dice[2] instanceof game.system.api.dice.diceTypes.AdvantageDie ? this.dice[2] : null;
}
get dDisadvantage() {
return this.dice[2] instanceof game.system.api.dice.diceTypes.DisadvantageDie ? this.dice[2] : null;
}
get advantageFaces() {
@ -65,6 +67,11 @@ export default class DualityRoll extends D20Roll {
this._advantageNumber = Number(value);
}
get extraDice() {
const { DualityDie, AdvantageDie, DisadvantageDie } = game.system.api.dice.diceTypes;
return this.dice.filter(x => ![DualityDie, AdvantageDie, DisadvantageDie].some(die => x instanceof die));
}
setRallyChoices() {
return this.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally');
@ -118,22 +125,28 @@ export default class DualityRoll extends D20Roll {
/** @inheritDoc */
static fromData(data) {
data.terms[0].class = foundry.dice.terms.Die.name;
data.terms[2].class = foundry.dice.terms.Die.name;
data.terms[0].class = 'DualityDie';
data.terms[2].class = 'DualityDie';
if (data.options.roll.advantage?.type && data.terms[4]?.faces) {
data.terms[4].class = data.options.roll.advantage.type === 1 ? 'AdvantageDie' : 'DisadvantageDie';
}
return super.fromData(data);
}
createBaseDice() {
if (this.dice[0] instanceof foundry.dice.terms.Die && this.dice[1] instanceof foundry.dice.terms.Die) {
if (
this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie &&
this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie
) {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return;
}
this.terms[0] = new foundry.dice.terms.Die({
this.terms[0] = new game.system.api.dice.diceTypes.DualityDie({
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
});
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new foundry.dice.terms.Die({
this.terms[2] = new game.system.api.dice.diceTypes.DualityDie({
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
});
}
@ -371,63 +384,4 @@ export default class DualityRoll extends D20Roll {
if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
}
}
static async reroll(rollBase, dieIndex, diceType, updateResources = true) {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false });
const term = parsedRoll.terms[dieIndex];
await term.reroll(`/r1=${term.total}`);
const result = await parsedRoll.evaluate();
if (game.modules.get('dice-so-nice')?.active) {
const diceSoNiceRoll = {
_evaluated: true,
dice: [
new foundry.dice.terms.Die({
...term,
faces: term._faces,
results: term.results.filter(x => !x.rerolled)
})
],
options: { appearance: {} }
};
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`);
if (diceSoNicePresets[diceType]) {
diceSoNiceRoll.dice[0].options = diceSoNicePresets[diceType];
}
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
} else {
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
targets: parsedRoll.options.targets ?? [],
roll: {
advantage: parsedRoll.options.roll.advantage?.type,
difficulty: parsedRoll.options.roll.difficulty ? Number(parsedRoll.options.roll.difficulty) : null
}
});
const extraIndex = newRoll.advantage ? 3 : 2;
newRoll.extra = newRoll.extra.slice(extraIndex);
const actor = parsedRoll.options.source.actor
? await foundry.utils.fromUuid(parsedRoll.options.source.actor)
: null;
const config = {
source: { actor: parsedRoll.options.source.actor ?? '' },
targets: parsedRoll.targets,
roll: newRoll,
rerolledRoll: parsedRoll.options.roll,
resourceUpdates: new ResourceUpdateMap(actor)
};
if (updateResources) {
await DualityRoll.addDualityResourceUpdates(config);
await config.resourceUpdates.updateResources();
}
return { newRoll, parsedRoll };
}
}

View file

@ -21,8 +21,8 @@ export default class FateRoll extends D20Roll {
}
set dHope(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces);
// TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dHope.faces
this.dHope.faces = this.getFaces(faces);
}
get dFear() {
@ -31,8 +31,8 @@ export default class FateRoll extends D20Roll {
}
set dFear(faces) {
if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice();
this.dice[0].faces = this.getFaces(faces);
// TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dFear.faces
this.dFear.faces = this.getFaces(faces);
}
get isCritical() {
@ -43,6 +43,20 @@ export default class FateRoll extends D20Roll {
return this.data.fateType;
}
get withHope() {
return this.data.fateType === 'Hope';
}
get withFear() {
return this.data.fateType === 'Fear';
}
get totalLabel() {
const label = this.withHope ? 'DAGGERHEART.GENERAL.hope' : 'DAGGERHEART.GENERAL.fear';
return game.i18n.localize(label);
}
static getHooks(hooks) {
return [...(hooks ?? []), 'Fate'];
}

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs';
export default class DhpChatMessage extends foundry.documents.ChatMessage {
targetHook = null;
@ -78,25 +78,14 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
if (this.isContentVisible) {
if (this.type === 'dualityRoll') {
html.classList.add('duality');
switch (this.system.roll?.result?.duality) {
case 1:
html.classList.add('hope');
break;
case -1:
html.classList.add('fear');
break;
default:
html.classList.add('critical');
break;
}
if (this.system.roll.withHope) html.classList.add('hope');
else if (this.system.roll.withFear) html.classList.add('fear');
else html.classList.add('critical');
}
if (this.type === 'fateRoll') {
html.classList.add('fate');
if (this.system.roll?.fate.fateDie == 'Hope') {
html.classList.add('hope');
}
if (this.system.roll?.fate.fateDie == 'Fear') {
html.classList.add('fear');
if (this.system.roll?.fateDie) {
html.classList.add(this.system.roll.fateDie.toLowerCase());
}
}

View file

@ -240,12 +240,16 @@
display: flex;
align-items: center;
justify-content: space-evenly;
gap: 8px;
gap: 2px;
.trait-container {
width: 60px;
height: 60px;
span {
font-size: var(--font-size-10);
}
width: 65px;
height: 65px;
background: url(../assets/svg/trait-shield.svg) no-repeat;
background-size: 100%;
div {
filter: drop-shadow(0 0 3px black);

View file

@ -20,16 +20,22 @@
display: flex;
align-items: center;
justify-content: space-evenly;
gap: 8px;
gap: 2px;
.trait-container {
width: 60px;
height: 60px;
width: 65px;
height: 65px;
background: url(../assets/svg/trait-shield.svg) no-repeat;
background-size: 100%;
padding-top: 4px;
display: flex;
flex-direction: column;
align-items: center;
span {
font-size: var(--font-size-10);
}
div {
filter: drop-shadow(0 0 3px black);
text-shadow: 0 0 3px black;

View file

@ -384,6 +384,15 @@
justify-content: center;
width: 15px;
}
&.has-minus:before {
content: '-';
font-size: var(--font-size-20);
grid-area: c;
display: flex;
align-items: center;
justify-content: center;
width: 15px;
}
}
}

View file

@ -53,14 +53,14 @@
{{#if @root.advantage}}
{{#if (eq @root.advantage 1)}}
<div class="dice-option">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/adv/' @root.roll.dAdvantage.denomination '.svg'}}" alt="">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/adv/d' @root.roll.advantageFaces '.svg'}}" alt="">
<div class="dice-select">
<span class="label">{{localize "DAGGERHEART.GENERAL.Advantage.full"}}</span>
</div>
</div>
{{else if (eq @root.advantage -1)}}
<div class="dice-option">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/disadv/' @root.roll.dAdvantage.denomination '.svg'}}" alt="">
<img class="dice-icon" src="{{concat 'systems/daggerheart/assets/icons/dice/disadv/d' @root.roll.advantageFaces '.svg'}}" alt="">
<div class="dice-select">
<span class="label">{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}</span>
</div>
@ -158,7 +158,7 @@
{{/times}}
</select>
<select name="roll.dice.advantageFaces"{{#unless advantage}} disabled{{/unless}}>
{{selectOptions diceOptions selected=@root.roll.dAdvantage.denomination}}
{{selectOptions diceOptions selected=(concat 'd' @root.roll.advantageFaces)}}
</select>
</div>
{{#if abilities}}

View file

@ -7,7 +7,7 @@
{{#each damage.parts as |part|}}
<div class="roll-dice-container">
{{#each part.dice as |dice index|}}
<a class="roll-dice" data-action="rerollDamageDice" data-member-key="{{@../../../key}}" data-damage-key="{{@../../key}}" data-part="{{@../index}}" data-dice="{{index}}">
<a class="roll-dice" data-action="rerollDamageDice" data-member-key="{{../../../key}}" data-damage-key="{{@../../key}}" data-part="{{@../index}}" data-dice="{{index}}">
<span class="dice-label">{{dice.total}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" dice.dice ".svg"}}" />
</a>

View file

@ -62,32 +62,30 @@
</div>
</span>
{{#if rollData}}
{{#with rollData.options.roll}}
<div class="roll-data {{#if this.isCritical}}critical{{else}}{{#if (eq this.result.duality 1)}}hope{{else}}fear{{/if}}{{/if}}">
<div class="duality-label">{{this.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=this.result.label}}</div>
<div class="roll-dice-container">
<a class="roll-dice" data-action="rerollDice" data-member="{{@root.partId}}" data-dice-type="hope">
<span class="dice-label">{{this.hope.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" this.hope.dice ".svg"}}" />
</a>
<span class="roll-operator">+</span>
<a class="roll-dice" data-action="rerollDice" data-member="{{@root.partId}}" data-dice-type="fear">
<span class="dice-label">{{this.fear.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/fear/" this.fear.dice ".svg"}}" />
</a>
{{#if this.advantage.type}}
<span class="roll-operator">{{#if (eq this.advantage.type 1)}}+{{else}}-{{/if}}</span>
<span class="roll-dice">
<span class="dice-label">{{this.advantage.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/" (ifThen (eq this.advantage.type 1) "adv/" "disadv/") this.advantage.dice ".svg"}}" />
</span>
{{/if}}
<span class="roll-operator">{{#if (gte this.modifierTotal 0)}}+{{else}}-{{/if}}</span>
<span class="roll-value">{{positive this.modifierTotal}}</span>
</div>
{{#if roll}}
<div class="roll-data {{#if roll.withHope}}hope{{else if roll.withFear}}fear{{else}}critical{{/if}}">
<div class="duality-label">{{roll.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}</div>
<div class="roll-dice-container">
<a class="roll-dice" data-action="rerollDice" data-member="{{@root.partId}}" data-dice-type="hope">
<span class="dice-label">{{roll.dHope.total}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" roll.dHope.denomination ".svg"}}" />
</a>
<span class="roll-operator">+</span>
<a class="roll-dice" data-action="rerollDice" data-member="{{@root.partId}}" data-dice-type="fear">
<span class="dice-label">{{roll.dFear.total}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/fear/" roll.dFear.denomination ".svg"}}" />
</a>
{{#if roll.advantage.type}}
<span class="roll-operator">{{#if (eq roll.advantage.type 1)}}+{{else}}-{{/if}}</span>
<span class="roll-dice">
<span class="dice-label">{{roll.advantage.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/" (ifThen (eq roll.advantage.type 1) "adv/" "disadv/") roll.advantage.dice ".svg"}}" />
</span>
{{/if}}
<span class="roll-operator">{{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}}</span>
<span class="roll-value">{{positive roll.modifierTotal}}</span>
</div>
{{/with}}
</div>
{{else}}
<span class="hint">{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.makeYourRoll"}}</span>
{{/if}}

View file

@ -6,16 +6,16 @@
{{#if hintText}}
<div class="hint">{{localize hintText}}</div>
{{else}}
{{#if joinedRoll.rollData}}
{{#if joinedRoll.roll}}
<div class="result-container">
<span class="result-section-label">{{localize "DAGGERHEART.GENERAL.dualityRoll"}}</span>
<div class="result-info">
<div class="damage-info">{{joinedRoll.rollData.options.roll.total}}</div>
<div>{{localize "DAGGERHEART.GENERAL.withThing" thing=joinedRoll.rollData.options.roll.result.label}}</div>
<div class="damage-info">{{joinedRoll.roll.total}}</div>
<div>{{localize "DAGGERHEART.GENERAL.withThing" thing=joinedRoll.roll.totalLabel}}</div>
</div>
</div>
{{/if}}
{{#if hasDamage}}
{{#if joinedRoll.rollData.options.hasDamage}}
<div class="result-container">
<span class="result-section-label">{{localize "DAGGERHEART.GENERAL.damage"}}</span>
{{#each joinedRoll.rollData.options.damage as |damage key|}}

View file

@ -6,8 +6,8 @@
{{#if roll.isCritical}}
<span>{{localize "DAGGERHEART.GENERAL.criticalShort"}}</span>
{{else}}
{{#if (and roll.result (not (eq roll.type "reaction")))}}
<span>{{localize "DAGGERHEART.GENERAL.withThing" thing=roll.result.label}}</span>
{{#if (and roll.dHope (not (eq roll.type "reaction")))}}
<span>{{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}</span>
{{/if}}
{{/if}}
</span>
@ -29,48 +29,48 @@
<div class="dice-tooltip">
<div class="wrapper">
<div class="roll-dice">
{{#if roll.fate}}
{{#if (eq roll.fate.fateDie "Hope")}}
{{#if roll.fateDie}}
{{#if (eq roll.fateDie "Hope")}}
<div class="roll-die">
<label>{{localize "DAGGERHEART.GENERAL.hope"}}</label>
<div class="dice {{roll.fate.dice}} color-hope" data-die-index="0" data-type="hope">
{{roll.fate.value}}
<div class="dice {{roll.dHope.denomination}} color-hope" data-die-index="0" data-type="hope">
{{roll.dHope.total}}
</div>
</div>
{{/if}}
{{#if (eq roll.fate.fateDie "Fear")}}
{{#if (eq roll.fateDie "Fear")}}
<div class="roll-die">
<label>{{localize "DAGGERHEART.GENERAL.fear"}}</label>
<div class="dice {{roll.fate.dice}} color-fear" data-die-index="0" data-type="fear">
{{roll.fate.value}}
<div class="dice {{roll.dFear.denomination}} color-fear" data-die-index="0" data-type="fear">
{{roll.dFear.total}}
</div>
</div>
{{/if}}
{{else}}
{{#if roll.hope}}
{{#if roll.dHope}}
<div class="roll-die">
<label>{{localize "DAGGERHEART.GENERAL.hope"}}</label>
<div class="dice {{roll.hope.dice}} color-hope reroll-button" data-die-index="0" data-type="hope" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}">
{{#if roll.hope.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.hope.rerolled.rerolls.length}}"></i>{{/if}}
{{roll.hope.value}}
<div class="dice {{roll.dHope.denomination}} color-hope reroll-button" data-die-index="0" data-type="hope" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}">
{{#if roll.dHopehope.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.dHope.rerolled.rerolls.length}}"></i>{{/if}}
{{roll.dHope.total}}
</div>
</div>
<div class="roll-die has-plus">
<label>{{localize "DAGGERHEART.GENERAL.fear"}}</label>
<div class="dice {{roll.fear.dice}} color-fear reroll-button" data-die-index="2" data-type="fear" style="--svg-folder: 'fear';" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}">
{{#if roll.fear.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.fear.rerolled.rerolls.length}}"></i>{{/if}}
{{roll.fear.value}}
<div class="dice {{roll.dFear.denomination}} color-fear reroll-button" data-die-index="1" data-type="fear" style="--svg-folder: 'fear';" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerollThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}">
{{#if roll.dFear.rerolled.any}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.UI.Tooltip.diceIsRerolled" times=roll.dFear.rerolled.rerolls.length}}"></i>{{/if}}
{{roll.dFear.total}}
</div>
</div>
{{#if roll.advantage.type}}
{{#if roll.dAdvantage}}
<div class="roll-die has-plus">
{{#if (eq roll.advantage.type 1)}}
<label>{{localize "DAGGERHEART.GENERAL.Advantage.short"}}</label>
<div class="dice {{roll.advantage.dice}} color-adv">{{roll.advantage.value}}</div>
{{else}}
<label>{{localize "DAGGERHEART.GENERAL.Disadvantage.short"}}</label>
<div class="dice {{roll.advantage.dice}} color-dis">{{roll.advantage.value}}</div>
{{/if}}
<label>{{localize "DAGGERHEART.GENERAL.Advantage.short"}}</label>
<div class="dice {{roll.dAdvantage.denomination}} color-adv">{{roll.dAdvantage.total}}</div>
</div>
{{else if roll.dDisadvantage}}
<div class="roll-die has-minus">
<label>{{localize "DAGGERHEART.GENERAL.Disadvantage.short"}}</label>
<div class="dice {{roll.dDisadvantage.denomination}} color-dis">{{roll.dDisadvantage.total}}</div>
</div>
{{/if}}
{{#if roll.rally.dice}}
@ -79,21 +79,17 @@
<div class="dice {{roll.rally.dice}}">{{roll.rally.value}}</div>
</div>
{{/if}}
{{#each roll.extra}}
{{#each results}}
{{#unless discarded}}
<div class="roll-die has-plus">
<label></label>
<div class="dice {{../dice}}">{{result}}</div>
</div>
{{/unless}}
{{/each}}
{{#each roll.extraDice}}
<div class="roll-die has-plus">
<label></label>
<div class="dice {{this.denomination}}">{{this.total}}</div>
</div>
{{/each}}
{{else}}
{{#each roll.dice}}
{{#each results}}
<div class="roll-die {{#unless (or @../first discarded)}} has-plus{{/unless}}">
<div class="dice {{../dice}}{{#if discarded}} discarded{{else}}{{#if (and @../first ../../roll.advantage.type)}}{{#if (eq ../../roll.advantage.type 1)}} color-adv{{else}} color-dis{{/if}}{{/if}}{{#if success}} color-adv{{/if}}{{/if}}">
<div class="dice {{../denomination}}{{#if discarded}} discarded{{else}}{{#if (and @../first ../../roll.advantage.type)}}{{#if (eq ../../roll.advantage.type 1)}} color-adv{{else}} color-dis{{/if}}{{/if}}{{#if success}} color-adv{{/if}}{{/if}}">
{{result}}
</div>
</div>