Compare commits

...

11 commits

Author SHA1 Message Date
Carlos Fernandez
907bb8be50 Fix taking damage 2026-07-18 01:06:58 -04:00
Carlos Fernandez
2ef78f2c89 Prevent assigning main damage on healing actions 2026-07-18 00:16:51 -04:00
Carlos Fernandez
23ebe6f6b5 Fix adversary damage 2026-07-17 23:55:05 -04:00
Carlos Fernandez
6ae07bd3a7 Fix newly created adversaries lacking damage 2026-07-17 21:33:40 -04:00
Carlos Fernandez
b833be4f1f Fix tags fix 2026-07-17 21:19:38 -04:00
Carlos Fernandez
c5a64c2def Move type to main damage only 2026-07-17 21:12:35 -04:00
WBHarry
37f64ed023 Merge branch 'rework/damage-and-damage-resource-split' of https://github.com/Foundryborne/daggerheart into rework/damage-and-damage-resource-split 2026-07-18 03:09:44 +02:00
WBHarry
42ec4f8c30 Fixed ChatMessage damage creation 2026-07-18 03:09:37 +02:00
Carlos Fernandez
ed30fd2122 Fix most issues with damage effects tab 2026-07-17 20:52:00 -04:00
WBHarry
1f12a98c63 Sheet fixes 2026-07-18 00:49:56 +02:00
WBHarry
ce47c63ce6 Corrected ChatDamageData and made damage.main nullable 2026-07-18 00:30:14 +02:00
20 changed files with 480 additions and 376 deletions

View file

@ -51,7 +51,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
const context = await super._prepareContext(_options);
context.config = CONFIG.DH;
context.title = this.config.title ?? this.title;
context.formula = this.roll.constructFormula(this.config);
const { damageFormula, resourceFormulas } = this.roll.constructFormulas(this.config);
context.damageFormula = damageFormula;
context.resourceFormulas = resourceFormulas;
context.hasHealing = this.config.hasHealing;
context.directDamage = this.config.directDamage;
context.selectedMessageMode = this.config.selectedMessageMode;
@ -73,7 +77,8 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
static updateRollConfiguration(_event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, data.roll);
foundry.utils.mergeObject(this.config.damageFormula, data.damageFormula);
foundry.utils.mergeObject(this.config.resourceFormulas, data.resourceFormulas);
foundry.utils.mergeObject(this.config.modifiers, data.modifiers);
this.config.selectedMessageMode = data.selectedMessageMode;

View file

@ -1,4 +1,4 @@
import { getUnusedDamageTypes } from '../../helpers/utils.mjs';
import { DHDamageData } from '../../data/fields/action/damageField.mjs';
import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api;
@ -31,8 +31,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
removeElement: this.removeElement,
removeTransformActor: this.removeTransformActor,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage,
addDamage: this.#onAddDamage,
removeDamage: this.#onRemoveDamage,
addDamageResource: this.#onAddDamageResource,
removeDamageResource: this.#onRemoveDamageResource,
editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
@ -157,9 +159,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
if (this.action.damage) {
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack')
const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes);
context.allDamageTypesUsed = allKeys.every(k => k in this.action._source.damage.resources);
if (this.action.damage?.main?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
}
@ -299,53 +301,71 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addDamage(_event) {
if (!this.action.damage.parts) return;
/** @this DHActionBaseConfig */
static #onAddDamage() {
if (!this.action.damage || this.action.damage?.main) return;
const choices = getUnusedDamageTypes(this.action._source.damage.parts);
const data = this.action.toObject();
data.damage.main = {
...DHDamageData.schema.getInitialValue(),
applyTo: 'hitPoints',
type: 'physical'
};
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** @this DHActionBaseConfig */
static #onRemoveDamage() {
if (!this.action.damage?.main) return;
const data = this.action.toObject();
data.damage.main = null;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** @this DHActionBaseConfig */
static #onAddDamageResource(_event) {
if (!this.action.damage) return;
const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes);
const unused = allKeys.filter(k => !(k in this.action._source.damage.resources));
const choices = unused.map(k => ({ value: k, label: _loc(CONFIG.DH.GENERAL.healingTypes[k].label) }));
const content = new foundry.data.fields.StringField({
label: game.i18n.localize('Damage Type'),
label: _loc('DAGGERHEART.GENERAL.damageType'),
choices,
required: true
}).toFormGroup(
{},
{
}).toFormGroup({}, {
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}
).outerHTML;
}).outerHTML;
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue();
part.applyTo = type;
if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial;
data.damage.parts[type] = part;
data.damage.resources[type] = {
...this.action.schema.fields.damage.fields.resources.element.getInitialValue(),
applyTo: type
};
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
};
const typeDialog = new foundry.applications.api.DialogV2({
buttons: [
foundry.utils.mergeObject(
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true
},
{ callback: callback }
)
default: true,
callback
}
],
content: content,
rejectClose: false,
modal: false,
window: {
title: game.i18n.localize('Add Damage')
/** @todo localize */
title: 'Add Damage'
},
position: { width: 300 }
});
@ -353,12 +373,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
typeDialog.render(true);
}
static removeDamage(_event, button) {
if (!this.action.damage.parts) return;
/** @this DHActionBaseConfig */
static #onRemoveDamageResource(_event, button) {
if (!this.action.damage?.resources) return;
const data = this.action.toObject();
const key = button.dataset.key;
delete data.damage.parts[key];
data.damage.parts[`${key}`] = _del;
data.damage.resources[key] = _del;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}

View file

@ -1,3 +1,4 @@
import { DHDamageData } from '../../data/fields/action/damageField.mjs';
import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -8,8 +9,10 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
classes: ['adversary-settings'],
position: { width: 455, height: 'auto' },
actions: {
addExperience: DHAdversarySettings.#addExperience,
removeExperience: DHAdversarySettings.#removeExperience
addExperience: DHAdversarySettings.#onAddExperience,
removeExperience: DHAdversarySettings.#onRemoveExperience,
addDamage: this.#onAddDamage,
removeDamage: this.#onRemoveDamage
}
};
@ -71,7 +74,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
* Adds a new experience entry to the actor.
* @type {ApplicationClickAction}
*/
static async #addExperience() {
static async #onAddExperience() {
const newExperience = {
name: 'Experience',
modifier: 0
@ -83,7 +86,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
* Removes an experience entry from the actor.
* @type {ApplicationClickAction}
*/
static async #removeExperience(_, target) {
static async #onRemoveExperience(_, target) {
const experience = this.actor.system.experiences[target.dataset.experience];
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
@ -98,4 +101,28 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del });
}
/**
* @this DHAdversarySettings
* @type {ApplicationClickAction}
*/
static #onAddDamage() {
this.actor.update({
'system.attack.damage.main': {
...DHDamageData.schema.getInitialValue(),
applyTo: 'hitPoints',
type: 'physical'
}
});
}
/**
* @this DHAdversarySettings
* @type {ApplicationClickAction}
*/
static #onRemoveDamage() {
this.actor.update({
'system.attack.damage.main': null
});
}
}

View file

@ -73,7 +73,7 @@ export default class DHAttackAction extends DHDamageAction {
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active;
for (const { value, valueAlt, type } of damage.parts) {
for (const { value, valueAlt, type } of [damage.main, ...damage.resources].filter(d => !!d)) {
const usedValue = useAltDamage ? valueAlt : value;
const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
const str = damageString
@ -82,7 +82,7 @@ export default class DHAttackAction extends DHDamageAction {
x: game.i18n.localize('DAGGERHEART.GENERAL.damage')
});
const icons = Array.from(type)
const icons = Array.from(type ?? [])
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
.filter(Boolean);

View file

@ -429,11 +429,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
get hasDamage() {
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing';
return this.type !== 'healing' && Boolean(this.damage.main) || Boolean(this.damage.resources.length);
}
get hasHealing() {
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing';
return this.type === 'healing' && Boolean(this.damage.main) || Boolean(this.damage.resources.length);
}
get hasSave() {
@ -470,10 +470,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}, {});
}
if (source.damage && source.damage.resources === undefined) {
if (source.damage?.parts && !source.damage.resources && !source.damage.main) {
source.damage.main = null;
source.damage.resources = {};
for (const [partKey, part] of Object.entries(source.damage.parts)) {
if (partKey === 'hitPoints') {
if (partKey === 'hitPoints' && source.type !== 'healing') {
source.damage.main = {
...part,
includeBase: source.damage.includeBase,
@ -484,6 +485,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
source.damage.resources[partKey] = part;
}
}
delete source.damage.parts;
}
}
}

View file

@ -8,11 +8,8 @@ export default class DHDamageAction extends DHBaseAction {
* @returns Formula string
*/
getDamageFormula() {
const strings = [];
for (const { value } of this.damage.parts) {
strings.push(Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}));
}
if (!this.damage.main) return '';
return strings.join(' + ');
return Roll.replaceFormulaData(this.damage.main.value.getFormula(), this.actor?.getRollData() ?? {});
}
}

View file

@ -84,8 +84,7 @@ export default class DhpAdversary extends DhCreature {
type: 'attack'
},
damage: {
parts: {
hitPoints: {
main: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
@ -94,7 +93,6 @@ export default class DhpAdversary extends DhCreature {
}
}
}
}
}),
experiences: new fields.TypedObjectField(
new fields.SchemaField({

View file

@ -104,8 +104,7 @@ export default class DhCharacter extends DhCreature {
trait: 'strength'
},
damage: {
parts: {
hitPoints: {
main: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
@ -117,7 +116,6 @@ export default class DhCharacter extends DhCreature {
}
}
}
}
}),
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({

View file

@ -188,19 +188,28 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
}
static migrateData(source) {
if (source.hasDamage && !source.damage.types) {
source.damage = {
types: Object.keys(source.damage).reduce((acc, key) => {
if (source.hasDamage && !source.damage.resources === undefined) {
const getRoll = key => {
const damageData = source.damage[key];
const oldRoll = damageData.parts[0]?.roll;
acc[key] = oldRoll ? {
return oldRoll ? {
...oldRoll,
options: {
...oldRoll.options,
damageTypes: damageData.parts[0].damageTypes ?? []
}
} : null;
};
source.damage = {
main: source.damage.hitPoints ? getRoll('hitPoints') : null,
resources: Object.keys(source.damage).reduce((acc, key) => {
if (key === 'hitPoints') return acc;
const roll = getRoll(key);
if (!roll) return acc;
acc[key] = roll;
return acc;
}, {})
};

View file

@ -11,27 +11,29 @@ export class ChatDamageData extends foundry.abstract.DataModel {
const fields = foundry.data.fields;
return {
damage: new fields.JSONField({validate: ChatDamageData.#validateRoll}),
main: new fields.JSONField({ nullable: true, validate: ChatDamageData.#validateRoll}),
resources: new fields.TypedObjectField(new fields.JSONField({validate: ChatDamageData.#validateRoll}))
};
}
get active() {
return Boolean(Object.keys(this.types).length);
return !!this.main || Boolean(Object.keys(this.resources).length);
}
static #validateRoll(rollJSON) {
if (rollJSON) {
const roll = JSON.parse(rollJSON);
if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated');
}
}
_prepareRolls() {
for (const key of Object.keys(this.types)) {
const type = this.types[key];
try {
this.types[key] = Roll.fromData(type);
this.types[key].options.modifierTotal = CONFIG.Dice.daggerheart.DHRoll.calculateTotalModifiers(type);
} catch {}
if (this.main) {
this.main = Roll.fromData(this.main);
}
for (const key of Object.keys(this.resources)) {
this.resources[key] = Roll.fromData(this.resources[key]);
}
}

View file

@ -12,11 +12,10 @@ export default class DamageField extends fields.SchemaField {
/** @inheritDoc */
constructor(options, context = {}) {
const damageFields = {
main: new fields.EmbeddedDataField(DHDamageData),
super({
main: new fields.EmbeddedDataField(DHDamageData, { nullable: true }),
resources: new IterableTypedObjectField(DHResourceData)
};
super(damageFields, options, context);
}, options, context);
}
/**
@ -32,25 +31,23 @@ export default class DamageField extends fields.SchemaField {
this.hasRoll &&
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
!force
)
) {
return;
}
let formulas = this.damage.parts.map(p => ({
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
applyTo: p.applyTo
}));
const damageFormula = this.damage.main ?
DamageField.formatFormulas.call(this, [this.damage.main], config)[0] : null;
const resourceFormulas = DamageField.formatFormulas.call(this, this.damage.resources, config);
if (!formulas.length) return false;
formulas = DamageField.formatFormulas.call(this, formulas, config);
if (!damageFormula && !resourceFormulas.length) return false;
messageId = config.message?._id ?? messageId;
const message = game.messages.get(messageId);
const damageConfig = {
dialog: {},
...config,
roll: formulas,
damageFormula,
resourceFormulas,
data: this.getRollData(),
isCritical: Boolean(message?.system.roll?.isCritical)
};
@ -83,7 +80,7 @@ export default class DamageField extends fields.SchemaField {
const targetDamage = [];
const damagePromises = [];
for (let target of targets) {
for (const target of targets) {
const actor = foundry.utils.fromUuidSync(target.actorId);
if (!actor) continue;
if (!config.hasHealing && config.onSave && target.saved?.success === true) {
@ -105,14 +102,12 @@ export default class DamageField extends fields.SchemaField {
actor.takeHealing(config.damage.types).then(updates => targetDamage.push({ token, updates }))
);
else {
const configDamage = foundry.utils.deepClone(config.damage.types);
const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
if (configDamage.hitPoints) {
configDamage.hitPoints = configDamage.hitPoints.toJSON();
configDamage.hitPoints.total = Math.ceil(
configDamage.hitPoints.total * hpDamageMultiplier * hpDamageTakenMultiplier
);
const configDamage = config.damage.clone();
configDamage.main &&= configDamage.main.toJSON();
if (configDamage.main) {
const multiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
const takenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
configDamage.main.total = Math.ceil(configDamage.main.total * multiplier * takenMultiplier);
}
damagePromises.push(
@ -175,11 +170,17 @@ export default class DamageField extends fields.SchemaField {
/**
* Prepare formulas for Damage Roll
* Must be called within Action context or similar.
* @param {object[]} formulas Array of formatted formulas object
* @param {DHResourceData[]} damageData Array of DHResourceData
* @param {object} data Action getRollData
* @returns
*/
static formatFormulas(formulas, data) {
static formatFormulas(damageData, data) {
const formulas = damageData.map(x => ({
formula: DamageField.getFormulaValue.call(this, x, data).getFormula(this.actor),
damageTypes: x.applyTo === 'hitPoints' && !x.type.size ? new Set(['physical']) : x.type,
applyTo: x.applyTo
}));
const formattedFormulas = [];
formulas.forEach(formula => {
if (isNaN(formula.formula))
@ -190,6 +191,7 @@ export default class DamageField extends fields.SchemaField {
if (same) same.formula += ` + ${formula.formula}`;
else formattedFormulas.push(formula);
});
return formattedFormulas;
}
@ -279,17 +281,6 @@ export class DHResourceData extends foundry.abstract.DataModel {
static defineSchema() {
return {
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
type: new fields.SetField(
new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes,
initial: 'physical',
nullable: false,
required: true
}),
{
label: game.i18n.localize('DAGGERHEART.GENERAL.type')
}
),
applyTo: new fields.StringField({
choices: CONFIG.DH.GENERAL.healingTypes,
required: true,
@ -321,7 +312,18 @@ export class DHDamageData extends DHResourceData {
choices: CONFIG.DH.GENERAL.groupAttackRange,
blank: true,
label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label'
})
}),
type: new fields.SetField(
new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes,
initial: 'physical',
nullable: false,
required: true
}),
{
label: game.i18n.localize('DAGGERHEART.GENERAL.type')
}
)
};
}
}

View file

@ -67,8 +67,7 @@ export default class DHWeapon extends AttachableItem {
type: 'attack'
},
damage: {
parts: {
hitPoints: {
main: {
type: ['physical'],
value: {
multiplier: 'prof',
@ -77,7 +76,6 @@ export default class DHWeapon extends AttachableItem {
}
}
}
}
}),
rules: new fields.SchemaField({
attack: new fields.SchemaField({
@ -230,11 +228,11 @@ export default class DHWeapon extends AttachableItem {
game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`)
];
for (const { value, type } of attack.damage.parts) {
for (const { value, type } of [attack.damage.main, ...attack.damage.resources]) {
const parts = value.custom.enabled ? [game.i18n.localize('DAGGERHEART.GENERAL.custom')] : [value.dice];
if (!value.custom.enabled && value.bonus) parts.push(value.bonus.signedString());
if (type.size > 0) {
if (type?.size) {
const typeTags = Array.from(type)
.map(t => game.i18n.localize(`DAGGERHEART.CONFIG.DamageType.${t}.abbreviation`))
.join(' | ');
@ -258,10 +256,10 @@ export default class DHWeapon extends AttachableItem {
if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`));
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
for (const { value, type } of damage.parts) {
for (const { value, type } of [damage.main, ...damage.resources].filter(d => !!d)) {
const str = Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {});
const icons = Array.from(type)
const icons = Array.from(type ?? [])
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
.filter(Boolean);

View file

@ -13,16 +13,27 @@ export default class DamageRoll extends DHRoll {
static DefaultDialog = DamageDialog;
static createRollInstance(config) {
return new this(undefined, config.data, config);
}
/** @inheritdoc */
static async buildEvaluate(roll, config = {}) {
if (config.dialog.configure === false) roll.constructFormula(config);
if (config.dialog.configure === false) roll.constructFormulas(config);
for (const roll of config.roll) {
const evaluateRoll = async roll => {
await roll.roll.evaluate();
roll.roll.options = { damageTypes: roll.damageTypes ? [...roll.damageTypes] : [] };
return roll.roll;
}
if (!config.damage?.types) config.damage = { types: {} };
config.damage.types[roll.applyTo] = roll.roll;
config.damage.main = await evaluateRoll(config.damageFormula);
config.damage.main.options = { damageTypes:
config.damageFormula.damageTypes ? [...config.damageFormula.damageTypes] : []
};
for (const roll of config.resourceFormulas) {
config.damage.resources[roll.applyTo] = await evaluateRoll(roll);
}
roll._evaluated = true;
@ -51,7 +62,8 @@ export default class DamageRoll extends DHRoll {
if (config.source?.message) {
chatMessage.update({ 'system.damage': {
...config.damage.toObject(),
types: config.damage.types
main: config.damage.main,
resources: config.damage.resources
}});
}
}
@ -125,38 +137,48 @@ export default class DamageRoll extends DHRoll {
return changeKeys;
}
constructFormula(config) {
constructFormulas(config) {
return {
damageFormula: this.constructFormula(this.options.damageFormula, config, true),
resourceFormulas: this.options.resourceFormulas.map(x => this.constructFormula(x, config))
};
}
constructFormula(formulaData, config, isDamage) {
this.options.isCritical = config.isCritical;
for (const [index, part] of this.options.roll.entries()) {
const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id;
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
part.modifiers = this.applyBaseBonus(part);
this.addModifiers(part);
part.modifiers?.forEach(m => {
part.roll.terms.push(...this.formatModifier(m.value));
const isHitpointPart = formulaData.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id;
formulaData.roll = new Roll(Roll.replaceFormulaData(formulaData.formula, config.data));
formulaData.roll.terms = Roll.parse(formulaData.roll.formula, config.data);
if (formulaData.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
formulaData.modifiers = this.applyBaseBonus(formulaData);
this.addModifiers(formulaData);
formulaData.modifiers?.forEach(m => {
formulaData.roll.terms.push(...this.formatModifier(m.value));
});
}
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
if (isDamage && formulaData.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 (
modifier.beforeCrit === true &&
(modifier.enabled || modifier.value)
) modifier.callback(formulaData);
}
}
if (part.extraFormula) {
part.roll.terms.push(
if (formulaData.extraFormula) {
formulaData.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(part.extraFormula, this.options.data)
...this.constructor.parse(formulaData.extraFormula, this.options.data)
);
}
if (config.damageOptions.groupAttack?.numAttackers > 1 && isHitpointPart) {
const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm];
for (const term of part.roll.terms) {
for (const term of formulaData.roll.terms) {
if (damageTypes.some(type => term instanceof type)) {
term.number *= config.damageOptions.groupAttack.numAttackers;
}
@ -164,23 +186,23 @@ export default class DamageRoll extends DHRoll {
}
if (config.isCritical && isHitpointPart) {
const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
const total = formulaData.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
if (total > 0) {
part.roll.terms.push(...this.formatModifier(total));
formulaData.roll.terms.push(...this.formatModifier(total));
}
}
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
if (isDamage && formulaData.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);
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(formulaData);
}
}
part.roll._formula = this.constructor.getFormula(part.roll.terms);
}
return this.options.roll;
formulaData.roll._formula = this.constructor.getFormula(formulaData.roll.terms);
return formulaData;
}
/* To Remove When Reaction System */

View file

@ -41,6 +41,10 @@ export default class DHRoll extends BaseRoll {
return config;
}
static createRollInstance(config) {
return new this(config.roll.formula, config.data, config);
}
/**
* @param {Partial<RollConfig>} config
* @returns {Promise<RollConfig>}
@ -58,7 +62,7 @@ export default class DHRoll extends BaseRoll {
this.temporaryModifierBuilder(config);
let roll = new this(config.roll.formula, config.data, config);
let roll = this.createRollInstance(config);
if (config.dialog.configure !== false) {
// Open Roll Dialog
const DialogClass = config.dialog?.class ?? this.DefaultDialog;

View file

@ -656,26 +656,32 @@ export default class DhpActor extends Actor {
return;
}
const updates = [];
Object.entries(damages).forEach(([key, damage]) => {
if (key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
damage.total = this.calculateDamage(damage.total, damage.damageTypes);
const update = updates.find(u => u.key === key);
if (update) {
update.value += damage.total;
update.damageTypes.add(...new Set(damage.damageTypes));
} else updates.push({ value: damage.total, key, damageTypes: new Set(damage.damageTypes) });
});
if (damages.main) {
damages.main.total = this.calculateDamage(damages.main.total, damages.main.damageTypes);
}
if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null;
if (!updates.length) return;
// Convert deducted resources and damage to a record of updates, merging damage to hp with hp marked
const updates = [];
for (const [key, damage] of Object.entries(damages.resources)) {
updates.push({ key, value: damage.total });
}
if (damages.main) {
const existing = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
const value = this.convertDamageToThreshold(damages.main.total) + (existing?.value ?? 0);
const damageTypes = new Set(damages.main.options.damageTypes);
if (existing) {
existing.value = value;
existing.damageTypes = damageTypes;
} else {
updates.push({ value, damageTypes, key: CONFIG.DH.GENERAL.healingTypes.hitPoints.id });
}
}
if (!updates.some(u => u.value !== 0)) return; // early return if nothing to do
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (hpDamage?.value) {
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) {
if (hpDamage && this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.total, hpDamage.damageTypes)) {
const armorSlotResult = await this.owner.query(
'armorSlot',
{
@ -689,7 +695,7 @@ export default class DhpActor extends Actor {
);
if (armorSlotResult) {
const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
hpDamage.value = modifiedDamage;
for (const armorChange of armorChanges) {
updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid });
}
@ -699,8 +705,7 @@ export default class DhpActor extends Actor {
else updates.push({ value: stressSpent, key: 'stress' });
}
}
}
if (this.type === 'adversary') {
} else if (hpDamage && this.type === 'adversary') {
const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => {
return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value);
}, 0);
@ -713,7 +718,6 @@ export default class DhpActor extends Actor {
hpDamage.value -= 1;
}
}
}
const results = await game.system.registeredTriggers.runTrigger(
CONFIG.DH.TRIGGER.triggers.postDamageReduction.id,

View file

@ -700,19 +700,6 @@ export async function RefreshFeatures(
return refreshedActors;
}
export function getUnusedDamageTypes(parts) {
const usedKeys = Object.keys(parts);
return Object.keys(CONFIG.DH.GENERAL.healingTypes).reduce((acc, key) => {
if (!usedKeys.includes(key))
acc.push({
value: key,
label: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label)
});
return acc;
}, []);
}
/** Returns resolved armor sources ordered by application order */
export function getArmorSources(actor) {
const rawArmorSources = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData);

View file

@ -1,92 +1,114 @@
{{#unless (eq @root.source.type 'healing')}}
<fieldset class="one-column">
<legend class="with-icon">
{{localize "DAGGERHEART.GENERAL.damage"}}
{{#if source.main}}
<a data-action="removeDamage"><i class="fa-solid fa-trash icon-button"></i></a>
{{else}}
<a data-action="addDamage"><i class="fa-solid fa-plus icon-button"></i></a>
{{/if}}
</legend>
<fieldset class="one-column">
{{#if source.main}}
<div class="nest-inputs">
{{#if @root.hasBaseDamage}}
{{formField @root.fields.damage.fields.main.fields.includeBase value=@root.source.damage.includeBase name="damage.main.includeBase" classes="checkbox" localize=true }}
{{/if}}
{{#unless (eq @root.source.type 'healing')}}
{{formField baseFields.main.fields.direct value=source.main.direct name=(concat path "damage.main.direct") localize=true classes="checkbox"}}
{{/unless}}
{{#if (and @root.isNPC (not (eq path 'system.attack.')))}}
{{formField baseFields.main.fields.groupAttack value=source.main.groupAttack name=(concat path "damage.main.groupAttack") localize=true classes="select"}}
{{/if}}
</div>
{{> damageData damage=source.main fields=fields.main.fields basePath=(concat path "damage.main")}}
{{#if horde}}
{{> hordeDamage source=source.main fields=fields.main.fields basePath=(concat path "damage.main")}}
{{/if}}
{{#if (ne @root.source.type 'healing')}}
{{formField fields.main.fields.type value=source.main.type name=(concat path "damage.main.type") localize=true}}
{{/if}}
{{/if}}
</fieldset>
{{/unless}}
{{#unless (eq path 'system.attack.')}}
{{! In the future, consider allowing this even on NPCs}}
<fieldset class="one-column">
<legend class="with-icon">
{{#if (eq @root.source.type 'healing')}}
{{localize "DAGGERHEART.GENERAL.healing"}}
{{else}}
{{localize "DAGGERHEART.GENERAL.damage"}}
{{localize "DAGGERHEART.GENERAL.Resource.plural"}}
{{/if}}
{{#unless (eq path 'system.attack.')}}<a data-action="addDamage" {{#if @root.allDamageTypesUsed}}disabled{{/if}}><i class="fa-solid fa-plus icon-button"></i></a>{{/unless}}
{{#unless @root.allDamageTypesUsed}}<a data-action="addDamageResource"><i class="fa-solid fa-plus icon-button"></i></a>{{/unless}}
</legend>
{{#each source.resources as |dmg key|}}
<div class="nest-inputs">
{{#if @root.hasBaseDamage}}
{{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase name="damage.includeBase" classes="checkbox" localize=true }}
{{/if}}
{{#unless (eq @root.source.type 'healing')}}
{{formField baseFields.direct value=source.direct name=(concat path "damage.direct") localize=true classes="checkbox"}}
{{/unless}}
{{#if (and @root.isNPC (not (eq path 'system.attack.')))}}
{{formField baseFields.groupAttack value=source.groupAttack name=(concat path "damage.groupAttack") localize=true classes="select"}}
{{/if}}
</div>
{{!-- Handlebars uses Symbol.Iterator to produce index|key. This isn't compatible with our parts object, so we instead use applyTo, which is the same value --}}
{{#each source.parts as |dmg key|}}
<div class="nest-inputs">
<fieldset{{#if dmg.base}} disabled{{/if}} class="one-column{{#if ../path}} no-style{{/if}}">
<fieldset{{#if dmg.base}} disabled{{/if}} class="one-column">
<legend class="with-icon">
{{localize (concat "DAGGERHEART.CONFIG.HealingType." dmg.applyTo ".name")}}
{{#unless (or dmg.base ../path)}}
<a data-action="removeDamage" data-key="{{dmg.applyTo}}"><i class="fas fa-trash"></i></a>
<a data-action="removeDamageResource" data-key="{{key}}"><i class="fas fa-trash"></i></a>
{{/unless}}
</legend>
{{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}}
{{formField ../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." dmg.applyTo ".resultBased") localize=true classes="checkbox"}}
{{/if}}
{{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}}
<div class="nest-inputs">
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}</legend>
{{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}}
</fieldset>
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}</legend>
{{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" key=dmg.applyTo path=../path}}
</fieldset>
</div>
{{else}}
{{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}}
{{/if}}
{{#if (and (eq dmg.applyTo 'hitPoints') (ne @root.source.type 'healing'))}}
{{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." dmg.applyTo ".type") localize=true}}
{{/if}}
{{#if ../horde}}
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}}</legend>
<div class="nest-inputs">
<input type="hidden" name="{{../path}}damage.parts.{{dmg.applyTo}}.valueAlt.multiplier" value="flat">
{{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }}
{{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.dice") classes="inline-child" localize=true}}
{{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." dmg.applyTo ".valueAlt.bonus") localize=true classes="inline-child"}}
</div>
</fieldset>
{{/if}}
<input type="hidden" name="{{concat ../path "damage.parts." dmg.applyTo ".base"}}" value="{{dmg.base}}">
{{> damageData damage=dmg fields=../fields.resources.element.fields basePath=(concat ../path "damage.resources." dmg.applyTo)}}
</fieldset>
</div>
{{/each}}
</fieldset>
</fieldset>
{{/unless}}
{{#*inline "formula"}}
{{#unless dmg.base}}
{{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat path "damage.parts." key "." target ".custom.enabled") classes="checkbox" localize=true}}
{{#unless isBase}}
{{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat basePath ".custom.enabled") classes="checkbox" localize=true}}
{{/unless}}
{{#if source.custom.enabled}}
{{formField fields.custom.fields.formula value=source.custom.formula name=(concat path "damage.parts." key "." target ".custom.formula") localize=true}}
{{formField fields.custom.fields.formula value=source.custom.formula name=(concat basePath ".custom.formula") localize=true}}
{{else}}
<div class="nest-inputs">
{{#unless @root.isNPC}}
{{formField fields.multiplier value=source.multiplier name=(concat path "damage.parts." key "." target ".multiplier") localize=true}}
{{formField fields.multiplier value=source.multiplier name=(concat basePath ".multiplier") localize=true}}
{{/unless}}
{{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat path "damage.parts." key "." target ".flatMultiplier") localize=true }}{{/if}}
{{formField fields.dice value=source.dice name=(concat path "damage.parts." key "." target ".dice") localize=true}}
{{formField fields.bonus value=source.bonus name=(concat path "damage.parts." key "." target ".bonus") localize=true}}
{{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat basePath ".flatMultiplier") localize=true }}{{/if}}
{{formField fields.dice value=source.dice name=(concat basePath ".dice") localize=true}}
{{formField fields.bonus value=source.bonus name=(concat basePath ".bonus") localize=true}}
</div>
{{/if}}
{{#if @root.isNPC}}
<input type="hidden" name="{{path}}damage.parts.{{key}}.{{target}}.multiplier" value="flat">
<input type="hidden" name="{{basePath}}.{{target}}.multiplier" value="flat">
{{/if}}
{{/inline}}
{{#*inline "damageData"}}
{{#if (and (not @root.isNPC) @root.hasRoll (not damage.base))}}
{{formField fields.resultBased value=damage.resultBased name=(concat basePath ".resultBased") localize=true classes="checkbox"}}
{{/if}}
{{#if (and (not @root.isNPC) @root.hasRoll (not damage.base) damage.resultBased)}}
<div class="nest-inputs">
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}</legend>
{{> formula key=damage.applyTo fields=fields.value.fields type=fields.type isBase=damage.base source=damage.value basePath=(concat basePath ".value")}}
</fieldset>
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}</legend>
{{> formula key=damage.applyTo fields=fields.valueAlt.fields type=fields.type isBase=damage.base source=damage.valueAlt basePath=(concat basePath ".valueAlt")}}
</fieldset>
</div>
{{else}}
{{> formula key=damage.applyTo fields=fields.value.fields type=fields.type isBase=damage.base source=damage.value basePath=(concat basePath ".value")}}
{{/if}}
<input type="hidden" name="{{concat basePath ".base"}}" value="{{damage.base}}">
{{/inline}}
{{#*inline "hordeDamage"}}
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}}</legend>
<div class="nest-inputs">
<input type="hidden" name="{{basePath}}.valueAlt.multiplier" value="flat">
{{formField fields.valueAlt.fields.flatMultiplier value=source.valueAlt.flatMultiplier name=(concat basePath ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }}
{{formField fields.valueAlt.fields.dice value=source.valueAlt.dice name=(concat basePath ".valueAlt.dice") classes="inline-child" localize=true}}
{{formField fields.valueAlt.fields.bonus value=source.valueAlt.bonus name=(concat basePath ".valueAlt.bonus") localize=true classes="inline-child"}}
</div>
</fieldset>
{{/inline}}

View file

@ -16,31 +16,10 @@
</fieldset>
{{/if}}
{{#each @root.formula}}
<div class="damage-formula">
<span class="damage-resource"><b>{{localize "DAGGERHEART.GENERAL.formula"}}:</b> {{roll.formula}}</span>
<span class="damage-details">
{{#with (lookup @root.config.GENERAL.healingTypes applyTo)}}
{{localize label}}
{{/with}}
{{#unless @root.hasHealing}}
{{#if damageTypes}}
{{#each damageTypes as | type | }}
{{#with (lookup @root.config.GENERAL.damageTypes type)}}
<i class="fa-solid {{icon}}"></i>
{{/with}}
{{/each}}
{{/if}}
{{/unless}}
</span>
</div>
<div class="bonuses form-group flexrow">
<input type="text" value="{{extraFormula}}" name="roll.{{ @index }}.extraFormula" placeholder="{{localize "DAGGERHEART.GENERAL.situationalBonus"}}">
<button class="critical-chip" data-action="toggleCritical">
<span><i class="{{#if @root.isCritical}}fa-solid{{else}}fa-regular{{/if}} fa-circle"></i></span>
<span class="label">{{localize "DAGGERHEART.GENERAL.criticalShort"}}</span>
</button>
</div>
{{> formula @root.damageFormula path="damageFormula"}}
{{#each @root.resourceFormulas}}
{{> formula path=(concat "resourceFormulas." @key)}}
{{/each}}
{{#if damageOptions.groupAttack}}
@ -88,3 +67,30 @@
</button>
</div>
</section>
{{#*inline "formula"}}
<div class="damage-formula">
<span class="damage-resource"><b>{{localize "DAGGERHEART.GENERAL.formula"}}:</b> {{roll.formula}}</span>
<span class="damage-details">
{{#with (lookup @root.config.GENERAL.healingTypes applyTo)}}
{{localize label}}
{{/with}}
{{#unless @root.hasHealing}}
{{#if damageTypes}}
{{#each damageTypes as | type | }}
{{#with (lookup @root.config.GENERAL.damageTypes type)}}
<i class="fa-solid {{icon}}"></i>
{{/with}}
{{/each}}
{{/if}}
{{/unless}}
</span>
</div>
<div class="bonuses form-group flexrow">
<input type="text" value="{{extraFormula}}" name={{concat path ".extraFormula"}} placeholder="{{localize "DAGGERHEART.GENERAL.situationalBonus"}}">
<button class="critical-chip" data-action="toggleCritical">
<span><i class="{{#if @root.isCritical}}fa-solid{{else}}fa-regular{{/if}} fa-circle"></i></span>
<span class="label">{{localize "DAGGERHEART.GENERAL.criticalShort"}}</span>
</button>
</div>
{{/inline}}

View file

@ -5,7 +5,7 @@
>
{{#if fields.roll}}{{> 'systems/daggerheart/templates/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}}
{{#if fields.save}}{{> 'systems/daggerheart/templates/actionTypes/save.hbs' fields=fields.save.fields source=source.save}}{{/if}}
{{#if fields.damage}}{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=fields.damage.fields.parts.element.fields source=source.damage baseFields=fields.damage.fields }}{{/if}}
{{#if fields.damage}}{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=fields.damage.fields source=source.damage baseFields=fields.damage.fields }}{{/if}}
{{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}}
{{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}}
{{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}}

View file

@ -22,5 +22,5 @@
</div>
{{formGroup systemFields.criticalThreshold value=document._source.system.criticalThreshold label="DAGGERHEART.ACTIONS.Settings.criticalThreshold" name="system.criticalThreshold" localize=true}}
</fieldset>
{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack." baseFields=systemFields.attack.fields.damage.fields horde=(eq document._source.system.type 'horde')}}
{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields source=document.system.attack.damage path="system.attack." baseFields=systemFields.attack.fields.damage.fields horde=(eq document._source.system.type 'horde')}}
</section>