Compare commits

...

16 commits

Author SHA1 Message Date
WBHarry
541052b013 Moved the flag vs global setting logic to documents/scene 2026-04-05 21:46:53 +02:00
WBHarry
a4a5d18098 Merge branch 'main' into feature/1766-Group-Attack 2026-04-05 21:45:15 +02:00
WBHarry
f42fa2668b Renamed groupAttack.nr to groupAttack.numAttackers 2026-04-05 21:10:38 +02:00
WBHarry
4c2d31b2f4
Fixed so that expanded damage info without any dice will show the correct value (#1780) 2026-04-05 19:28:27 +02:00
WBHarry
67d142df3d Fixed migration 2026-04-05 17:27:02 +02:00
WBHarry
fdfd8c5a8d Fixed selecting which roll to use in TagTeamRolls becoming impossible when using an Ability option 2026-04-05 11:28:41 +02:00
WBHarry
dbcef140a2 Fixed armorEffects erroring on isSuppressed when not on an actor 2026-04-05 11:09:00 +02:00
WBHarry
90f4339898 Restoring current version number 2026-04-05 10:23:02 +02:00
WBHarry
0d7469801e
Updated the longrest repair armor to the new armor max path along with a migration (#1777) 2026-04-04 23:22:25 +02:00
WBHarry
70e21f34db Corrected system.json 2026-04-04 13:21:17 +02:00
WBHarry
7057504a9e
Fixes (#1774) 2026-04-04 13:01:24 +02:00
WBHarry
331f1ebf75 Fixed prose-mirror width 2026-04-04 12:42:50 +02:00
WBHarry
f91c140d34
Fixed so that multi term expressions get evaluated into a single number (#1772) 2026-04-04 11:48:41 +02:00
WBHarry
3a117ef117
Added evasion to party resources (#1771) 2026-04-03 23:32:30 +02:00
WBHarry
01619ef067 Corrected github deploy manifest path 2026-04-03 19:15:20 +02:00
WBHarry
622b38ac08 Fixed certain fields in actionConfig not getting translated 2026-04-03 19:05:49 +02:00
26 changed files with 149 additions and 78 deletions

View file

@ -35,7 +35,7 @@ jobs:
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://github.com/${{github.repository}}
manifest: https://raw.githubusercontent.com/{{github.repository}}/main/system.json
manifest: https://raw.githubusercontent.com/${{github.repository}}/main/system.json
download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/system.zip
# Create a zip file with all files required by the module to add to the release

View file

@ -78,9 +78,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.config.selectedMessageMode = data.selectedMessageMode;
if (data.damageOptions) {
const groupAttackNr = data.damageOptions.groupAttack?.nr;
if (typeof groupAttackNr !== 'number' || groupAttackNr % 1 !== 0) {
data.damageOptions.groupAttack.nr = null;
const numAttackers = data.damageOptions.groupAttack?.numAttackers;
if (typeof numAttackers !== 'number' || numAttackers % 1 !== 0) {
data.damageOptions.groupAttack.numAttackers = null;
}
foundry.utils.mergeObject(this.config.damageOptions, data.damageOptions);
@ -98,7 +98,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
const range = this.config.damageOptions.groupAttack.range;
const groupAttackTokens = game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(actorId, range);
this.config.damageOptions.groupAttack.nr = groupAttackTokens.length;
this.config.damageOptions.groupAttack.numAttackers = groupAttackTokens.length;
this.render();
}

View file

@ -366,8 +366,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
let rollIsSelected = false;
for (const member of Object.values(members)) {
const rollFinished = Boolean(member.rollData);
const damageFinished =
member.rollData?.options?.hasDamage !== undefined ? member.rollData.options.damage : true;
const damageFinished = member.rollData?.options?.hasDamage ? Boolean(member.rollData.options.damage) : true;
rollsAreFinished = rollsAreFinished && rollFinished && damageFinished;
rollIsSelected = rollIsSelected || member.selected;

View file

@ -492,7 +492,7 @@ export const defaultRestOptions = {
value: {
custom: {
enabled: true,
formula: '@system.armorScore'
formula: '@system.armorScore.max'
}
}
}

View file

@ -294,7 +294,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
config.damageOptions = {
groupAttack: this.damage.groupAttack
? {
nr: Math.max(groupAttackTokens.length, 1),
numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.groupAttack
}
: null

View file

@ -44,7 +44,8 @@ export default class ArmorChange extends foundry.abstract.DataModel {
label: 'Armor',
defaultPriority: 20,
handler: (actor, change, _options, _field, replacementData) => {
const parsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent);
const baseParsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent);
const parsedMax = new Roll(baseParsedMax).evaluateSync().total;
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
@ -110,6 +111,8 @@ export default class ArmorChange extends foundry.abstract.DataModel {
};
get isSuppressed() {
if (!this.parent.parent?.actor) return false;
switch (this.value.interaction) {
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id:
return !this.parent.parent?.actor.system.armor;

View file

@ -234,13 +234,8 @@ export default class DamageField extends fields.SchemaField {
if (!canvas.scene) return [];
const targets = Array.from(game.user.targets);
const { custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
const sceneMeasurements = canvas.scene?.flags.daggerheart?.rangeMeasurement;
const globalMeasurements = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
const rangeSettings = sceneMeasurements?.setting === custom.id ? sceneMeasurements : globalMeasurements;
const rangeSettings = canvas.scene?.rangeSettings;
if (!rangeSettings) return [];
const maxDistance = rangeSettings[range];
return canvas.scene.tokens.filter(x => {

View file

@ -170,11 +170,11 @@ export default class DamageRoll extends DHRoll {
);
}
if (config.damageOptions.groupAttack?.nr > 1 && isHitpointPart) {
if (config.damageOptions.groupAttack?.numAttackers > 1 && isHitpointPart) {
const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm];
for (const term of part.roll.terms) {
if (damageTypes.some(type => term instanceof type)) {
term.number *= config.damageOptions.groupAttack.nr;
term.number *= config.damageOptions.groupAttack.numAttackers;
}
}
}

View file

@ -246,7 +246,7 @@ 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
*/
@ -256,7 +256,7 @@ export default class DHRoll extends Roll {
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;
const multiplier = roll.terms[i - 1]?.operator === ' - ' ? -1 : 1;
modifierTotal += multiplier * termTotal;
}
}
@ -272,7 +272,7 @@ export default class DHRoll extends Roll {
const changeKeys = this.getActionChangeKeys();
return (
this.options.effects?.reduce((acc, effect) => {
if (effect.system.changes.some(x => changeKeys.some(key => x.key.includes(key)))) {
if (effect.system.changes.some(x => changeKeys.some(key => x.key?.includes(key)))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,

View file

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

View file

@ -43,9 +43,10 @@ export default class DualityDie extends foundry.dice.terms.Die {
options: { appearance: {} }
};
const preset = await getDiceSoNicePreset(diceSoNice[key], faces);
diceSoNiceRoll.dice[0].options.appearance = preset.appearance;
diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile;
const diceAppearance = await this.getDiceSoNiceAppearance(options.liveRoll.roll);
diceSoNiceRoll.dice[0].options.appearance = diceAppearance.appearance;
diceSoNiceRoll.dice[0].options.modelFile = diceAppearance.modelFile;
diceSoNiceRoll.dice[0].results = diceSoNiceRoll.dice[0].results.filter(x => x.active);
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
} else {
@ -59,4 +60,11 @@ export default class DualityDie extends foundry.dice.terms.Die {
this.#updateResources(oldDuality, newDuality, options.liveRoll.actor);
}
}
/**
* Overridden by extending classes HopeDie and FearDie
*/
async getDiceSoNiceAppearance() {
return {};
}
}

View file

@ -0,0 +1,9 @@
import { getDiceSoNicePresets } from '../../config/generalConfig.mjs';
import DualityDie from './dualityDie.mjs';
export default class FearDie extends DualityDie {
async getDiceSoNiceAppearance(roll) {
const { fear } = await getDiceSoNicePresets(roll, this.denomination, this.denomination);
return fear;
}
}

View file

@ -0,0 +1,9 @@
import { getDiceSoNicePresets } from '../../config/generalConfig.mjs';
import DualityDie from './dualityDie.mjs';
export default class HopeDie extends DualityDie {
async getDiceSoNiceAppearance(roll) {
const { hope } = await getDiceSoNicePresets(roll, this.denomination, this.denomination);
return hope;
}
}

View file

@ -24,7 +24,7 @@ export default class DualityRoll extends D20Roll {
}
get dHope() {
if (!(this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
if (!(this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie)) this.createBaseDice();
return this.dice[0];
}
@ -34,7 +34,7 @@ export default class DualityRoll extends D20Roll {
}
get dFear() {
if (!(this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice();
if (!(this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie)) this.createBaseDice();
return this.dice[1];
}
@ -68,8 +68,8 @@ export default class DualityRoll extends D20Roll {
}
get extraDice() {
const { DualityDie, AdvantageDie, DisadvantageDie } = game.system.api.dice.diceTypes;
return this.dice.filter(x => ![DualityDie, AdvantageDie, DisadvantageDie].some(die => x instanceof die));
const { HopeDie, FearDie, AdvantageDie, DisadvantageDie } = game.system.api.dice.diceTypes;
return this.dice.filter(x => ![HopeDie, FearDie, AdvantageDie, DisadvantageDie].some(die => x instanceof die));
}
setRallyChoices() {
@ -125,8 +125,8 @@ export default class DualityRoll extends D20Roll {
/** @inheritDoc */
static fromData(data) {
data.terms[0].class = 'DualityDie';
data.terms[2].class = 'DualityDie';
data.terms[0].class = 'HopeDie';
data.terms[2].class = 'FearDie';
if (data.options.roll.advantage?.type && data.terms[4]?.faces) {
data.terms[4].class = data.options.roll.advantage.type === 1 ? 'AdvantageDie' : 'DisadvantageDie';
}
@ -135,18 +135,18 @@ export default class DualityRoll extends D20Roll {
createBaseDice() {
if (
this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie &&
this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie
this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie &&
this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie
) {
this.terms = [this.terms[0], this.terms[1], this.terms[2]];
return;
}
this.terms[0] = new game.system.api.dice.diceTypes.DualityDie({
this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({
faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12
});
this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' });
this.terms[2] = new game.system.api.dice.diceTypes.DualityDie({
this.terms[2] = new game.system.api.dice.diceTypes.FearDie({
faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12
});
}

View file

@ -1,6 +1,16 @@
import DHToken from './token.mjs';
export default class DhScene extends Scene {
get rangeSettings() {
const { custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
const sceneMeasurements = this.flags.daggerheart?.rangeMeasurement;
const globalMeasurements = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
return sceneMeasurements?.setting === custom.id ? sceneMeasurements : globalMeasurements;
}
/** A map of `TokenDocument` IDs embedded in this scene long with new dimensions from actor size-category changes */
#sizeSyncBatch = new Map();

View file

@ -118,13 +118,6 @@ const getTemplateDistance = range => {
const rangeNumber = Number(range);
if (!Number.isNaN(rangeNumber)) return rangeNumber;
const { custom } = CONFIG.DH.GENERAL.sceneRangeMeasurementSetting;
const sceneMeasurements = canvas.scene?.flags.daggerheart?.rangeMeasurement;
const globalMeasurements = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement;
const settings = sceneMeasurements?.setting === custom.id ? sceneMeasurements : globalMeasurements;
return settings[range];
const settings = canvas.scene?.rangeSettings;
return settings ? settings[range] : 0;
};

View file

@ -1,3 +1,4 @@
import { defaultRestOptions } from '../config/generalConfig.mjs';
import { RefreshType, socketEvent } from './socket.mjs';
export async function runMigrations() {
@ -341,6 +342,18 @@ export async function runMigrations() {
lastMigrationVersion = '2.0.0';
}
if (foundry.utils.isNewerVersion('2.0.4', lastMigrationVersion)) {
const downtimeMoves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew);
if (downtimeMoves.restMoves.longRest.moves.repairArmor) {
await downtimeMoves.updateSource({
'restMoves.longRest.moves.repairArmor': defaultRestOptions.longRest().repairArmor
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, downtimeMoves.toObject());
}
lastMigrationVersion = '2.0.4';
}
//#endregion
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);

View file

@ -4,6 +4,7 @@
.application.daggerheart {
prose-mirror {
height: 100% !important;
width: 100%;
.editor-menu {
background-color: transparent;

View file

@ -155,6 +155,26 @@ body.game:is(.performance-low, .noblur) {
}
}
.stat-section {
position: relative;
display: flex;
gap: 10px;
background-color: light-dark(transparent, @dark-blue);
color: light-dark(@dark-blue, @golden);
padding: 5px 10px;
border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 3px;
align-items: center;
width: fit-content;
h4 {
font-size: var(--font-size-12);
font-weight: bold;
text-transform: uppercase;
color: light-dark(@dark-blue, @golden);
}
}
.threshold-section {
display: flex;
align-self: center;

View file

@ -2,7 +2,7 @@
"id": "daggerheart",
"title": "Daggerheart",
"description": "An unofficial implementation of the Daggerheart system",
"version": "2.0.1",
"version": "2.0.3",
"compatibility": {
"minimum": "14.359",
"verified": "14.359",
@ -297,7 +297,7 @@
"background": "systems/daggerheart/assets/logos/FoundrybornBackgroundLogo.png",
"primaryTokenAttribute": "resources.hitPoints",
"secondaryTokenAttribute": "resources.stress",
"url": "https://your/hosted/system/repo/",
"manifest": "https://your/hosted/system/repo/system.json",
"download": "https://your/packaged/download/archive.zip"
"url": "https://github.com/Foundryborne/daggerheart",
"manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/main/system.json",
"download": "https://github.com/Foundryborne/daggerheart/releases/download/2.0.3/system.zip"
}

View file

@ -1,12 +1,12 @@
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.range"}}{{#if fields.target}} & {{localize "DAGGERHEART.GENERAL.Target.single"}}{{/if}}</legend>
{{formField fields.range value=source.range label="DAGGERHEART.GENERAL.range" name=(concat path "range") localize=true}}
{{formField fields.range value=source.range label=(localize "DAGGERHEART.GENERAL.range") name=(concat path "range") localize=true}}
{{#if fields.target}}
<div class="nest-inputs">
{{#if (and source.target.type (not (eq source.target.type 'self')))}}
{{ formField fields.target.amount value=source.target.amount label="DAGGERHEART.GENERAL.amount" name=(concat path "target.amount") localize=true}}
{{ formField fields.target.amount value=source.target.amount label=(localize "DAGGERHEART.GENERAL.amount") name=(concat path "target.amount") localize=true}}
{{/if}}
{{ formField fields.target.type value=source.target.type label="DAGGERHEART.GENERAL.Target.single" name=(concat path "target.type") localize=true }}
{{ formField fields.target.type value=source.target.type label=(localize "DAGGERHEART.GENERAL.Target.single") name=(concat path "target.type") localize=true }}
</div>
{{/if}}
</fieldset>

View file

@ -4,7 +4,7 @@
{{#if @root.hasBaseDamage}}{{formInput fields.useDefault name="roll.useDefault" value=source.useDefault dataset=(object tooltip="Use default Item values" tooltipDirection="UP")}}{{/if}}
</legend>
{{formField fields.type label="Type" name="roll.type" value=source.type localize=true choices=@root.getRollTypeOptions}}
{{formField fields.type label="DAGGERHEART.GENERAL.type" name="roll.type" value=source.type localize=true choices=@root.getRollTypeOptions localize=true}}
{{#if (eq source.type "diceSet")}}
<div class="nest-inputs">
{{formField fields.diceRolling.fields.multiplier name="roll.diceRolling.multiplier" value=source.diceRolling.multiplier localize=true}}
@ -17,13 +17,13 @@
<div class="nest-inputs">
{{#unless (eq source.type 'spellcast')}}
{{#if @root.isNPC}}
{{formField fields.bonus label="DAGGERHEART.GENERAL.Modifier.single" name="roll.bonus" value=source.bonus placeholder=@root.baseAttackBonus disabled=(not source.type)}}
{{formField fields.bonus label="DAGGERHEART.GENERAL.Modifier.single" name="roll.bonus" value=source.bonus placeholder=@root.baseAttackBonus disabled=(not source.type) localize=true}}
{{else}}
{{formField fields.trait label="DAGGERHEART.GENERAL.Trait.single" name="roll.trait" value=source.trait localize=true disabled=(not source.type)}}
{{/if}}
{{/unless}}
{{formField fields.difficulty label="DAGGERHEART.GENERAL.difficulty" name="roll.difficulty" value=source.difficulty disabled=(not source.type)}}
{{formField fields.advState label="DAGGERHEART.ACTIONS.Config.advantageState" name="roll.advState" value=source.advState localize=true disabled=(not source.type)}}
{{formField fields.difficulty label="DAGGERHEART.GENERAL.difficulty" name="roll.difficulty" value=source.difficulty localize=true disabled=(not source.type)}}
{{formField fields.advState label="DAGGERHEART.ACTIONS.Config.advantageState" name="roll.advState" value=source.advState localize=true localize=true disabled=(not source.type)}}
</div>
{{/if}}
</fieldset>

View file

@ -3,7 +3,7 @@
<p class="hint">{{localize "DAGGERHEART.ACTIONS.Settings.saveHint"}}</p>
<div class="nest-inputs">
{{formField fields.trait label="DAGGERHEART.GENERAL.Trait.single" name="save.trait" value=source.trait localize=true}}
{{formField fields.difficulty label="DAGGERHEART.GENERAL.difficulty" name="save.difficulty" value=source.difficulty disabled=(not source.trait) placeholder=@root.baseSaveDifficulty}}
{{formField fields.damageMod label="DAGGERHEART.ACTIONS.Config.damageOnSave" name="save.damageMod" value=source.damageMod localize=true disabled=(not source.trait)}}
{{formField fields.difficulty label="DAGGERHEART.GENERAL.difficulty" name="save.difficulty" value=source.difficulty disabled=(not source.trait) placeholder=@root.baseSaveDifficulty localize=true}}
{{formField fields.damageMod label="DAGGERHEART.ACTIONS.Config.damageOnSave" name="save.damageMod" value=source.damageMod localize=true localize=true disabled=(not source.trait)}}
</div>
</fieldset>

View file

@ -48,7 +48,7 @@
<legend>{{localize "DAGGERHEART.ACTIONS.Settings.groupAttack.label"}}</legend>
<div class="group-attack-inner-container">
<input type="text" data-dtype="Number" name="damageOptions.groupAttack.nr" value="{{damageOptions.groupAttack.nr}}" />
<input type="text" data-dtype="Number" name="damageOptions.groupAttack.numAttackers" value="{{damageOptions.groupAttack.numAttackers}}" />
<div class="group-attack-tools">
<select name="damageOptions.groupAttack.range">

View file

@ -87,6 +87,12 @@
</div>
{{/unless}}
{{#if (eq actor.type 'character')}}
<div class="stat-section">
<h4>{{localize "DAGGERHEART.GENERAL.evasion"}}: {{actor.system.evasion}}</h4>
</div>
{{/if}}
{{#unless (eq actor.type 'companion')}}
<div class="threshold-section">
<h4 class="threshold-label">{{localize "DAGGERHEART.GENERAL.DamageThresholds.minor"}}</h4>

View file

@ -33,31 +33,32 @@
<div class="roll-formula">{{total}}</div></span></label>
{{/if}}
<div class="roll-dice">
{{#each dice}}
{{#each results}}
{{#unless discarded}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
<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 "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}
{{result}}
{{#if dice.length}}
{{#each dice}}
{{#each results}}
{{#unless discarded}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
<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 "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}
{{result}}
</div>
</div>
</div>
{{/unless}}
{{/unless}}
{{/each}}
{{/each}}
{{/each}}
{{#if modifierTotal}}
<div class="roll-die{{#if (gt modifierTotal 0)}} has-plus{{/if}}">
<div class="font-20">{{modifierTotal}}</div>
</div>
{{/if}}
{{#unless dice.length}}
{{#if modifierTotal}}
<div class="roll-die{{#if (gt modifierTotal 0)}} has-plus{{/if}}">
<div class="font-20">{{modifierTotal}}</div>
</div>
{{/if}}
{{else}}
<div class="roll-die">
<div class="font-20">{{total}}</div>
</div>
{{/unless}}
{{/if}}
</div>
{{/each}}
</fieldset>