[Rework] Damage and Damage Resource split (#2094)

This commit is contained in:
WBHarry 2026-07-18 23:31:38 +02:00 committed by GitHub
parent c90c6afc19
commit 3b68c6c895
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 730 additions and 531 deletions

View file

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

View file

@ -553,23 +553,27 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const { memberKey } = button.dataset; const { memberKey } = button.dataset;
this.updatePartyData( this.updatePartyData(
{ {
[`system.tagTeam.members.${memberKey}.damageRollData.types`]: [`system.tagTeam.members.${memberKey}.damageRollData`]: {
_replace({}) main: null,
resources: _replace({})
}
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
); );
} }
static async #rerollDamageDice(_, button) { static async #rerollDamageDice(_, button) {
const { memberKey, damageKey, diceIndex, resultIndex } = button.dataset; const { isResource, memberKey, damageKey, diceIndex, resultIndex } = button.dataset;
const memberData = this.party.system.tagTeam.members[memberKey]; const memberData = this.party.system.tagTeam.members[memberKey];
await memberData.damageRollData.rerollDamageDie(damageKey, diceIndex, resultIndex); await memberData.damageRollData.rerollDamageDie(isResource, damageKey, diceIndex, resultIndex);
const basePath = `system.tagTeam.members.${memberKey}.damageRollData`;
const updatePath = isResource ? `${basePath}.resources.${damageKey}` : `${basePath}.main`;
const updateValue = isResource ?
memberData.damageRollData.resources[damageKey] : memberData.damageRollData.main;
this.updatePartyData( this.updatePartyData(
{ {
[`system.tagTeam.members.${memberKey}.damageRollData.types`]: { [updatePath]: updateValue.toJSON()
[damageKey]: memberData.damageRollData.types[damageKey].toJSON()
}
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
); );
@ -577,19 +581,19 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
async getCriticalDamage(origDamage) { async getCriticalDamage(origDamage) {
const newDamage = origDamage ? ChatDamageData.fromJSON(JSON.stringify(origDamage)) : null; const newDamage = origDamage ? ChatDamageData.fromJSON(JSON.stringify(origDamage)) : null;
for (let key in newDamage?.types ?? {}) { if (newDamage?.main) {
const criticalDamage = await getCritDamageBonus(newDamage.types[key].formula); const criticalDamage = await getCritDamageBonus(newDamage.main.formula);
if (!criticalDamage) continue; if (criticalDamage) {
const criticalTerm = new foundry.dice.terms.NumericTerm({ number: criticalDamage, evaluated: true });
const criticalTerm = new foundry.dice.terms.NumericTerm({ number: criticalDamage, evaluated: true }); criticalTerm.evaluate();
criticalTerm.evaluate(); newDamage.main = await Roll.fromTerms([
newDamage.types[key] = await Roll.fromTerms([ ...origDamage.main.terms,
...origDamage.types[key].terms, new foundry.dice.terms.OperatorTerm({ operator: '+' }),
new foundry.dice.terms.OperatorTerm({ operator: '+' }), criticalTerm
criticalTerm ]);
]); newDamage.main.options = foundry.utils.deepClone(origDamage.main.options);
newDamage.types[key].options = foundry.utils.deepClone(origDamage.types[key].options); }
} }
return newDamage; return newDamage;
} }
@ -644,25 +648,47 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
? await this.getCriticalDamage(secondaryRoll.damageRollData) ? await this.getCriticalDamage(secondaryRoll.damageRollData)
: secondaryRoll.damageRollData; : secondaryRoll.damageRollData;
if (mainRoll.damageRollData) { if (mainRoll.damageRollData) {
for (const [key, damage] of Object.entries(secondaryDamage.types ?? {})) { if (secondaryDamage.main) {
if (key in mainRoll.damageRollData.types) { if (mainRoll.damageRollData.main) {
mainRoll.damageRollData.types[key] = Roll.fromTerms([ mainRoll.damageRollData.main = Roll.fromTerms([
...baseMainRoll.damageRollData.types[key].terms, ...baseMainRoll.damageRollData.main.terms,
new foundry.dice.terms.OperatorTerm({ operator: '+' }), new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...baseSecondaryRoll.damageRollData.types[key].terms ...baseSecondaryRoll.damageRollData.main.terms
]); ]);
/* Joining the roll.options of both rolls */ /* Joining the roll.options of both rolls */
const joinedDamageTypes = new Set([ const joinedDamageTypes = new Set([
...baseMainRoll.damageRollData.types[key].options.damageTypes, ...baseMainRoll.damageRollData.main.options.damageTypes,
...baseSecondaryRoll.damageRollData.types[key].options.damageTypes ...baseSecondaryRoll.damageRollData.main.options.damageTypes
]); ]);
mainRoll.damageRollData.types[key].options = { mainRoll.damageRollData.main.options = {
...baseMainRoll.damageRollData.types[key].options, ...baseMainRoll.damageRollData.main.options,
damageTypes: [...joinedDamageTypes] damageTypes: [...joinedDamageTypes]
}; };
} else { } else {
mainRoll.damageRollData.types[key] = damage; mainRoll.damageRollData.main = secondaryDamage.main;
}
}
for (const [key, damage] of Object.entries(secondaryDamage.resources ?? {})) {
if (key in mainRoll.damageRollData.resources) {
mainRoll.damageRollData.resources[key] = Roll.fromTerms([
...baseMainRoll.damageRollData.resources[key].terms,
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...baseSecondaryRoll.damageRollData.resources[key].terms
]);
/* Joining the roll.options of both rolls */
const joinedDamageTypes = new Set([
...baseMainRoll.damageRollData.resources[key].options.damageTypes,
...baseSecondaryRoll.damageRollData.resources[key].options.damageTypes
]);
mainRoll.damageRollData.resources[key].options = {
...baseMainRoll.damageRollData.resources[key].options,
damageTypes: [...joinedDamageTypes]
};
} else {
mainRoll.damageRollData.resources[key] = damage;
} }
} }
} else { } else {
@ -727,8 +753,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
...mainRoll.options, ...mainRoll.options,
damage: joinedRoll.damageRollData?.toJSON() damage: joinedRoll.damageRollData?.toJSON()
}; };
for (const type of Object.keys(joinedRoll.damageRollData?.types ?? {})) {
systemData.damage.types[type] = joinedRoll.damageRollData.types[type].toJSON(); if (joinedRoll.damageRollData.main) {
systemData.damage.main = joinedRoll.damageRollData.toJSON();
}
for (const type of Object.keys(joinedRoll.damageRollData?.resources ?? {})) {
systemData.damage.resources[type] = joinedRoll.damageRollData.resources[type].toJSON();
} }
const cls = getDocumentClass('ChatMessage'), const cls = getDocumentClass('ChatMessage'),

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'; import DaggerheartSheet from '../sheets/daggerheart-sheet.mjs';
const { ApplicationV2 } = foundry.applications.api; const { ApplicationV2 } = foundry.applications.api;
@ -31,8 +31,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
removeElement: this.removeElement, removeElement: this.removeElement,
removeTransformActor: this.removeTransformActor, removeTransformActor: this.removeTransformActor,
editEffect: this.editEffect, editEffect: this.editEffect,
addDamage: this.addDamage, addDamage: this.#onAddDamage,
removeDamage: this.removeDamage, removeDamage: this.#onRemoveDamage,
addDamageResource: this.#onAddDamageResource,
removeDamageResource: this.#onRemoveDamageResource,
editDoc: this.editDoc, editDoc: this.editDoc,
addTrigger: this.addTrigger, addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger, removeTrigger: this.removeTrigger,
@ -157,9 +159,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH; context.config = CONFIG.DH;
if (this.action.damage) { if (this.action.damage) {
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length; const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes);
context.allDamageTypesUsed = allKeys.every(k => k in this.action._source.damage.resources);
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack') if (this.action.damage?.main?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.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) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
} }
static addDamage(_event) { /** @this DHActionBaseConfig */
if (!this.action.damage.parts) return; 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({ const content = new foundry.data.fields.StringField({
label: game.i18n.localize('Damage Type'), label: _loc('DAGGERHEART.GENERAL.damageType'),
choices, choices,
required: true required: true
}).toFormGroup( }).toFormGroup({}, {
{}, name: 'type',
{ localize: true,
name: 'type', nameAttr: 'value',
localize: true, labelAttr: 'label'
nameAttr: 'value', }).outerHTML;
labelAttr: 'label'
}
).outerHTML;
const callback = (_, button) => { const callback = (_, button) => {
const data = this.action.toObject(); const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value; const type = choices[button.form.elements.type.value].value;
const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue(); data.damage.resources[type] = {
part.applyTo = type; ...this.action.schema.fields.damage.fields.resources.element.getInitialValue(),
if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) applyTo: type
part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial; };
data.damage.parts[type] = part;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}; };
const typeDialog = new foundry.applications.api.DialogV2({ const typeDialog = new foundry.applications.api.DialogV2({
buttons: [ buttons: [
foundry.utils.mergeObject( {
{ action: 'ok',
action: 'ok', label: 'Confirm',
label: 'Confirm', icon: 'fas fa-check',
icon: 'fas fa-check', default: true,
default: true callback
}, }
{ callback: callback }
)
], ],
content: content, content: content,
rejectClose: false, rejectClose: false,
modal: false, modal: false,
window: { window: {
title: game.i18n.localize('Add Damage') /** @todo localize */
title: 'Add Damage'
}, },
position: { width: 300 } position: { width: 300 }
}); });
@ -353,12 +373,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
typeDialog.render(true); typeDialog.render(true);
} }
static removeDamage(_event, button) { /** @this DHActionBaseConfig */
if (!this.action.damage.parts) return; static #onRemoveDamageResource(_event, button) {
if (!this.action.damage?.resources) return;
const data = this.action.toObject(); const data = this.action.toObject();
const key = button.dataset.key; const key = button.dataset.key;
delete data.damage.parts[key]; data.damage.resources[key] = _del;
data.damage.parts[`${key}`] = _del;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); 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'; import DHBaseActorSettings from '../sheets/api/actor-setting.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -8,8 +9,10 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
classes: ['adversary-settings'], classes: ['adversary-settings'],
position: { width: 455, height: 'auto' }, position: { width: 455, height: 'auto' },
actions: { actions: {
addExperience: DHAdversarySettings.#addExperience, addExperience: this.#onAddExperience,
removeExperience: DHAdversarySettings.#removeExperience removeExperience: this.#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. * Adds a new experience entry to the actor.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #addExperience() { static async #onAddExperience() {
const newExperience = { const newExperience = {
name: 'Experience', name: 'Experience',
modifier: 0 modifier: 0
@ -83,7 +86,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
* Removes an experience entry from the actor. * Removes an experience entry from the actor.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #removeExperience(_, target) { static async #onRemoveExperience(_, target) {
const experience = this.actor.system.experiences[target.dataset.experience]; const experience = this.actor.system.experiences[target.dataset.experience];
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {
@ -98,4 +101,28 @@ export default class DHAdversarySettings extends DHBaseActorSettings {
await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del }); 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

@ -110,7 +110,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
return message.system.hasRoll && (game.user.isGM || message.isAuthor); return message.system.hasRoll && (game.user.isGM || message.isAuthor);
}, },
callback: async li => { onClick: async (_event, li) => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
const reroll = await message.rolls[0].reroll({ liveRoll: true }); const reroll = await message.rolls[0].reroll({ liveRoll: true });
message.update({ rolls: [reroll] }); message.update({ rolls: [reroll] });
@ -126,7 +126,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
: false; : false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage; return (game.user.isGM || message.isAuthor) && hasRolledDamage;
}, },
callback: async li => { onClick: async (_event, li) => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
const update = await message.system.getRerolledDamage(); const update = await message.system.getRerolledDamage();
message.update(update); message.update(update);
@ -251,15 +251,17 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
} }
const message = game.messages.get(messageData._id); const message = game.messages.get(messageData._id);
const target = event.target.closest('[data-die-index]'); const target = event.target.closest('[data-result]');
if (target.dataset.type === 'damage') { if (target.dataset.type === 'damage') {
const { damageType, dice, result } = target.dataset; const { isResource, damageType, dice, result } = target.dataset;
await message.system.damage.rerollDamageDie(damageType, dice, result); await message.system.damage.rerollDamageDie(isResource, damageType, dice, result);
const updatePath = isResource ? `system.damage.resources.${damageType}` : 'system.damage.main';
const updateValue = isResource ?
message.system.damage.resources[damageType] : message.system.damage.main;
await message.update({ await message.update({
'system.damage.types': { [updatePath]: updateValue.toJSON()
[damageType]: message.system.damage.types[damageType].toJSON()
}
}); });
} else { } else {
const rerollDice = message.system.roll.dice[target.dataset.dieIndex]; const rerollDice = message.system.roll.dice[target.dataset.dieIndex];

View file

@ -13,18 +13,19 @@ export default class DHAttackAction extends DHDamageAction {
if (this.damage.includeBase) { if (this.damage.includeBase) {
const baseDamage = this.getParentHitPointDamage(); const baseDamage = this.getParentHitPointDamage();
if (baseDamage) { if (baseDamage) {
if (!this.damage.parts.hitPoints) { if (!this.damage.main) {
this.damage.parts.hitPoints = baseDamage; this.damage.main = baseDamage;
} else { } else {
for (const type of baseDamage.type) this.damage.parts.hitPoints.type.add(type); for (const type of baseDamage.type) this.damage.main.type.add(type);
this.damage.parts.hitPoints.value.custom = { this.damage.main.value.custom = {
enabled: true, enabled: true,
formula: `${baseDamage.value.getFormula()} + ${this.damage.parts.hitPoints.value.getFormula()}` formula: `${baseDamage.value.getFormula()} + ${this.damage.main.value.getFormula()}`
}; };
} }
} }
} }
if (this.roll.useDefault) { if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait; this.roll.trait = this.item.system.attack.roll.trait;
this.roll.type = 'attack'; this.roll.type = 'attack';
@ -33,18 +34,18 @@ export default class DHAttackAction extends DHDamageAction {
} }
getParentHitPointDamage() { getParentHitPointDamage() {
return this.item?.system?.attack.damage.parts.hitPoints; return this.item?.system?.attack.damage.main;
} }
get damageFormula() { get damageFormula() {
const hitPointsPart = this.damage.parts.hitPoints; const hitPointsPart = this.damage.main;
if (!hitPointsPart) return '0'; if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula(); return hitPointsPart.value.getFormula();
} }
get altDamageFormula() { get altDamageFormula() {
const hitPointsPart = this.damage.parts.hitPoints; const hitPointsPart = this.damage.main;
if (!hitPointsPart) return '0'; if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula(); return hitPointsPart.valueAlt.getFormula();
@ -73,7 +74,7 @@ export default class DHAttackAction extends DHDamageAction {
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; 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 usedValue = useAltDamage ? valueAlt : value;
const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
const str = damageString const str = damageString
@ -82,7 +83,7 @@ export default class DHAttackAction extends DHDamageAction {
x: game.i18n.localize('DAGGERHEART.GENERAL.damage') 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) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
.filter(Boolean); .filter(Boolean);

View file

@ -289,7 +289,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasEffect: this.hasEffect, hasEffect: this.hasEffect,
hasSave: this.hasSave, hasSave: this.hasSave,
onSave: this.save?.damageMod, onSave: this.save?.damageMod,
isDirect: !!this.damage?.direct,
selectedMessageMode: game.settings.get('core', 'messageMode'), selectedMessageMode: game.settings.get('core', 'messageMode'),
data: this.getRollData(), data: this.getRollData(),
evaluate: this.hasRoll, evaluate: this.hasRoll,
@ -307,20 +306,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}; };
if (this.damage) { if (this.damage) {
config.isDirect = this.damage.direct; config.isDirect = !!this.damage.main?.direct;
const groupAttackTokens = this.damage.groupAttack const groupAttackTokens = this.damage.main?.groupAttack
? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens( ? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(
this.actor.id, this.actor.id,
this.damage.groupAttack this.damage.main.groupAttack
) )
: null; : null;
config.damageOptions = { config.damageOptions = {
groupAttack: this.damage.groupAttack groupAttack: this.damage.main?.groupAttack
? { ? {
numAttackers: Math.max(groupAttackTokens.length, 1), numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.groupAttack range: this.damage.main.groupAttack
} }
: null : null
}; };
@ -430,11 +429,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
} }
get hasDamage() { get hasDamage() {
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing'; return this.type !== 'healing' && (Boolean(this.damage.main) || !foundry.utils.isEmpty(this.damage.resources));
} }
get hasHealing() { get hasHealing() {
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing'; return this.type === 'healing' && !foundry.utils.isEmpty(this.damage.resources);
} }
get hasSave() { get hasSave() {
@ -470,6 +469,25 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return acc; return acc;
}, {}); }, {});
} }
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' && source.type !== 'healing') {
source.damage.main = {
...part,
includeBase: source.damage.includeBase,
direct: source.damage.direct,
groupAttack: source.damage.groupAttack
};
} else {
source.damage.resources[partKey] = part;
}
}
delete source.damage.parts;
}
} }
} }

View file

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

View file

@ -84,13 +84,11 @@ export default class DhpAdversary extends DhCreature {
type: 'attack' type: 'attack'
}, },
damage: { damage: {
parts: { main: {
hitPoints: { type: ['physical'],
type: ['physical'], applyTo: 'hitPoints',
applyTo: 'hitPoints', value: {
value: { multiplier: 'flat'
multiplier: 'flat'
}
} }
} }
} }

View file

@ -104,15 +104,13 @@ export default class DhCharacter extends DhCreature {
trait: 'strength' trait: 'strength'
}, },
damage: { damage: {
parts: { main: {
hitPoints: { type: ['physical'],
type: ['physical'], applyTo: 'hitPoints',
applyTo: 'hitPoints', value: {
value: { custom: {
custom: { enabled: true,
enabled: true, formula: '@profd4'
formula: '@profd4'
}
} }
} }
} }
@ -838,7 +836,7 @@ export default class DhCharacter extends DhCreature {
isReversed: true isReversed: true
}; };
this.attack.damage.parts.hitPoints.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`; this.attack.damage.main.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
// Clamp resources (must be done last to ensure all updates occur) // Clamp resources (must be done last to ensure all updates occur)
this.resources.clamp(); this.resources.clamp();

View file

@ -40,17 +40,17 @@ export function getTierAdjustedAdversary(source, tier) {
// Store initial attack damage for abilities that have you deal a "standard attack" // Store initial attack damage for abilities that have you deal a "standard attack"
const initialAttack = { const initialAttack = {
type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(), type: source.system.attack.damage?.main?.type?.toSorted(),
value: getFormula(source.system.attack.damage?.parts.hitPoints?.value) value: getFormula(source.system.attack.damage?.main?.value)
}; };
// Update damage of base attack. // Update damage of base attack.
try { try {
const damage = source.system.attack.damage; const damage = source.system.attack.damage;
if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary'); if (!damage?.main) throw new Error('Unexpected missing damage in adversary');
for (const property of ['value', 'valueAlt']) { for (const property of ['value', 'valueAlt']) {
const data = damage.parts.hitPoints[property]; const data = damage.main[property];
const previousFormula = getFormula(data); const previousFormula = getFormula(data);
const value = calculateAdjustedDamage(previousFormula, 'attack', damageMeta); const value = calculateAdjustedDamage(previousFormula, 'attack', damageMeta);
applyAdjustedDamage(data, value); applyAdjustedDamage(data, value);
@ -82,12 +82,12 @@ export function getTierAdjustedAdversary(source, tier) {
// Update damage in item actions and convert all formula matches in the descriptions to the new damage // Update damage in item actions and convert all formula matches in the descriptions to the new damage
for (const action of Object.values(item.system.actions)) { for (const action of Object.values(item.system.actions)) {
if (!action.damage?.parts.hitPoints) continue; if (!action.damage?.main) continue;
try { try {
// Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead // Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead
const result = []; const result = [];
for (const property of ['value', 'valueAlt']) { for (const property of ['value', 'valueAlt']) {
const { [property]: data, type: damageType } = action.damage.parts.hitPoints; const { [property]: data, type: damageType } = action.damage.main;
const previousFormula = getFormula(data); const previousFormula = getFormula(data);
const isActuallyAttack = const isActuallyAttack =
previousFormula === initialAttack.value && previousFormula === initialAttack.value &&
@ -199,7 +199,7 @@ function calculateAdjustedDamage(formula, type, { currentDamageRange, newDamageR
} }
/** /**
* Get formula from either damage parts *or* a simple formula object. * Get formula from either damage data *or* a simple formula object.
* @returns {string} the new formula data * @returns {string} the new formula data
*/ */
function getFormula(data) { function getFormula(data) {

View file

@ -137,12 +137,17 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
if (!this.damage.active) return; if (!this.damage.active) return;
const rerolls = []; const rerolls = [];
const update = { system: { damage: { types: {} } } }; const update = { system: { damage: { main: null, resources: _replace({}) } } };
for (const key of Object.keys(this.damage.types)) { if (this.damage.main) {
const type = this.damage.types[key]; const reroll = await this.damage.main.reroll();
const reroll = await type.reroll();
rerolls.push(reroll); rerolls.push(reroll);
update.system.damage.types[key] = reroll.toJSON(); update.system.damage.main = reroll.toJSON();
}
for (const key of Object.keys(this.damage.resources)) {
const reroll = await this.damage.resources[key].reroll();
rerolls.push(reroll);
update.system.damage.resources[key] = reroll.toJSON();
} }
await triggerChatRollFx(rerolls); await triggerChatRollFx(rerolls);
@ -188,22 +193,36 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
} }
static migrateData(source) { static migrateData(source) {
if (source.hasDamage && !source.damage.types) { const { main, resources, ...flatDamageKeys } = source.damage ?? {};
source.damage = { if (source.damage && !main && !resources) {
types: Object.keys(source.damage).reduce((acc, key) => { source.damage.main = null;
const damageData = source.damage[key]; source.damage.resources = {};
const oldRoll = damageData.parts[0]?.roll;
acc[key] = oldRoll ? {
...oldRoll,
options: {
...oldRoll.options,
damageTypes: damageData.parts[0].damageTypes ?? []
}
} : null;
return acc; const getRoll = key => {
}, {}) const damageData = source.damage[key];
}; const oldRoll = damageData.parts[0]?.roll;
return oldRoll ? JSON.stringify({
...oldRoll,
class: 'BaseRoll',
options: {
...oldRoll.options,
damageTypes: damageData.parts[0].damageTypes ?? []
}
}) : null;
};
for (const key of Object.keys(flatDamageKeys)) {
if (key === 'hitPoints' && source.hasDamage && !source.hasHealing) {
source.damage.main = getRoll('hitPoints');
}
else {
source.damage.resources[key] = getRoll(key);
}
}
}
for (const key of Object.keys(flatDamageKeys)) {
delete source.damage[key];
} }
return source; return source;

View file

@ -11,31 +11,31 @@ export class ChatDamageData extends foundry.abstract.DataModel {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
types: new fields.TypedObjectField(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() { get active() {
return Boolean(Object.keys(this.types).length); return !!this.main || Boolean(Object.keys(this.resources).length);
} }
static #validateRoll(rollJSON) { static #validateRoll(rollJSON) {
const roll = JSON.parse(rollJSON); if (rollJSON) {
if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated'); 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 {}
} }
} }
async rerollDamageDie(damageType, dice, resultIndex) { _prepareRolls() {
const reroll = this.types[damageType]; this.main &&= Roll.fromData(this.main);
for (const key of Object.keys(this.resources)) {
this.resources[key] = Roll.fromData(this.resources[key]);
}
}
async rerollDamageDie(isResource, damageType, dice, resultIndex) {
const reroll = isResource ? this.resources[damageType] : this.main;
const rerollDice = reroll.dice[dice]; const rerollDice = reroll.dice[dice];
await rerollDice.rerollResult(resultIndex); await rerollDice.rerollResult(resultIndex);
await reroll._evaluate(); await reroll._evaluate();

View file

@ -12,20 +12,10 @@ export default class DamageField extends fields.SchemaField {
/** @inheritDoc */ /** @inheritDoc */
constructor(options, context = {}) { constructor(options, context = {}) {
const damageFields = { super({
parts: new IterableTypedObjectField(DHDamageData), main: new fields.EmbeddedDataField(DHDamageData, { nullable: true }),
includeBase: new fields.BooleanField({ resources: new IterableTypedObjectField(DHResourceData)
initial: false, }, options, context);
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
}),
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }),
groupAttack: new fields.StringField({
choices: CONFIG.DH.GENERAL.groupAttackRange,
blank: true,
label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label'
})
};
super(damageFields, options, context);
} }
/** /**
@ -41,25 +31,23 @@ export default class DamageField extends fields.SchemaField {
this.hasRoll && this.hasRoll &&
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
!force !force
) ) {
return; return;
}
let formulas = this.damage.parts.map(p => ({ const damageFormula = this.damage.main ?
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor), DamageField.formatFormulas.call(this, [this.damage.main], config)[0] : null;
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type, const resourceFormulas = DamageField.formatFormulas.call(this, this.damage.resources, config);
applyTo: p.applyTo
}));
if (!formulas.length) return false; if (!damageFormula && !resourceFormulas.length) return false;
formulas = DamageField.formatFormulas.call(this, formulas, config);
messageId = config.message?._id ?? messageId; messageId = config.message?._id ?? messageId;
const message = game.messages.get(messageId); const message = game.messages.get(messageId);
const damageConfig = { const damageConfig = {
dialog: {}, dialog: {},
...config, ...config,
roll: formulas, damageFormula,
resourceFormulas,
data: this.getRollData(), data: this.getRollData(),
isCritical: Boolean(message?.system.roll?.isCritical) isCritical: Boolean(message?.system.roll?.isCritical)
}; };
@ -92,7 +80,7 @@ export default class DamageField extends fields.SchemaField {
const targetDamage = []; const targetDamage = [];
const damagePromises = []; const damagePromises = [];
for (let target of targets) { for (const target of targets) {
const actor = foundry.utils.fromUuidSync(target.actorId); const actor = foundry.utils.fromUuidSync(target.actorId);
if (!actor) continue; if (!actor) continue;
if (!config.hasHealing && config.onSave && target.saved?.success === true) { if (!config.hasHealing && config.onSave && target.saved?.success === true) {
@ -111,17 +99,15 @@ export default class DamageField extends fields.SchemaField {
: actor.prototypeToken; : actor.prototypeToken;
if (config.hasHealing) if (config.hasHealing)
damagePromises.push( damagePromises.push(
actor.takeHealing(config.damage.types).then(updates => targetDamage.push({ token, updates })) actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
); );
else { else {
const configDamage = foundry.utils.deepClone(config.damage.types); const configDamage = config.damage.clone();
const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; configDamage.main &&= configDamage.main.toJSON();
const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; if (configDamage.main) {
if (configDamage.hitPoints) { const multiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
configDamage.hitPoints = configDamage.hitPoints.toJSON(); const takenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
configDamage.hitPoints.total = Math.ceil( configDamage.main.total = Math.ceil(configDamage.main.total * multiplier * takenMultiplier);
configDamage.hitPoints.total * hpDamageMultiplier * hpDamageTakenMultiplier
);
} }
damagePromises.push( damagePromises.push(
@ -184,13 +170,19 @@ export default class DamageField extends fields.SchemaField {
/** /**
* Prepare formulas for Damage Roll * Prepare formulas for Damage Roll
* Must be called within Action context or similar. * 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 * @param {object} data Action getRollData
* @returns * @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.type ?? new Set(),
applyTo: x.applyTo
}));
const formattedFormulas = []; const formattedFormulas = [];
formulas.forEach(formula => { for (const formula of formulas) {
if (isNaN(formula.formula)) if (isNaN(formula.formula))
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data)); formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data));
const same = formattedFormulas.find( const same = formattedFormulas.find(
@ -198,7 +190,8 @@ export default class DamageField extends fields.SchemaField {
); );
if (same) same.formula += ` + ${formula.formula}`; if (same) same.formula += ` + ${formula.formula}`;
else formattedFormulas.push(formula); else formattedFormulas.push(formula);
}); }
return formattedFormulas; return formattedFormulas;
} }
@ -295,6 +288,7 @@ export class DHResourceData extends foundry.abstract.DataModel {
/** @override */ /** @override */
static defineSchema() { static defineSchema() {
return { return {
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
applyTo: new fields.StringField({ applyTo: new fields.StringField({
choices: CONFIG.DH.GENERAL.healingTypes, choices: CONFIG.DH.GENERAL.healingTypes,
required: true, required: true,
@ -317,7 +311,16 @@ export class DHDamageData extends DHResourceData {
static defineSchema() { static defineSchema() {
return { return {
...super.defineSchema(), ...super.defineSchema(),
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }), includeBase: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
}),
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }),
groupAttack: new fields.StringField({
choices: CONFIG.DH.GENERAL.groupAttackRange,
blank: true,
label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label'
}),
type: new fields.SetField( type: new fields.SetField(
new fields.StringField({ new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes, choices: CONFIG.DH.GENERAL.damageTypes,

View file

@ -67,13 +67,11 @@ export default class DHWeapon extends AttachableItem {
type: 'attack' type: 'attack'
}, },
damage: { damage: {
parts: { main: {
hitPoints: { type: ['physical'],
type: ['physical'], value: {
value: { multiplier: 'prof',
multiplier: 'prof', dice: 'd8'
dice: 'd8'
}
} }
} }
} }
@ -230,11 +228,12 @@ export default class DHWeapon extends AttachableItem {
game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`) game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`)
]; ];
for (const { value, type } of attack.damage.parts) { if (attack.damage.main) {
const { value, type } = attack.damage.main;
const parts = value.custom.enabled ? [game.i18n.localize('DAGGERHEART.GENERAL.custom')] : [value.dice]; 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 (!value.custom.enabled && value.bonus) parts.push(value.bonus.signedString());
if (type.size > 0) { if (type?.size) {
const typeTags = Array.from(type) const typeTags = Array.from(type)
.map(t => game.i18n.localize(`DAGGERHEART.CONFIG.DamageType.${t}.abbreviation`)) .map(t => game.i18n.localize(`DAGGERHEART.CONFIG.DamageType.${t}.abbreviation`))
.join(' | '); .join(' | ');
@ -243,7 +242,7 @@ export default class DHWeapon extends AttachableItem {
tags.push(parts.join('')); tags.push(parts.join(''));
} }
return tags; return tags;
} }
@ -258,10 +257,10 @@ export default class DHWeapon extends AttachableItem {
if (roll.trait) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${roll.trait}.short`)); 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`)); 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 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) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
.filter(Boolean); .filter(Boolean);

View file

@ -13,16 +13,31 @@ export default class DamageRoll extends DHRoll {
static DefaultDialog = DamageDialog; static DefaultDialog = DamageDialog;
static createRollInstance(config) {
return new this(undefined, config.data, config);
}
/** @inheritdoc */ /** @inheritdoc */
static async buildEvaluate(roll, config = {}) { 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(); await roll.roll.evaluate();
roll.roll.options = { damageTypes: roll.damageTypes ? [...roll.damageTypes] : [] }; roll.roll.options = { damageTypes: roll.damageTypes ? [...roll.damageTypes] : [] };
return roll.roll;
}
if (!config.damage?.types) config.damage = { types: {} }; if (!config.damage) config.damage = { main: null, resources: {} };
config.damage.types[roll.applyTo] = roll.roll;
if (config.damageFormula) {
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; roll._evaluated = true;
@ -36,9 +51,10 @@ export default class DamageRoll extends DHRoll {
const diceRolls = []; const diceRolls = [];
if (game.modules.get('dice-so-nice')?.active) { if (game.modules.get('dice-so-nice')?.active) {
config.mute = true; config.mute = true;
const pool = foundry.dice.terms.PoolTerm.fromRolls( const pool = foundry.dice.terms.PoolTerm.fromRolls([
Object.values(config.damage.types) ...(config.damage.main ? [config.damage.main] : []),
); ...Object.values(config.damage.resources)
]);
diceRolls.push(Roll.fromTerms([pool])); diceRolls.push(Roll.fromTerms([pool]));
} }
@ -51,7 +67,8 @@ export default class DamageRoll extends DHRoll {
if (config.source?.message) { if (config.source?.message) {
chatMessage.update({ 'system.damage': { chatMessage.update({ 'system.damage': {
...config.damage.toObject(), ...config.damage.toObject(),
types: config.damage.types main: config.damage.main,
resources: config.damage.resources
}}); }});
} }
} }
@ -104,12 +121,10 @@ export default class DamageRoll extends DHRoll {
const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'); const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage');
const changeKeys = []; const changeKeys = [];
for (const roll of this.options.roll) { for (const damageType of this.options.damageFormula?.damageTypes?.values?.() ?? []) {
for (const damageType of roll.damageTypes?.values?.() ?? []) { changeKeys.push(`system.bonuses.${type}.${damageType}`);
changeKeys.push(`system.bonuses.${type}.${damageType}`);
}
} }
const item = this.data.parent?.items?.get(this.options.source.item); const item = this.data.parent?.items?.get(this.options.source.item);
if (item) { if (item) {
switch (item.type) { switch (item.type) {
@ -125,62 +140,69 @@ export default class DamageRoll extends DHRoll {
return changeKeys; 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) {
if (!formulaData) return null;
this.options.isCritical = config.isCritical; this.options.isCritical = config.isCritical;
for (const [index, part] of this.options.roll.entries()) {
const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id; formulaData.roll = new Roll(Roll.replaceFormulaData(formulaData.formula, config.data));
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); formulaData.roll.terms = Roll.parse(formulaData.roll.formula, config.data);
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { if (formulaData.extraFormula) {
part.modifiers = this.applyBaseBonus(part); formulaData.roll.terms.push(
this.addModifiers(part); new foundry.dice.terms.OperatorTerm({ operator: '+' }),
part.modifiers?.forEach(m => { ...this.constructor.parse(formulaData.extraFormula, this.options.data)
part.roll.terms.push(...this.formatModifier(m.value)); );
}); }
if (isDamage && 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 */
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (
modifier.beforeCrit === true &&
(modifier.enabled || modifier.value)
) modifier.callback(formulaData);
} }
/* To Remove When Reaction System */ /* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { for (const mod in config.modifiers) {
for (const mod in config.modifiers) { const modifier = config.modifiers[mod];
const modifier = config.modifiers[mod]; if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(formulaData);
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
}
} }
if (part.extraFormula) { if (config.damageOptions.groupAttack?.numAttackers > 1) {
part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(part.extraFormula, this.options.data)
);
}
if (config.damageOptions.groupAttack?.numAttackers > 1 && isHitpointPart) {
const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm]; 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)) { if (damageTypes.some(type => term instanceof type)) {
term.number *= config.damageOptions.groupAttack.numAttackers; term.number *= config.damageOptions.groupAttack.numAttackers;
} }
} }
} }
if (config.isCritical && isHitpointPart) { if (config.isCritical) {
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) { 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) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
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 */ /* To Remove When Reaction System */

View file

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

View file

@ -656,26 +656,23 @@ export default class DhpActor extends Actor {
return; return;
} }
const updates = []; if (damages.main) {
damages.main.total = this.calculateDamage(damages.main.total, damages.main.damageTypes);
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 (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null;
if (!updates.length) return; // Convert deducted resources to a record of updates. Return if nothing to do.
const updates = Object.entries(damages.resources).map(([key, damage]) => ({ key, value: damage.total }));
if (!updates.some(u => u.value) && !damages.main) return;
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); if (damages.main) {
if (hpDamage?.value) { const hpDamage = {
hpDamage.value = this.convertDamageToThreshold(hpDamage.value); value: this.convertDamageToThreshold(damages.main.total),
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) { damageTypes: new Set(damages.main.options.damageTypes),
key: CONFIG.DH.GENERAL.healingTypes.hitPoints.id
};
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.total, hpDamage.damageTypes)) {
const armorSlotResult = await this.owner.query( const armorSlotResult = await this.owner.query(
'armorSlot', 'armorSlot',
{ {
@ -689,7 +686,7 @@ export default class DhpActor extends Actor {
); );
if (armorSlotResult) { if (armorSlotResult) {
const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult; const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
updates.find(u => u.key === 'hitPoints').value = modifiedDamage; hpDamage.value = modifiedDamage;
for (const armorChange of armorChanges) { for (const armorChange of armorChanges) {
updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid }); updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid });
} }
@ -699,20 +696,24 @@ export default class DhpActor extends Actor {
else updates.push({ value: stressSpent, key: 'stress' }); else updates.push({ value: stressSpent, key: 'stress' });
} }
} }
} } else if (this.type === 'adversary') {
if (this.type === 'adversary') {
const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => { const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => {
return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value); return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value);
}, 0); }, 0);
hpDamage.value = Math.max(hpDamage.value - reducedSeverity, 0); hpDamage.value = Math.max(hpDamage.value - reducedSeverity, 0);
if (this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]) {
if ( hpDamage.value = Math.max(0, hpDamage.value - 1);
hpDamage.value &&
this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]
) {
hpDamage.value -= 1;
} }
} }
// Merge existing hitPoint deduction with finalised damage deduction
const existing = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (existing) {
existing.value += hpDamage.value;
existing.damageTypes = hpDamage.damageTypes;
} else {
updates.push(hpDamage);
}
} }
const results = await game.system.registeredTriggers.runTrigger( const results = await game.system.registeredTriggers.runTrigger(
@ -727,12 +728,10 @@ export default class DhpActor extends Actor {
for (var result of results) resourceMap.addResources(result.updates); for (var result of results) resourceMap.addResources(result.updates);
resourceMap.updateResources(); resourceMap.updateResources();
} }
updates.forEach( for (const u of updates) {
u => u.value = u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value;
(u.value = }
u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value)
);
await this.modifyResource(updates); await this.modifyResource(updates);
@ -741,6 +740,28 @@ export default class DhpActor extends Actor {
return updates; return updates;
} }
async takeHealing(healings) {
if (Hooks.call(`${CONFIG.DH.id}.preTakeHealing`, this, healings) === false) return null;
const updates = Object.entries(healings.resources).map(([key, damage]) => ({
key,
value: damage.total
}));
updates.forEach(
u =>
(u.value = !(u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false)
? u.value * -1
: u.value)
);
await this.modifyResource(updates);
if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null;
return updates;
}
calculateDamage(baseDamage, type) { calculateDamage(baseDamage, type) {
if (this.canResist(type, 'immunity')) return 0; if (this.canResist(type, 'immunity')) return 0;
if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2);
@ -765,30 +786,6 @@ export default class DhpActor extends Actor {
return reduction === Infinity ? 0 : reduction; return reduction === Infinity ? 0 : reduction;
} }
async takeHealing(healings) {
if (Hooks.call(`${CONFIG.DH.id}.preTakeHealing`, this, healings) === false) return null;
const updates = [];
Object.entries(healings).forEach(([key, healing]) => {
const update = updates.find(u => u.key === key);
if (update) update.value += healing.roll.total;
else updates.push({ value: healing.roll.total, key });
});
updates.forEach(
u =>
(u.value = !(u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false)
? u.value * -1
: u.value)
);
await this.modifyResource(updates);
if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null;
return updates;
}
/** /**
* Resources are modified asynchronously, so be careful not to update the same resource in * Resources are modified asynchronously, so be careful not to update the same resource in
* quick succession. * quick succession.

View file

@ -48,9 +48,8 @@ export default class RegisterHandlebarsHelpers {
return formula; return formula;
} }
static damageSymbols(damageParts) { static damageSymbols(damageData) {
const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))]; const symbols = damageData.type.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon);
const symbols = allTypes.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon);
return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`)); return new Handlebars.SafeString(Array.from(symbols).map(symbol => `<i class="fa-solid ${symbol}"></i>`));
} }

View file

@ -700,19 +700,6 @@ export async function RefreshFeatures(
return refreshedActors; 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 */ /** Returns resolved armor sources ordered by application order */
export function getArmorSources(actor) { export function getArmorSources(actor) {
const rawArmorSources = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData); const rawArmorSources = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData);

View file

@ -7,7 +7,8 @@ export class Migration_2_5_2 extends MigrationHandlerBase {
async updateActiveEffectSource(effectSource, item) { async updateActiveEffectSource(effectSource, item) {
let shouldUpdate = false; let shouldUpdate = false;
const newChanges = []; const newChanges = [];
const srdItem = item?._stats.compendiumSource ?
const srdItem = item?._stats?.compendiumSource ?
await foundry.utils.fromUuid(item?._stats.compendiumSource) : await foundry.utils.fromUuid(item?._stats.compendiumSource) :
null; null;
for (let i = 0; i < effectSource.system.changes.length; i++) { for (let i = 0; i < effectSource.system.changes.length; i++) {

View file

@ -17,6 +17,13 @@
} }
} }
.section-header {
font-size: var(--font-size-20);
color: light-dark(@dark, @beige);
text-align: center;
margin-bottom: -12px;
}
.bonuses { .bonuses {
gap: 4px; gap: 4px;
.critical-chip { .critical-chip {

View file

@ -1,92 +1,114 @@
{{#unless (eq @root.source.type 'healing')}}
<fieldset class="one-column"> <fieldset class="one-column">
<legend class="with-icon"> <legend class="with-icon">
{{#if (eq @root.source.type 'healing')}}
{{localize "DAGGERHEART.GENERAL.healing"}}
{{else}}
{{localize "DAGGERHEART.GENERAL.damage"}} {{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>
{{#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}} {{/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}} </fieldset>
</legend> {{/unless}}
<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 --}} {{#unless (eq path 'system.attack.')}}
{{#each source.parts as |dmg key|}} {{! In the future, consider allowing this even on NPCs}}
<div class="nest-inputs"> <fieldset class="one-column">
<fieldset{{#if dmg.base}} disabled{{/if}} class="one-column{{#if ../path}} no-style{{/if}}"> <legend class="with-icon">
<legend class="with-icon"> {{#if (eq @root.source.type 'healing')}}
{{localize (concat "DAGGERHEART.CONFIG.HealingType." dmg.applyTo ".name")}} {{localize "DAGGERHEART.GENERAL.healing"}}
{{#unless (or dmg.base ../path)}} {{else}}
<a data-action="removeDamage" data-key="{{dmg.applyTo}}"><i class="fas fa-trash"></i></a> {{localize "DAGGERHEART.GENERAL.Resource.plural"}}
{{/unless}} {{/if}}
</legend> {{#unless @root.allDamageTypesUsed}}<a data-action="addDamageResource"><i class="fa-solid fa-plus icon-button"></i></a>{{/unless}}
</legend>
{{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}} {{#each source.resources as |dmg key|}}
{{formField ../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." dmg.applyTo ".resultBased") localize=true classes="checkbox"}} <div class="nest-inputs">
{{/if}} <fieldset{{#if dmg.base}} disabled{{/if}} class="one-column">
{{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}} <legend class="with-icon">
<div class="nest-inputs"> {{localize (concat "DAGGERHEART.CONFIG.HealingType." dmg.applyTo ".name")}}
<fieldset class="one-column"> {{#unless (or dmg.base ../path)}}
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}</legend> <a data-action="removeDamageResource" data-key="{{key}}"><i class="fas fa-trash"></i></a>
{{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" key=dmg.applyTo path=../path}} {{/unless}}
</fieldset> </legend>
<fieldset class="one-column"> {{> damageData damage=dmg fields=../fields.resources.element.fields basePath=(concat ../path "damage.resources." dmg.applyTo)}}
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}</legend> </fieldset>
{{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" key=dmg.applyTo path=../path}} </div>
</fieldset> {{/each}}
</div> </fieldset>
{{else}} {{/unless}}
{{> 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}}">
</fieldset>
</div>
{{/each}}
</fieldset>
{{#*inline "formula"}} {{#*inline "formula"}}
{{#unless dmg.base}} {{#unless isBase}}
{{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat path "damage.parts." key "." target ".custom.enabled") classes="checkbox" localize=true}} {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat basePath ".custom.enabled") classes="checkbox" localize=true}}
{{/unless}} {{/unless}}
{{#if source.custom.enabled}} {{#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}} {{else}}
<div class="nest-inputs"> <div class="nest-inputs">
{{#unless @root.isNPC}} {{#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}} {{/unless}}
{{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat path "damage.parts." key "." target ".flatMultiplier") localize=true }}{{/if}} {{#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 path "damage.parts." key "." target ".dice") localize=true}} {{formField fields.dice value=source.dice name=(concat basePath ".dice") localize=true}}
{{formField fields.bonus value=source.bonus name=(concat path "damage.parts." key "." target ".bonus") localize=true}} {{formField fields.bonus value=source.bonus name=(concat basePath ".bonus") localize=true}}
</div> </div>
{{/if}} {{/if}}
{{#if @root.isNPC}} {{#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}} {{/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}} {{/inline}}

View file

@ -15,34 +15,7 @@
{{/each}} {{/each}}
</fieldset> </fieldset>
{{/if}} {{/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>
{{/each}}
{{#if damageOptions.groupAttack}} {{#if damageOptions.groupAttack}}
<fieldset class="group-attack-container"> <fieldset class="group-attack-container">
<legend>{{localize "DAGGERHEART.ACTIONS.Settings.groupAttack.label"}}</legend> <legend>{{localize "DAGGERHEART.ACTIONS.Settings.groupAttack.label"}}</legend>
@ -59,6 +32,45 @@
</div> </div>
</fieldset> </fieldset>
{{/if}} {{/if}}
{{#if @root.damageFormula}}
{{#with @root.damageFormula}}
<div class="damage-formula">
<span class="damage-resource"><b>{{localize "DAGGERHEART.GENERAL.formula"}}:</b> {{roll.formula}}</span>
<span class="damage-details">
{{localize "DAGGERHEART.GENERAL.damage"}}
{{#if damageTypes}}
{{#each damageTypes as | type | }}
{{#with (lookup @root.config.GENERAL.damageTypes type)}}
<i class="fa-solid {{icon}}"></i>
{{/with}}
{{/each}}
{{/if}}
</span>
</div>
<div class="bonuses form-group flexrow">
<input type="text" value="{{extraFormula}}" name="damageFormula.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>
{{/with}}
{{/if}}
{{#each @root.resourceFormulas}}
<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}}
</span>
</div>
<div class="bonuses form-group flexrow">
<input type="text" value="{{extraFormula}}" name={{concat "resourceFormulas." @key ".extraFormula"}} placeholder="{{localize "DAGGERHEART.GENERAL.situationalBonus"}}">
</div>
{{/each}}
{{#unless (empty @root.modifiers)}} {{#unless (empty @root.modifiers)}}
<fieldset class="modifier-container two-columns"> <fieldset class="modifier-container two-columns">
@ -76,6 +88,7 @@
{{/each}} {{/each}}
</fieldset> </fieldset>
{{/unless}} {{/unless}}
<div class="damage-section-controls"> <div class="damage-section-controls">
{{#if directDamage}} {{#if directDamage}}
<select class="roll-mode-select" name="selectedMessageMode"> <select class="roll-mode-select" name="selectedMessageMode">

View file

@ -1,7 +1,15 @@
{{#each damage.types as |roll key|}} {{#if damage.main}}
{{> damage roll=damage.main label=(localize "DAGGERHEART.GENERAL.damage") memberKey=key}}
{{/if}}
{{#each damage.resources as |roll key|}}
{{> damage roll=roll label=(localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")) memberKey=../key isResource="true" }}
{{/each}}
{{#*inline "damage"}}
<div class="roll-data {{#if isCritical}}critical{{/if}}"> <div class="roll-data {{#if isCritical}}critical{{/if}}">
<div class="duality-label"> <div class="duality-label">
<span>{{localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")}}:</span> <span>{{label}}:</span>
<span>{{roll.total}}</span> <span>{{roll.total}}</span>
</div> </div>
@ -9,7 +17,7 @@
{{#each roll.dice}} {{#each roll.dice}}
{{#each results as |result index|}} {{#each results as |result index|}}
{{#if result.active}} {{#if result.active}}
<a class="roll-dice" data-action="rerollDamageDice" data-member-key="{{../../../key}}" data-damage-key="{{@../../key}}" data-dice-index="{{@../key}}" data-result-index="{{index}}"> <a class="roll-dice" data-action="rerollDamageDice" data-member-key="{{../../memberKey}}" data-damage-key="{{@../../key}}" {{#if ../../isResource}}data-is-resource="true"{{/if}} data-dice-index="{{@../key}}" data-result-index="{{index}}">
<span class="dice-label">{{result.result}}</span> <span class="dice-label">{{result.result}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" ../denomination ".svg"}}" /> <img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" ../denomination ".svg"}}" />
</a> </a>
@ -25,4 +33,4 @@
{{/if}} {{/if}}
</div> </div>
</div> </div>
{{/each}} {{/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.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.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.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.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}} {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}}

View file

@ -22,5 +22,5 @@
</div> </div>
{{formGroup systemFields.criticalThreshold value=document._source.system.criticalThreshold label="DAGGERHEART.ACTIONS.Settings.criticalThreshold" name="system.criticalThreshold" localize=true}} {{formGroup systemFields.criticalThreshold value=document._source.system.criticalThreshold label="DAGGERHEART.ACTIONS.Settings.criticalThreshold" name="system.criticalThreshold" localize=true}}
</fieldset> </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> </section>

View file

@ -18,26 +18,26 @@
</fieldset> </fieldset>
<fieldset class="two-columns"> <fieldset class="two-columns">
{{#with systemFields.attack.fields.damage.fields.parts.element.fields as | fields | }} {{#with systemFields.attack.fields.damage.fields.main.fields as | fields | }}
{{#with ../document.system.attack.damage.parts.hitPoints as | source | }} {{#with ../document.system.attack.damage.main as | source | }}
<legend>{{localize "DAGGERHEART.GENERAL.damage"}}</legend> <legend>{{localize "DAGGERHEART.GENERAL.damage"}}</legend>
<span>{{localize "DAGGERHEART.ACTIONS.Config.general.customFormula"}}</span> <span>{{localize "DAGGERHEART.ACTIONS.Config.general.customFormula"}}</span>
{{formInput fields.value.fields.custom.fields.enabled value=source.value.custom.enabled name="system.attack.damage.parts.hitPoints.value.custom.enabled"}} {{formInput fields.value.fields.custom.fields.enabled value=source.value.custom.enabled name="system.attack.damage.main.value.custom.enabled"}}
{{#if source.value.custom.enabled}} {{#if source.value.custom.enabled}}
<span>{{localize "DAGGERHEART.ACTIONS.Config.general.formula"}}</span> <span>{{localize "DAGGERHEART.ACTIONS.Config.general.formula"}}</span>
{{formInput fields.value.fields.custom.fields.formula value=source.value.custom.formula name="system.attack.damage.parts.hitPoints.value.custom.formula"}} {{formInput fields.value.fields.custom.fields.formula value=source.value.custom.formula name="system.attack.damage.main.value.custom.formula"}}
{{else}} {{else}}
<span>{{localize "DAGGERHEART.GENERAL.Dice.single"}}</span> <span>{{localize "DAGGERHEART.GENERAL.Dice.single"}}</span>
{{formInput fields.value.fields.dice value=source.value.dice name="system.attack.damage.parts.hitPoints.value.dice"}} {{formInput fields.value.fields.dice value=source.value.dice name="system.attack.damage.main.value.dice"}}
<span>{{localize "DAGGERHEART.GENERAL.bonus"}}</span> <span>{{localize "DAGGERHEART.GENERAL.bonus"}}</span>
{{formInput fields.value.fields.bonus value=source.value.bonus name="system.attack.damage.parts.hitPoints.value.bonus" localize=true}} {{formInput fields.value.fields.bonus value=source.value.bonus name="system.attack.damage.main.value.bonus" localize=true}}
{{/if}} {{/if}}
<span>{{localize "DAGGERHEART.GENERAL.type"}}</span> <span>{{localize "DAGGERHEART.GENERAL.type"}}</span>
{{formInput fields.type value=source.type name="system.attack.damage.parts.hitPoints.type" localize=true}} {{formInput fields.type value=source.type name="system.attack.damage.main.type" localize=true}}
<span>{{localize "DAGGERHEART.CONFIG.DamageType.direct.name"}}</span> <span>{{localize "DAGGERHEART.CONFIG.DamageType.direct.name"}}</span>
{{formInput @root.systemFields.attack.fields.damage.fields.direct value=@root.document.system.attack.damage.direct name="system.attack.damage.direct" localize=true}} {{formInput @root.systemFields.attack.fields.damage.fields.main.fields.direct value=@root.document.system.attack.damage.main.direct name="system.attack.damage.main.direct" localize=true}}
<input type="hidden" name="system.attack.damage.parts.hitPoints.value.multiplier" value="{{source.value.multiplier}}"> <input type="hidden" name="system.attack.damage.main.value.multiplier" value="{{source.value.multiplier}}">
{{/with}} {{/with}}
{{/with}} {{/with}}
</fieldset> </fieldset>
<fieldset class="two-columns"> <fieldset class="two-columns">

View file

@ -10,60 +10,79 @@
</div> </div>
<div class="roll-part-extra on-reduced"> <div class="roll-part-extra on-reduced">
<div class="wrapper"> <div class="wrapper">
{{#each damage.types as | roll index | }} {{#if damage.main}}
<div class="roll-formula">{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}: {{roll.total}}</div> {{> formula roll=damage.main label=(localize "DAGGERHEART.GENERAL.damage") }}
{{/if}}
{{#each damage.resources as | roll index | }}
{{> formula roll=roll label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll'))) }}
{{/each}} {{/each}}
</div> </div>
</div> </div>
<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">
{{#each damage.types as | roll index | }} {{#if damage.main}}
<fieldset> {{> damage label=(localize "DAGGERHEART.GENERAL.damage") roll=damage.main isDirect=isDirect }}
<legend> {{/if}}
{{#if ../hasHealing}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')}}{{else}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}{{/if}} <div class="roll-formula">{{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}</div>{{#if (and (eq index "hitPoints") ../isDirect)}} <div class="roll-formula">{{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}</div>{{/if}}
</legend>
{{#if (and (not @root.hasHealing) roll.options.damageTypes.length)}} {{#each damage.resources as | roll index | }}
<label class="roll-part-header"><span> {{> damage
{{#each roll.options.damageTypes}} label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')))
{{localize (concat 'DAGGERHEART.CONFIG.ArmorFeature.' this '.name')}} roll=roll
{{#unless @last}}/{{/unless}} isResource=true
{{/each}} }}
<div class="roll-formula">{{roll.total}}</div></span></label>
{{/if}}
<div class="roll-dice">
{{#if roll.dice.length}}
{{#each roll.dice}}
{{#each results}}
{{#if active}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
<div
class="dice reroll-button {{../denomination}}"
data-die-index="0" data-type="damage" data-damage-type="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}"
>
{{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}
{{result}}
</div>
</div>
{{/if}}
{{/each}}
{{/each}}
{{#if roll.modifierTotal}}
<div class="roll-die{{#if (gt roll.modifierTotal 0)}} has-plus{{/if}}">
<div class="font-20">{{roll.modifierTotal}}</div>
</div>
{{/if}}
{{else}}
<div class="roll-die">
<div class="font-20">{{roll.total}}</div>
</div>
{{/if}}
</div>
</fieldset>
{{/each}} {{/each}}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{#*inline "formula"}}
<div class="roll-formula">{{label}}: {{roll.total}}</div>
{{/inline}}
{{#*inline "damage"}}
<fieldset>
<legend>
{{label}}
<div class="roll-formula">{{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}</div>{{#if isDirect}} <div class="roll-formula">{{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}</div>{{/if}}
</legend>
{{#if (and (not @root.hasHealing) roll.options.damageTypes.length)}}
<label class="roll-part-header"><span>
{{#each roll.options.damageTypes}}
{{localize (concat 'DAGGERHEART.CONFIG.ArmorFeature.' this '.name')}}
{{#unless @last}}/{{/unless}}
{{/each}}
<div class="roll-formula">{{roll.total}}</div></span></label>
{{/if}}
<div class="roll-dice">
{{#if roll.dice.length}}
{{#each roll.dice}}
{{#each results}}
{{#if active}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
<div
class="dice reroll-button {{../denomination}}"
data-type="damage" data-damage-type="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}" {{#if ../../isResource}}data-is-resource="true"{{/if}}
>
{{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}
{{result}}
</div>
</div>
{{/if}}
{{/each}}
{{/each}}
{{#if roll.modifierTotal}}
<div class="roll-die{{#if (gt roll.modifierTotal 0)}} has-plus{{/if}}">
<div class="font-20">{{roll.modifierTotal}}</div>
</div>
{{/if}}
{{else}}
<div class="roll-die">
<div class="font-20">{{roll.total}}</div>
</div>
{{/if}}
</div>
</fieldset>
{{/inline}}

View file

@ -23,7 +23,7 @@
</div> </div>
{{/if}} {{/if}}
<div class="tag"> <div class="tag">
<span>{{{damageFormula attack}}} {{{damageSymbols attack.damage.parts}}}</span> <span>{{{damageFormula attack}}} {{{damageSymbols attack.damage.main}}}</span>
</div> </div>
</div> </div>
{{#if description}} {{#if description}}

View file

@ -23,7 +23,7 @@
{{/with}} {{/with}}
</div> </div>
<div class="tag"> <div class="tag">
<span>{{{damageFormula item.system.attack}}} {{{damageSymbols item.system.attack.damage.parts}}}</span> <span>{{{damageFormula item.system.attack}}} {{{damageSymbols item.system.attack.damage.main}}}</span>
</div> </div>
</div> </div>
{{#if description}} {{#if description}}