Merged with main

This commit is contained in:
WBHarry 2025-08-10 01:31:05 +02:00
commit 130b2bf100
65 changed files with 1085 additions and 860 deletions

View file

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

View file

@ -510,7 +510,8 @@
}, },
"CLASS": { "CLASS": {
"Feature": { "Feature": {
"rallyDice": "Bardic Rally Dice" "rallyDice": "Bardic Rally Dice",
"short": "Rally"
} }
}, },
"CONFIG": { "CONFIG": {
@ -2264,11 +2265,6 @@
"abilityCheckTitle": "{ability} Check" "abilityCheckTitle": "{ability} Check"
}, },
"featureTitle": "Class Feature", "featureTitle": "Class Feature",
"foundationCard": {
"ancestryTitle": "Ancestry Card",
"communityTitle": "Community Card",
"subclassFeatureTitle": "Subclass Feature"
},
"healingRoll": { "healingRoll": {
"title": "Heal - {damage}", "title": "Heal - {damage}",
"heal": "Heal", "heal": "Heal",

View file

@ -6,6 +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 RerollDialog } from './rerollDialog.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

@ -45,6 +45,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
return this.config.title; return this.config.title;
} }
get actor() {
return this.config?.data?.parent;
}
/** @override */ /** @override */
static PARTS = { static PARTS = {
header: { header: {
@ -69,9 +73,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
icon icon
})); }));
this.config.costs ??= [];
if (this.config.costs?.length) { if (this.config.costs?.length) {
const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call( const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(
this.action, this.action ?? { actor: this.actor },
this.config.costs this.config.costs
); );
context.costs = updatedCosts.map(x => ({ context.costs = updatedCosts.map(x => ({
@ -80,7 +85,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
? this.action.parent.parent.name ? this.action.parent.parent.name
: game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label) : game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label)
})); }));
context.canRoll = game.system.api.fields.ActionFields.CostField.hasCost.call(this.action, updatedCosts); context.canRoll = game.system.api.fields.ActionFields.CostField.hasCost.call(
this.action ?? { actor: this.actor },
updatedCosts
);
this.config.data.scale = this.config.costs[0].total; this.config.data.scale = this.config.costs[0].total;
} }
if (this.config.uses?.max) { if (this.config.uses?.max) {
@ -143,6 +151,20 @@ 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') {
this.config.costs =
this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1
? this.config.costs.filter(x => x.extKey !== button.dataset.key)
: [
...this.config.costs,
{
extKey: button.dataset.key,
key: 'hope',
value: 1,
name: this.config.data?.experiences?.[button.dataset.key]?.name
}
];
}
this.render(); this.render();
} }
@ -152,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

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

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

View file

@ -631,13 +631,33 @@ export default class CharacterSheet extends DHBaseActorSheet {
}, },
hasRoll: true hasRoll: true
}; };
this.document.diceRoll({ const result = await this.document.diceRoll({
...config, ...config,
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel ability: abilityLabel
}) })
}); });
setTimeout(() => {
this.consumeResource(result?.costs);
}, 50);
}
async consumeResource(costs) {
if (!costs?.length) return;
const usefulResources = foundry.utils.deepClone(this.actor.system.resources);
const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => {
const resource = usefulResources[c.key];
return {
key: c.key,
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
target: resource.target,
keyIsID: resource.keyIsID
};
});
await this.actor.modifyResource(resources);
} }
//TODO: redo toggleEquipItem method //TODO: redo toggleEquipItem method

View file

@ -101,8 +101,7 @@ export default class DhpEnvironment extends DHBaseActorSheet {
const item = event.currentTarget.closest('.inventory-item'); const item = event.currentTarget.closest('.inventory-item');
if (item) { if (item) {
const adversary = game.actors.find(x => x.type === 'adversary' && x.id === item.dataset.itemId); const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
const adversaryData = { type: 'Actor', uuid: adversary.uuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0); event.dataTransfer.setDragImage(item, 60, 0);
} }

View file

@ -51,9 +51,8 @@ import { ItemBrowser } from '../../ui/itemBrowser.mjs';
*/ */
/** /**
* @template {Constructor<foundry.applications.api.DocumentSheet>} BaseDocumentSheet * @template {new (...args: any[]) => {}} T
* @param {BaseDocumentSheet} Base - The base class to extend. * @arg Base {T}
* @returns {BaseDocumentSheet}
*/ */
export default function DHApplicationMixin(Base) { export default function DHApplicationMixin(Base) {
class DHSheetV2 extends HandlebarsApplicationMixin(Base) { class DHSheetV2 extends HandlebarsApplicationMixin(Base) {
@ -123,12 +122,13 @@ export default function DHApplicationMixin(Base) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement)); this._dragDrop.forEach(d => d.bind(htmlElement));
} }
/**@inheritdoc */ /**@inheritdoc */
async _onFirstRender(context, options) { async _onFirstRender(context, options) {
await super._onFirstRender(context, options); await super._onFirstRender(context, options);
const docs = []; const docs = [];
for (var docData of this.relatedDocs) { for (const docData of this.relatedDocs) {
const doc = await foundry.utils.fromUuid(docData.uuid); const doc = await foundry.utils.fromUuid(docData.uuid);
docs.push(doc); docs.push(doc);
} }
@ -247,6 +247,9 @@ export default function DHApplicationMixin(Base) {
/* Context Menu */ /* Context Menu */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Create all configured context menus for this application ins tance.
*/
_createContextMenus() { _createContextMenus() {
for (const config of this.options.contextMenus) { for (const config of this.options.contextMenus) {
const { handler, selector, options } = config; const { handler, selector, options } = config;
@ -257,9 +260,9 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Get the set of ContextMenu options for DomainCards. * Get the set of ContextMenu options for ActiveEffects.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet} * @this {DHSheetV2}
* @protected * @protected
*/ */
static #getEffectContextOptions() { static #getEffectContextOptions() {
@ -305,8 +308,13 @@ export default function DHApplicationMixin(Base) {
} }
/** /**
* Get the set of ContextMenu options. * Get the common ContextMenu options for an element.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance * @param {Object} options
* @param {boolean} [options.usable=false] - Whether to include an option to use the item or apply damage.
* @param {boolean} [options.toChat=false] - Whether to include an option to send the item to chat.
* @param {boolean} [options.deletable=true] - Whether to include an option to delete the item.
*
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]}
*/ */
_getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) { _getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) {
const options = [ const options = [
@ -325,7 +333,7 @@ export default function DHApplicationMixin(Base) {
} }
]; ];
if (usable) if (usable) {
options.unshift({ options.unshift({
name: 'DAGGERHEART.GENERAL.damage', name: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion', icon: 'fa-solid fa-explosion',
@ -340,15 +348,16 @@ export default function DHApplicationMixin(Base) {
} }
}); });
options.unshift({ options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst', icon: 'fa-solid fa-burst',
condition: target => { condition: target => {
const doc = getDocFromElementSync(target); const doc = getDocFromElementSync(target);
return doc && !(doc.type === 'domainCard' && doc.system.inVault); return doc && !(doc.type === 'domainCard' && doc.system.inVault);
}, },
callback: async (target, event) => (await getDocFromElement(target)).use(event) callback: async (target, event) => (await getDocFromElement(target)).use(event)
}); });
}
if (toChat) if (toChat)
options.push({ options.push({

View file

@ -7,8 +7,6 @@ const { ActorSheetV2 } = foundry.applications.sheets;
/** /**
* A base actor sheet extending {@link ActorSheetV2} via {@link DHApplicationMixin} * A base actor sheet extending {@link ActorSheetV2} via {@link DHApplicationMixin}
* @extends ActorSheetV2
* @mixes DHSheetV2
*/ */
export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/** @inheritDoc */ /** @inheritDoc */
@ -106,7 +104,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/** /**
* Get the set of ContextMenu options for Features. * Get the set of ContextMenu options for Features.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {DHSheetV2} * @this {DHBaseActorSheet}
* @protected * @protected
*/ */
static #getFeatureContextOptions() { static #getFeatureContextOptions() {

View file

@ -7,8 +7,6 @@ const { ItemSheetV2 } = foundry.applications.sheets;
/** /**
* A base item sheet extending {@link ItemSheetV2} via {@link DHApplicationMixin} * A base item sheet extending {@link ItemSheetV2} via {@link DHApplicationMixin}
* @extends ItemSheetV2
* @mixes DHSheetV2
*/ */
export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/** @inheritDoc */ /** @inheritDoc */
@ -108,7 +106,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/** /**
* Get the set of ContextMenu options for Features. * Get the set of ContextMenu options for Features.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {DHSheetV2} * @this {DHBaseItemSheet}
* @protected * @protected
*/ */
static #getFeatureContextOptions() { static #getFeatureContextOptions() {

View file

@ -23,8 +23,21 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
_getEntryContextOptions() { _getEntryContextOptions() {
return [ return [
...super._getEntryContextOptions(), ...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', name: 'Reroll Damage',
icon: '<i class="fa-solid fa-dice"></i>', icon: '<i class="fa-solid fa-dice"></i>',
condition: li => { condition: li => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
@ -35,7 +48,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}, },
callback: li => { callback: li => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true }); new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true });
} }
} }
]; ];
@ -54,14 +67,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.simple-roll-button').forEach(element => html.querySelectorAll('.simple-roll-button').forEach(element =>
element.addEventListener('click', event => this.onRollSimple(event, data.message)) element.addEventListener('click', event => this.onRollSimple(event, data.message))
); );
html.querySelectorAll('.target-container').forEach(element => {
element.addEventListener('mouseenter', this.hoverTarget);
element.addEventListener('mouseleave', this.unhoverTarget);
element.addEventListener('click', this.clickTarget);
});
html.querySelectorAll('.button-target-selection').forEach(element => {
element.addEventListener('click', event => this.onTargetSelection(event, data.message));
});
html.querySelectorAll('.healing-button').forEach(element => html.querySelectorAll('.healing-button').forEach(element =>
element.addEventListener('click', event => this.onHealing(event, data.message)) element.addEventListener('click', event => this.onHealing(event, data.message))
); );
@ -159,33 +164,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}); });
} }
onTargetSelection(event, message) {
event.stopPropagation();
const msg = ui.chat.collection.get(message._id);
msg.system.targetMode = Boolean(event.target.dataset.targetHit);
}
hoverTarget(event) {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (!token?.controlled) token._onHoverIn(event, { hoverOutOthers: true });
}
unhoverTarget(event) {
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (!token?.controlled) token._onHoverOut(event);
}
clickTarget(event) {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (!token) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.attackTargetDoesNotExist'));
return;
}
game.canvas.pan(token);
}
async onRollSimple(event, message) { async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage', const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),

View file

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

View file

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

View file

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

View file

@ -115,7 +115,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
if (this.chatDisplay) await this.toChat(); if (this.chatDisplay) await this.toChat();
let { byPassRoll } = options, let { byPassRoll } = options,
config = this.prepareConfig(event, byPassRoll); config = this.prepareConfig(event, byPassRoll);
for (let i = 0; i < this.constructor.extraSchemas.length; i++) { for (let i = 0; i < this.constructor.extraSchemas.length; i++) {
@ -145,9 +144,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config); if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config);
else if (this.trigger) await this.trigger(event, config); else if (this.trigger) await this.trigger(event, config);
else if (this.hasSave || this.hasEffect) { else if (this.hasSave || this.hasEffect) {
const roll = new Roll(''); const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true; roll._evaluated = true;
if (this.hasTarget) config.targetSelection = config.targets.length > 0;
await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
} }
} }
@ -180,7 +178,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasHealing: this.damage?.parts?.length && this.type === 'healing', hasHealing: this.damage?.parts?.length && this.type === 'healing',
hasEffect: !!this.effects?.length, hasEffect: !!this.effects?.length,
hasSave: this.hasSave, hasSave: this.hasSave,
hasTarget: true,
selectedRollMode: game.settings.get('core', 'rollMode'), selectedRollMode: game.settings.get('core', 'rollMode'),
isFastForward: event.shiftKey, isFastForward: event.shiftKey,
data: this.getRollData(), data: this.getRollData(),
@ -223,24 +220,26 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
} }
} }
const resources = config.costs const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(config.costs)
.filter( .filter(
c => c =>
c.enabled !== false && (!successCost && (!c.consumeOnSuccess || config.roll?.success)) ||
((!successCost && (!c.consumeOnSuccess || config.roll?.success)) || (successCost && c.consumeOnSuccess)
(successCost && c.consumeOnSuccess))
) )
.map(c => { .reduce((a, c) => {
const resource = usefulResources[c.key]; const resource = usefulResources[c.key];
return { if (resource) {
key: c.key, a.push({
value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), key: c.key,
target: resource.target, value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1),
keyIsID: resource.keyIsID target: resource.target,
}; keyIsID: resource.keyIsID
}); });
return a;
}
}, []);
await this.actor.modifyResource(resources); await (this.actor.system.partner ?? this.actor).modifyResource(resources);
if ( if (
config.uses?.enabled && config.uses?.enabled &&
((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) || ((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) ||
@ -248,8 +247,11 @@ 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) {
(config.message ?? config.parent).update({ 'system.successConsumed': true }); setTimeout(() => {
(config.message ?? config.parent).update({ 'system.successConsumed': true });
}, 50);
}
} }
/* */ /* */
@ -368,15 +370,15 @@ 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);
msgTarget =
chatMessage.system.targets.find(mt => mt.id === targetId) ??
chatMessage.system.oldTargets.find(mt => mt.id === targetId);
msgTarget.saved = changes;
await chatMessage.update({ await chatMessage.update({
system: { flags: {
targets: chatMessage.system.targets, [game.system.id]: {
oldTargets: chatMessage.system.oldTargets reactionRolls: {
[targetId]: changes
}
}
} }
}); });
}, 100); }, 100);

View file

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

View file

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

View file

@ -3,7 +3,6 @@ export default class DHAbilityUse extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
title: new fields.StringField({}),
origin: new fields.StringField({}), origin: new fields.StringField({}),
img: new fields.StringField({}), img: new fields.StringField({}),
name: new fields.StringField({}), name: new fields.StringField({}),

View file

@ -18,15 +18,11 @@ const targetsField = () =>
); );
export default class DHActorRoll extends foundry.abstract.TypeDataModel { export default class DHActorRoll extends foundry.abstract.TypeDataModel {
targetHook = null;
static defineSchema() { static defineSchema() {
return { return {
title: new fields.StringField(), title: new fields.StringField(),
roll: new fields.ObjectField(), roll: new fields.ObjectField(),
targets: targetsField(), targets: targetsField(),
oldTargets: targetsField(),
targetSelection: new fields.BooleanField({ initial: false }),
hasRoll: new fields.BooleanField({ initial: false }), hasRoll: new fields.BooleanField({ initial: false }),
hasDamage: new fields.BooleanField({ initial: false }), hasDamage: new fields.BooleanField({ initial: false }),
hasHealing: new fields.BooleanField({ initial: false }), hasHealing: new fields.BooleanField({ initial: false }),
@ -63,66 +59,59 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
return actionItem.system.actionsList?.find(a => a.id === this.source.action); return actionItem.system.actionsList?.find(a => a.id === this.source.action);
} }
get messageTemplate() {
return 'systems/daggerheart/templates/ui/chat/roll.hbs';
}
get targetMode() { get targetMode() {
return this.targetSelection; return this.parent.targetSelection;
} }
set targetMode(mode) { set targetMode(mode) {
this.targetSelection = mode; if (!this.parent.isAuthor) return;
this.updateTargets(); this.parent.targetSelection = mode;
this.registerTargetHook(); this.registerTargetHook();
this.parent.update({ this.updateTargets();
system: { }
targetSelection: this.targetSelection,
oldTargets: this.oldTargets get hitTargets() {
return this.currentTargets.filter(t => t.hit || !this.hasRoll || !this.targetMode);
}
async updateTargets() {
if (!ui.chat.collection.get(this.parent.id)) return;
let targets;
if (this.targetMode) targets = this.targets;
else
targets = Array.from(game.user.targets).map(t =>
game.system.api.fields.ActionFields.TargetField.formatTarget(t)
);
await this.parent.update({
flags: {
[game.system.id]: {
targets: targets,
targetMode: this.targetMode
}
} }
}); });
} }
get hitTargets() {
return this.currentTargets.filter(t => t.hit || !this.hasRoll || !this.targetSelection);
}
async updateTargets() {
this.currentTargets = this.getTargetList();
if (!this.targetSelection) {
this.currentTargets.forEach(ct => {
if (this.targets.find(t => t.actorId === ct.actorId)) return;
const indexTarget = this.oldTargets.findIndex(ot => ot.actorId === ct.actorId);
if (indexTarget === -1) this.oldTargets.push(ct);
});
if (this.hasSave) this.setPendingSaves();
if (this.currentTargets.length) {
if (!this.parent._id) return;
const updates = await this.parent.update({
system: {
oldTargets: this.oldTargets
}
});
if (!updates && ui.chat.collection.get(this.parent.id)) ui.chat.updateMessage(this.parent);
}
}
}
registerTargetHook() { registerTargetHook() {
if (this.targetSelection && this.targetHook !== null) { if (!this.parent.isAuthor) return;
Hooks.off('targetToken', this.targetHook); if (this.targetMode && this.parent.targetHook !== null) {
this.targetHook = null; Hooks.off('targetToken', this.parent.targetHook);
} else if (!this.targetSelection && this.targetHook === null) { return (this.parent.targetHook = null);
this.targetHook = Hooks.on('targetToken', foundry.utils.debounce(this.updateTargets.bind(this), 50)); } else if (!this.targetMode && this.parent.targetHook === null) {
return (this.parent.targetHook = Hooks.on(
'targetToken',
foundry.utils.debounce(this.updateTargets.bind(this), 50)
));
} }
} }
prepareDerivedData() { prepareDerivedData() {
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.updateTargets(); this.currentTargets = this.getTargetList();
this.registerTargetHook();
if (this.targetSelection === true) { if (this.targetMode === true && this.hasRoll) {
this.targetShort = this.targets.reduce( this.targetShort = this.targets.reduce(
(a, c) => { (a, c) => {
if (c.hit) a.hit += 1; if (c.hit) a.hit += 1;
@ -136,23 +125,28 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER'); this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER');
this.canButtonApply = game.user.isGM;
} }
getTargetList() { getTargetList() {
return this.targetSelection !== true const targets =
? Array.from(game.user.targets).map(t => { this.targetMode && this.parent.isAuthor
const target = game.system.api.fields.ActionFields.TargetField.formatTarget(t), ? this.targets
oldTarget = : (this.parent.getFlag(game.system.id, 'targets') ?? this.targets),
this.targets.find(ot => ot.actorId === target.actorId) ?? reactionRolls = this.parent.getFlag(game.system.id, 'reactionRolls');
this.oldTargets.find(ot => ot.actorId === target.actorId);
if (oldTarget) return oldTarget; if (reactionRolls) {
return target; Object.entries(reactionRolls).forEach(([k, r]) => {
}) const target = targets.find(t => t.id === k);
: this.targets; if (target) target.saved = r;
});
}
return targets;
} }
setPendingSaves() { setPendingSaves() {
this.pendingSaves = this.targetSelection this.pendingSaves = this.targetMode
? this.targets.filter(target => target.hit && target.saved.success === null).length > 0 ? this.targets.filter(target => target.hit && target.saved.success === null).length > 0
: this.currentTargets.filter(target => target.saved.success === null).length > 0; : this.currentTargets.filter(target => target.saved.success === null).length > 0;
} }

View file

@ -12,7 +12,10 @@ export default class CostField extends fields.ArrayField {
value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }), 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,6 +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 (
@ -70,7 +74,9 @@ export default class CostField extends fields.ArrayField {
} }
static getResources(costs) { static getResources(costs) {
const actorResources = 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);
const itemResources = {}; const itemResources = {};
for (let itemResource of costs) { for (let itemResource of costs) {
if (itemResource.keyIsID) { if (itemResource.keyIsID) {
@ -89,7 +95,13 @@ 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) : [];
return realCosts; let mergedCosts = [];
realCosts.forEach(c => {
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);
if (getCost) getCost.total += c.total;
else mergedCosts.push(c);
});
return mergedCosts;
} }
static formatMax(max) { static formatMax(max) {

View file

@ -15,6 +15,7 @@ export default class TargetField extends fields.SchemaField {
static prepareConfig(config) { static prepareConfig(config) {
if (!this.target?.type) return []; if (!this.target?.type) return [];
config.hasTarget = true;
let targets; let targets;
if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id) if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id)
targets = [this.actor.token ?? this.actor.prototypeToken]; targets = [this.actor.token ?? this.actor.prototypeToken];

View file

@ -229,7 +229,7 @@ export function ActionMixin(Base) {
} }
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);
} }
@ -285,6 +285,7 @@ export function ActionMixin(Base) {
} }
}; };
ChatMessage.applyRollMode(msg, game.settings.get('core', 'rollMode'));
cls.create(msg); cls.create(msg);
} }
} }

View file

@ -139,17 +139,15 @@ export default class D20Roll extends DHRoll {
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config); const data = super.postEvaluate(roll, config);
data.type = config.roll?.type; data.type = config.roll?.type;
data.difficulty = config.roll.difficulty;
if (config.targets?.length) { if (config.targets?.length) {
config.targetSelection = true;
config.targets.forEach(target => { config.targets.forEach(target => {
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.difficulty = 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

@ -15,7 +15,6 @@ export default class DamageRoll extends DHRoll {
const parts = config.roll.map(r => this.postEvaluate(r)); const parts = config.roll.map(r => this.postEvaluate(r));
config.damage = this.unifyDamageRoll(parts); config.damage = this.unifyDamageRoll(parts);
// config.targetSelection = config.targets?.length
} }
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
@ -30,16 +29,18 @@ 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);
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))
), ),
diceRoll = Roll.fromTerms([pool]); diceRoll = Roll.fromTerms([pool]);
await game.dice3d.showForRoll(diceRoll, game.user, true); await game.dice3d.showForRoll(diceRoll, game.user, true, chatMessage.whisper, chatMessage.blind);
} }
await super.buildPost(roll, config, message); await super.buildPost(roll, config, message);
if (config.source?.message) { if (config.source?.message) {
const chatMessage = ui.chat.collection.get(config.source.message);
chatMessage.update({ 'system.damage': config.damage }); chatMessage.update({ 'system.damage': config.damage });
} }
} }
@ -102,14 +103,14 @@ export default class DamageRoll extends DHRoll {
} }
constructFormula(config) { constructFormula(config) {
this.options.roll.forEach(part => { this.options.roll.forEach((part, index) => {
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
this.constructFormulaPart(config, part); this.constructFormulaPart(config, part, index);
}); });
return this.options.roll; return this.options.roll;
} }
constructFormulaPart(config, part) { constructFormulaPart(config, part, index) {
part.roll.terms = Roll.parse(part.roll.formula, config.data); part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
@ -120,6 +121,14 @@ export default class DamageRoll extends DHRoll {
}); });
} }
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
if (part.extraFormula) { if (part.extraFormula) {
part.roll.terms.push( part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }), new foundry.dice.terms.OperatorTerm({ operator: '+' }),
@ -132,9 +141,106 @@ export default class DamageRoll extends DHRoll {
criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll); criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll);
part.roll.terms.push(...this.formatModifier(criticalBonus)); part.roll.terms.push(...this.formatModifier(criticalBonus));
} }
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
return (part.roll._formula = this.constructor.getFormula(part.roll.terms)); return (part.roll._formula = this.constructor.getFormula(part.roll.terms));
} }
/* To Remove When Reaction System */
static temporaryModifierBuilder(config) {
const mods = {};
if (config.data?.parent) {
if (config.data.parent.appliedEffects) {
// Bardic Rally
mods.rally = {
label: 'DAGGERHEART.CLASS.Feature.rallyDice',
values: config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value });
return a;
}, []),
value: null,
beforeCrit: true,
callback: part => {
const rallyFaces = config.modifiers.rally.values.find(
r => r.value === config.modifiers.rally.value
)?.label;
part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.parse(`1${rallyFaces}`)
);
}
};
}
const item = config.data.parent.items?.get(config.source.item);
if (item) {
// Massive (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'massive'))
mods.massive = {
label: CONFIG.DH.ITEM.weaponFeatures.massive.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`);
part.roll.terms[0].number += 1;
}
};
// Powerful (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'powerful'))
mods.powerful = {
label: CONFIG.DH.ITEM.weaponFeatures.powerful.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`);
part.roll.terms[0].number += 1;
}
};
// Brutal (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'brutal'))
mods.brutal = {
label: CONFIG.DH.ITEM.weaponFeatures.brutal.label,
enabled: true,
beforeCrit: true,
callback: part => {
part.roll.terms[0].modifiers.push(`x${part.roll.terms[0].faces}`);
}
};
// Serrated (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'serrated'))
mods.serrated = {
label: CONFIG.DH.ITEM.weaponFeatures.serrated.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`sc8`);
}
};
// Self-Correcting (Weapon Feature)
if (item.system.itemFeatures.find(f => f.value === 'selfCorrecting'))
mods.selfCorrecting = {
label: CONFIG.DH.ITEM.weaponFeatures.selfCorrecting.label,
enabled: true,
callback: part => {
part.roll.terms[0].modifiers.push(`sc6`);
}
};
}
}
config.modifiers = mods;
return mods;
}
static async reroll(target, message) { static async reroll(target, message) {
const { damageType, part, dice, result } = target.dataset; const { damageType, part, dice, result } = target.dataset;
const rollPart = message.system.damage[damageType].parts[part]; const rollPart = message.system.damage[damageType].parts[part];

View file

@ -2,19 +2,19 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
export default class DHRoll extends Roll { export default class DHRoll extends Roll {
baseTerms = []; baseTerms = [];
constructor(formula, data, options) { constructor(formula, data = {}, options = {}) {
super(formula, data, options); super(formula, data, options);
if (!this.data || !Object.keys(this.data).length) this.data = options.data; if (!this.data || !Object.keys(this.data).length) this.data = options.data;
} }
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';
static CHAT_TEMPLATE = 'systems/daggerheart/templates/ui/chat/roll.hbs';
static DefaultDialog = D20RollDialog; static DefaultDialog = D20RollDialog;
static async build(config = {}, message = {}) { static async build(config = {}, message = {}) {
@ -34,6 +34,8 @@ export default class DHRoll extends Roll {
this.applyKeybindings(config); this.applyKeybindings(config);
this.temporaryModifierBuilder(config);
let roll = new this(config.roll.formula, config.data, config); let roll = new this(config.roll.formula, config.data, config);
if (config.dialog.configure !== false) { if (config.dialog.configure !== false) {
// Open Roll Dialog // Open Roll Dialog
@ -64,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 = {}) {
@ -92,10 +93,37 @@ export default class DHRoll extends Roll {
system: config, system: config,
rolls: [roll] rolls: [roll]
}; };
if(roll._evaluated) return await cls.create(msg, { rollMode: config.selectedRollMode }); config.selectedRollMode ??= game.settings.get('core', 'rollMode');
if (roll._evaluated) return await cls.create(msg, { rollMode: config.selectedRollMode });
return msg; return msg;
} }
/** @inheritDoc */
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) {
if (!this._evaluated) return;
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, chatData);
}
/** @inheritDoc */
async _prepareChatRenderContext({ flavor, isPrivate = false, ...options } = {}) {
if (isPrivate) {
return {
user: game.user.id,
flavor: null,
title: '???',
roll: {
total: '??'
},
hasRoll: true,
isPrivate
};
} else {
options.message.system.user = game.user.id;
return options.message.system;
}
}
static applyKeybindings(config) { static applyKeybindings(config) {
if (config.event) if (config.event)
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey); config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
@ -178,11 +206,15 @@ export default class DHRoll extends Roll {
} }
return modifierTotal; return modifierTotal;
} }
static temporaryModifierBuilder(config) {
return {};
}
} }
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) ||
@ -207,7 +239,9 @@ export const registerRollDiceHooks = () => {
if (updates.length) { if (updates.length) {
const target = actor.system.partner ?? actor; const target = actor.system.partner ?? actor;
if (!['dead', 'unconcious'].some(x => actor.statuses.has(x))) { if (!['dead', 'unconcious'].some(x => actor.statuses.has(x))) {
target.modifyResource(updates); setTimeout(() => {
target.modifyResource(updates);
}, 50);
} }
} }

View file

@ -124,15 +124,20 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
return tags; return tags;
} }
/**
* Create a new ChatMessage to display this documents data.
* @param {String} origin - uuid of a document. TODO: This needs to be reviewed.
*/
async toChat(origin) { async toChat(origin) {
/**@type {foundry.documents.ChatMessage} */
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const actor = game.actors.get(cls.getSpeaker().actor); const speaker = cls.getSpeaker();
const actor = cls.getSpeakerActor(speaker);
const systemData = { const systemData = {
action: { img: this.img, name: this.name }, action: { img: this.img, name: this.name },
actor: { name: actor.name, img: actor.img }, actor: { name: actor?.name, img: actor?.img },
author: this.author, speaker,
speaker: cls.getSpeaker(), origin,
origin: origin,
description: this.description, description: this.description,
actions: [] actions: []
}; };

View file

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

View file

@ -1,21 +1,61 @@
export default class DhpChatMessage extends foundry.documents.ChatMessage { export default class DhpChatMessage extends foundry.documents.ChatMessage {
async renderHTML() { targetHook = null;
if (this.system.messageTemplate) targetSelection = null;
this.content = await foundry.applications.handlebars.renderTemplate(this.system.messageTemplate, {
...this.system,
_source: this.system._source
});
async renderHTML() {
const actor = game.actors.get(this.speaker.actor); const actor = game.actors.get(this.speaker.actor);
const actorData = 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 });
this.applyPermission(html);
if (this.type === 'dualityRoll') { this.enrichChatMessage(html);
this.addChatListeners(html);
return html;
}
/* -------------------------------------------- */
/** @inheritDoc */
prepareData() {
if (this.isAuthor && this.targetSelection === null) this.targetSelection = this.system.targets?.length > 0;
super.prepareData();
}
/* -------------------------------------------- */
/** @inheritDoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
if (this.system.registerTargetHook) this.system.registerTargetHook();
}
/* -------------------------------------------- */
/** @inheritDoc */
async _preDelete(options, user) {
if (this.targetHook !== null) Hooks.off('targetToken', this.targetHook);
return super._preDelete(options, user);
}
enrichChatMessage(html) {
const elements = html.querySelectorAll('[data-perm-id]');
elements.forEach(e => {
const uuid = e.dataset.permId,
document = fromUuidSync(uuid);
if (!document) return;
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER'));
});
if (this.isContentVisible && this.type === 'dualityRoll') {
html.classList.add('duality'); html.classList.add('duality');
switch (this.system.roll?.result?.duality) { switch (this.system.roll?.result?.duality) {
case 1: case 1:
@ -29,36 +69,9 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
break; break;
} }
} }
this.enrichChatMessage(html);
return html;
} }
applyPermission(html) { addChatListeners(html) {
const elements = html.querySelectorAll('[data-perm-id]');
elements.forEach(e => {
const uuid = e.dataset.permId,
document = fromUuidSync(uuid);
if (!document) return;
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER'));
});
}
async _preCreate(data, options, user) {
options.speaker = ChatMessage.getSpeaker();
const rollActorOwner = data.rolls?.[0]?.data?.parent?.owner;
if (rollActorOwner) {
data.author = rollActorOwner ? rollActorOwner.id : data.author;
await this.updateSource({ author: rollActorOwner ?? user });
}
return super._preCreate(data, options, rollActorOwner ?? user);
}
enrichChatMessage(html) {
html.querySelectorAll('.damage-button').forEach(element => html.querySelectorAll('.damage-button').forEach(element =>
element.addEventListener('click', this.onDamage.bind(this)) element.addEventListener('click', this.onDamage.bind(this))
); );
@ -66,10 +79,20 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
html.querySelectorAll('.duality-action-effect').forEach(element => html.querySelectorAll('.duality-action-effect').forEach(element =>
element.addEventListener('click', this.onApplyEffect.bind(this)) element.addEventListener('click', this.onApplyEffect.bind(this))
); );
html.querySelectorAll('.roll-target').forEach(element => {
element.addEventListener('mouseenter', this.hoverTarget);
element.addEventListener('mouseleave', this.unhoverTarget);
element.addEventListener('click', this.clickTarget);
});
html.querySelectorAll('.button-target-selection').forEach(element => {
element.addEventListener('click', this.onTargetSelection.bind(this));
});
} }
getTargetList() { getTargetList() {
const targets = this.system.hitTargets; const targets = this.system.hitTargets ?? [];
return targets.map(target => game.canvas.tokens.documentCollection.find(t => t.actor?.uuid === target.actorId)); return targets.map(target => game.canvas.tokens.documentCollection.find(t => t.actor?.uuid === target.actorId));
} }
@ -141,9 +164,36 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
} }
consumeOnSuccess() { consumeOnSuccess() {
if (!this.system.successConsumed && !this.system.targetSelection) { if (!this.system.successConsumed && !this.targetSelection) {
const action = this.system.action; const action = this.system.action;
if (action) action.consume(this.system, true); if (action) action.consume(this.system, true);
} }
} }
hoverTarget(event) {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (token && !token?.controlled) token._onHoverIn(event, { hoverOutOthers: true });
}
unhoverTarget(event) {
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (token && !token?.controlled) token._onHoverOut(event);
}
clickTarget(event) {
event.stopPropagation();
const token = canvas.tokens.get(event.currentTarget.dataset.token);
if (!token) {
ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.attackTargetDoesNotExist'));
return;
}
game.canvas.pan(token);
}
onTargetSelection(event) {
event.stopPropagation();
if (!event.target.classList.contains('target-selected'))
this.system.targetMode = Boolean(event.target.dataset.targetHit);
}
} }

View file

@ -142,19 +142,16 @@ export default class DHItem extends foundry.documents.Item {
} }
} }
/**
* Create a new ChatMessage to display this documents data
* @param {String} origin - uuid of a document. TODO: This needs to be reviewed.
*/
async toChat(origin) { async toChat(origin) {
/**@type {foundry.documents.ChatMessage} */
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const item = await foundry.utils.fromUuid(origin); const item = await foundry.utils.fromUuid(origin);
const systemData = { const systemData = {
title:
this.type === 'ancestry'
? game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.ancestryTitle')
: this.type === 'community'
? game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.communityTitle')
: this.type === 'feature'
? game.i18n.localize('TYPES.Item.feature')
: game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.subclassFeatureTitle'),
origin: origin, origin: origin,
img: this.img, img: this.img,
item: { item: {
@ -170,7 +167,6 @@ export default class DHItem extends foundry.documents.Item {
type: 'abilityUse', type: 'abilityUse',
user: game.user.id, user: game.user.id,
actor: item.parent, actor: item.parent,
author: this.author,
speaker: cls.getSpeaker(), speaker: cls.getSpeaker(),
system: systemData, system: systemData,
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'), title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.displayInChat'),

View file

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

View file

@ -45,7 +45,7 @@
}, },
"effects": [ "effects": [
{ {
"name": "Base", "name": "Tusks",
"type": "base", "type": "base",
"_id": "klEyAxQa5YHXVnrl", "_id": "klEyAxQa5YHXVnrl",
"img": "icons/creatures/abilities/fang-tooth-blood-red.webp", "img": "icons/creatures/abilities/fang-tooth-blood-red.webp",

View file

@ -14,16 +14,7 @@
"description": "", "description": "",
"chatDisplay": false, "chatDisplay": false,
"actionType": "action", "actionType": "action",
"cost": [ "cost": [],
{
"scalable": false,
"key": "hitPoints",
"value": 1,
"keyIsID": false,
"step": null,
"consumeOnSuccess": false
}
],
"uses": { "uses": {
"value": null, "value": null,
"max": "1", "max": "1",
@ -67,7 +58,7 @@
{ {
"key": "system.bonuses.rally", "key": "system.bonuses.rally",
"mode": 2, "mode": 2,
"value": "1d6", "value": "d6",
"priority": null "priority": null
} }
], ],

View file

@ -14,16 +14,7 @@
"description": "", "description": "",
"chatDisplay": true, "chatDisplay": true,
"actionType": "action", "actionType": "action",
"cost": [ "cost": [],
{
"scalable": false,
"key": "hitPoints",
"value": 1,
"keyIsID": false,
"step": null,
"consumeOnSuccess": false
}
],
"uses": { "uses": {
"value": null, "value": null,
"max": "1", "max": "1",
@ -67,7 +58,7 @@
{ {
"key": "system.bonuses.rally", "key": "system.bonuses.rally",
"mode": 2, "mode": 2,
"value": "1d8", "value": "d8",
"priority": null "priority": null
} }
], ],

View file

@ -64,7 +64,7 @@
"effects": [ "effects": [
{ {
"name": "Gifted Tracker", "name": "Gifted Tracker",
"img": "icons/svg/item-bag.svg", "img": "systems/daggerheart/assets/icons/domains/domain-card/sage.png",
"origin": "Compendium.daggerheart.domains.Item.VZ2b4zfRzV73XTuT", "origin": "Compendium.daggerheart.domains.Item.VZ2b4zfRzV73XTuT",
"transfer": false, "transfer": false,
"_id": "47Oh2weCdmuvKHM9", "_id": "47Oh2weCdmuvKHM9",
@ -104,12 +104,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.346", "coreVersion": "13.347",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1754114056078, "createdTime": 1754114056078,
"modifiedTime": 1754114073478, "modifiedTime": 1754670410126,
"lastModifiedBy": "Q9NoTaEarn3VMS6Z" "lastModifiedBy": "49DaecTcBSc5d0DA"
}, },
"_key": "!items.effects!VZ2b4zfRzV73XTuT.47Oh2weCdmuvKHM9" "_key": "!items.effects!VZ2b4zfRzV73XTuT.47Oh2weCdmuvKHM9"
} }

View file

@ -389,7 +389,7 @@
"effects": [ "effects": [
{ {
"name": "Healed by Healing Hands", "name": "Healed by Healing Hands",
"img": "icons/svg/item-bag.svg", "img": "systems/daggerheart/assets/icons/domains/domain-card/splendor.png",
"origin": "Compendium.daggerheart.domains.Item.WTlhnQMajc1r8i50", "origin": "Compendium.daggerheart.domains.Item.WTlhnQMajc1r8i50",
"transfer": false, "transfer": false,
"_id": "sd5liP4ZcVeTMAoW", "_id": "sd5liP4ZcVeTMAoW",
@ -422,12 +422,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.346", "coreVersion": "13.347",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1754263407455, "createdTime": 1754263407455,
"modifiedTime": 1754263727114, "modifiedTime": 1754670504951,
"lastModifiedBy": "Q9NoTaEarn3VMS6Z" "lastModifiedBy": "49DaecTcBSc5d0DA"
}, },
"_key": "!items.effects!WTlhnQMajc1r8i50.sd5liP4ZcVeTMAoW" "_key": "!items.effects!WTlhnQMajc1r8i50.sd5liP4ZcVeTMAoW"
} }

View file

@ -66,7 +66,7 @@
"effects": [ "effects": [
{ {
"name": "Life Ward", "name": "Life Ward",
"img": "icons/svg/item-bag.svg", "img": "systems/daggerheart/assets/icons/domains/domain-card/splendor.png",
"origin": "Compendium.daggerheart.domains.Item.OszbCj0jTqq2ADx9", "origin": "Compendium.daggerheart.domains.Item.OszbCj0jTqq2ADx9",
"transfer": false, "transfer": false,
"_id": "E7Ou4OMEy3TeK1Gf", "_id": "E7Ou4OMEy3TeK1Gf",
@ -99,12 +99,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.346", "coreVersion": "13.347",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1754264687962, "createdTime": 1754264687962,
"modifiedTime": 1754264717646, "modifiedTime": 1754670535710,
"lastModifiedBy": "Q9NoTaEarn3VMS6Z" "lastModifiedBy": "49DaecTcBSc5d0DA"
}, },
"_key": "!items.effects!OszbCj0jTqq2ADx9.E7Ou4OMEy3TeK1Gf" "_key": "!items.effects!OszbCj0jTqq2ADx9.E7Ou4OMEy3TeK1Gf"
} }

View file

@ -37,7 +37,7 @@
} }
}, },
"_id": "UJTsJlnhi5Zi0XQ2", "_id": "UJTsJlnhi5Zi0XQ2",
"img": "icons/magic/life/heart-cross-blue.webp", "img": "systems/daggerheart/assets/icons/domains/domain-card/bone.png",
"changes": [ "changes": [
{ {
"key": "system.rules.damageReduction.thresholdImmunities.minor", "key": "system.rules.damageReduction.thresholdImmunities.minor",
@ -67,12 +67,12 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.346", "coreVersion": "13.347",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1754303484332, "createdTime": 1754303484332,
"modifiedTime": 1754303570504, "modifiedTime": 1754670012467,
"lastModifiedBy": "MQSznptE5yLT7kj8" "lastModifiedBy": "49DaecTcBSc5d0DA"
}, },
"_key": "!items.effects!zbxPl81kbWEegKQN.UJTsJlnhi5Zi0XQ2" "_key": "!items.effects!zbxPl81kbWEegKQN.UJTsJlnhi5Zi0XQ2"
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -132,7 +132,7 @@
"image": {}, "image": {},
"text": { "text": {
"format": 1, "format": 1,
"content": "<blockquote><p>This product includes materials from the Daggerheart System Reference Document 1.0, © Critical Role, LLC. under the terms of the Darrington Press Community Gaming (DPCGL) License. More information can be found at <a href=\"https://www.daggerheart.com/\" title=\"Daggerheart Official Website\">https://www.daggerheart.com</a>. There are no previous modifications by others.</p><p></p></blockquote><h1>The Foundryborne Team</h1><p></p><p>The Foundryborne Team consists of:</p><p></p><ul><li><p><strong>@harryfuralle</strong></p></li><li><p><strong>@cptn_cosmo</strong></p></li><li><p><strong>@molilo</strong></p></li><li><p><strong>@joaquinp98</strong></p></li><li><p><strong>@dapoulp</strong></p></li><li><p>@ikraik</p></li><li><p>@irktheimp</p></li><li><p>@jacobwojo</p></li><li><p>@vyrth</p></li><li><p>@.ontaro</p></li><li><p>@saatsin</p></li><li><p>@david.xyz</p></li></ul><p></p><p>With Art from:</p><p></p><ul><li><p>UsernameIsInUse</p></li></ul><p></p><p>We would also like to thank the FoundryVTT team for their support in publishing this system.</p><p>And, of course, special thanks to the teams at Critical Role and Darrington Press for making such a wonderful game and updating the license to allow a FoundryVTT version of the system.</p><p></p><h1>The Foundryborne Community</h1><p></p><p>Without our amazing community this project would not have been possible.</p><p>You kept us going with both direct contributions and just endless support!</p><p>We thank you with all our hearts.</p><p><a href=\"https://foundryborne.online/\" title=\"Foundryborne official website\">Come join us!</a></p>" "content": "<blockquote><p>This product includes materials from the Daggerheart System Reference Document 1.0, © Critical Role, LLC. under the terms of the Darrington Press Community Gaming (DPCGL) License. More information can be found at <a href=\"https://www.daggerheart.com/\" title=\"Daggerheart Official Website\">https://www.daggerheart.com</a>. There are no previous modifications by others.</p><p></p></blockquote><h1>The Foundryborne Team</h1><p></p><p>The Foundryborne Team consists of:</p><p></p><ul><li><p><strong>@harryfuralle</strong></p></li><li><p><strong>@cptn_cosmo</strong></p></li><li><p><strong>@molilo</strong></p></li><li><p><strong>@joaquinp98</strong></p></li><li><p><strong>@dapoulp</strong></p></li><li><p>@ikraik</p></li><li><p>@irktheimp</p></li><li><p>@jacobwojo</p></li><li><p>@vyrth</p></li><li><p>@.ontaro</p></li><li><p>@saatsin</p></li><li><p>@david.xyz</p></li><li><p>@<span style=\"color:oklab(0.988044 0.0000450313 0.0000197887);font-family:'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:oklab(0.262384 0.00252247 -0.00889932);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">lazjen</span></p></li></ul><p></p><p>With Art from:</p><p></p><ul><li><p>UsernameIsInUse</p></li></ul><p></p><p>And special thanks to our hard working community testers:<br /></p><ul><li><p><span style=\"color:oklab(0.988044 0.0000450313 0.0000197887);font-family:'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:oklab(0.262384 0.00252247 -0.00889932);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">@lazjen</span></p></li></ul><p></p><p>We would also like to thank the FoundryVTT team for their support in publishing this system.</p><p>And, of course, special thanks to the teams at Critical Role and Darrington Press for making such a wonderful game and updating the license to allow a FoundryVTT version of the system.</p><p></p><h1>The Foundryborne Community</h1><p></p><p>Without our amazing community this project would not have been possible.</p><p>You kept us going with both direct contributions and just endless support!</p><p>We thank you with all our hearts.</p><p><a href=\"https://foundryborne.online/\" title=\"Foundryborne official website\">Come join us!</a></p>"
}, },
"video": { "video": {
"controls": true, "controls": true,
@ -153,8 +153,8 @@
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"createdTime": 1754225939902, "createdTime": 1754225939902,
"modifiedTime": 1754226994508, "modifiedTime": 1754668980876,
"lastModifiedBy": "l5jB3XmcVXOTQpRZ" "lastModifiedBy": "Cf0YKwnZ1OHBZWl8"
}, },
"_key": "!journal.pages!g7NhKvwltwafmMyR.dP6xSKEld4TSqHhK" "_key": "!journal.pages!g7NhKvwltwafmMyR.dP6xSKEld4TSqHhK"
} }

View file

@ -23,7 +23,7 @@
} }
}, },
"_id": "RSmscgGyuHJucF6C", "_id": "RSmscgGyuHJucF6C",
"img": "icons/magic/life/heart-cross-blue.webp", "img": "icons/sundries/documents/document-letter-blue.webp",
"changes": [ "changes": [
{ {
"key": "system.bonuses.rally", "key": "system.bonuses.rally",
@ -53,10 +53,11 @@
"compendiumSource": null, "compendiumSource": null,
"duplicateSource": null, "duplicateSource": null,
"exportSource": null, "exportSource": null,
"coreVersion": "13.346", "coreVersion": "13.347",
"systemId": "daggerheart", "systemId": "daggerheart",
"systemVersion": "0.0.1", "systemVersion": "0.0.1",
"lastModifiedBy": null "lastModifiedBy": "49DaecTcBSc5d0DA",
"modifiedTime": 1754669077252
}, },
"_key": "!items.effects!eCoEWkWuZPMZ9C6a.RSmscgGyuHJucF6C" "_key": "!items.effects!eCoEWkWuZPMZ9C6a.RSmscgGyuHJucF6C"
} }

View file

@ -32,6 +32,7 @@
.message-header-metadata { .message-header-metadata {
flex: none; flex: none;
display: flex; display: flex;
flex-direction: column;
.message-metadata { .message-metadata {
font-family: @font-body; font-family: @font-body;
@ -73,6 +74,13 @@
.message-content { .message-content {
padding-bottom: 8px; padding-bottom: 8px;
.flavor-text {
font-size: var(--font-size-12);
line-height: 20px;
color: var(--color-dark-4);
text-align: center;
display: block;
}
} }
} }
} }

View file

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

View file

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

View file

@ -11,333 +11,6 @@
} }
} }
/* &.roll {
.dice-flavor {
text-align: center;
font-weight: bold;
}
.dice-tooltip {
.dice-rolls {
&.duality {
display: flex;
gap: 0.25rem;
> .roll {
background-image: none;
.reroll-button {
border: none;
background: initial;
width: 42px;
&:hover {
background: var(--button-background-color);
border: 1px solid var(--button-border-color);
}
}
}
}
&.rerollable {
position: relative;
flex: none;
.dice-rerolled {
z-index: 2;
position: absolute;
right: 0;
font-size: 12px;
cursor: help;
}
.reroll-button {
border: none;
background: initial;
&:hover {
background: var(--button-background-color);
border: 1px solid var(--button-border-color);
}
}
}
// margin: 0;
> .roll {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
margin-bottom: 4px;
.dice-container {
display: flex;
flex-direction: column;
gap: 2px;
position: relative;
.dice-title {
color: var(--color-light-1);
text-shadow: 0 0 1px black;
}
.dice-rerolled {
z-index: 2;
position: absolute;
right: -2px;
font-size: 12px;
cursor: help;
}
.dice-inner-container {
display: flex;
align-items: center;
justify-content: center;
position: relative;
&.hope,
&.fear {
.dice-wrapper {
clip-path: polygon(
50% 0%,
80% 10%,
100% 35%,
100% 70%,
80% 90%,
50% 100%,
20% 90%,
0% 70%,
0% 35%,
20% 10%
);
}
}
.dice-wrapper {
height: 24px;
width: 24px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
.dice {
height: 26px;
width: 26px;
max-width: unset;
position: absolute;
}
}
.dice-value {
position: absolute;
font-weight: bold;
font-size: 16px;
}
&.hope {
.dice-wrapper {
background: black;
.dice {
filter: brightness(0) saturate(100%) invert(79%) sepia(79%) saturate(333%)
hue-rotate(352deg) brightness(102%) contrast(103%);
}
}
.dice-value {
color: var(--color-dark-1);
text-shadow: 0 0 4px white;
}
}
&.fear {
.dice-wrapper {
background: white;
.dice {
filter: brightness(0) saturate(100%) invert(12%) sepia(88%) saturate(4321%)
hue-rotate(221deg) brightness(92%) contrast(110%);
}
}
.dice-value {
color: var(--color-light-1);
text-shadow: 0 0 4px black;
}
}
&.advantage {
.dice-wrapper {
.dice {
filter: brightness(0) saturate(100%) invert(18%) sepia(92%) saturate(4133%)
hue-rotate(96deg) brightness(104%) contrast(107%);
}
}
}
&.disadvantage {
.dice-wrapper {
.dice {
filter: brightness(0) saturate(100%) invert(9%) sepia(78%) saturate(6903%)
hue-rotate(11deg) brightness(93%) contrast(117%);
}
}
}
}
}
}
}
.damage-resource {
font-weight: 600;
margin-top: 5px;
}
}
.dice-total {
&.duality {
&.hope {
border-color: @hope;
border-width: 3px;
background: rgba(@hope, 0.5);
}
&.fear {
border-color: @fear;
border-width: 3px;
background: rgba(@fear, 0.5);
}
&.critical {
border-color: @critical;
border-width: 3px;
background: rgba(@critical, 0.5);
}
}
.dice-total-value {
.hope {
color: @hope;
}
.fear {
color: @fear;
}
.critical {
color: @critical;
}
}
}
.dice-total-label {
font-size: 12px;
font-weight: bold;
font-variant: all-small-caps;
margin: -@fullMargin 0;
}
.target-selection {
display: flex;
justify-content: space-around;
input[type='radio'] {
display: none;
&:checked + label {
text-shadow: 0px 0px 4px #ce5937;
}
&:not(:checked) + label {
opacity: 0.75;
}
}
label {
cursor: pointer;
opacity: 0.75;
&.target-selected {
text-shadow: 0px 0px 4px #ce5937;
opacity: 1;
}
}
}
.target-section {
margin-top: 5px;
.target-container {
display: flex;
transition: all 0.2s ease-in-out;
&:hover {
filter: drop-shadow(0 0 3px @secondaryShadow);
border-color: gold;
}
&.hidden {
display: none;
border: 0;
}
&.hit {
background: @hit;
}
&.miss {
background: @miss;
}
img,
.target-save-container {
width: 22px;
height: 22px;
align-self: center;
border-color: transparent;
}
img {
flex: 0;
margin-left: 8px;
}
.target-save-container {
margin-right: 8px;
justify-content: center;
display: flex;
align-items: center;
min-height: unset;
border: 1px solid black;
}
.target-inner-container {
flex: 1;
display: flex;
justify-content: center;
font-size: var(--font-size-16);
}
&:not(:has(.target-save-container)) .target-inner-container {
margin-right: @hugeMargin;
}
}
}
.dice-actions {
display: flex;
gap: 4px;
button {
flex: 1;
height: 40px;
font-family: @font-body;
font-weight: 600;
}
}
.dice-result {
.roll-damage-button,
.damage-button,
.duality-action {
margin-top: 5px;
}
}
&:not(.expanded) .dice-tooltip {
grid-template-rows: 0fr;
}
} */
button { button {
&.inner-button { &.inner-button {
--button-size: 1.25rem; --button-size: 1.25rem;
@ -349,19 +22,6 @@
} }
} }
} }
[data-use-perm='false'] {
pointer-events: none;
border-color: transparent;
}
[data-view-perm='false'] {
> * {
display: none;
}
&::after {
content: '??';
}
}
} }
.daggerheart, .daggerheart,
@ -370,6 +30,20 @@
--text-color: light-dark(@dark-blue, @golden); --text-color: light-dark(@dark-blue, @golden);
--bg-color: light-dark(@dark-blue-40, @golden-40); --bg-color: light-dark(@dark-blue-40, @golden-40);
[data-use-perm='false'] {
pointer-events: none;
border-color: transparent;
}
[data-view-perm='false'] {
&[data-perm-hidden='true'],
> * {
display: none;
}
&::after {
content: '??';
}
}
&.duality { &.duality {
&.hope { &.hope {
--text-color: @golden; --text-color: @golden;
@ -412,7 +86,7 @@
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
align-items: center; align-items: center;
color: light-dark(@dark, @beige); color: light-dark(@dark, @beige);
margin: 5px 0; margin: 10px 0;
span { span {
display: flex; display: flex;
@ -450,7 +124,6 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 5px 0;
.dice-tooltip { .dice-tooltip {
width: 100%; width: 100%;
@ -489,6 +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: 0.75;
.roll-result-value { .roll-result-value {
font-size: var(--font-size-24); font-size: var(--font-size-24);
@ -503,10 +177,6 @@
} }
} }
} }
.roll-difficulty {
margin-top: -5px;
}
} }
} }
@ -584,7 +254,7 @@
.button-target-selection { .button-target-selection {
flex: 1; flex: 1;
text-align: center; text-align: center;
padding: 5px 0; margin: -5px 0;
} }
.button-target-selection:hover, .button-target-selection:hover,
@ -604,6 +274,11 @@
width: 100%; width: 100%;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
border-radius: 3px;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.target-img { .target-img {
border-radius: 50%; border-radius: 50%;
@ -644,6 +319,7 @@
padding: 3px 5px; padding: 3px 5px;
width: fit-content; width: fit-content;
margin: auto; margin: auto;
white-space: nowrap;
} }
.roll-difficulty, .roll-difficulty,
@ -710,6 +386,19 @@
margin-top: 0; margin-top: 0;
} }
.damage-section[data-action='expandRoll'] {
.on-reduced {
.wrapper {
flex-wrap: wrap;
gap: 10px 5px;
}
.roll-formula {
font-size: var(--font-size-16);
}
}
}
.target-section { .target-section {
.roll-part-content { .roll-part-content {
gap: 10px; gap: 10px;

View file

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

View file

@ -5,25 +5,29 @@
<li class="scalable-input"> <li class="scalable-input">
<div class="form-group span-2"> <div class="form-group span-2">
<div class="form-fields nest-inputs"> <div class="form-fields nest-inputs">
<input name="uses.enabled" type="checkbox"{{#if uses.enabled}} checked{{/if}}> <input id="action-uses" name="uses.enabled" type="checkbox"{{#if uses.enabled}} checked{{/if}}>
<label for="uses.enabled">Uses{{#if uses.consumeOnSuccess}}<span class="hint">{{localize "DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.short"}}{{/if}}</span></label> <label for="action-uses">Uses{{#if uses.consumeOnSuccess}}<span class="hint">{{localize "DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.short"}}{{/if}}</span></label>
</div> </div>
</div> </div>
<label class="modifier-label">1/{{uses.remaining}}</label> <label class="modifier-label" for="action-uses">1/{{uses.remaining}}</label>
</li> </li>
{{/if}} {{/if}}
{{#each costs as | cost index |}} {{#each costs as | cost index |}}
<li class="scalable-input"> <li class="scalable-input">
<div class="form-group{{#unless (and scalable maxStep)}} span-2{{/unless}}"> <div class="form-group{{#unless (and scalable maxStep)}} span-2{{/unless}}">
<div class="form-fields nest-inputs"> <div class="form-fields nest-inputs">
<input name="costs.{{index}}.enabled" type="checkbox"{{#if enabled}} checked{{/if}}> <input id="action-costs-{{index}}" name="costs.{{index}}.enabled" type="checkbox"{{#if enabled}} checked{{/if}}>
<label>{{label}}{{#if cost.consumeOnSuccess}}<span class="hint">{{localize "DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.short"}}</span>{{/if}}</label> <label for="action-costs-{{index}}">
{{label}}
{{#if cost.consumeOnSuccess}}<span class="hint">{{localize "DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.short"}}</span>{{/if}}
{{#if cost.extKey}}<span class="hint">{{name}}</span>{{/if}}
</label>
</div> </div>
</div> </div>
{{#if (and scalable maxStep)}} {{#if (and scalable maxStep)}}
<input type="range" value="{{scale}}" min="0" max="{{maxStep}}" step="1" name="costs.{{index}}.scale" data-tooltip="{{localize "DAGGERHEART.ACTIONS.Settings.cost.stepTooltip" step=step}}" data-tooltip-direction="UP"> <input type="range" value="{{scale}}" min="0" max="{{maxStep}}" step="1" name="costs.{{index}}.scale" data-tooltip="{{localize "DAGGERHEART.ACTIONS.Settings.cost.stepTooltip" step=step}}" data-tooltip-direction="UP">
{{/if}} {{/if}}
<label class="modifier-label">{{total}}/{{max}}</label> <label class="modifier-label" for="action-costs-{{index}}">{{total}}/{{max}}</label>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ Parameters:
<legend> <legend>
{{localize title}} {{localize title}}
{{#if canCreate}} {{#if canCreate}}
<a data-action="{{ifThen (or (eq type 'effect') (eq type 'feature')) 'createDoc' 'addNewItem' }}" data-document-class="{{ifThen (eq type 'effect') 'ActiveEffect' 'Item' }}" <a data-action="{{ifThen (or (eq type 'effect') (eq type 'feature') (eq type 'action')) 'createDoc' 'addNewItem' }}" data-document-class="{{ifThen (eq type 'effect') 'ActiveEffect' 'Item' }}"
data-type="{{ifThen (eq type 'effect') 'base' type}}" data-type="{{ifThen (eq type 'effect') 'base' type}}"
{{#if inVault}}data-in-vault="{{inVault}}"{{/if}} {{#if inVault}}data-in-vault="{{inVault}}"{{/if}}
{{#if disabled}} data-disabled="{{disabled}}"{{/if}} {{#if disabled}} data-disabled="{{disabled}}"{{/if}}

View file

@ -3,17 +3,19 @@
<header class="message-header flexrow"> <header class="message-header flexrow">
<div class="message-header-main"> <div class="message-header-main">
<img class="actor-img" src="{{actor.img}}" /> <img class="actor-img" src="{{actor.img}}" />
{{#if (eq message.type 'base')}} <div class="message-sub-header-container">
<div class="message-sub-header-container"> {{#unless actor.name}}
<h4>{{actor.name}}</h4> <h4>{{author.name}}</h4>
<div>{{author.name}}</div> {{else}}
</div> {{#if (eq message.type 'base')}}
{{else}} <h4>{{actor.name}}</h4>
<div class="message-sub-header-container"> <div>{{author.name}}</div>
<h4>{{ifThen message.title message.title alias}}</h4> {{else}}
<div>{{actor.name}} {{#if author.isGM}}(GM){{/if}}</div> <h4>{{ifThen message.title message.title alias}}</h4>
</div> <div>{{actor.name}} {{#if author.isGM}}(GM){{/if}}</div>
{{/if}} {{/if}}
{{/unless}}
</div>
</div> </div>
<div class="message-header-metadata"> <div class="message-header-metadata">
<span class="message-metadata"> <span class="message-metadata">
@ -33,13 +35,12 @@
{{#if isWhisper}} {{#if isWhisper}}
<span class="whisper-to">{{localize 'CHAT.To'}}: {{whisperTo}}</span> <span class="whisper-to">{{localize 'CHAT.To'}}: {{whisperTo}}</span>
{{/if}} {{/if}}
{{#if message.flavor}}
<span class="flavor-text">{{{message.flavor}}}</span>
{{/if}}
</div> </div>
</header> </header>
<div class="message-content"> <div class="message-content">
{{{message.content}}} {{{message.content}}}
{{#if message.flavor}}
<span class="flavor-text">{{{message.flavor}}}</span>
{{/if}}
</div> </div>
</li> </li>

View file

@ -1,17 +1,17 @@
<div class="roll-buttons"> <div class="roll-buttons">
{{#if hasDamage}} {{#if hasDamage}}
{{#unless (empty damage)}} {{#unless (empty damage)}}
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.damageRoll.dealDamage"}}</button> {{#if canButtonApply}}<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.damageRoll.dealDamage"}}</button>{{/if}}
{{else}} {{else}}
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollDamage"}}</button> <button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollDamage"}}</button>
{{/unless}} {{/unless}}
{{/if}} {{/if}}
{{#if hasHealing}} {{#if hasHealing}}
{{#unless (empty damage)}} {{#unless (empty damage)}}
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.healingRoll.applyHealing"}}</button> {{#if canButtonApply}}<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.healingRoll.applyHealing"}}</button>{{/if}}
{{else}} {{else}}
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollHealing"}}</button> <button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollHealing"}}</button>
{{/unless}} {{/unless}}
{{/if}} {{/if}}
{{#if hasEffect}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}} {{#if (and hasEffect canButtonApply)}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}}
</div> </div>

View file

@ -14,14 +14,15 @@
</div> </div>
{{#if roll.difficulty}} {{#if roll.difficulty}}
<span class="roll-difficulty{{#unless roll.success}} is-miss{{/unless}}"> <span class="roll-difficulty{{#unless roll.success}} is-miss{{/unless}}">
{{#if canViewSecret}} {{!-- {{#if canViewSecret}} --}}
difficulty {{roll.difficulty}} difficulty {{roll.difficulty}}
{{else}} {{!-- {{else}}
{{localize (ifThen roll.success "DAGGERHEART.GENERAL.success" "DAGGERHEART.GENERAL.failure")}} {{localize (ifThen roll.success "DAGGERHEART.GENERAL.success" "DAGGERHEART.GENERAL.failure")}}
{{/if}} {{/if}} --}}
</span> </span>
{{/if}} {{/if}}
</div> </div>
{{#unless isPrivate}}
<div class="dice-roll" data-action="expandRoll"> <div class="dice-roll" data-action="expandRoll">
<div class="roll-part-header"><div><span>{{localize "DAGGERHEART.GENERAL.formula"}}</span></div></div> <div class="roll-part-header"><div><span>{{localize "DAGGERHEART.GENERAL.formula"}}</span></div></div>
<div class="roll-part-content dice-result"> <div class="roll-part-content dice-result">
@ -56,7 +57,7 @@
{{/if}} {{/if}}
{{#if roll.rally.dice}} {{#if roll.rally.dice}}
<div class="roll-die has-plus"> <div class="roll-die has-plus">
<label>{{localize "DAGGERHEART.GENERAL.fear"}}</label> <label>{{localize "DAGGERHEART.CLASS.Feature.short"}}</label>
<div class="dice {{roll.rally.dice}}">{{roll.rally.value}}</div> <div class="dice {{roll.rally.dice}}">{{roll.rally.value}}</div>
</div> </div>
{{/if}} {{/if}}
@ -87,4 +88,5 @@
</div> </div>
</div> </div>
</div> </div>
{{/unless}}
</div> </div>

View file

@ -14,12 +14,12 @@
<div class="roll-part-content dice-result"> <div class="roll-part-content dice-result">
<div class="dice-tooltip"> <div class="dice-tooltip">
<div class="wrapper"> <div class="wrapper">
{{#if targets.length}} {{#if (and parent.isAuthor targets.length)}}
<div class="target-selector"> <div class="target-selector">
<div class="roll-part-header"><div></div></div> <div class="roll-part-header"><div></div></div>
<div class="target-choice"> <div class="target-choice">
<div class="button-target-selection{{#if targetSelection}} target-selected{{/if}}" data-target-hit="true">{{localize "DAGGERHEART.UI.Chat.damageRoll.hitTarget"}}</div> <div class="button-target-selection{{#if targetMode}} target-selected{{/if}}" data-target-hit="true">{{localize "DAGGERHEART.UI.Chat.damageRoll.hitTarget"}}</div>
<div class="button-target-selection{{#unless targetSelection}} target-selected{{/unless}}">{{localize "DAGGERHEART.UI.Chat.damageRoll.currentTarget"}}</div> <div class="button-target-selection{{#unless targetMode}} target-selected{{/unless}}">{{localize "DAGGERHEART.UI.Chat.damageRoll.currentTarget"}}</div>
</div> </div>
<div class="roll-part-header"><div></div></div> <div class="roll-part-header"><div></div></div>
</div> </div>
@ -29,8 +29,8 @@
<div class="roll-target" data-token="{{id}}"> <div class="roll-target" data-token="{{id}}">
<img class="target-img" src="{{img}}"> <img class="target-img" src="{{img}}">
<div class="target-data"> <div class="target-data">
<div class="target-name" data-perm-id="{{actorId}}">{{name}}</div> <div class="target-name" data-perm-id="{{actorId}}"><span>{{name}}</span></div>
{{#if (and ../targetSelection ../hasRoll)}} {{#if (and ../targetMode ../hasRoll)}}
<div class="target-hit-status {{#if hit}}is-hit{{else}}is-miss{{/if}}"> <div class="target-hit-status {{#if hit}}is-hit{{else}}is-miss{{/if}}">
{{#if hit}} {{#if hit}}
{{localize "DAGGERHEART.GENERAL.hit.single"}} {{localize "DAGGERHEART.GENERAL.hit.single"}}
@ -40,7 +40,7 @@
</div> </div>
{{/if}} {{/if}}
</div> </div>
{{#if (and ../hasSave (or hit (not @root.targetSelection)))}} {{#if (and ../hasSave (or hit (not @root.hasRoll)))}}
<div class="target-save{{#if saved.result includeZero=true}} is-rolled{{/if}}" data-perm-id="{{actorId}}"> <div class="target-save{{#if saved.result includeZero=true}} is-rolled{{/if}}" data-perm-id="{{actorId}}">
<i class="fa-solid {{#if saved.result includeZero=true}}{{#if saved.success}}fa-check{{else}}fa-xmark{{/if}}{{else}}fa-shield{{/if}} fa-lg"></i> <i class="fa-solid {{#if saved.result includeZero=true}}{{#if saved.success}}fa-check{{else}}fa-xmark{{/if}}{{else}}fa-shield{{/if}} fa-lg"></i>
</div> </div>

View file

@ -5,4 +5,4 @@
{{#if hasTarget}}{{> 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs'}}{{/if}} {{#if hasTarget}}{{> 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs'}}{{/if}}
<div class="roll-part-header"><div></div></div> <div class="roll-part-header"><div></div></div>
</div> </div>
{{> 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs'}} {{#if (or parent.isAuthor canButtonApply)}}{{> 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs'}}{{/if}}