Compare commits

..

11 commits
main ... 2.5.3

Author SHA1 Message Date
WBHarry
557bf93853 Merge branch 'main' into v14 2026-07-13 17:04:42 +02:00
WBHarry
870692bfa2 Merge branch 'main' into v14 2026-07-12 00:40:19 +02:00
WBHarry
54461a0d68 Merge branch 'main' into v14 2026-07-11 01:47:54 +02:00
WBHarry
82d45cd5fb Merge branch 'main' into v14 2026-07-03 11:33:27 +02:00
WBHarry
ea3ad2fa11 Merge branch 'v14' of https://github.com/Foundryborne/daggerheart into v14 2026-07-03 11:33:13 +02:00
WBHarry
8b8f741b74 Merge branch 'main' into v13 2026-07-03 11:17:22 +02:00
Carlos Fernandez
b73c70aeb8 Also remove fieldset for npc features and fix padding issue in full screen notes 2026-07-03 01:42:34 -04:00
Carlos Fernandez
7409a577f6 Remove certain fieldsets and adjust scrollsbars 2026-07-03 00:58:48 -04:00
WBHarry
2b725e8760 Merge branch 'main' into v13 2026-06-29 14:25:25 +02:00
WBHarry
f02e97f0cd Revert "Moved v13 countdown migration over to a migrateData"
This reverts commit c5308617e1.
2026-06-27 23:10:33 +02:00
WBHarry
c5308617e1 Moved v13 countdown migration over to a migrateData 2026-06-27 22:31:36 +02:00
102 changed files with 1333 additions and 2566 deletions

13
daggerheart.d.ts vendored
View file

@ -11,9 +11,6 @@ import * as documents from './module/documents/_module.mjs';
import { macros } from './module/_module.mjs'; import { macros } from './module/_module.mjs';
import * as dice from './module/dice/_module.mjs'; import * as dice from './module/dice/_module.mjs';
import * as fields from './module/data/fields/_module.mjs'; import * as fields from './module/data/fields/_module.mjs';
import { gameSettings } from './module/config/settingsConfig.mjs';
import DhCountdowns from './module/data/countdowns.mjs';
import DhAutomation from './module/data/settings/Automation.mjs';
// Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such
@ -106,13 +103,3 @@ declare module '@client/packages/system.mjs' {
}; };
} }
} }
declare module '@client/helpers/client-settings.mjs' {
// Add explicit typed overrides for auto complete. These require /** @type {"string"} on the vars themselves to work */
export default interface ClientSettings {
get(namespace: 'daggerheart', key: typeof gameSettings.Automation): DhAutomation;
get(namespace: 'daggerheart', key: typeof gameSettings.Homebrew): DhHomebrew;
get(namespace: 'daggerheart', key: typeof gameSettings.Countdowns): DhCountdowns;
get(namespace: 'daggerheart', key: string): unknown;
}
}

View file

@ -6,7 +6,6 @@ import * as documents from './module/documents/_module.mjs';
import { macros } from './module/_module.mjs'; import { macros } from './module/_module.mjs';
import * as collections from './module/documents/collections/_module.mjs'; import * as collections from './module/documents/collections/_module.mjs';
import * as dice from './module/dice/_module.mjs'; import * as dice from './module/dice/_module.mjs';
import * as die from './module/dice/die/_module.mjs';
import * as fields from './module/data/fields/_module.mjs'; import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
@ -24,7 +23,7 @@ import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM; CONFIG.DH = SYSTEM;
CONFIG.TextEditor.enrichers.push(...enricherConfig); CONFIG.TextEditor.enrichers.push(...enricherConfig);
CONFIG.Dice.rolls = [Roll = BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll]; CONFIG.Dice.rolls = [BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll];
CONFIG.Dice.daggerheart = { CONFIG.Dice.daggerheart = {
DHRoll: DHRoll, DHRoll: DHRoll,
DualityRoll: DualityRoll, DualityRoll: DualityRoll,
@ -39,8 +38,6 @@ CONFIG.RegionBehavior.dataModels = {
}; };
Object.assign(CONFIG.Dice.termTypes, dice.diceTypes); Object.assign(CONFIG.Dice.termTypes, dice.diceTypes);
CONFIG.Dice.terms.d = die.BaseDie;
CONFIG.Dice.types = [die.BaseDie, CONFIG.Dice.terms.f];
CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.documentClass = documents.DhpActor;
CONFIG.Actor.dataModels = models.actors.config; CONFIG.Actor.dataModels = models.actors.config;
@ -270,10 +267,6 @@ Hooks.on('i18nInit', () => {
}); });
Hooks.on('setup', () => { Hooks.on('setup', () => {
if (game.user.isGM) {
document.body.dataset.gm = true;
}
CONFIG.statusEffects = [ CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)), ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({ ...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({

View file

@ -104,10 +104,8 @@
"startCountdown": "Start Countdown" "startCountdown": "Start Countdown"
}, },
"damage": { "damage": {
"addResource": "Add Resource",
"multiplier": "Multiplier", "multiplier": "Multiplier",
"flatMultiplier": "Flat Multiplier", "flatMultiplier": "Flat Multiplier"
"markResources": "Mark Resources"
}, },
"general": { "general": {
"customFormula": "Custom Formula", "customFormula": "Custom Formula",
@ -126,15 +124,6 @@
"damageOnSave": "Damage on Save", "damageOnSave": "Damage on Save",
"useDefaultItemValues": "Use default Item values" "useDefaultItemValues": "Use default Item values"
}, },
"Reload": {
"checkReload": "Check Reload",
"reloadRequired": "Reload Required!",
"notRolled": "Not Rolled",
"checkFailed": "Check Failed",
"checkPassed": "Check Passed",
"rerollConfirmationTitle": "Reroll Reload Check",
"rerollConfirmationText": "Are you sure you want to reload the reload check?"
},
"RollField": { "RollField": {
"diceRolling": { "diceRolling": {
"compare": "Should be", "compare": "Should be",
@ -1331,11 +1320,6 @@
"short": "V. Far" "short": "V. Far"
} }
}, },
"ReloadChoices": {
"off": { "label": "Don't Use" },
"manual": { "label": "Manual" },
"auto": { "label": "Automatic" }
},
"RollTypes": { "RollTypes": {
"trait": { "trait": {
"name": "Trait" "name": "Trait"
@ -2235,9 +2219,7 @@
}, },
"Resource": { "Resource": {
"single": "Resource", "single": "Resource",
"plural": "Resources", "plural": "Resources"
"unloaded": "The weapon is not loaded",
"loaded": "The weapon is loaded"
}, },
"Roll": { "Roll": {
"attack": "Attack Roll", "attack": "Attack Roll",
@ -2501,6 +2483,7 @@
"reroll": "Reroll", "reroll": "Reroll",
"rerolled": "Rerolled", "rerolled": "Rerolled",
"rerollThing": "Reroll {thing}", "rerollThing": "Reroll {thing}",
"resource": "Resource",
"result": { "result": {
"single": "Result", "single": "Result",
"plural": "Results" "plural": "Results"
@ -2567,9 +2550,6 @@
}, },
"identifier": { "identifier": {
"label": "Identifier" "label": "Identifier"
},
"gmNotes": {
"label": "GM Notes"
} }
}, },
"Ancestry": { "Ancestry": {
@ -2585,9 +2565,6 @@
"severe": "Severe Threshold" "severe": "Severe Threshold"
} }
}, },
"Base": {
"addGMNote": "Add GM Note"
},
"Beastform": { "Beastform": {
"FIELDS": { "FIELDS": {
"beastformType": { "label": "Beastform Type" }, "beastformType": { "label": "Beastform Type" },
@ -2704,9 +2681,6 @@
"displayFear": { "displayFear": {
"label": "Display Fear" "label": "Display Fear"
}, },
"fearPosition": {
"label": "Fear Position"
},
"displayCountdownUI": { "displayCountdownUI": {
"label": "Display Countdown UI" "label": "Display Countdown UI"
}, },
@ -2758,13 +2732,6 @@
"token": "Tokens", "token": "Tokens",
"bar": "Bar", "bar": "Bar",
"hide": "Hide" "hide": "Hide"
},
"fearPosition": {
"free": "Free",
"topCenter": "Top + Center",
"bottomCenter": "Bottom + Center",
"rightTop": "Right + Top",
"leftBottom": "Left + Bottom"
} }
}, },
"Automation": { "Automation": {
@ -2823,10 +2790,6 @@
"hint": "Effects with defined range dependency will automatically turn on/off depending on range" "hint": "Effects with defined range dependency will automatically turn on/off depending on range"
} }
}, },
"reload": {
"label": "Reload Checking",
"hint": "If the system should automatically roll reload checks or wait for the manual press of a button in chat"
},
"resourceScrollTexts": { "resourceScrollTexts": {
"label": "Show Resource Change Scrolltexts", "label": "Show Resource Change Scrolltexts",
"hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this." "hint": "When a character is damaged, uses armor etc, a scrolling text will briefly appear by the token to signify this."
@ -3288,8 +3251,7 @@
"knowTheTide": "Know The Tide gained a token", "knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"noTokenTargeted": "No token is targeted", "noTokenTargeted": "No token is targeted",
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM", "behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM"
"reloadRequired": "The {weapon} must be reloaded to be used!"
}, },
"Progress": { "Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry." "migrationLabel": "Performing system migration. Please wait and do not close Foundry."

View file

@ -51,11 +51,7 @@ 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;
@ -77,11 +73,7 @@ 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

@ -185,6 +185,8 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
if (result === undefined) return; if (result === undefined) return;
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
@ -200,6 +202,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
img: this.selectedMove.img, img: this.selectedMove.img,
description: game.i18n.localize(this.selectedMove.description), description: game.i18n.localize(this.selectedMove.description),
result: result, result: result,
open: autoExpandDescription ? 'open' : '',
showRiskItAllButton: this.showRiskItAllButton, showRiskItAllButton: this.showRiskItAllButton,
riskItAllButtonLabel: this.riskItAllButtonLabel, riskItAllButtonLabel: this.riskItAllButtonLabel,
riskItAllHope: this.riskItAllHope riskItAllHope: this.riskItAllHope

View file

@ -196,6 +196,9 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
.filter(x => x.testUserPermission(game.user, 'LIMITED')) .filter(x => x.testUserPermission(game.user, 'LIMITED'))
.filter(x => x.uuid !== this.actor.uuid); .filter(x => x.uuid !== this.actor.uuid);
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
user: game.user.id, user: game.user.id,
@ -216,7 +219,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
actor: { name: this.actor.name, img: this.actor.img }, actor: { name: this.actor.name, img: this.actor.img },
moves: moves, moves: moves,
characters: characters, characters: characters,
selfId: this.actor.uuid selfId: this.actor.uuid,
open: autoExpandDescription ? 'open' : ''
} }
), ),
flags: { flags: {

View file

@ -1,5 +1,4 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
import { ChatDamageData } from '../../data/chat-message/chatDamageData.mjs';
import { MemberData } from '../../data/tagTeamData.mjs'; import { MemberData } from '../../data/tagTeamData.mjs';
import { getCritDamageBonus, shouldUseHopeFearAutomation } from '../../helpers/utils.mjs'; import { getCritDamageBonus, shouldUseHopeFearAutomation } from '../../helpers/utils.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
@ -141,7 +140,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const hasRolled = Boolean(data.rollData); const hasRolled = Boolean(data.rollData);
if (!hasRolled) return false; if (!hasRolled) return false;
return !data.rollData.options.hasDamage || data.damageRollData.active; return !data.rollData.options.hasDamage || Boolean(data.rollData.options.damage);
}); });
return context; return context;
@ -182,7 +181,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
const critSelected = !selectedRoll const critSelected = !selectedRoll
? undefined ? undefined
: (selectedRoll?.roll?.isCritical ?? false); : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members); partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members);
partContext.joinedRoll = await this.getJoinedRoll({ partContext.joinedRoll = await this.getJoinedRoll({
@ -236,7 +235,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
} }
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
const critSelected = !selectedRoll ? undefined : (selectedRoll?.roll?.isCritical ?? false); const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
const damage = data.rollData?.options?.damage;
return { return {
...data, ...data,
@ -247,9 +247,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
hasRolled: Boolean(data.rollData), hasRolled: Boolean(data.rollData),
rollOptions, rollOptions,
damageRollOptions, damageRollOptions,
damage: data.damageRollData, damage: damage,
critDamage: await this.getCriticalDamage(data.damageRollData), critDamage: await this.getCriticalDamage(damage),
useCritDamage: critSelected || (critSelected === undefined && data.roll?.isCritical) useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
}; };
} }
@ -379,7 +379,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
let rollIsSelected = false; let rollIsSelected = false;
for (const member of Object.values(members)) { for (const member of Object.values(members)) {
const rollFinished = Boolean(member.rollData); const rollFinished = Boolean(member.rollData);
const damageFinished = member.rollData?.options?.hasDamage ? member.damageRollData.active : true; const damageFinished = member.rollData?.options?.hasDamage ? Boolean(member.rollData.options.damage) : true;
rollsAreFinished = rollsAreFinished && rollFinished && damageFinished; rollsAreFinished = rollsAreFinished && rollFinished && damageFinished;
rollIsSelected = rollIsSelected || member.selected; rollIsSelected = rollIsSelected || member.selected;
@ -540,10 +540,17 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
await action.workflow.get('damage').execute(config, null, true); await action.workflow.get('damage').execute(config, null, true);
if (!config.damage) return; if (!config.damage) return;
const current = this.party.system.tagTeam.members[memberKey].rollData;
await this.updatePartyData( await this.updatePartyData(
{ {
[`system.tagTeam.members.${memberKey}.damageRollData`]: config.damage [`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: config.damage
}
}
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
); );
@ -551,11 +558,15 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
static async #removeDamageRoll(_, button) { static async #removeDamageRoll(_, button) {
const { memberKey } = button.dataset; const { memberKey } = button.dataset;
const current = this.party.system.tagTeam.members[memberKey].rollData;
this.updatePartyData( this.updatePartyData(
{ {
[`system.tagTeam.members.${memberKey}.damageRollData`]: { [`system.tagTeam.members.${memberKey}.rollData`]: {
main: null, ...current,
resources: _replace({}) options: {
...current.options,
damage: null
}
} }
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
@ -563,44 +574,89 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
} }
static async #rerollDamageDice(_, button) { static async #rerollDamageDice(_, button) {
const { isResource, memberKey, damageKey, diceIndex, resultIndex } = button.dataset; const { memberKey, damageKey, part, dice } = button.dataset;
const memberData = this.party.system.tagTeam.members[memberKey]; const memberData = this.party.system.tagTeam.members[memberKey];
await memberData.damageRollData.rerollDamageDie(isResource, damageKey, diceIndex, resultIndex); const partData = memberData.rollData.options.damage[damageKey].parts[part];
const activeDiceResultKey = Object.keys(partData.dice[dice].results).find(
index => partData.dice[dice].results[index].active
);
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(
partData,
dice,
activeDiceResultKey
);
const rollData = this.party.system.tagTeam.members[memberKey].rollData;
rollData.options.damage[damageKey].parts = rollData.options.damage[damageKey].parts.map((damagePart, index) => {
if (index !== Number.parseInt(part)) return damagePart;
return {
...damagePart,
total: parsedRoll.total,
dice: rerolledDice
};
});
rollData.options.damage[damageKey].total = rollData.options.damage[damageKey].parts.reduce((acc, part) => {
acc += part.total;
return acc;
}, 0);
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(
{ {
[updatePath]: updateValue.toJSON() [`system.tagTeam.members.${memberKey}.rollData`]: rollData
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
); );
} }
async getCriticalDamage(origDamage) { async getCriticalDamage(damage) {
const newDamage = origDamage ? ChatDamageData.fromJSON(JSON.stringify(origDamage)) : null; const newDamage = foundry.utils.deepClone(damage);
if (newDamage?.main) { for (let key in newDamage) {
const criticalDamage = await getCritDamageBonus(newDamage.main.formula); var damage = newDamage[key];
if (criticalDamage) { damage.formula = '';
const criticalTerm = new foundry.dice.terms.NumericTerm({ number: criticalDamage, evaluated: true }); damage.total = 0;
criticalTerm.evaluate();
newDamage.main = await Roll.fromTerms([ for (let part of damage.parts) {
...origDamage.main.terms, const criticalDamage = await getCritDamageBonus(part.formula);
new foundry.dice.terms.OperatorTerm({ operator: '+' }), if (criticalDamage) {
criticalTerm part.modifierTotal += criticalDamage;
]); part.total += criticalDamage;
newDamage.main.options = foundry.utils.deepClone(origDamage.main.options); part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
part.roll = new Roll(part.formula);
}
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
damage.total += part.total;
} }
} }
return newDamage;
}
async getNonCriticalDamage(config) {
const newDamage = foundry.utils.deepClone(config.damage);
for (let key in newDamage) {
var damage = newDamage[key];
damage.formula = '';
damage.total = 0;
for (let part of damage.parts) {
const critDamageBonus = await getCritDamageBonus(part.formula);
part.modifierTotal -= critDamageBonus;
part.total -= critDamageBonus;
part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
part.roll = new Roll(part.formula);
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
damage.total += part.total;
}
}
return newDamage; return newDamage;
} }
static async #selectRoll(_, button) { static async #selectRoll(_, button) {
const { memberKey } = button.dataset; const { memberKey } = button.dataset;
this.updatePartyData( this.updatePartyData(
{ {
[`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce( [`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce(
@ -611,12 +667,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
{} {}
) )
}, },
/* Selecting a roll must update all member sections hbs to display the correct damage information incase of a critical */ this.getUpdatingParts(button)
[
...Object.keys(this.party.system.tagTeam.members),
this.constructor.PARTS.rollSelection.id,
this.constructor.PARTS.result.id
]
); );
} }
@ -634,65 +685,29 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null;
const mainRoll = new MemberData(baseMainRoll.toObject()); const mainRoll = new MemberData(baseMainRoll.toObject());
mainRoll.damageRollData = baseMainRoll.damageRollData ? const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData;
ChatDamageData.fromJSON(JSON.stringify(baseMainRoll.damageRollData)) : null; const systemData = mainRoll.rollData.options;
const secondaryRoll = new MemberData(baseSecondaryRoll.toObject()); const isCritical = overrideIsCritical ?? systemData.roll.isCritical;
secondaryRoll.damageRollData = baseSecondaryRoll.damageRollData ? if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage);
ChatDamageData.fromJSON(JSON.stringify(baseSecondaryRoll.damageRollData)) : null;
const isCritical = overrideIsCritical ?? mainRoll.roll.isCritical; if (secondaryRollData?.options.hasDamage) {
if (isCritical) mainRoll.damageRollData = await this.getCriticalDamage(mainRoll.damageRollData);
if (secondaryRoll.damageRollData) {
const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical)
? await this.getCriticalDamage(secondaryRoll.damageRollData) ? await this.getCriticalDamage(secondaryRollData.options.damage)
: secondaryRoll.damageRollData; : secondaryRollData.options.damage;
if (mainRoll.damageRollData) { if (systemData.damage) {
if (secondaryDamage.main) { for (const [key, damage] of Object.entries(secondaryDamage ?? {})) {
if (mainRoll.damageRollData.main) { if (key in systemData.damage) {
mainRoll.damageRollData.main = Roll.fromTerms([ systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula]
...baseMainRoll.damageRollData.main.terms, .filter(x => x)
new foundry.dice.terms.OperatorTerm({ operator: '+' }), .join(' + ');
...baseSecondaryRoll.damageRollData.main.terms systemData.damage[key].total += damage.total;
]); systemData.damage[key].parts.push(...damage.parts);
/* Joining the roll.options of both rolls */
const joinedDamageTypes = new Set([
...baseMainRoll.damageRollData.main.options.damageTypes,
...baseSecondaryRoll.damageRollData.main.options.damageTypes
]);
mainRoll.damageRollData.main.options = {
...baseMainRoll.damageRollData.main.options,
damageTypes: [...joinedDamageTypes]
};
} else { } else {
mainRoll.damageRollData.main = secondaryDamage.main; systemData.damage[key] = damage;
}
}
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 {
mainRoll.damageRollData = secondaryDamage; systemData.damage = secondaryDamage;
} }
} }
@ -747,27 +762,13 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor); const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor);
mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle'); mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
/* This could assumably be done better. For some reason rolls don't get correctly done through rollData.toJSON */
const systemData = {
...mainRoll.options,
damage: joinedRoll.damageRollData?.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'),
msgData = { msgData = {
type: 'dualityRoll', type: 'dualityRoll',
user: game.user.id, user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'), title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: mainActor }), speaker: cls.getSpeaker({ actor: mainActor }),
system: systemData, system: mainRoll.options,
rolls: [JSON.stringify(joinedRoll.roll)], rolls: [JSON.stringify(joinedRoll.roll)],
sound: null, sound: null,
flags: { core: { RollTable: true } } flags: { core: { RollTable: true } }

View file

@ -1,4 +1,4 @@
import { DHDamageData } from '../../data/fields/action/damageField.mjs'; import { getUnusedDamageTypes } from '../../helpers/utils.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,10 +31,8 @@ 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.#onAddDamage, addDamage: this.addDamage,
removeDamage: this.#onRemoveDamage, removeDamage: this.removeDamage,
addDamageResource: this.#onAddDamageResource,
removeDamageResource: this.#onRemoveDamageResource,
editDoc: this.editDoc, editDoc: this.editDoc,
addTrigger: this.addTrigger, addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger, removeTrigger: this.removeTrigger,
@ -159,9 +157,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) {
const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes); context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
context.allDamageTypesUsed = allKeys.every(k => k in this.action._source.damage.resources);
if (this.action.damage?.main?.hasOwnProperty('includeBase') && this.action.type === 'attack') if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack; context.hasBaseDamage = !!this.action.parent.attack;
} }
@ -255,7 +253,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
const submitData = this._prepareSubmitData(event, formData); const submitData = this._prepareSubmitData(event, formData);
const data = foundry.utils.mergeObject(this.action.toObject(), submitData); const data = foundry.utils.mergeObject(this.action.toObject(), submitData);
this.action = (await this.action.update(data)) ?? this.action; this.action = await this.action.update(data);
this.sheetUpdate?.(this.action); this.sheetUpdate?.(this.action);
this.render(); this.render();
@ -301,70 +299,53 @@ 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) });
} }
/** @this DHActionBaseConfig */ static addDamage(_event) {
static #onAddDamage() { if (!this.action.damage.parts) return;
if (!this.action.damage || this.action.damage?.main) return;
const data = this.action.toObject(); const choices = getUnusedDamageTypes(this.action._source.damage.parts);
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: _loc('DAGGERHEART.GENERAL.Resource.single'), label: game.i18n.localize('Damage Type'),
choices, choices,
required: true required: true
}).toFormGroup({}, { }).toFormGroup(
name: 'type', {},
localize: true, {
nameAttr: 'value', name: 'type',
labelAttr: 'label' localize: true,
}).outerHTML; nameAttr: 'value',
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;
data.damage.resources[type] = { const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue();
...this.action.schema.fields.damage.fields.resources.element.getInitialValue(), part.applyTo = type;
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;
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', {
label: 'Confirm', action: 'ok',
icon: 'fas fa-check', label: 'Confirm',
default: true, icon: 'fas fa-check',
callback default: true
} },
{ callback: callback }
)
], ],
content: content, content: content,
rejectClose: false, rejectClose: false,
modal: false, modal: false,
window: { window: {
title: _loc('DAGGERHEART.ACTIONS.Config.damage.addResource') title: game.i18n.localize('Add Damage')
}, },
position: { width: 300 } position: { width: 300 }
}); });
@ -372,12 +353,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
typeDialog.render(true); typeDialog.render(true);
} }
/** @this DHActionBaseConfig */ static removeDamage(_event, button) {
static #onRemoveDamageResource(_event, button) { if (!this.action.damage.parts) return;
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;
data.damage.resources[key] = _del; delete data.damage.parts[key];
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,4 +1,3 @@
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 */
@ -9,10 +8,8 @@ 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: this.#onAddExperience, addExperience: DHAdversarySettings.#addExperience,
removeExperience: this.#onRemoveExperience, removeExperience: DHAdversarySettings.#removeExperience
addDamage: this.#onAddDamage,
removeDamage: this.#onRemoveDamage
} }
}; };
@ -74,7 +71,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 #onAddExperience() { static async #addExperience() {
const newExperience = { const newExperience = {
name: 'Experience', name: 'Experience',
modifier: 0 modifier: 0
@ -86,7 +83,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 #onRemoveExperience(_, target) { static async #removeExperience(_, 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: {
@ -101,28 +98,4 @@ 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

@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getArmorSources, getDocFromElement, getDocFromElementSync, itemAbleRollParse, sortBy } from '../../../helpers/utils.mjs'; import { getArmorSources, getDocFromElement, getDocFromElementSync, sortBy } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -29,7 +29,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
toggleResourceDice: CharacterSheet.#toggleResourceDice, toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie, advanceResourceDie: CharacterSheet.#advanceResourceDie,
toggleItemReload: CharacterSheet.#onToggleItemReload,
cancelBeastform: CharacterSheet.#cancelBeastform, cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement, toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime, useDowntime: this.useDowntime,
@ -955,21 +954,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
/** */
static #advanceResourceDie(_, target) { static #advanceResourceDie(_, target) {
this.updateResourceDie(target, true); this.updateResourceDie(target, true);
} }
static async #onToggleItemReload(_, target) {
const item = await getDocFromElement(target);
if (!item || !item.system.resource?.max)
return;
await item.update({
'system.resource.value': item.system.needsReload ?
itemAbleRollParse(item.system.resource.max, this.document, item) : 0
})
}
lowerResourceDie(event) { lowerResourceDie(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

View file

@ -30,8 +30,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
addFeature: DHBaseItemSheet.#addFeature, addFeature: DHBaseItemSheet.#addFeature,
deleteFeature: DHBaseItemSheet.#deleteFeature, deleteFeature: DHBaseItemSheet.#deleteFeature,
addResource: DHBaseItemSheet.#addResource, addResource: DHBaseItemSheet.#addResource,
removeResource: DHBaseItemSheet.#removeResource, removeResource: DHBaseItemSheet.#removeResource
editGMNote: DHBaseItemSheet.#onEditGMNote
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.drop-section' }, { dragSelector: null, dropSelector: '.drop-section' },
@ -77,16 +76,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options); await super._preparePartContext(partId, context, options);
const TextEditor = foundry.applications.ux.TextEditor.implementation;
switch (partId) { switch (partId) {
case 'description': case 'description':
context.enrichedDescription = await this.document.system.getEnrichedDescription({ gmNotes: false }); context.enrichedDescription = await this.document.system.getEnrichedDescription();
context.enrichedGMNotes = await TextEditor.implementation.enrichHTML(this.item.system.gmNotes, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
})
break; break;
case 'effects': case 'effects':
await this._prepareEffectsContext(context, options); await this._prepareEffectsContext(context, options);
@ -338,45 +331,4 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
} }
} }
} }
/**
* Handles the Add GM Note button being pressed. This is only used when an item has no GM notes.
* Later edits to a GM note instead go through the normal editor toggle workflow.
* @this DHBaseItemSheet
*/
static #onEditGMNote() {
// Open the editor, which might be hidden. We remove the css class to hide temporarily
// so that menu auto resizing functions properly.
const editor = this.element.querySelector('prose-mirror[name="system.gmNotes"]');
const wasHidden = editor.classList.contains('hide-if-inactive');
editor.classList.remove('hide-if-inactive');
editor.open = true;
window.setTimeout(() => {
if (wasHidden) editor.classList.add('hide-if-inactive');
}, 0);
}
/** @inheritdoc */
async _onRender(context, options) {
await super._onRender(context, options);
// Render an add gmnotes button if there are no set GM notes.
// We need to re-render on close since its possible to prosemirror to close *without* triggering a full re-render
if (game.user.isGM && !this.item.system.gmNotes) {
const description = this.element.querySelector('[name="system.description"]');
const addButton = () => {
if (description.querySelector('[data-action=editGMNote]')) return;
const button = document.createElement('button');
button.type = 'button';
button.classList.add('icon', 'toggle', 'fa-regular', 'fa-note-medical');
button.dataset.action = 'editGMNote';
button.dataset.tooltip = 'DAGGERHEART.ITEMS.Base.addGMNote';
description.appendChild(button);
}
addButton();
description.addEventListener('close', () => addButton());
}
}
} }

View file

@ -1,7 +1,6 @@
import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs';
import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs'; import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs';
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
import FearTracker from './fearTracker.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) { constructor(options) {
@ -111,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);
}, },
onClick: async (_event, li) => { callback: async 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] });
@ -127,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;
}, },
onClick: async (_event, li) => { callback: async 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);
@ -153,9 +152,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
html.querySelectorAll('.risk-it-all-button').forEach(element => html.querySelectorAll('.risk-it-all-button').forEach(element =>
element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data)) element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data))
); );
for (const element of html.querySelectorAll('.roll-reload-check')) {
element.addEventListener('click', event => this.onRollReloadCheck(event, message));
}
}; };
setupHooks() { setupHooks() {
@ -183,15 +179,28 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
} }
async onRollSimple(event, message) { async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage'; const buttonType = event.target.dataset.type ?? 'damage',
const total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0); total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
const targets = Array.from(game.user.targets); damages = {
hitPoints: {
parts: [
{
applyTo: 'hitPoints',
damageTypes: [],
total
}
]
}
},
targets = Array.from(game.user.targets);
if (targets.length === 0) if (targets.length === 0)
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected'));
for (const target of targets) {
if (buttonType === 'healing') target.actor.takeHealing({ hitPoints: total }); targets.forEach(target => {
else target.actor.takeDamage({ total }); if (buttonType === 'healing') target.actor.takeHealing(damages);
} else target.actor.takeDamage(damages);
});
} }
async abilityUseButton(event, message) { async abilityUseButton(event, message) {
@ -247,17 +256,26 @@ 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-result]'); const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') { if (target.dataset.type === 'damage') {
const { isResource, damageType, dice, result } = target.dataset; const { damageType, part, dice, result } = target.dataset;
await message.system.damage.rerollDamageDie(isResource, damageType, dice, result); const damagePart = message.system.damage[damageType].parts[part];
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result);
const updatePath = isResource ? `system.damage.resources.${damageType}` : 'system.damage.main'; const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => {
const updateValue = isResource ? if (index !== Number(part)) return damagePart;
message.system.damage.resources[damageType] : message.system.damage.main; return {
await message.update({ ...damagePart,
[updatePath]: updateValue.toJSON() total: parsedRoll.total,
dice: rerolledDice
};
});
const updateMessage = game.messages.get(message._id);
await updateMessage.update({
[`system.damage.${damageType}`]: {
total: parsedRoll.total,
parts: damageParts
}
}); });
} else { } else {
const rerollDice = message.system.roll.dice[target.dataset.dieIndex]; const rerollDice = message.system.roll.dice[target.dataset.dieIndex];
@ -279,27 +297,4 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const actor = game.actors.get(event.target.dataset.actorId); const actor = game.actors.get(event.target.dataset.actorId);
new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true }); new game.system.api.applications.dialogs.RiskItAllDialog(actor, resourceValue).render({ force: true });
} }
_toggleNotifications({ closing = false } = {}) {
super._toggleNotifications(closing)
FearTracker.handleOffSet();
}
async onRollReloadCheck(_event, messageData) {
const message = game.messages.get(messageData._id);
if (message.system.reloadCheckValue) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: _loc('DAGGERHEART.ACTIONS.Reload.rerollConfirmationTitle')
},
content: _loc('DAGGERHEART.ACTIONS.Reload.rerollConfirmationText')
});
if (!confirmed) return;
}
const { rollValue } = await message.system.action.handleReload?.({ awaitRoll: true });
await message.update({ 'system.reloadCheckValue': rollValue });
}
} }

View file

@ -153,6 +153,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown } data: { refreshType: RefreshType.Countdown }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
} }
static #addCountdown() { static #addCountdown() {

View file

@ -12,11 +12,14 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) { export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
previousCountdownData = null; previousCountdownData = null;
changedCountdownsForAnimation = new Set();
constructor(options = {}) { constructor(options = {}) {
super(options); super(options);
this.previousCountdownData = this.previousCountdownData =
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).countdowns; game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).countdowns;
this.setupHooks();
} }
/** @inheritDoc */ /** @inheritDoc */
@ -77,15 +80,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return frame; return frame;
} }
/** @inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
this._createContextMenu(this._getCountdownContextOptions, '.countdown-container[data-countdown]', {
parentClassHooks: false, fixed: true
});
}
/** @inheritdoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
@ -100,7 +95,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
/* Handle animations to draw attention to countdown values changing */ /* Handle animations to draw attention to countdown values changing */
const typesToAnimate = new Set(); const typesToAnimate = new Set();
for (const countdownKey of options.animate ?? []) { for (const countdownKey of this.changedCountdownsForAnimation) {
const shimmerAnimation = [ const shimmerAnimation = [
{ backgroundPositionX: '98%' }, { backgroundPositionX: '98%' },
{ backgroundPositionX: '0%' } { backgroundPositionX: '0%' }
@ -132,6 +127,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
const element = this.element.querySelector(`.header-type-toggles .header-type[data-type="${type}"]`); const element = this.element.querySelector(`.header-type-toggles .header-type[data-type="${type}"]`);
element?.animate(pulseAnimation, pulseTiming); element?.animate(pulseAnimation, pulseTiming);
} }
this.changedCountdownsForAnimation.clear();
} }
/** Returns countdown data filtered by ownership */ /** Returns countdown data filtered by ownership */
@ -140,15 +137,17 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
const values = Object.entries(setting.countdowns).map(([key, countdown]) => ({ const values = Object.entries(setting.countdowns).map(([key, countdown]) => ({
key, key,
countdown, countdown,
ownership: countdown.getUserLevel(game.user) ownership: DhCountdowns.#getPlayerOwnership(game.user, setting, countdown)
})); }));
return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE); return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE);
} }
_getCountdownData() { _getCountdownData() {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
return this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { return this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => {
const playersWithAccess = game.users.reduce((acc, user) => { const playersWithAccess = game.users.reduce((acc, user) => {
const ownership = countdown.getUserLevel(user); const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown);
if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
acc.push(user); acc.push(user);
} }
@ -214,6 +213,19 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return context; return context;
} }
static #getPlayerOwnership(user, setting, countdown) {
if (user.isGM) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const playerOwnership = countdown.ownership[user.id];
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
? setting.defaultOwnership
: playerOwnership;
}
cooldownRefresh = ({ refreshType }) => {
if (refreshType === RefreshType.Countdown) this.render();
};
static canPerformEdit() { static canPerformEdit() {
if (game.user.isGM) return true; if (game.user.isGM) return true;
@ -311,12 +323,18 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
action: socketEvent.Refresh, action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown } data: { refreshType: RefreshType.Countdown }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
setupHooks() {
Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
} }
async close(options) { async close(options) {
/* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */ /* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */
if (options.closeKey) return; if (options.closeKey) return;
Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
return super.close(options); return super.close(options);
} }
@ -359,30 +377,4 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
} }
/**
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]}
*/
_getCountdownContextOptions() {
/** @param {HTMLElement} element */
const getCountdownFromElement = element => {
const id = element.closest('[data-countdown]').dataset.countdown;
if (!id) return null;
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
return setting.countdowns[id ?? ''];
}
return [
{
label: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash',
visible: element => {
return getCountdownFromElement(element)?.isOwner;
},
onClick: (_, target) => {
getCountdownFromElement(target)?.delete();
}
}
];
}
} }

View file

@ -13,14 +13,6 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV2) { export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options = {}) { constructor(options = {}) {
super(options); super(options);
this._dragData = {
isDragging: false,
startX: 0,
startY: 0,
startLeft: 0,
startTop: 0
}
} }
/** @inheritDoc */ /** @inheritDoc */
@ -29,20 +21,19 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
classes: [], classes: [],
tag: 'div', tag: 'div',
window: { window: {
frame: false, frame: true,
title: 'DAGGERHEART.GENERAL.fear', title: 'DAGGERHEART.GENERAL.fear',
positioned: true, positioned: true,
resizable: true, resizable: true,
minimizable: false minimizable: false
}, },
classes: ['daggerheart', 'dh-style', 'fear-tracker'],
actions: { actions: {
setFear: FearTracker.setFear, setFear: FearTracker.setFear,
increaseFear: FearTracker.increaseFear increaseFear: FearTracker.increaseFear
}, },
position: { position: {
width: 540, width: 222,
height: 'auto' height: 222
} }
}; };
@ -62,10 +53,6 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear; return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear;
} }
get fearPosition() {
return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).fearPosition;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Rendering */ /* Rendering */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -76,48 +63,15 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
current = this.currentFear, current = this.currentFear,
max = this.maxFear, max = this.maxFear,
percent = (current / max) * 100, percent = (current / max) * 100,
isGM = game.user.isGM, isGM = game.user.isGM;
locked = false,
isFree = this.fearPosition == 'free';
return { display, current, max, percent, isGM, locked, isFree }; return { display, current, max, percent, isGM };
} }
/** @override */ /** @override */
async _onRender(context, options) { async _preFirstRender(context, options) {
await super._onRender(context, options); options.position =
game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position;
this.#setupDragging();
this.#setupResizing();
const fearPosition = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).fearPosition;
if (options.isFirstRender) FearTracker.handleOffSet();
if (!options.force) return;
this.handleStyleElement(fearPosition);
switch (fearPosition) {
case 'topCenter':
document.getElementById('ui-top')?.appendChild(this.element);
break;
case 'bottomCenter':
document.getElementById('ui-bottom')?.prepend(this.element);
break;
case 'rightTop':
document.getElementById('ui-right-column-1')?.appendChild(this.element);
break;
case 'leftBottom':
document.getElementById('ui-left-column-1')?.insertBefore(this.element, document.getElementById('players'));
break;
default:
document.body?.appendChild(this.element);
const position =
game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position;
this.setPosition(position);
break;
}
} }
/** @override */ /** @override */
@ -126,31 +80,15 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
} }
handleStyleElement(fearPosition) {
for (const position of Object.values(CONFIG.DH.GENERAL.fearPosition)) {
this.element.classList.remove(position.value);
}
this.element.classList.add(fearPosition);
}
static handleOffSet() {
const fearTracker = document.getElementById('resources');
const hotbar = document.getElementById('hotbar');
if (!fearTracker) return;
const offset = Number(hotbar.style.getPropertyValue('--offset').replace(/px$/, '')) || 0;
if (offset > 0) return;
fearTracker.style.setProperty('--offset', `${offset - 13}px`);
}
_onPosition(position) { _onPosition(position) {
game.user.setFlag(CONFIG.DH.id, 'app.resources.position', position); game.user.setFlag(CONFIG.DH.id, 'app.resources.position', position);
} }
async close(options = {}) {
if (!options.allowed) return;
else super.close(options);
}
static async setFear(event, target) { static async setFear(event, target) {
if (!game.user.isGM) return; if (!game.user.isGM) return;
const fearCount = Number(target.dataset.index ?? 0); const fearCount = Number(target.dataset.index ?? 0);
@ -172,119 +110,4 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
value value
); );
} }
// TODO: Remove methods later to use Foundry's dragger and resize methods
/* -------------------------------------------- */
/* Dragging handlers */
/* -------------------------------------------- */
#setupDragging() {
const dragHandle = this.element.querySelector('.drag-handle');
if (!dragHandle) return;
dragHandle.addEventListener('mousedown', this.#onDragStart.bind(this));
}
#onDragStart(event) {
if (event.button !== 0) return;
this._dragData.isDragging = true;
this._dragData.startX = event.clientX;
this._dragData.startY = event.clientY;
const rect = this.element.getBoundingClientRect();
this._dragData.startLeft = rect.left;
this._dragData.startTop = rect.top;
this.element.style.cursor = 'grabbing';
this._dragHandler = this.#onDragging.bind(this);
this._dragEndHandler = this.#onDragEnd.bind(this);
window.addEventListener('mousemove', this._dragHandler);
window.addEventListener('mouseup', this._dragEndHandler);
}
#onDragging(event) {
if (!this._dragData.isDragging) return;
const dragX = event.clientX - this._dragData.startX;
const dragY = event.clientY - this._dragData.startY;
this.element.style.left = `${this._dragData.startLeft + dragX}px`;
this.element.style.top = `${this._dragData.startTop + dragY}px`;
}
#onDragEnd() {
if (!this._dragData.isDragging) return;
this._dragData.isDragging = false;
this.element.style.cursor = '';
if (this._dragHandler) window.removeEventListener('mousemove', this._dragHandler);
if (this._dragEndHandler) window.removeEventListener('mouseup', this._dragEndHandler);
const rect = this.element.getBoundingClientRect();
const pos = { top: rect.top, left: rect.left };
this.setPosition(pos);
}
/* -------------------------------------------- */
/* Resize handlers */
/* -------------------------------------------- */
#setupResizing() {
const resizeHandle = this.element.querySelector('.resize-handle');
if (!resizeHandle) return;
resizeHandle.addEventListener('mousedown', this.#onResizeStart.bind(this));
}
#onResizeStart(e) {
if (e.button !== 0) return;
e.stopPropagation();
let maxAllowedWidth = 10000;
this._resizeData = {
isResizing: true,
startX: e.clientX,
startY: e.clientY,
startWidth: this.element.offsetWidth,
startHeight: this.element.offsetHeight,
maxAllowedWidth: Math.max(50, maxAllowedWidth)
};
this._resizeHandler = this.#onResizing.bind(this);
this._resizeEndHandler = this.#onResizeEnd.bind(this);
window.addEventListener('mousemove', this._resizeHandler);
window.addEventListener('mouseup', this._resizeEndHandler);
}
#onResizing(e) {
if (!this._resizeData?.isResizing) return;
const currentDx = e.clientX - this._resizeData.startX;
const potentialWidth = Math.max(50, this._resizeData.startWidth + currentDx);
const width = Math.min(potentialWidth, this._resizeData.maxAllowedWidth);
this.element.style.width = `${width}px`;
if (width < 100) {
this.element.classList.add('narrow');
} else {
this.element.classList.remove('narrow');
}
}
#onResizeEnd() {
if (!this._resizeData?.isResizing) return;
this._resizeData.isResizing = false;
if (this._resizeHandler) window.removeEventListener('mousemove', this._resizeHandler);
if (this._resizeEndHandler) window.removeEventListener('mouseup', this._resizeEndHandler);
let width = parseFloat(this.element.style.width);
if (isNaN(width)) {
width = this.element.getBoundingClientRect().width;
}
this.setPosition({ width: width });
}
} }

View file

@ -825,7 +825,7 @@ export const refreshTypes = {
export const itemAbilityCosts = { export const itemAbilityCosts = {
resource: { resource: {
id: 'resource', id: 'resource',
label: 'DAGGERHEART.GENERAL.Resource.single', label: 'DAGGERHEART.GENERAL.resource',
group: 'Global' group: 'Global'
}, },
quantity: { quantity: {
@ -924,14 +924,6 @@ export const fearDisplay = {
hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' }
}; };
export const fearPosition = {
free: { value: 'free', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.free' },
topCenter: { value: 'topCenter', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.topCenter' },
bottomCenter: { value: 'bottomCenter', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.bottomCenter' },
rightTop: { value: 'rightTop', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.rightTop' },
leftBottom: { value: 'leftBottom', label: 'DAGGERHEART.SETTINGS.Appearance.fearPosition.leftBottom' }
};
export const basicOwnershiplevels = { export const basicOwnershiplevels = {
0: { value: 0, label: 'OWNERSHIP.NONE' }, 0: { value: 0, label: 'OWNERSHIP.NONE' },
2: { value: 2, label: 'OWNERSHIP.OBSERVER' }, 2: { value: 2, label: 'OWNERSHIP.OBSERVER' },

View file

@ -27,10 +27,8 @@ export const menu = {
}; };
export const gameSettings = { export const gameSettings = {
/** @type {'Automation'} */
Automation: 'Automation', Automation: 'Automation',
Metagaming: 'Metagaming', Metagaming: 'Metagaming',
/** @type {'Homebrew'} */
Homebrew: 'Homebrew', Homebrew: 'Homebrew',
appearance: 'Appearance', appearance: 'Appearance',
GlobalOverrides: 'GlobalOverrides', GlobalOverrides: 'GlobalOverrides',
@ -39,7 +37,6 @@ export const gameSettings = {
Fear: 'ResourcesFear' Fear: 'ResourcesFear'
}, },
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
/** @type {'Countdowns'} */
Countdowns: 'Countdowns', Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion', LastMigrationVersion: 'LastMigrationVersion',
SpotlightRequestQueue: 'SpotlightRequestQueue', SpotlightRequestQueue: 'SpotlightRequestQueue',
@ -62,18 +59,3 @@ export const actionAutomationChoices = {
label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.always' label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.always'
} }
}; };
export const reloadChoices = {
off: {
id: 'off',
label: 'DAGGERHEART.CONFIG.ReloadChoices.off.label'
},
manual: {
id: 'manual',
label: 'DAGGERHEART.CONFIG.ReloadChoices.manual.label'
},
auto: {
id: 'auto',
label: 'DAGGERHEART.CONFIG.ReloadChoices.auto.label'
}
};

View file

@ -12,7 +12,6 @@ import * as HOOKS from './hooksConfig.mjs';
import * as TRIGGER from './triggerConfig.mjs'; import * as TRIGGER from './triggerConfig.mjs';
import * as ITEMBROWSER from './itemBrowserConfig.mjs'; import * as ITEMBROWSER from './itemBrowserConfig.mjs';
/** @type {"daggerheart"} */
export const SYSTEM_ID = 'daggerheart'; export const SYSTEM_ID = 'daggerheart';
export const SYSTEM = { export const SYSTEM = {

View file

@ -1,7 +0,0 @@
import { DhCountdown } from './countdowns.mjs'
declare module './countdowns.mjs' {
export default interface DhCountdowns {
countdowns: Record<string, DhCountdown>;
}
}

View file

@ -13,19 +13,18 @@ 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.main) { if (!this.damage.parts.hitPoints) {
this.damage.main = baseDamage; this.damage.parts.hitPoints = baseDamage;
} else { } else {
for (const type of baseDamage.type) this.damage.main.type.add(type); for (const type of baseDamage.type) this.damage.parts.hitPoints.type.add(type);
this.damage.main.value.custom = { this.damage.parts.hitPoints.value.custom = {
enabled: true, enabled: true,
formula: `${baseDamage.value.getFormula()} + ${this.damage.main.value.getFormula()}` formula: `${baseDamage.value.getFormula()} + ${this.damage.parts.hitPoints.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';
@ -34,28 +33,24 @@ export default class DHAttackAction extends DHDamageAction {
} }
getParentHitPointDamage() { getParentHitPointDamage() {
return this.item?.system?.attack.damage.main; return this.item?.system?.attack.damage.parts.hitPoints;
} }
get damageFormula() { get damageFormula() {
const hitPointsPart = this.damage.main; const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0'; if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula(); return hitPointsPart.value.getFormula();
} }
get altDamageFormula() { get altDamageFormula() {
const hitPointsPart = this.damage.main; const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0'; if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula(); return hitPointsPart.valueAlt.getFormula();
} }
async use(event, options) { async use(event, options) {
if (this.item?.system.needsReload) {
return ui.notifications.error(_loc('DAGGERHEART.UI.Notifications.reloadRequired', { weapon: this.item.name }));
}
const result = await super.use(event, options); const result = await super.use(event, options);
if (result?.message?.system.action?.roll?.type === 'attack') { if (result?.message?.system.action?.roll?.type === 'attack') {
@ -66,23 +61,6 @@ export default class DHAttackAction extends DHDamageAction {
return result; return result;
} }
async handleReload(options = { awaitRoll: false }) {
const roll = await new Roll('1d6').evaluate();
if (game.modules.get('dice-so-nice')?.active) {
if (options.awaitRoll)
await game.dice3d.showForRoll(roll, game.user, true);
else
game.dice3d.showForRoll(roll, game.user, true);
}
const needsReload = roll.total === 1;
if (needsReload) {
this.item.update({ 'system.resource.value': 0 });
}
return { needsReload, rollValue: roll.total };
}
/** /**
* Generate a localized label array for this item subtype. * Generate a localized label array for this item subtype.
* @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects. * @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects.
@ -95,7 +73,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.main, ...damage.resources].filter(d => !!d)) { for (const { value, valueAlt, type } of damage.parts) {
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
@ -104,7 +82,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

@ -256,8 +256,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat();
await this.toChat(null, config);
return config; return config;
} }
@ -289,6 +288,7 @@ 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,
@ -306,20 +306,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}; };
if (this.damage) { if (this.damage) {
config.isDirect = !!this.damage.main?.direct; config.isDirect = this.damage.direct;
const groupAttackTokens = this.damage.main?.groupAttack const groupAttackTokens = this.damage.groupAttack
? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens( ? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(
this.actor.id, this.actor.id,
this.damage.main.groupAttack this.damage.groupAttack
) )
: null; : null;
config.damageOptions = { config.damageOptions = {
groupAttack: this.damage.main?.groupAttack groupAttack: this.damage.groupAttack
? { ? {
numAttackers: Math.max(groupAttackTokens.length, 1), numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.main.groupAttack range: this.damage.groupAttack
} }
: null : null
}; };
@ -429,11 +429,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
} }
get hasDamage() { get hasDamage() {
return this.type !== 'healing' && (Boolean(this.damage?.main) || !foundry.utils.isEmpty(this.damage?.resources)); return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing';
} }
get hasHealing() { get hasHealing() {
return this.type === 'healing' && !foundry.utils.isEmpty(this.damage?.resources); return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing';
} }
get hasSave() { get hasSave() {
@ -469,25 +469,6 @@ 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,8 +8,11 @@ export default class DHDamageAction extends DHBaseAction {
* @returns Formula string * @returns Formula string
*/ */
getDamageFormula() { getDamageFormula() {
if (!this.damage.main) return ''; const strings = [];
for (const { value } of this.damage.parts) {
strings.push(Roll.replaceFormulaData(value.getFormula(), this.actor?.getRollData() ?? {}));
}
return Roll.replaceFormulaData(this.damage.main.value.getFormula(), this.actor?.getRollData() ?? {}); return strings.join(' + ');
} }
} }

View file

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

View file

@ -104,13 +104,15 @@ export default class DhCharacter extends DhCreature {
trait: 'strength' trait: 'strength'
}, },
damage: { damage: {
main: { parts: {
type: ['physical'], hitPoints: {
applyTo: 'hitPoints', type: ['physical'],
value: { applyTo: 'hitPoints',
custom: { value: {
enabled: true, custom: {
formula: '@profd4' enabled: true,
formula: '@profd4'
}
} }
} }
} }
@ -836,7 +838,7 @@ export default class DhCharacter extends DhCreature {
isReversed: true isReversed: true
}; };
this.attack.damage.main.value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`; this.attack.damage.parts.hitPoints.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?.main?.type?.toSorted(), type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(),
value: getFormula(source.system.attack.damage?.main?.value) value: getFormula(source.system.attack.damage?.parts.hitPoints?.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?.main) throw new Error('Unexpected missing damage in adversary'); if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary');
for (const property of ['value', 'valueAlt']) { for (const property of ['value', 'valueAlt']) {
const data = damage.main[property]; const data = damage.parts.hitPoints[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?.main) continue; if (!action.damage?.parts.hitPoints) 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.main; const { [property]: data, type: damageType } = action.damage.parts.hitPoints;
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 data *or* a simple formula object. * Get formula from either damage parts *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

@ -1,5 +1,4 @@
import { triggerChatRollFx } from '../../helpers/utils.mjs'; import { triggerChatRollFx } from '../../helpers/utils.mjs';
import { ChatDamageData } from './chatDamageData.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -42,7 +41,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
hasEffect: new fields.BooleanField({ initial: false }), hasEffect: new fields.BooleanField({ initial: false }),
hasSave: new fields.BooleanField({ initial: false }), hasSave: new fields.BooleanField({ initial: false }),
hasTarget: new fields.BooleanField({ initial: false }), hasTarget: new fields.BooleanField({ initial: false }),
reloadCheckValue: new fields.NumberField({ integer: true, nullable: true, initial: null }),
isDirect: new fields.BooleanField({ initial: false }), isDirect: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(), onSave: new fields.StringField(),
source: new fields.SchemaField({ source: new fields.SchemaField({
@ -51,7 +49,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
originItem: originItemField(), originItem: originItemField(),
action: new fields.StringField() action: new fields.StringField()
}), }),
damage: new fields.EmbeddedDataField(ChatDamageData), damage: new fields.ObjectField(),
damageOptions: new fields.ObjectField(), damageOptions: new fields.ObjectField(),
costs: new fields.ArrayField(new fields.ObjectField()), costs: new fields.ArrayField(new fields.ObjectField()),
successConsumed: new fields.BooleanField({ initial: false }) successConsumed: new fields.BooleanField({ initial: false })
@ -76,14 +74,10 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
return fromUuidSync(this.source.actor); return fromUuidSync(this.source.actor);
} }
get item() { get actionItem() {
const actionActor = this.actionActor; const actionActor = this.actionActor;
if (!actionActor || !this.source.item) return null; if (!actionActor || !this.source.item) return null;
return actionActor.items.get(this.source.item);
}
get actionItem() {
switch (this.source.originItem.type) { switch (this.source.originItem.type) {
case CONFIG.DH.ITEM.originItemType.restMove: case CONFIG.DH.ITEM.originItemType.restMove:
const restMoves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves; const restMoves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves;
@ -91,18 +85,11 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
this.source.originItem.actionIndex this.source.originItem.actionIndex
]; ];
default: default:
return this.item?.system.actionsList?.find(a => a.id === this.source.action); const item = actionActor.items.get(this.source.item);
return item ? item.system.actionsList?.find(a => a.id === this.source.action) : null;
} }
} }
get hasReload() {
return this.item?.system.hasReload;
}
get reloadCheckFailed() {
return this.reloadCheckValue === 1;
}
get action() { get action() {
const { actionActor, actionItem: itemAction } = this; const { actionActor, actionItem: itemAction } = this;
if (!this.source.action) return null; if (!this.source.action) return null;
@ -145,21 +132,28 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
}); });
} }
/* TODO: Change how damage data is stored somehow to enable better rerolling */
async getRerolledDamage() { async getRerolledDamage() {
if (!this.damage.active) return; if (!this.damage) return;
const rerolls = []; const rerolls = [];
const update = { system: { damage: { main: null, resources: _replace({}) } } }; const update = { system: { damage: {} } };
if (this.damage.main) { for (const partKey in this.damage) {
const reroll = await this.damage.main.reroll(); const part = this.damage[partKey];
rerolls.push(reroll); const testRoll = Roll.fromData(part.parts[0].roll);
update.system.damage.main = reroll.toJSON(); const rerolled = await testRoll.reroll();
} rerolls.push(rerolled);
for (const key of Object.keys(this.damage.resources)) { if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
const reroll = await this.damage.resources[key].reroll(); const partData = update.system.damage[partKey].parts[0];
rerolls.push(reroll); update.system.damage[partKey].total = rerolled.total;
update.system.damage.resources[key] = reroll.toJSON(); partData.modifierTotal = rerolled.terms.reduce((acc, x) => {
if (x.isDeterministic && !x.operator) acc += x.total;
return acc;
}, 0);
partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination }));
partData.total = rerolled.total;
partData.roll = rerolled.toJSON();
} }
await triggerChatRollFx(rerolls); await triggerChatRollFx(rerolls);
@ -204,43 +198,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
this.isGM = game.user.isGM; //temp this.isGM = game.user.isGM; //temp
} }
static migrateData(source) {
const { main, resources, ...flatDamageKeys } = source.damage ?? {};
if (source.damage && !main && !resources) {
source.damage.main = null;
source.damage.resources = {};
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;
}
getTargetList() { getTargetList() {
const targets = const targets =
this.targetMode && this.parent.isAuthor this.targetMode && this.parent.isAuthor

View file

@ -1,58 +0,0 @@
import { triggerChatRollFx } from '../../helpers/utils.mjs';
export class ChatDamageData extends foundry.abstract.DataModel {
constructor(data = {}, options = {}) {
super(data, options);
this._prepareRolls();
}
static defineSchema() {
const fields = foundry.data.fields;
return {
main: new fields.JSONField({ nullable: true, validate: ChatDamageData.#validateRoll}),
resources: new fields.TypedObjectField(new fields.JSONField({validate: ChatDamageData.#validateRoll}))
};
}
get active() {
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() {
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];
await rerollDice.rerollResult(resultIndex);
await reroll._evaluate();
const rerolledResult = rerollDice.results[rerollDice.results.length - 1];
if (rerolledResult) {
const fakeRoll = {
_evaluated: true,
dice: [new foundry.dice.terms.Die({
...rerollDice,
results: [rerolledResult],
total: rerolledResult.value,
faces: rerollDice.faces
})],
options: { appearance: {} }
};
await triggerChatRollFx([fakeRoll]);
}
}
}

View file

@ -1,4 +1,3 @@
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import FormulaField from './fields/formulaField.mjs'; import FormulaField from './fields/formulaField.mjs';
export default class DhCountdowns extends foundry.abstract.DataModel { export default class DhCountdowns extends foundry.abstract.DataModel {
@ -15,36 +14,19 @@ export default class DhCountdowns extends foundry.abstract.DataModel {
}; };
} }
/** @inheritdoc */ handleChange() {
_initialize(options) {
super._initialize(options);
for (const [id, countdown] of Object.entries(this.countdowns)) {
countdown.id = id;
}
}
async handleChange() {
const previousCountdowns = foundry.ui.countdowns.previousCountdownData; const previousCountdowns = foundry.ui.countdowns.previousCountdownData;
const changedCountdowns = Object.entries(this.countdowns).reduce((acc, [key, countdown]) => { const changedCountdowns = Object.entries(this.countdowns).reduce((acc, [key, countdown]) => {
const previous = previousCountdowns[key]; const previousCountdown = previousCountdowns[key];
const currentChanged = !previous || (previous.progress.current !== countdown.progress.current); if (!previousCountdown || (previousCountdown.progress.current !== countdown.progress.current)) {
if (currentChanged && previous?.progress.start === countdown.progress.start) {
acc.push(key); acc.push(key);
} }
return acc; return acc;
}, []); }, []);
// Re-render countdowns applications. When the change is due to an actual update, resync the editor for (const countdownKey of changedCountdowns)
if (!foundry.utils.equals(previousCountdowns, this.countdowns)) { foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey);
await foundry.ui.countdowns.render({ animate: changedCountdowns });
for (const instance of game.system.api.applications.ui.CountdownEdit.instances()) {
instance.data = this;
await instance.render();
}
}
// Inform modules of updates
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
} }
static migrateData(source) { static migrateData(source) {
@ -178,14 +160,6 @@ export class DhCountdown extends foundry.abstract.DataModel {
}, {}); }, {});
} }
/**
* A boolean indicator for whether the current game User has ownership rights for this countdown
* @returns {boolean}
*/
get isOwner() {
return this.getUserLevel(game.user) === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
}
/** @inheritDoc */ /** @inheritDoc */
static migrateData(source) { static migrateData(source) {
if (source.progress.max) { if (source.progress.max) {
@ -196,29 +170,4 @@ export class DhCountdown extends foundry.abstract.DataModel {
return super.migrateData(source); return super.migrateData(source);
} }
/**
* Get the explicit permission level that a User has over this Document, a value in CONST.DOCUMENT_OWNERSHIP_LEVELS.
* Compendium content ignores the ownership field in favor of User role-based ownership. Otherwise, Documents use
* granular per-User ownership definitions and Embedded Documents defer to their parent ownership.
*
* @param {BaseUser} [user=game.user] The User being tested
* @returns {DocumentOwnershipNumber} A numeric permission level from {@link CONST.DOCUMENT_OWNERSHIP_LEVELS}
*/
getUserLevel(user) {
if (user.isGM) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const playerOwnership = this.ownership[user.id];
return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
? setting.defaultOwnership
: playerOwnership;
}
async delete() {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const data = foundry.utils.deepClone(setting._source);
delete data.countdowns[this.id];
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data);
}
} }

View file

@ -12,10 +12,20 @@ export default class DamageField extends fields.SchemaField {
/** @inheritDoc */ /** @inheritDoc */
constructor(options, context = {}) { constructor(options, context = {}) {
super({ const damageFields = {
main: new fields.EmbeddedDataField(DHDamageData, { nullable: true }), parts: new IterableTypedObjectField(DHDamageData),
resources: new IterableTypedObjectField(DHResourceData) includeBase: new fields.BooleanField({
}, options, context); 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'
})
};
super(damageFields, options, context);
} }
/** /**
@ -31,23 +41,25 @@ 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;
}
const damageFormula = this.damage.main ? let formulas = this.damage.parts.map(p => ({
DamageField.formatFormulas.call(this, [this.damage.main], config)[0] : null; formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
const resourceFormulas = DamageField.formatFormulas.call(this, this.damage.resources, config); damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
applyTo: p.applyTo
}));
if (!damageFormula && !resourceFormulas.length) return false; if (!formulas.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,
damageFormula, roll: formulas,
resourceFormulas,
data: this.getRollData(), data: this.getRollData(),
isCritical: Boolean(message?.system.roll?.isCritical) isCritical: Boolean(message?.system.roll?.isCritical)
}; };
@ -80,7 +92,7 @@ export default class DamageField extends fields.SchemaField {
const targetDamage = []; const targetDamage = [];
const damagePromises = []; const damagePromises = [];
for (const target of targets) { for (let 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) {
@ -102,12 +114,13 @@ export default class DamageField extends fields.SchemaField {
actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates })) actor.takeHealing(config.damage).then(updates => targetDamage.push({ token, updates }))
); );
else { else {
const configDamage = config.damage.clone(); const configDamage = foundry.utils.deepClone(config.damage);
configDamage.main &&= configDamage.main.toJSON(); const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
if (configDamage.main) { const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
const multiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1; if (configDamage.hitPoints) {
const takenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier; for (const part of configDamage.hitPoints.parts) {
configDamage.main.total = Math.ceil(configDamage.main.total * multiplier * takenMultiplier); part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
}
} }
damagePromises.push( damagePromises.push(
@ -170,19 +183,13 @@ 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 {DHResourceData[]} damageData Array of DHResourceData * @param {object[]} formulas Array of formatted formulas object
* @param {object} data Action getRollData * @param {object} data Action getRollData
* @returns * @returns
*/ */
static formatFormulas(damageData, data) { static formatFormulas(formulas, 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 = [];
for (const formula of formulas) { formulas.forEach(formula => {
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(
@ -190,8 +197,7 @@ 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;
} }
@ -269,18 +275,10 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
}; };
} }
/**
* @returns {string} the formula associated with this damage field
*/
getFormula() { getFormula() {
if (this.custom.enabled) return this.custom.formula; const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`,
bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : '';
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`; return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`;
if (!multiplier) return String(this.bonus || 0);
const dice = `${multiplier ?? 1}${this.dice}`;
const sign = this.bonus < 0 ? ' - ' : ' + ';
return this.bonus ? `${dice} ${sign} ${Math.abs(this.bonus)}` : dice;
} }
} }
@ -288,7 +286,6 @@ 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,
@ -311,16 +308,7 @@ export class DHDamageData extends DHResourceData {
static defineSchema() { static defineSchema() {
return { return {
...super.defineSchema(), ...super.defineSchema(),
includeBase: new fields.BooleanField({ base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
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

@ -23,7 +23,7 @@ export default class DHSummonField extends fields.ArrayField {
super(summonFields, options, context); super(summonFields, options, context);
} }
static async execute(config) { static async execute() {
if (!canvas.scene) { if (!canvas.scene) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error'));
return; return;
@ -36,7 +36,6 @@ export default class DHSummonField extends fields.ArrayField {
const rolls = []; const rolls = [];
const summonData = []; const summonData = [];
const chatMessageData = [];
for (const summon of this.summon) { for (const summon of this.summon) {
const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item)); const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item));
await roll.evaluate(); await roll.evaluate();
@ -55,18 +54,17 @@ export default class DHSummonField extends fields.ArrayField {
tokenPreviewName: `${actor.prototypeToken.name}${remaining > 1 ? ` (${remaining}x)` : ''}` tokenPreviewName: `${actor.prototypeToken.name}${remaining > 1 ? ` (${remaining}x)` : ''}`
}); });
} }
chatMessageData.push({
data: actor,
quantity: countNumber
});
} }
if (rolls.length) await triggerChatRollFx(rolls); if (rolls.length) await triggerChatRollFx(rolls);
this.actor.sheet?.minimize(); this.actor.sheet?.minimize();
await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: this.actor.token?.elevation }); DHSummonField.handleSummon(summonData, this.actor);
this.actor.sheet?.maximize(); }
config.summonData = chatMessageData;
static async handleSummon(summonData, actionActor) {
await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: actionActor.token?.elevation });
return actionActor.sheet?.maximize();
} }
} }

View file

@ -238,7 +238,6 @@ export function ActionMixin(Base) {
result = this.parent; result = this.parent;
} else { } else {
result = await this.item.update({ [path]: updates }, options); result = await this.item.update({ [path]: updates }, options);
if (!result) return result;
} }
return this.inCollection return this.inCollection
@ -270,7 +269,10 @@ export function ActionMixin(Base) {
return this.delete(); return this.delete();
} }
async toChat(origin, config) { async toChat(origin) {
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const systemData = { const systemData = {
title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'), title: game.i18n.localize('DAGGERHEART.CONFIG.FeatureForm.action'),
@ -280,7 +282,7 @@ export function ActionMixin(Base) {
img: this.baseAction ? this.parent.parent.img : this.img, img: this.baseAction ? this.parent.parent.img : this.img,
tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'], tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'],
areas: this.areas, areas: this.areas,
summon: config?.summonData summon: this.summon
}, },
source: { source: {
actor: this.actor.uuid, actor: this.actor.uuid,
@ -305,7 +307,7 @@ export function ActionMixin(Base) {
system: systemData, system: systemData,
content: await foundry.applications.handlebars.renderTemplate( content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/action.hbs', 'systems/daggerheart/templates/ui/chat/action.hbs',
systemData { ...systemData, open: autoExpandDescription ? 'open' : '' }
), ),
flags: { flags: {
daggerheart: { daggerheart: {

View file

@ -49,10 +49,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}) })
}; };
if (this.metadata.hasDescription) { if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true });
schema.description = new fields.HTMLField({ required: true, nullable: true });
schema.gmNotes = new fields.HTMLField({ required: true, nullable: true });
}
if (this.metadata.hasResource) { if (this.metadata.hasResource) {
schema.resource = new fields.SchemaField( schema.resource = new fields.SchemaField(
@ -137,7 +134,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
/** /**
* Augments the description for the item with type specific info to display. Implemented in applicable item subtypes. * Augments the description for the item with type specific info to display. Implemented in applicable item subtypes.
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' } * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {Promise<{ prefix: string | null; value: string | null; suffix: string | null }>} * @returns {string}
*/ */
async getDescriptionData(_options) { async getDescriptionData(_options) {
return { prefix: null, value: this.description, suffix: null }; return { prefix: null, value: this.description, suffix: null };
@ -148,24 +145,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' } * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
async getEnrichedDescription({ gmNotes = true } = {}) { async getEnrichedDescription() {
if (!this.metadata.hasDescription) return ''; if (!this.metadata.hasDescription) return '';
const { prefix, value, suffix } = await this.getDescriptionData(); const { prefix, value, suffix } = await this.getDescriptionData();
let fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n<hr>\n'); const fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n<hr>\n');
if (this.gmNotes && gmNotes) {
const gmNotesElement = document.createElement('section');
gmNotesElement.classList.add('gm-notes-section');
gmNotesElement.dataset.visibility = 'gm';
const header = document.createElement('header');
header.classList.add('gm-notes');
header.textContent = _loc('DAGGERHEART.ITEMS.FIELDS.gmNotes.label');
gmNotesElement.innerHTML = header.outerHTML + this.gmNotes;
fullDescription += gmNotesElement.outerHTML;
}
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, { return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this.parent, relativeTo: this,
rollData: this.getRollData(), rollData: this.getRollData(),
secrets: this.parent.isOwner secrets: this.parent.isOwner
}); });

View file

@ -67,11 +67,13 @@ export default class DHWeapon extends AttachableItem {
type: 'attack' type: 'attack'
}, },
damage: { damage: {
main: { parts: {
type: ['physical'], hitPoints: {
value: { type: ['physical'],
multiplier: 'prof', value: {
dice: 'd8' multiplier: 'prof',
dice: 'd8'
}
} }
} }
} }
@ -116,14 +118,6 @@ export default class DHWeapon extends AttachableItem {
return this.weaponFeatures; return this.weaponFeatures;
} }
get hasReload() {
return Boolean(this.weaponFeatures.find(x => x.value === 'reloading'));
}
get needsReload() {
return this.hasReload && this.resource.value === 0;
}
/**@inheritdoc */ /**@inheritdoc */
async getDescriptionData() { async getDescriptionData() {
const baseDescription = this.description; const baseDescription = this.description;
@ -236,12 +230,11 @@ export default class DHWeapon extends AttachableItem {
game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`) game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`)
]; ];
if (attack.damage.main) { for (const { value, type } of attack.damage.parts) {
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) { if (type.size > 0) {
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(' | ');
@ -250,7 +243,7 @@ export default class DHWeapon extends AttachableItem {
tags.push(parts.join('')); tags.push(parts.join(''));
} }
return tags; return tags;
} }
@ -265,10 +258,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.main, ...damage.resources].filter(d => !!d)) { for (const { value, type } of damage.parts) {
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

@ -41,11 +41,6 @@ export default class DhAppearance extends foundry.abstract.DataModel {
choices: CONFIG.DH.GENERAL.fearDisplay, choices: CONFIG.DH.GENERAL.fearDisplay,
initial: CONFIG.DH.GENERAL.fearDisplay.token.value initial: CONFIG.DH.GENERAL.fearDisplay.token.value
}), }),
fearPosition: new StringField({
required: true,
choices: CONFIG.DH.GENERAL.fearPosition,
initial: CONFIG.DH.GENERAL.fearPosition.topCenter.value
}),
displayCountdownUI: new BooleanField({ initial: true }), displayCountdownUI: new BooleanField({ initial: true }),
diceSoNice: new SchemaField({ diceSoNice: new SchemaField({
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }), hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),

View file

@ -196,13 +196,6 @@ export default class DhAutomation extends foundry.abstract.DataModel {
}) })
}) })
}), }),
reload: new fields.StringField({
required: true,
choices: CONFIG.DH.SETTINGS.reloadChoices,
initial: CONFIG.DH.SETTINGS.reloadChoices.manual.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.label',
hint: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.hint'
}),
autoExpireActiveEffects: new fields.BooleanField({ autoExpireActiveEffects: new fields.BooleanField({
required: true, required: true,
initial: true, initial: true,

View file

@ -1,5 +1,3 @@
import { ChatDamageData } from './chat-message/chatDamageData.mjs';
export default class TagTeamData extends foundry.abstract.DataModel { export default class TagTeamData extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -39,7 +37,6 @@ export class MemberData extends foundry.abstract.DataModel {
}), }),
rollChoice: new fields.StringField({ nullable: true, initial: null }), rollChoice: new fields.StringField({ nullable: true, initial: null }),
rollData: new fields.JSONField({ nullable: true, initial: null }), rollData: new fields.JSONField({ nullable: true, initial: null }),
damageRollData: new fields.EmbeddedDataField(ChatDamageData),
selected: new fields.BooleanField({ initial: false }) selected: new fields.BooleanField({ initial: false })
}; };
} }

View file

@ -4,8 +4,4 @@ export default class BaseRoll extends Roll {
/** @inheritdoc */ /** @inheritdoc */
static TOOLTIP_TEMPLATE = 'systems/daggerheart/templates/ui/chat/foundryRollTooltip.hbs'; static TOOLTIP_TEMPLATE = 'systems/daggerheart/templates/ui/chat/foundryRollTooltip.hbs';
get modifierTotal() {
return this.total - this.dice.reduce((acc, dice) => acc + dice.total, 0);
}
} }

View file

@ -13,34 +13,36 @@ 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 = {}, message = {}) {
if (config.dialog.configure === false) roll.constructFormulas(config); if (config.dialog.configure === false) roll.constructFormula(config);
for (const roll of config.roll) await roll.roll.evaluate();
const evaluateRoll = async roll => {
await roll.roll.evaluate();
roll.roll.options = { damageTypes: roll.damageTypes ? [...roll.damageTypes] : [] };
return roll.roll;
}
if (!config.damage) config.damage = { main: null, resources: {} };
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;
const parts = [];
for (const rollData of config.roll) {
const roll = rollData.roll;
parts.push({
...rollData,
...roll.options.roll,
total: roll.total,
formula: roll.formula,
dice: roll.dice.map(d => ({
dice: d.denomination,
total: d.total,
formula: d.formula,
results: d.results
})),
damageTypes: [...(rollData.damageTypes ?? [])],
roll,
type: config.type,
modifierTotal: this.calculateTotalModifiers(roll)
});
rollData.roll = JSON.stringify(roll.toJSON());
}
config.damage = this.unifyDamageRoll(parts);
} }
static async buildPost(roll, config, message) { static async buildPost(roll, config, message) {
@ -51,10 +53,9 @@ 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(
...(config.damage.main ? [config.damage.main] : []), Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
...Object.values(config.damage.resources) );
]);
diceRolls.push(Roll.fromTerms([pool])); diceRolls.push(Roll.fromTerms([pool]));
} }
@ -65,14 +66,22 @@ export default class DamageRoll extends DHRoll {
await super.buildPost(roll, config, message); await super.buildPost(roll, config, message);
if (config.source?.message) { if (config.source?.message) {
chatMessage.update({ 'system.damage': { chatMessage.update({ 'system.damage': config.damage });
...config.damage.toObject(),
main: config.damage.main,
resources: config.damage.resources
}});
} }
} }
static unifyDamageRoll(rolls) {
const unified = {};
rolls.forEach(r => {
const resource = unified[r.applyTo] ?? { formula: '', total: 0, parts: [] };
resource.formula += `${resource.formula !== '' ? ' + ' : ''}${r.formula}`;
resource.total += r.total;
resource.parts.push(r);
unified[r.applyTo] = resource;
});
return unified;
}
static formatGlobal(rolls) { static formatGlobal(rolls) {
let formula, total; let formula, total;
const applyTo = new Set(rolls.flatMap(r => r.applyTo)); const applyTo = new Set(rolls.flatMap(r => r.applyTo));
@ -121,10 +130,12 @@ 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 damageType of this.options.damageFormula?.damageTypes?.values?.() ?? []) { for (const roll of this.options.roll) {
changeKeys.push(`system.bonuses.${type}.${damageType}`); for (const damageType of roll.damageTypes?.values?.() ?? []) {
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) {
@ -140,69 +151,62 @@ export default class DamageRoll extends DHRoll {
return changeKeys; return changeKeys;
} }
constructFormulas(config) { constructFormula(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()) {
formulaData.roll = new Roll(Roll.replaceFormulaData(formulaData.formula, config.data)); const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id;
formulaData.roll.terms = Roll.parse(formulaData.roll.formula, config.data); part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (formulaData.extraFormula) { if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
formulaData.roll.terms.push( part.modifiers = this.applyBaseBonus(part);
new foundry.dice.terms.OperatorTerm({ operator: '+' }), this.addModifiers(part);
...this.constructor.parse(formulaData.extraFormula, this.options.data) part.modifiers?.forEach(m => {
); 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 */
for (const mod in config.modifiers) { if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
const modifier = config.modifiers[mod]; for (const mod in config.modifiers) {
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(formulaData); const modifier = config.modifiers[mod];
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
}
} }
if (config.damageOptions.groupAttack?.numAttackers > 1) { if (part.extraFormula) {
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 formulaData.roll.terms) { for (const term of part.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) { if (config.isCritical && isHitpointPart) {
const total = formulaData.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
if (total > 0) { if (total > 0) {
formulaData.roll.terms.push(...this.formatModifier(total)); part.roll.terms.push(...this.formatModifier(total));
} }
} }
}
formulaData.roll._formula = this.constructor.getFormula(formulaData.roll.terms); /* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
return formulaData; 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;
} }
/* To Remove When Reaction System */ /* To Remove When Reaction System */
@ -295,4 +299,76 @@ export default class DamageRoll extends DHRoll {
config.modifiers = mods; config.modifiers = mods;
return mods; return mods;
} }
static async reroll(rollPart, dice, result) {
let diceIndex = 0;
let parsedRoll = game.system.api.dice.DamageRoll.fromData({
...rollPart.roll,
terms: rollPart.roll.terms.map(term => {
const isDie = term.class === 'Die';
const fixedTerm = {
...term,
...(isDie ? { results: rollPart.dice[diceIndex].results } : {})
};
if (isDie) diceIndex++;
return fixedTerm;
}),
class: 'DamageRoll',
evaluated: false
});
const parsedDiceTerms = Object.keys(parsedRoll.terms).reduce((acc, key) => {
const term = parsedRoll.terms[key];
if (term instanceof CONFIG.Dice.termTypes.DiceTerm) acc[Object.keys(acc).length] = term;
return acc;
}, {});
const term = parsedDiceTerms[dice];
const termResult = parsedDiceTerms[dice].results[result];
const newIndex = parsedDiceTerms[dice].results.length;
await term.reroll(`/r1=${termResult.result}`);
const diceRolls = [];
if (game.modules.get('dice-so-nice')?.active) {
const newResult = parsedDiceTerms[dice].results[newIndex];
diceRolls.push({
_evaluated: true,
dice: [
new foundry.dice.terms.Die({
...term,
total: newResult.result,
faces: term._faces,
results: [newResult]
})
],
options: { appearance: {} }
});
}
await triggerChatRollFx(diceRolls);
await parsedRoll.evaluate();
const results = parsedRoll.dice[dice].results.map(result => ({
...result,
discarded: !result.active
}));
const newResult = results.splice(results.length - 1, 1);
results.splice(Number(result) + 1, 0, newResult[0]);
const rerolledDice = parsedRoll.dice.map((x, index) => {
const isRerollDice = index === Number(dice);
if (!isRerollDice) return { ...x, dice: x.denomination };
return {
dice: parsedRoll.dice[dice].denomination,
total: parsedRoll.dice[dice].total,
results: results.map(result => ({
...result,
hasRerolls: result.hasRerolls || isRerollDice
}))
};
});
return { parsedRoll, rerolledDice };
}
} }

View file

@ -1,8 +1,7 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import { triggerChatRollFx } from '../helpers/utils.mjs'; import { triggerChatRollFx } from '../helpers/utils.mjs';
import BaseRoll from './baseRoll.mjs';
export default class DHRoll extends BaseRoll { export default class DHRoll extends Roll {
baseTerms = []; baseTerms = [];
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false })); super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false }));
@ -41,10 +40,6 @@ 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>}
@ -62,7 +57,7 @@ export default class DHRoll extends BaseRoll {
this.temporaryModifierBuilder(config); this.temporaryModifierBuilder(config);
let roll = this.createRollInstance(config); let roll = new this(config.roll.formula, config.data, config);
if (config.dialog.configure !== false) { if (config.dialog.configure !== false) {
// Open Roll Dialog // Open Roll Dialog
const DialogClass = config.dialog?.class ?? this.DefaultDialog; const DialogClass = config.dialog?.class ?? this.DefaultDialog;
@ -117,11 +112,7 @@ export default class DHRoll extends BaseRoll {
static async toMessage(roll, config) { static async toMessage(roll, config) {
const item = config.data.parent?.items?.get?.(config.source.item) ?? null; const item = config.data.parent?.items?.get?.(config.source.item) ?? null;
const actions = item ? [ const action = item ? item.system.actions.get(config.source.action) : null;
...item.system.actions,
...(item.system.attack?.id === config.source.action ? [item.system.attack] : [])
] : [];
const action = actions.find(x => x.id === config.source.action);
let actionDescription = null; let actionDescription = null;
if (action?.chatDisplay) { if (action?.chatDisplay) {
actionDescription = action actionDescription = action
@ -133,14 +124,6 @@ export default class DHRoll extends BaseRoll {
config.actionChatMessageHandled = true; config.actionChatMessageHandled = true;
} }
const reloadSetting =
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).reload;
const useReload =
item?.system.hasReload &&
action?.type === 'attack' &&
reloadSetting === CONFIG.DH.SETTINGS.reloadChoices.auto.id;
const reloadResult = useReload ? await action?.handleReload?.() : {};
const cls = getDocumentClass('ChatMessage'), const cls = getDocumentClass('ChatMessage'),
msgData = { msgData = {
type: this.messageType, type: this.messageType,
@ -148,11 +131,7 @@ export default class DHRoll extends BaseRoll {
title: roll.title, title: roll.title,
speaker: cls.getSpeaker({ actor: roll.data?.parent }), speaker: cls.getSpeaker({ actor: roll.data?.parent }),
sound: config.mute ? null : CONFIG.sounds.dice, sound: config.mute ? null : CONFIG.sounds.dice,
system: { system: { ...config, actionDescription },
...config,
actionDescription,
reloadCheckValue: reloadResult.rollValue
},
rolls: [roll] rolls: [roll]
}; };
@ -174,17 +153,14 @@ export default class DHRoll extends BaseRoll {
if (!this._evaluated) return; if (!this._evaluated) return;
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, { return foundry.applications.handlebars.renderTemplate(template, {
roll: this, roll: this,
...chatData, ...chatData,
action: chatData.action,
parent: chatData.parent, parent: chatData.parent,
targetMode: chatData.targetMode, targetMode: chatData.targetMode,
areas: chatData.action?.areas, areas: chatData.action?.areas,
metagamingSettings, metagamingSettings
automationSettings
}); });
} }

View file

@ -3,7 +3,6 @@ import HopeDie from './hopeDie.mjs';
import FearDie from './fearDie.mjs'; import FearDie from './fearDie.mjs';
import AdvantageDie from './advantageDie.mjs'; import AdvantageDie from './advantageDie.mjs';
import DisadvantageDie from './disadvantageDie.mjs'; import DisadvantageDie from './disadvantageDie.mjs';
export { default as BaseDie } from './baseDie.mjs';
export const diceTypes = { export const diceTypes = {
DualityDie, DualityDie,

View file

@ -1,6 +1,4 @@
import BaseDie from './baseDie.mjs'; export default class AdvantageDie extends foundry.dice.terms.Die {
export default class AdvantageDie extends BaseDie {
constructor(options) { constructor(options) {
super(options); super(options);

View file

@ -1,12 +0,0 @@
export default class BaseDie extends foundry.dice.terms.Die {
async rerollResult(resultIndex) {
const result = this.results[resultIndex];
result.rerolled = true;
result.active = false;
await this.roll({ reroll: true });
const rerolledResult = this.results[this.results.length - 1];
this.results.splice(this.results.length - 1, 1);
this.results.splice(resultIndex, 0, rerolledResult);
}
}

View file

@ -1,6 +1,4 @@
import BaseDie from './baseDie.mjs'; export default class DisadvantageDie extends foundry.dice.terms.Die {
export default class DisadvantageDie extends BaseDie {
constructor(options) { constructor(options) {
super(options); super(options);

View file

@ -1,7 +1,6 @@
import BaseDie from './baseDie.mjs';
import { updateResourcesForDualityReroll } from '../helpers.mjs'; import { updateResourcesForDualityReroll } from '../helpers.mjs';
export default class DualityDie extends BaseDie { export default class DualityDie extends foundry.dice.terms.Die {
constructor(options) { constructor(options) {
super(options); super(options);

View file

@ -409,4 +409,8 @@ export default class DualityRoll extends D20Roll {
return rerolled; return rerolled;
} }
fromJSON(json) {
return super.fromJSON(json);
}
} }

View file

@ -622,11 +622,6 @@ export default class DhpActor extends Actor {
return rollData; return rollData;
} }
/**
* Checks to see if damage can be reduced in one way or another.
* @param {number} hpDamage the amount of marked hp that will be marked
* @param {Set<string>} types a list of damage types
*/
#canReduceDamage(hpDamage, types) { #canReduceDamage(hpDamage, types) {
const { stressDamageReduction, disabledArmor, reduceSeverity, thresholdImmunities } = const { stressDamageReduction, disabledArmor, reduceSeverity, thresholdImmunities } =
this.system.rules.damageReduction; this.system.rules.damageReduction;
@ -653,32 +648,35 @@ export default class DhpActor extends Actor {
return canUseArmor || canUseStress || hasReduceSeverity || hasThresholdImmunity; return canUseArmor || canUseStress || hasReduceSeverity || hasThresholdImmunity;
} }
async takeDamage(args, isDirect = false) { async takeDamage(damages, isDirect = false) {
args = this.#parseDamageArgs(args); if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, damages) === false) return null;
if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, args) === false) return null;
if (this.type === 'companion') { if (this.type === 'companion') {
await this.modifyResource([{ value: 1, key: 'stress' }]); await this.modifyResource([{ value: 1, key: 'stress' }]);
return; return;
} }
const updates = args.resourceUpdates; const updates = [];
if (args.main) {
// todo: avoid side effects, but hook currently requires it
args.main.value = this.calculateDamage(args.main.value, args.main.damageTypes);
}
if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, args) === false) return null; Object.entries(damages).forEach(([key, damage]) => {
damage.parts.forEach(part => {
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
part.total = this.calculateDamage(part.total, part.damageTypes);
const update = updates.find(u => u.key === key);
if (update) {
update.value += part.total;
update.damageTypes.add(...new Set(part.damageTypes));
} else updates.push({ value: part.total, key, damageTypes: new Set(part.damageTypes) });
});
});
// Convert deducted resources to a record of updates. Return if nothing to do. if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null;
if (!updates.some(u => u.value) && !args.main) return;
if (args.main) { if (!updates.length) return;
const hpDamage = {
value: this.convertDamageToThreshold(args.main.value), const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
damageTypes: new Set(args.main.damageTypes), if (hpDamage?.value) {
key: CONFIG.DH.GENERAL.healingTypes.hitPoints.id hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
};
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) { if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) {
const armorSlotResult = await this.owner.query( const armorSlotResult = await this.owner.query(
'armorSlot', 'armorSlot',
@ -693,7 +691,7 @@ export default class DhpActor extends Actor {
); );
if (armorSlotResult) { if (armorSlotResult) {
const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult; const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
hpDamage.value = modifiedDamage; updates.find(u => u.key === 'hitPoints').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 });
} }
@ -703,23 +701,19 @@ 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)]) {
hpDamage.value = Math.max(0, hpDamage.value - 1);
}
}
// Merge existing hitPoint deduction with finalised damage deduction if (
const existing = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); hpDamage.value &&
if (existing) { this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]
existing.value += hpDamage.value; ) {
existing.damageTypes = hpDamage.damageTypes; hpDamage.value -= 1;
} else { }
updates.push(hpDamage);
} }
} }
@ -735,10 +729,12 @@ 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();
} }
for (const u of updates) { updates.forEach(
u.value = u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value; u =>
} (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);
@ -747,36 +743,6 @@ export default class DhpActor extends Actor {
return updates; return updates;
} }
async takeHealing(args) {
args = this.#parseDamageArgs({ resources: 'resources' in args ? args.resources : args });
if (Hooks.call(`${CONFIG.DH.id}.preTakeHealing`, this, args) === false) return null;
const updates = args.resourceUpdates;
for (const u of updates) {
const shouldFlip = !(u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false);
u.value = shouldFlip ? u.value * -1 : u.value;
}
await this.modifyResource(updates);
if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null;
return updates;
}
/** Parse damage args that may be coming from takeHealing or takeDamage. Used to simplify macro usage and roll vs non-roll usage */
#parseDamageArgs(args = {}) {
const damageRoll = 'total' in args ? args : (args.main ?? args.damage);
const damageValue = typeof damageRoll === 'number' ? damageRoll : damageRoll?.total;
const damageTypes = Array.from(damageRoll?.options?.damageTypes ?? damageRoll?.damageTypes ?? []);
const main = typeof damageValue === 'number' ? { key: 'damage', value: damageValue, damageTypes } : null;
const resourceUpdates = Object.entries(args.resources ?? {}).map(([key, damage]) => ({
key,
value: typeof damage === 'number' ? damage : damage?.total ?? 0
}));
return { main, resourceUpdates };
}
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);
@ -801,6 +767,32 @@ 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]) => {
healing.parts.forEach(part => {
const update = updates.find(u => u.key === key);
if (update) update.value += part.total;
else updates.push({ value: part.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

@ -3,13 +3,6 @@ import { emitGMUpdate, emitGMCreate, GMUpdateEvent } from '../systemRegistration
export default class DhpChatMessage extends foundry.documents.ChatMessage { export default class DhpChatMessage extends foundry.documents.ChatMessage {
targetHook = null; targetHook = null;
static #EXPAND_SECTIONS = [
{ selector: 'roll-section [data-action="expandRoll"]', key: 'roll' },
{ selector: 'damage-section', key: 'damage' },
{ selector: 'target-section', key: 'target' },
{ selector: 'description-section', key: 'desc' }
];
async renderHTML() { async renderHTML() {
const actor = game.actors.get(this.speaker.actor); const actor = game.actors.get(this.speaker.actor);
const actorData = const actorData =
@ -96,26 +89,23 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
} }
} }
// Check registered selectors and the main item section for expanding
// Preserving during re-render is handled by core foundry on anything with [data-action=expandRoll]
const autoExpandRoll = game.settings.get( const autoExpandRoll = game.settings.get(
CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance CONFIG.DH.id,
).expandRollMessage; CONFIG.DH.SETTINGS.gameSettings.appearance
for (const { selector, key } of DhpChatMessage.#EXPAND_SECTIONS) { ).expandRollMessage,
const elements = html.querySelectorAll(selector); rollSections = html.querySelectorAll('.roll-part'),
for (const element of elements) { itemDesc = html.querySelector('.domain-card-move');
element.classList.toggle('expanded', autoExpandRoll[key]); rollSections.forEach(s => {
} if (s.classList.contains('roll-section')) {
} const toExpand = s.querySelector('[data-action="expandRoll"]');
toExpand.classList.toggle('expanded', autoExpandRoll.roll);
// Auto expand the item description. These are not preserved by foundry during re-renders } else if (s.classList.contains('damage-section'))
const itemDesc = html.querySelector('details'); s.classList.toggle('expanded', autoExpandRoll.damage);
if (itemDesc) { else if (s.classList.contains('target-section')) s.classList.toggle('expanded', autoExpandRoll.target);
const existing = document.querySelector(`.chat-message[data-message-id="${this.id}"] details`); else if (s.classList.contains('description-section'))
if (existing?.hasAttribute('open') ?? autoExpandRoll.desc) { s.classList.toggle('expanded', autoExpandRoll.desc);
itemDesc.setAttribute('open', ''); });
} if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', '');
}
} }
if (!this.isAuthor && !this.speakerActor?.isOwner) { if (!this.isAuthor && !this.speakerActor?.isOwner) {

View file

@ -19,10 +19,10 @@ export default class DhTokenManager {
} }
} }
const placedData = await canvas.tokens.placeTokens( return await canvas.tokens.placeTokens(
[ [
{ {
...(await actor.getTokenDocument()).toObject(), ...actor.prototypeToken.toObject(),
actorId: actor.id, actorId: actor.id,
displayName: 50, displayName: 50,
...tokenData ...tokenData
@ -30,8 +30,6 @@ export default class DhTokenManager {
], ],
{ create: false } { create: false }
); );
return placedData?.[0] ?? null;
} }
/** /**
@ -48,24 +46,22 @@ export default class DhTokenManager {
const createElevation = elevation ?? level.elevation.bottom; const createElevation = elevation ?? level.elevation.bottom;
for (const tokenData of tokensData) { for (const tokenData of tokensData) {
const previewToken = await this.createPreview(tokenData.actor, { const previewTokens = await this.createPreview(tokenData.actor, {
name: tokenData.tokenPreviewName, name: tokenData.tokenPreviewName,
level: game.user.viewedLevel, level: game.user.viewedLevel,
elevation: createElevation, elevation: createElevation,
flags: { daggerheart: { createPlacement: true } } flags: { daggerheart: { createPlacement: true } }
}); });
if (!previewToken) return null; if (!previewTokens?.length) return null;
const finalTokenData = {
...previewToken.toObject(),
name: tokenData.actor.prototypeToken.name,
displayName: tokenData.actor.prototypeToken.displayName,
flags: tokenData.actor.prototypeToken.flags
};
await canvas.scene.createEmbeddedDocuments( await canvas.scene.createEmbeddedDocuments(
'Token', 'Token',
[finalTokenData], previewTokens.map(x => ({
...x.toObject(),
name: tokenData.actor.prototypeToken.name,
displayName: tokenData.actor.prototypeToken.displayName,
flags: tokenData.actor.prototypeToken.flags
})),
{ controlObject: true, parent: canvas.scene } { controlObject: true, parent: canvas.scene }
); );
} }

View file

@ -4,234 +4,199 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
#wide = false; #wide = false;
#bordered = false; #bordered = false;
/** @inheritdoc */
async activate(element, options = {}) { async activate(element, options = {}) {
this.#wide = false; const { TextEditor } = foundry.applications.ux;
this.#bordered = false;
const isMacro = document.getElementById('action-bar').contains(element);
const macro = isMacro ? game.macros.get(game.user.hotbar[Number(element.dataset.slot)] ?? null) : null;
const macroItemUuid = macro?.type === 'script' ? macro.command.match(/await game\.system\.api\.applications\.ui\.DhHotbar\.useItem\("([^"]+)"\);/)?.[1] : null;
if (macroItemUuid && await fromUuid(macroItemUuid, { strict: false })) {
element.dataset.tooltip = `#item#${macroItemUuid}`;
options.direction = this.constructor.TOOLTIP_DIRECTIONS.UP;
}
let html = options.html; let html = options.html;
const key = element.dataset.tooltip?.match(/^#([\w-]+)#/)?.[1]; if (element.dataset.tooltip?.startsWith('#battlepoints#')) {
switch (key) { this.#wide = true;
case 'battlepoints': this.#bordered = true;
return this.#activateBattlepoints(element, options);
case 'effect-display': html = await this.getBattlepointHTML(element.dataset.combatId);
html = await this.#activateEffectDisplay(element, options); options.direction = this._determineItemTooltipDirection(element);
break; super.activate(element, { ...options, html: html });
case 'item':
html = await this.#activateItem(element, options); const lockedTooltip = this.lockTooltip();
break; lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => {
case 'attack': element.addEventListener('input', this.toggleModifier.bind(this));
html = await this.#activateAttack(element, options); });
break; return;
case 'shortRest': } else {
case 'longRest': this.#wide = false;
html = await this.#activateRest(element, options); this.#bordered = false;
break; }
case 'advantage':
case 'disadvantage': if (element.dataset.tooltip === '#effect-display#') {
html = await this.#activateAdvantageDisadvantage(element, options); this.#bordered = true;
break; let effect = {};
case 'deathMove': if (element.dataset.uuid) {
html = await this.#activateDeathMove(element, options); const effectItem = await foundry.utils.fromUuid(element.dataset.uuid);
break; const effectData = effectItem.toObject();
effect = {
...effectData,
name: game.i18n.localize(effectData.name)
};
if (effectData.type === 'beastform') {
const beastformData = {
features: [],
advantageOn: effectData.system.advantageOn,
beastformAttackData: game.system.api.data.items.DHBeastform.getBeastformAttackData(effectItem)
};
const features = effectItem.parent.items.filter(x => effectItem.system.featureIds.includes(x.id));
for (const feature of features) {
const featureData = feature.toObject();
featureData.enrichedDescription = await feature.system.getEnrichedDescription();
beastformData.features.push(featureData);
}
effect.description = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs',
{
item: { system: beastformData }
}
);
} else {
effect.description = game.i18n.localize(
effectData.description ?? effectData.parent.system.description
);
}
} else {
const conditions = CONFIG.DH.GENERAL.conditions();
const condition = conditions[element.dataset.condition];
effect = {
...condition,
name: game.i18n.localize(condition.name),
description: game.i18n.localize(condition.description),
appliedBy: element.dataset.appliedBy,
isLockedCondition: true
};
}
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/effect-display.hbs`,
{
effect
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
} else {
this.#bordered = false;
}
if (element.dataset.tooltip?.startsWith('#item#')) {
const itemUuid = element.dataset.tooltip.slice(6);
const item = await foundry.utils.fromUuid(itemUuid);
if (item) {
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
const isEffect = item instanceof ActiveEffect;
await this.enrichText(item);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/${type}.hbs`,
{
item: item,
description: item.system?.enrichedDescription ?? item.enrichedDescription,
config: CONFIG.DH,
allDomains: CONFIG.DH.DOMAIN.allDomains()
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
}
} else {
const attack = element.dataset.tooltip?.startsWith('#attack#');
if (attack) {
const actorUuid = element.dataset.tooltip.slice(8);
const actor = await foundry.utils.fromUuid(actorUuid);
const attack = actor.system.attack;
const description = await TextEditor.enrichHTML(attack.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/attack.hbs`,
{
attack: attack,
description: description,
parent: actor,
config: CONFIG.DH
}
);
this.tooltip.innerHTML = html;
}
const shortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const longRest = element.dataset.tooltip?.startsWith('#longRest#');
if (shortRest || longRest) {
const key = element.dataset.tooltip.slice(shortRest ? 11 : 10);
const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[
element.dataset.restType
].moves;
const move = moves[key];
const description = await TextEditor.enrichHTML(move.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move,
description: description
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
}
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#');
if (isAdvantage || isDisadvantage) {
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
}
}
const deathMove = element.dataset.tooltip?.startsWith('#deathMove#');
if (deathMove) {
const name = element.dataset.deathName;
const img = element.dataset.deathImg;
const description = element.dataset.deathDescription;
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/death-move.hbs`,
{
move: { name: name, img: img, description: description }
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
}
} }
this.noOffset = options.noOffset; this.noOffset = options.noOffset;
super.activate(element, { ...options, html });
}
async #activateBattlepoints(element, options) {
this.#wide = true;
this.#bordered = true;
const html = await this.getBattlepointHTML(element.dataset.combatId);
options.direction = this._determineItemTooltipDirection(element);
super.activate(element, { ...options, html: html }); super.activate(element, { ...options, html: html });
const lockedTooltip = this.lockTooltip();
lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => {
element.addEventListener('input', this.toggleModifier.bind(this));
});
}
async #activateEffectDisplay(element, options) {
this.#bordered = true;
let effect = {};
if (element.dataset.uuid) {
const effectItem = await foundry.utils.fromUuid(element.dataset.uuid);
const effectData = effectItem.toObject();
effect = {
...effectData,
name: game.i18n.localize(effectData.name)
};
if (effectData.type === 'beastform') {
const beastformData = {
features: [],
advantageOn: effectData.system.advantageOn,
beastformAttackData: game.system.api.data.items.DHBeastform.getBeastformAttackData(effectItem)
};
const features = effectItem.parent.items.filter(x => effectItem.system.featureIds.includes(x.id));
for (const feature of features) {
const featureData = feature.toObject();
featureData.enrichedDescription = await feature.system.getEnrichedDescription();
beastformData.features.push(featureData);
}
effect.description = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs',
{
item: { system: beastformData }
}
);
} else {
effect.description = game.i18n.localize(
effectData.description ?? effectData.parent.system.description
);
}
} else {
const conditions = CONFIG.DH.GENERAL.conditions();
const condition = conditions[element.dataset.condition];
effect = {
...condition,
name: game.i18n.localize(condition.name),
description: game.i18n.localize(condition.description),
appliedBy: element.dataset.appliedBy,
isLockedCondition: true
};
}
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/effect-display.hbs`,
{
effect
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
return html;
}
async #activateItem(element, options) {
const itemUuid = element.dataset.tooltip.slice(6);
const item = await foundry.utils.fromUuid(itemUuid);
if (item) {
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
const isEffect = item instanceof ActiveEffect;
await this.enrichText(item);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/${type}.hbs`,
{
item: item,
description: item.system?.enrichedDescription ?? item.enrichedDescription,
config: CONFIG.DH,
allDomains: CONFIG.DH.DOMAIN.allDomains()
}
);
this.tooltip.innerHTML = html;
options.direction ??= this._determineItemTooltipDirection(element);
return html;
}
return null;
}
async #activateAttack(element, options) {
const actorUuid = element.dataset.tooltip.slice(8);
const actor = await foundry.utils.fromUuid(actorUuid);
const attack = actor.system.attack;
const description = await foundry.applications.ux.TextEditor.enrichHTML(attack.description);
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/attack.hbs`,
{
attack: attack,
description: description,
parent: actor,
config: CONFIG.DH
}
);
this.tooltip.innerHTML = html;
return html;
}
async #activateAdvantageDisadvantage(element, options) {
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
return html;
}
return null;
}
async #activateDeathMove(element, options) {
const name = element.dataset.deathName;
const img = element.dataset.deathImg;
const description = element.dataset.deathDescription;
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/death-move.hbs`,
{
move: { name: name, img: img, description: description }
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
return html;
}
async #activateRest(element, options) {
const isShortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const key = element.dataset.tooltip.slice(isShortRest ? 11 : 10);
const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[
element.dataset.restType
].moves;
const move = moves[key];
const description = await foundry.applications.ux.TextEditor.enrichHTML(move.description);
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move,
description: description
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
return html;
} }
_setAnchor(direction) { _setAnchor(direction) {

View file

@ -48,8 +48,9 @@ export default class RegisterHandlebarsHelpers {
return formula; return formula;
} }
static damageSymbols(damageData) { static damageSymbols(damageParts) {
const symbols = damageData.type.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon); const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))];
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,6 +700,19 @@ 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,8 +7,7 @@ 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

@ -1,20 +0,0 @@
import { MigrationHandlerBase } from './base.mjs';
export class Migration_2_6_0 extends MigrationHandlerBase {
version = '2.6.0';
/** @inheritdoc */
async updateActorSource(actor) {
if (actor.type === 'party' && Object.keys(actor.system.tagTeam.members).length) {
return {
_id: actor._id,
system: {
tagTeam: {
initiator: null,
members: _replace({})
}
}
};
}
}
}

View file

@ -1,9 +1,8 @@
/** /**
* @import DHItem from "../../documents/item.mjs"; * @import DHItem from "../../documents/item.mjs";
* @import DhActor from "../../documents/actor.mjs";
*/ */
/**
/**
* The base class of an async migration. * The base class of an async migration.
* These are generally run between versions for things that require compendiums or must be done in post. * These are generally run between versions for things that require compendiums or must be done in post.
* The migrate() functions calls the various updateXSource() functions. * The migrate() functions calls the various updateXSource() functions.
@ -23,16 +22,6 @@ export class MigrationHandlerBase {
return null; return null;
} }
/**
* Update a world actor
* @param {DhActor} actor
* @returns {Promise<object>}
* @protected
*/
async updateActorSource(actor) {
return null;
}
async migrate() { async migrate() {
// todo: handle more than just migrating effects. Right now this can only migrate effects // todo: handle more than just migrating effects. Right now this can only migrate effects
// NOTE: the preload is hardcoded, we should not hardcode it // NOTE: the preload is hardcoded, we should not hardcode it
@ -71,39 +60,10 @@ export class MigrationHandlerBase {
} }
}; };
const updateActor = async actor => { for (const actor of game.actors) {
const actorUpdate = await this.updateActorSource(actor);
if (actorUpdate) {
batch.push({
action: 'update',
documentName: 'Actor',
updates: [actorUpdate]
});
}
const aeUpdates = [];
for (const item of actor.items) { for (const item of actor.items) {
await updateItem(item); await updateItem(item);
} }
for (const effect of actor.effects) {
const changes = await this.updateActiveEffectSource(effect.toObject(), { parent: actor });
if (changes) aeUpdates.push(changes);
}
if (aeUpdates.length) {
batch.push({
action: 'update',
documentName: 'ActiveEffect',
updates: aeUpdates,
parent: actor
});
}
}
for (const actor of game.actors) {
await updateActor(actor);
progress.advance(); progress.advance();
} }
for (const item of game.items) { for (const item of game.items) {

View file

@ -1,6 +1,5 @@
import { defaultRestOptions } from '../config/generalConfig.mjs'; import { defaultRestOptions } from '../config/generalConfig.mjs';
import { Migration_2_5_2 } from './migration-handlers/2_5_2.mjs'; import { Migration_2_5_2 } from './migration-handlers/2_5_2.mjs';
import { Migration_2_6_0 } from './migration-handlers/2_6_0.mjs';
export async function runMigrations() { export async function runMigrations() {
let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion); let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion);
@ -330,8 +329,7 @@ export async function runMigrations() {
/* -------------------------------------------- */ /* -------------------------------------------- */
const migrations = [ const migrations = [
new Migration_2_5_2(), new Migration_2_5_2()
new Migration_2_6_0()
].filter(m => m.version && foundry.utils.isNewerVersion(m.version, lastMigrationVersion)); ].filter(m => m.version && foundry.utils.isNewerVersion(m.version, lastMigrationVersion));
for (const handler of migrations) { for (const handler of migrations) {

View file

@ -128,15 +128,6 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 48, "page": 48,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,15 +128,6 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 46, "page": 46,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,15 +128,6 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 50, "page": 50,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,15 +128,6 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 49, "page": 49,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -128,15 +128,6 @@
"source": "Daggerheart SRD", "source": "Daggerheart SRD",
"page": 51, "page": 51,
"artist": "" "artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
} }
}, },
"effects": [], "effects": [],

View file

@ -17,13 +17,6 @@
} }
} }
.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

@ -7,12 +7,12 @@
.chat-log .chat-message { .chat-log .chat-message {
background-image: url('../assets/parchments/dh-parchment-light.png'); background-image: url('../assets/parchments/dh-parchment-light.png');
.message-header .message-header-main .message-metadata, .message-header .message-header-metadata .message-metadata,
.message-header .message-header-main .name { .message-header .message-header-main .message-sub-header-container {
color: @dark; color: @dark;
} }
.message-header .message-header-main h4 { .message-header .message-header-main .message-sub-header-container h4 {
color: @dark-blue; color: @dark-blue;
} }
@ -42,12 +42,27 @@
.message-header { .message-header {
display: flex; display: flex;
gap: 8px; gap: 4px;
padding: 8px; padding: 8px;
align-items: center;
.portrait { .message-header-metadata {
flex: 0 0 auto; flex: none;
display: flex;
flex-direction: column;
.message-metadata {
font-family: @font-body;
color: @beige;
}
}
.message-header-main {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
.actor-img { .actor-img {
border-radius: 50%; border-radius: 50%;
width: 40px; width: 40px;
@ -55,60 +70,20 @@
object-fit: cover; object-fit: cover;
object-position: top center; object-position: top center;
} }
}
.message-header-main { .message-sub-header-container {
display: flex;
align-items: center;
column-gap: 4px;
row-gap: 2px;
flex: 1;
overflow: hidden;
display: grid;
grid-template:
"title metadata"
"subtitle subtitle";
h4 {
font-size: var(--font-size-16);
font-weight: bold;
margin-bottom: 0;
font-family: @font-subtitle;
color: @golden;
white-space: nowrap;
text-overflow: ellipsis;
flex: 1;
overflow: hidden;
align-self: flex-end;
}
.message-metadata {
font-family: @font-body;
color: @beige;
white-space: nowrap;
align-items: baseline;
.message-timestamp {
font-size: var(--font-size-11);
}
}
.subtitle {
grid-area: subtitle;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
color: @beige; color: @beige;
gap: 4px;
align-items: baseline; h4 {
line-height: 1; font-size: var(--font-size-16);
.name { font-weight: bold;
flex: 1; margin-bottom: 0;
} font-family: @font-subtitle;
.whisper-to { color: @golden;
color: @color-text-subtle;
flex: 0;
white-space: nowrap;
} }
} }
} }

View file

@ -595,57 +595,31 @@
margin-top: 4px; margin-top: 4px;
color: light-dark(#14142599, #efe6d850); color: light-dark(#14142599, #efe6d850);
font-size: var(--font-size-12); font-size: var(--font-size-12);
padding-left: 16px; padding-left: 3px;
}
section.gm-notes-section {
padding-bottom: var(--spacer-4);
header.gm-notes + p {
margin-top: 0;
}
}
header.gm-notes {
position: relative;
display: flex;
gap: 6px;
align-items: center;
&::before,
&::after {
content: " ";
flex: 1;
border-bottom: 1px solid var(--color-dark-6);
}
&::before {
mask-image: linear-gradient(270deg, black 0%, black calc(100% - 10px), transparent 100%);
}
&::after {
mask-image: linear-gradient(270deg, transparent 0%, black 10px, black 100%);
}
margin-top: var(--spacer-8);
margin-bottom: var(--spacer-4);
font-size: var(--font-size-11);
text-transform: uppercase;
} }
secret-block { secret-block {
display: block; position: relative;
section.secret {
/** A buffer to make the hover behavior work a bit better. The bottom in the button needs to compensate */ background-color: @red-10;
@buffer: 8px; padding: 0;
margin-top: -@buffer; margin-top: 0.375rem;
padding-top: @buffer; &.revealed {
background-color: @green-10;
}
p {
margin: 0.5rem 0;
}
}
button.reveal { button.reveal {
--button-size: 1rem; --button-size: 0.875rem;
height: var(--button-size);
position: absolute; position: absolute;
margin: auto; margin: auto;
left: 0; left: 0;
right: 0; right: 0;
width: min-content; width: min-content;
padding: 1px 8px 0 8px; padding: 1px 8px 0 8px;
bottom: calc(100% - 0.4375rem - 1px); bottom: calc(100% - 0.4375rem - 2px);
background-color: var(--dh-window-button-color-bg); // todo: find a better var name background-color: var(--dh-window-button-color-bg); // todo: find a better var name
border-color: var(--color-secret-border); border-color: var(--color-secret-border);
@ -653,7 +627,6 @@
font-size: var(--font-size-10); font-size: var(--font-size-10);
user-select: none; user-select: none;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
visibility: hidden; visibility: hidden;
} }
@ -662,20 +635,6 @@
visibility: visible; visibility: visible;
} }
} }
/**
* The element inside a secret-block.
* This is separate since during prosemirror editing, the secret-block container does not exist.
*/
section.secret {
--color-secret-bg: @red-10;
--color-revealed-bg: @green-10;
position: relative;
padding: 0;
p {
margin: 0.5rem 0;
}
}
} }
.system-daggerheart { .system-daggerheart {
@ -896,8 +855,4 @@
right: 2px; right: 2px;
} }
} }
.gm-notes {
font-style: italic;
}
} }

View file

@ -3,7 +3,7 @@
.sheet.daggerheart.dh-style.item { .sheet.daggerheart.dh-style.item {
.tab.features { .tab.features {
padding: 7px 10px; padding: 0 10px;
overflow-y: auto; overflow-y: auto;
.feature-list { .feature-list {
display: flex; display: flex;

View file

@ -111,7 +111,3 @@ body.theme-light,
.themed.theme-light { .themed.theme-light {
color-scheme: light; color-scheme: light;
} }
body:not([data-gm=true]) [data-visibility="gm"] {
display: none;
}

View file

@ -144,10 +144,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
.unloaded {
opacity: 0.5;
}
} }
} }

View file

@ -12,14 +12,12 @@
}); });
.application.sheet.daggerheart.dh-style { .application.sheet.daggerheart.dh-style {
--portrait-size: 150px;
.item-sheet-header { .item-sheet-header {
display: flex; display: flex;
.profile { .profile {
height: var(--portrait-size); height: 150px;
width: var(--portrait-size); width: 150px;
object-fit: cover; object-fit: cover;
border-right: 1px solid light-dark(@dark-blue, @golden); border-right: 1px solid light-dark(@dark-blue, @golden);
border-bottom: 1px solid light-dark(@dark-blue, @golden); border-bottom: 1px solid light-dark(@dark-blue, @golden);
@ -36,24 +34,19 @@
text-align: center; text-align: center;
width: 80%; width: 80%;
.item-name { .item-name input[type='text'] {
display: flex; font-size: var(--font-size-32);
flex-direction: column; height: 42px;
margin: 10px 10px 0 10px; text-align: center;
input[type='text'] { width: 90%;
font-size: var(--font-size-30); transition: all 0.3s ease;
text-align: center; outline: 2px solid transparent;
width: 100%; border: 1px solid transparent;
transition: all 0.3s ease;
outline: 2px solid transparent;
border: 1px solid transparent;
text-overflow: ellipsis;
&:hover[type='text'], &:hover[type='text'],
&:focus[type='text'] { &:focus[type='text'] {
box-shadow: none; box-shadow: none;
outline: 2px solid light-dark(@dark-blue, @golden); outline: 2px solid light-dark(@dark-blue, @golden);
}
} }
} }

View file

@ -6,80 +6,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
overflow: hidden; overflow-y: hidden !important;
padding: 0; padding-top: 10px;
margin: 0;
.description-section { prose-mirror.active + .artist-attribution {
flex: 1; display: none;
display: flex;
flex-direction: column;
overflow: auto;
padding: 12px 16px 4px 16px;
.with-scroll-shadows();
prose-mirror {
button.toggle {
top: 0px;
right: 0;
}
button[data-action=editGMNote] {
right: calc(var(--button-size) + 4px);
}
&.inactive {
height: unset!important;
overflow: unset;
.editor-content {
position: relative;
overflow: unset;
// Allows content links to peek out
margin-top: -4px;
padding: 4px 0 0 0;
}
}
&.active {
--min-height: 250px;
padding: 8px 0 0 16px;
button[data-action=editGMNote] {
display: none;
}
.editor-content {
padding-right: 16px;
padding-bottom: 4px;
}
}
}
/** Hide editors that are empty when inactive if we need them to be */
prose-mirror.inactive.hide-if-inactive {
display: none;
}
&:has(prose-mirror.active) {
padding: 0;
}
/** Description should fill available room (with overriden exceptions) */
prose-mirror[name="system.description"] {
flex: 1 0;
}
&:has(prose-mirror[name="system.gmNotes"]:not(.hide-if-inactive)) {
prose-mirror.inactive {
--min-height: 3rem;
&[name="system.description"] {
flex: 0 0;
}
&[name="system.gmNotes"] {
flex: 1 0;
}
}
}
}
/** Hide other elements if an editor is open */
&:has(prose-mirror.active) {
prose-mirror.inactive,
header.gm-notes,
.artist-attribution {
display: none;
}
} }
} }
} }

View file

@ -93,6 +93,10 @@
padding: 8px 0 0 16px; padding: 8px 0 0 16px;
} }
} }
.artist-attribution {
padding-left: 16px;
}
} }
.search-section { .search-section {

View file

@ -1,6 +1,4 @@
.application.sheet.daggerheart.dh-style.beastform { .application.sheet.daggerheart.dh-style.beastform {
--portrait-size: 130px;
.settings.tab { .settings.tab {
.advantage-on-section { .advantage-on-section {
display: flex; display: flex;
@ -11,11 +9,4 @@
font-style: italic; font-style: italic;
} }
} }
.tab.features.active {
display: flex;
flex-direction: column;
gap: 10px;
padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px;
.stable-scroll-container();
}
} }

View file

@ -2,9 +2,17 @@
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.dh-style.feature { .application.sheet.daggerheart.dh-style.feature {
--portrait-size: 130px; .item-sheet-header {
display: flex;
.profile {
height: 130px;
width: 130px;
}
}
section.tab { section.tab {
height: 400px;
overflow-y: auto; overflow-y: auto;
} }
} }

View file

@ -1,6 +1,6 @@
@import './item-sheet-shared.less';
@import './beastform.less'; @import './beastform.less';
@import './class.less'; @import './class.less';
@import './domain-card.less'; @import './domain-card.less';
@import './feature.less'; @import './feature.less';
@import './heritage.less'; @import './heritage.less';
@import './item-sheet-shared.less';

View file

@ -1,4 +1,4 @@
.item.daggerheart.dh-style:where(.application.sheet) { .application.sheet.daggerheart.dh-style.item {
&.minimized { &.minimized {
.attribution-header-label { .attribution-header-label {
display: none; display: none;
@ -14,22 +14,4 @@
button.plain.inline-control { button.plain.inline-control {
flex: 0 0 auto; flex: 0 0 auto;
} }
.tab-navigation {
margin-bottom: 0;
}
/** Default tab stylings */
.tab.active {
padding-top: 8px;
.with-scroll-shadows();
&.effects {
display: flex;
flex-direction: column;
gap: 10px;
padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px;
.stable-scroll-container();
}
}
} }

View file

@ -133,11 +133,6 @@
height: 40px; height: 40px;
flex: 1 1 calc(50% - 5px); flex: 1 1 calc(50% - 5px);
span {
font-family: @font-body;
font-weight: 700;
}
&:nth-last-child(1):nth-child(odd) { &:nth-last-child(1):nth-child(odd) {
flex-basis: 100%; flex-basis: 100%;
} }

View file

@ -46,14 +46,9 @@
padding: 0 8px; padding: 0 8px;
button { button {
height: 36px; height: 40px;
flex: 1 1 calc(50% - 5px); flex: 1 1 calc(50% - 5px);
span {
font-family: @font-body;
font-weight: 700;
}
&:nth-last-child(1):nth-child(odd) { &:nth-last-child(1):nth-child(odd) {
flex-basis: 100%; flex-basis: 100%;
} }

View file

@ -621,74 +621,16 @@
} }
.roll-buttons { .roll-buttons {
margin-top: 8px;
display: flex; display: flex;
flex-direction: column;
gap: 5px; gap: 5px;
margin-top: 8px;
button { button {
height: 32px; height: 32px;
} flex: 1;
.main-buttons { &.end-button {
display: flex; flex: 0;
gap: 5px;
button {
flex: 1;
&.end-button {
flex: 0;
}
}
}
.extra-button-row {
width: 100%;
display: flex;
gap: 5px;
.image-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
img {
height: 24px;
filter: @dark-filter;
}
.dice-result {
position: absolute;
font-size: 18px;
color: @beige;
filter: drop-shadow(0 0 1px @light-white);
}
}
.reload-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
gap: 5px;
cursor: pointer;
padding: 5px;
transition: all 0.3s ease;
color: @golden;
background: @golden-40;
&.failed-check {
background: @red-40;
color: @red;
}
&.passed-check {
background: @green-40;
color: @green;
}
} }
} }
} }

View file

@ -32,7 +32,7 @@
background: var(--background); background: var(--background);
border-radius: 4px; border-radius: 4px;
opacity: var(--ui-fade-opacity); opacity: var(--ui-fade-opacity);
transition: opacity var(--ui-fade-delay) var(--ui-fade-duration); transition: opacity var(--ui-fade-duration);
} }
&:not(.performance-low, .noblur) { &:not(.performance-low, .noblur) {
@ -41,7 +41,6 @@
&:hover::before { &:hover::before {
opacity: 1; opacity: 1;
transition: opacity var(--ui-fade-duration);
} }
#ui-right:has(#effects-display .effect-container) & { #ui-right:has(#effects-display .effect-container) & {

View file

@ -1,221 +1,119 @@
:root { :root {
--hotbar-size: 60px; --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
} --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease;
#interface.theme-dark,
body.theme-dark {
.daggerheart.dh-style.fear-tracker {
--background: url(../assets/parchments/dh-parchment-dark.png);
}
}
#interface.theme-light,
body.theme-light {
.daggerheart.dh-style.fear-tracker {
--background: url('../assets/parchments/dh-parchment-light.png') no-repeat center;
}
}
#ui-middle:has(#hotbar.sm),
#ui-middle:has(#hotbar.md.offset) {
#resources {
&.top-center,
&.bottom-center {
width: calc((var(--hotbar-size) * 5) + 32px) !important;
}
}
} }
#resources { #resources {
position: static;
min-height: calc(var(--header-height) + 4rem); min-height: calc(var(--header-height) + 4rem);
min-width: 4rem; min-width: 4rem;
color: #d3d3d3; color: #d3d3d3;
pointer-events: all; transition: var(--fear-animation);
padding: var(--spacer-8); header,
max-width: 540px; .controls,
min-width: 100px; .window-resize-handle {
transition: var(--fear-animation);
&::before {
content: ' ';
position: absolute;
inset: 0;
background: var(--background);
border-radius: 8px;
opacity: var(--ui-fade-opacity);
transition: opacity var(--ui-fade-delay) var(--ui-fade-duration);
} }
.window-content {
&:hover::before { padding: 0.5rem;
opacity: 1; #resource-fear {
transition: opacity var(--ui-fade-duration);
}
&.free {
position: absolute;
}
&.topCenter,
&.bottomCenter {
margin: 1rem 0;
width: 100% !important;
transform: translateX(var(--offset));
transition: all 250ms ease;
}
&.rightTop {
width: 300px !important;
max-width: 300px;
}
&.leftBottom {
width: 200px !important;
max-width: 200px;
background: transparent;
}
&:not(.performance-low, .noblur) {
backdrop-filter: blur(5px);
}
#ui-right:has(#effects-display .effect-container) & {
right: 62px;
}
#resource-fear {
position: relative;
&:hover {
.fear-header {
opacity: 1;
height: 18.75px;
visibility: visible;
}
.resize-handle {
opacity: 1;
}
}
.fear-header {
display: flex;
gap: 5px;
pointer-events: all;
margin-bottom: 0.5rem;
opacity: 0;
height: 0;
visibility: hidden;
transition: all 0.3s ease;
.drag-handle {
cursor: grab;
}
.fear-title {
font-size: var(--font-size-13);
}
}
.fear-tokens {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem 0.25rem; gap: 0.5rem 0.25rem;
flex-wrap: wrap;
.fear-token { i {
font-size: var(--font-size-16); font-size: var(--font-size-18);
border: 1.5px double light-dark(@dark-15, @dark-golden-80); border: 1px solid rgba(0, 0, 0, 0.5);
border-radius: 50%; border-radius: 50%;
aspect-ratio: 1; aspect-ratio: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 2.5rem; width: 3rem;
background-color: @primary-color-fear; background-color: @primary-color-fear;
color: @beige; -webkit-box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75);
box-shadow: 0px 0px 5px 1px rgba(0, 0, 0, 0.75);
color: #d3d3d3;
flex-grow: 0; flex-grow: 0;
text-shadow: none;
&.inactive { &.inactive {
filter: grayscale(1) !important; filter: grayscale(1) !important;
opacity: 0.5; opacity: 0.5;
} }
} }
} .controls,
.resource-bar {
.resize-handle { border: 2px solid rgb(153 122 79);
position: absolute; background-color: rgb(24 22 46);
bottom: -12px;
right: -9px;
cursor: nwse-resize;
opacity: 0;
transition: all 0.3s ease;
}
.resource-bar {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
height: 30px;
width: 100%;
.progress-bar {
position: absolute;
appearance: none;
width: 100%;
height: 100%;
border: 1px solid light-dark(@dark-15, @dark-golden-80);
border-radius: 999px;
z-index: 0;
background: @dark-blue;
&::-webkit-progress-bar {
border: none;
background: @dark-blue;
border-radius: 999px;
}
&::-webkit-progress-value {
background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%);
border-radius: 999px;
}
} }
.controls {
.label { display: flex;
margin: auto 0; align-self: center;
z-index: 2; border-radius: 50%;
height: auto; align-items: center;
text-align: center; justify-content: center;
font-size: var(--font-size-18); width: 30px;
color: @beige; height: 30px;
} font-size: var(--font-size-20);
}
.controls {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
color: light-dark(@dark, @beige);
.disabled {
opacity: 0.5;
}
}
&.isGM {
.fear-token {
cursor: pointer; cursor: pointer;
transition: box-shadow 0.15s ease;
&:hover { &:hover {
box-shadow: 0 0 8px @primary-color-fear ; font-size: 1.5rem;
}
&.disabled {
opacity: 0.5;
}
}
.resource-bar {
display: flex;
justify-content: center;
border-radius: 6px;
font-size: var(--font-size-20);
overflow: hidden;
position: relative;
padding: 0.25rem 0.5rem;
flex: 1;
text-shadow: var(--shadow-text-stroke);
&:before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: var(--fear-percent);
max-width: 100%;
background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%);
z-index: 0;
border-radius: 4px;
}
span {
position: inherit;
z-index: 1;
}
&.fear {
}
}
&.isGM {
i {
cursor: pointer;
&:hover {
font-size: var(--font-size-20);
}
} }
} }
} }
} }
button[data-action='close'] {
display: none;
}
&:not(:hover):not(.minimized) {
background: transparent;
box-shadow: unset;
border-color: transparent;
header,
#resource-fear .controls,
.window-resize-handle {
opacity: 0;
}
}
&:has(.fear-bar) {
min-width: 200px;
}
} }

View file

@ -226,7 +226,7 @@
ul, ul,
ol { ol {
margin: 0.5rem 0; margin: 1rem 0;
padding: 0 0 0 1.25rem; padding: 0 0 0 1.25rem;
li { li {

View file

@ -2,15 +2,15 @@
"id": "daggerheart", "id": "daggerheart",
"title": "Daggerheart", "title": "Daggerheart",
"description": "An unofficial implementation of the Daggerheart system", "description": "An unofficial implementation of the Daggerheart system",
"version": "2.5.4", "version": "2.5.3",
"compatibility": { "compatibility": {
"minimum": "14.364", "minimum": "14.364",
"verified": "14.365", "verified": "14.364",
"maximum": "14" "maximum": "14"
}, },
"url": "https://github.com/Foundryborne/daggerheart", "url": "https://github.com/Foundryborne/daggerheart",
"manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json", "manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json",
"download": "https://github.com/Foundryborne/daggerheart/releases/download/2.5.4/system.zip", "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.5.3/system.zip",
"authors": [ "authors": [
{ {
"name": "WBHarry" "name": "WBHarry"
@ -256,34 +256,34 @@
}, },
"Item": { "Item": {
"ancestry": { "ancestry": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"community": { "community": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"class": { "class": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"subclass": { "subclass": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"feature": { "feature": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"domainCard": { "domainCard": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"loot": { "loot": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"consumable": { "consumable": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"weapon": { "weapon": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"armor": { "armor": {
"htmlFields": ["description", "gmNotes"] "htmlFields": ["description"]
}, },
"beastform": {} "beastform": {}
}, },

View file

@ -9,7 +9,7 @@
{{/if}} {{/if}}
<div class="nest-inputs"> <div class="nest-inputs">
{{formField ../fields.scalable label="DAGGERHEART.GENERAL.scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox" localize=true}} {{formField ../fields.scalable label="DAGGERHEART.GENERAL.scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox" localize=true}}
{{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="DAGGERHEART.GENERAL.Resource.single" value=cost.key name=(concat "cost." index ".key") localize=true blank=false}} {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="DAGGERHEART.GENERAL.resource" value=cost.key name=(concat "cost." index ".key") localize=true blank=false}}
{{formField ../fields.value label="DAGGERHEART.GENERAL.amount" value=cost.value name=(concat "cost." index ".value") localize=true}} {{formField ../fields.value label="DAGGERHEART.GENERAL.amount" value=cost.value name=(concat "cost." index ".value") localize=true}}
{{formField ../fields.step label="DAGGERHEART.GENERAL.step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable) localize=true}} {{formField ../fields.step label="DAGGERHEART.GENERAL.step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable) localize=true}}
<a class="btn" data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeElement" data-index="{{index}}"><i class="fas fa-trash"></i></a> <a class="btn" data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeElement" data-index="{{index}}"><i class="fas fa-trash"></i></a>

View file

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

@ -1,7 +1,7 @@
<fieldset class="action-category"> <fieldset class="action-category">
<legend class="action-category-label" data-action="toggleSection" data-section="effects"> <legend class="action-category-label" data-action="toggleSection" data-section="effects">
<div>{{localize "DAGGERHEART.GENERAL.Resource.single"}}</div> <div>{{localize "DAGGERHEART.GENERAL.resource"}}</div>
</legend> </legend>
<div class="action-category-data open"> <div class="action-category-data open">
<fieldset> <fieldset>

View file

@ -15,7 +15,34 @@
{{/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>
@ -32,45 +59,6 @@
</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">
@ -88,7 +76,6 @@
{{/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,36 +1,25 @@
{{#if damage.main}} {{#each damage as |damage key|}}
{{> 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>{{label}}:</span> <span>{{localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")}}:</span>
<span>{{roll.total}}</span> <span>{{damage.total}}</span>
</div> </div>
{{#each damage.parts as |part|}}
<div class="roll-dice-container"> <div class="roll-dice-container">
{{#each roll.dice}} {{#each part.dice as |dice index|}}
{{#each results as |result index|}} <a class="roll-dice" data-action="rerollDamageDice" data-member-key="{{../../../key}}" data-damage-key="{{@../../key}}" data-part="{{@../index}}" data-dice="{{index}}">
{{#if result.active}} <span class="dice-label">{{dice.total}}</span>
<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}}"> <img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" dice.dice ".svg"}}" />
<span class="dice-label">{{result.result}}</span> </a>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" ../denomination ".svg"}}" /> {{#unless @last}}
</a> <span class="roll-operator">+</span>
{{/if}} {{/unless}}
{{/each}} {{/each}}
{{#unless @last}} {{#if part.modifierTotal}}
<span class="roll-operator">+</span> {{#if part.dice.length}}<span class="roll-operator">{{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}}</span>{{/if}}
{{/unless}} <span class="roll-value">{{positive part.modifierTotal}}</span>
{{/each}} {{/if}}
{{#if roll.modifierTotal}} </div>
{{#if roll.dice.length}}<span class="roll-operator">{{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}}</span>{{/if}} {{/each}}
<span class="roll-value">{{positive roll.modifierTotal}}</span>
{{/if}}
</div>
</div> </div>
{{/inline}} {{/each}}

View file

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

View file

@ -64,10 +64,7 @@
{{#if roll}} {{#if roll}}
<div class="roll-data {{#if roll.withHope}}hope{{else if roll.withFear}}fear{{else}}critical{{/if}}"> <div class="roll-data {{#if roll.withHope}}hope{{else if roll.withFear}}fear{{else}}critical{{/if}}">
<div class="duality-label"> <div class="duality-label">{{roll.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}</div>
{{roll.total}}
{{#if roll.isCritical}}{{roll.totalLabel}}{{else}}{{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}{{/if}}
</div>
<div class="roll-dice-container"> <div class="roll-dice-container">
<a class="roll-dice" data-action="rerollDice" data-member="{{@root.partId}}" data-dice-type="hope"> <a class="roll-dice" data-action="rerollDice" data-member="{{@root.partId}}" data-dice-type="hope">
<span class="dice-label">{{roll.dHope.total}}</span> <span class="dice-label">{{roll.dHope.total}}</span>
@ -103,12 +100,15 @@
<a class="roll-button" data-action="makeDamageRoll" data-member-key="{{@root.partId}}" {{#unless readyToRoll}}disabled{{/unless}}> <a class="roll-button" data-action="makeDamageRoll" data-member-key="{{@root.partId}}" {{#unless readyToRoll}}disabled{{/unless}}>
<img src="systems/daggerheart/assets/icons/dice/hope/d20.svg" /> <img src="systems/daggerheart/assets/icons/dice/hope/d20.svg" />
</a> </a>
<a class="delete-button" data-action="removeDamageRoll" data-member-key="{{@root.partId}}" {{#unless damage.active}}disabled{{/unless}}>
<i class="fa-solid fa-trash"></i> {{#if damage}}
</a> <a class="delete-button" data-action="removeDamageRoll" data-member-key="{{@root.partId}}" {{#unless rollData.options.damage}}disabled{{/unless}}>
<i class="fa-solid fa-trash"></i>
</a>
{{/if}}
</div> </div>
</span> </span>
{{#if damage.active}} {{#if damage}}
{{#if useCritDamage}} {{#if useCritDamage}}
{{> "systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs" damage=critDamage isCritical=true }} {{> "systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs" damage=critDamage isCritical=true }}
{{else}} {{else}}

View file

@ -8,10 +8,6 @@
value=setting.displayFear value=setting.displayFear
localize=true}} localize=true}}
{{formGroup {{formGroup
fields.fearPosition
value=setting.fearPosition
localize=true}}
{{formGroup
fields.displayCountdownUI fields.displayCountdownUI
value=setting.displayCountdownUI value=setting.displayCountdownUI
localize=true}} localize=true}}

View file

@ -18,7 +18,6 @@
<p class="hint">{{localize (concat "DAGGERHEART.SETTINGS.Automation.FIELDS.roll." field.name ".hint")}}</p> <p class="hint">{{localize (concat "DAGGERHEART.SETTINGS.Automation.FIELDS.roll." field.name ".hint")}}</p>
</div> </div>
{{/each}} {{/each}}
{{formGroup settingFields.schema.fields.reload value=settingFields.reload localize=true}}
</fieldset> </fieldset>
<fieldset> <fieldset>

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 source=source.damage baseFields=fields.damage.fields }}{{/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.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 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.parts.element.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

@ -1,13 +1,7 @@
{{#if (eq item.system.resource.type 'simple')}} {{#if (eq item.system.resource.type 'simple')}}
<div class="item-resource"> <div class="item-resource">
{{#if item.system.hasReload}} <i class="{{#if item.system.resource.icon}}{{item.system.resource.icon}}{{else}}fa-solid fa-hashtag{{/if}}"></i>
<a data-action="toggleItemReload" title="{{localize (ifThen item.system.needsReload 'DAGGERHEART.GENERAL.Resource.unloaded' 'DAGGERHEART.GENERAL.Resource.loaded')}}"> <input type="number" id="{{item.uuid}}-resource" class="inventory-item-resource" value="{{item.system.resource.value}}" min="0" max="{{rollParsed item.system.resource.max item.actor item true}}" />
<i class="fa-solid fa-gun {{#if item.system.needsReload}}unloaded{{/if}}"></i>
</a>
{{else}}
<i class="{{#if item.system.resource.icon}}{{item.system.resource.icon}}{{else}}fa-solid fa-hashtag{{/if}}"></i>
<input type="number" id="{{item.uuid}}-resource" class="inventory-item-resource" value="{{item.system.resource.value}}" min="0" max="{{rollParsed item.system.resource.max item.actor item true}}" />
{{/if}}
</div> </div>
{{else if (eq item.system.resource.type 'diceValue')}} {{else if (eq item.system.resource.type 'diceValue')}}
<div class="item-resources"> <div class="item-resources">

View file

@ -1,25 +1,11 @@
<section <section
class='tab {{tabs.description.cssClass}} {{tabs.description.id}}' class='tab {{tabs.description.cssClass}} {{tabs.description.id}}'
data-tab='{{tabs.description.id}}' data-tab='{{tabs.description.id}}'
data-group='{{tabs.description.group}}' data-group='{{tabs.description.group}}'
> >
<div class="description-section"> {{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
{{#if (and systemFields.gmNotes @root.user.isGM)}} {{#if (and showAttribution document.system.attribution.artist)}}
<section class="gm-notes-section"> <label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label>
{{#if enrichedGMNotes}} {{/if}}
<header class="gm-notes">{{localize "DAGGERHEART.ITEMS.FIELDS.gmNotes.label"}}</header>
{{/if}}
<prose-mirror
name="system.gmNotes"
{{#unless enrichedGMNotes}}class="hide-if-inactive"{{/unless}}
toggled="true"
value="{{document.system.gmNotes}}"
>{{{enrichedGMNotes}}}</prose-mirror>
</section>
{{/if}}
</div>
{{#if (and showAttribution document.system.attribution.artist)}}
<label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label>
{{/if}}
</section> </section>

View file

@ -18,26 +18,26 @@
</fieldset> </fieldset>
<fieldset class="two-columns"> <fieldset class="two-columns">
{{#with systemFields.attack.fields.damage.fields.main.fields as | fields | }} {{#with systemFields.attack.fields.damage.fields.parts.element.fields as | fields | }}
{{#with ../document.system.attack.damage.main as | source | }} {{#with ../document.system.attack.damage.parts.hitPoints 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.main.value.custom.enabled"}} {{formInput fields.value.fields.custom.fields.enabled value=source.value.custom.enabled name="system.attack.damage.parts.hitPoints.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.main.value.custom.formula"}} {{formInput fields.value.fields.custom.fields.formula value=source.value.custom.formula name="system.attack.damage.parts.hitPoints.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.main.value.dice"}} {{formInput fields.value.fields.dice value=source.value.dice name="system.attack.damage.parts.hitPoints.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.main.value.bonus" localize=true}} {{formInput fields.value.fields.bonus value=source.value.bonus name="system.attack.damage.parts.hitPoints.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.main.type" localize=true}} {{formInput fields.type value=source.type name="system.attack.damage.parts.hitPoints.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.main.fields.direct value=@root.document.system.attack.damage.main.direct name="system.attack.damage.main.direct" localize=true}} {{formInput @root.systemFields.attack.fields.damage.fields.direct value=@root.document.system.attack.damage.direct name="system.attack.damage.direct" localize=true}}
<input type="hidden" name="system.attack.damage.main.value.multiplier" value="{{source.value.multiplier}}"> <input type="hidden" name="system.attack.damage.parts.hitPoints.value.multiplier" value="{{source.value.multiplier}}">
{{/with}} {{/with}}
{{/with}} {{/with}}
</fieldset> </fieldset>
<fieldset class="two-columns"> <fieldset class="two-columns">

View file

@ -16,10 +16,10 @@
{{#each action.summon}} {{#each action.summon}}
<div class="summon-container"> <div class="summon-container">
<div class="summon-label-container"> <div class="summon-label-container">
<img src="{{this.data.img}}" /> <img src="{{this.actor.img}}" />
<label>{{this.data.name}}</label> <label>{{this.actor.name}}</label>
</div> </div>
<span># {{this.quantity}}</span> <span># {{this.rolledCount}}</span>
</div> </div>
{{/each}} {{/each}}
</div> </div>

View file

@ -1,18 +1,23 @@
<li class="chat-message message flexcol {{cssClass}}" data-message-id="{{message._id}}" <li class="chat-message message flexcol {{cssClass}}" data-message-id="{{message._id}}"
{{#if borderColor}}style="border-color:{{borderColor}}"{{/if}}> {{#if borderColor}}style="border-color:{{borderColor}}"{{/if}}>
<header class="message-header flexrow"> <header class="message-header flexrow">
<div class="portrait">
<img class="actor-img" src="{{actor.img}}" />
</div>
<div class="message-header-main"> <div class="message-header-main">
{{#if message.title}} <img class="actor-img" src="{{actor.img}}" />
<h4>{{message.title}}</h4> <div class="message-sub-header-container">
{{else if (not actor.name)}} {{#if message.title}}
<h4>{{author.name}}</h4> <h4>{{message.title}}</h4>
{{else}} <div>{{alias}} {{#if author.isGM}}(GM){{/if}}</div>
<h4>{{alias}}</h4> {{else}}
{{/if}} {{#unless actor.name}}
<h4>{{author.name}}</h4>
{{else}}
<h4>{{alias}}</h4>
<div>{{author.name}}</div>
{{/unless}}
{{/if}}
</div>
</div>
<div class="message-header-metadata">
<span class="message-metadata"> <span class="message-metadata">
<time class="message-timestamp">{{timeSince message.timestamp}}</time> <time class="message-timestamp">{{timeSince message.timestamp}}</time>
{{#if canDelete}} {{#if canDelete}}
@ -27,19 +32,9 @@
{{/if}} {{/if}}
</span> </span>
<div class="subtitle"> {{#if isWhisper}}
<div class="name"> <span class="whisper-to">{{localize 'CHAT.To'}}: {{whisperTo}}</span>
{{#if message.title}} {{/if}}
{{alias}} {{#if author.isGM}}(GM){{/if}}
{{else if (not actor.name)}}
{{author.name}}
{{/if}}
</div>
{{#if isWhisper}}
<span class="whisper-to">{{localize 'CHAT.To'}}: {{whisperTo}}</span>
{{/if}}
</div>
</div> </div>
</header> </header>
<div class="message-content"> <div class="message-content">

View file

@ -1,40 +1,18 @@
<div class="roll-buttons"> <div class="roll-buttons">
<div class="main-buttons"> {{#if areas.length}}<button class="action-areas end-button"><i class="fa-solid fa-crosshairs"></i></button>{{/if}}
{{#if areas.length}}<button class="action-areas end-button"><i class="fa-solid fa-crosshairs"></i></button>{{/if}} {{#if hasDamage}}
{{#if hasDamage}} {{#unless (empty damage)}}
{{#if damage.active}} <button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.damageRoll.dealDamage"}}</button>
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.damageRoll.dealDamage"}}</button> {{else}}
{{else}} <button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollDamage"}}</button>
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollDamage"}}</button> {{/unless}}
{{/if}}
{{/if}}
{{#if hasHealing}}
{{#if damage.active}}
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.healingRoll.applyHealing"}}</button>
{{else}}
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollHealing"}}</button>
{{/if}}
{{/if}}
{{#if (and hasEffect)}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}}
</div>
{{#if parent.system.hasReload}}
<div class="extra-button-row">
<button class="roll-reload-check">
<div class="image-container">
<img class="dice-icon normal" src="{{concat 'systems/daggerheart/assets/icons/dice/default/d20.svg'}}" alt="">
{{#if reloadCheckValue}}<div class="dice-result">{{reloadCheckValue}}</div>{{/if}}
</div>
{{localize "DAGGERHEART.ACTIONS.Reload.checkReload"}}
</button>
<div class="reload-data {{#if reloadCheckValue}}{{ifThen parent.system.reloadCheckFailed 'failed-check' 'passed-check'}}{{/if}}">
{{#unless reloadCheckValue}}
{{localize "DAGGERHEART.ACTIONS.Reload.notRolled"}}
{{else}}
{{ifThen parent.system.reloadCheckFailed (localize "DAGGERHEART.ACTIONS.Reload.checkFailed") (localize "DAGGERHEART.ACTIONS.Reload.checkPassed")}}
{{/unless}}
</div>
</div>
{{/if}} {{/if}}
{{#if hasHealing}}
{{#unless (empty damage)}}
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.healingRoll.applyHealing"}}</button>
{{else}}
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollHealing"}}</button>
{{/unless}}
{{/if}}
{{#if (and hasEffect)}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}}
</div> </div>

View file

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

@ -1,6 +1,5 @@
<div class="chat-roll"> <div class="chat-roll">
<div class="roll-part-title"><span>{{title}}</span></div> <div class="roll-part-title"><span>{{title}}</span></div>
{{#if actionDescription}}{{> 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs'}}{{/if}} {{#if actionDescription}}{{> 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs'}}{{/if}}
{{#if hasRoll}} {{#if hasRoll}}
<div class="roll-part-header"><span>{{localize "Result"}}</span></div> <div class="roll-part-header"><span>{{localize "Result"}}</span></div>

View file

@ -1,48 +1,16 @@
<div> <div>
<div id="resource-fear" class="fear-tracker {{#if isGM}}isGM{{/if}}"> <div id="resource-fear" class="{{#if isGM}}isGM{{/if}}">
{{#if isFree}}
<div class="fear-header">
<div class="drag-handle">
<i class="fa-solid fa-grip-vertical"></i>
</div>
<span class="fear-title">{{localize 'DAGGERHEART.GENERAL.fear'}}</span>
</div>
{{/if}}
{{#if (eq display 'token')}} {{#if (eq display 'token')}}
<div class="fear-tokens"> {{#times max}}
{{#times max}} <i class="fas fa-skull {{#if (gte @this ../current)}} inactive{{/if}}" style="filter: hue-rotate(calc(({{this}}/{{../max}})*75deg))" data-index="{{this}}" data-action="setFear"></i>
<a {{/times}}
class="fear-token {{#if (gte @this ../current)}} inactive{{/if}}"
style="filter: hue-rotate(calc(({{this}}/{{../max}})*75deg))"
data-index="{{this}}" data-action="setFear"
>
<i class="fear-token fas fa-skull"></i>
</a>
{{/times}}
</div>
{{/if}} {{/if}}
{{#if (eq display 'bar')}} {{#if (eq display 'bar')}}
{{#if isGM}}<div class="controls control-minus {{#if (lte current 0)}} disabled{{/if}}" data-increment="-1" data-action="increaseFear">-</div>{{/if}}
<div class="resource-bar fear-bar" style="--fear-percent: {{percent}}%"> <div class="resource-bar fear-bar" style="--fear-percent: {{percent}}%">
<progress <span>{{current}}/{{max}}</span>
class='progress-bar stress-color'
value='{{current}}'
max='{{max}}'
></progress>
<h4 class="label">{{current}} / {{max}}</h4>
</div> </div>
{{#if isGM}} {{#if isGM}}<div class="controls control-plus {{#if (gte current max)}} disabled{{/if}}" data-increment="+1" data-action="increaseFear">+</div>{{/if}}
<div class="controls">
<a class="{{#if (lte current 0)}} disabled{{/if}}" data-increment="-1" data-action="increaseFear"><i class="fa-solid fa-minus"></i></a>
<a class="{{#if (gte current max)}} disabled{{/if}}" data-increment="+1" data-action="increaseFear"><i class="fa-solid fa-plus"></i></a>
</div>
{{/if}}
{{/if}}
{{#if isFree}}
<a class="resize-handle">
<i class="fa-solid fa-chevron-right" style="transform: rotate(45deg);"></i>
</a>
{{/if}} {{/if}}
</div> </div>
</div> </div>

Some files were not shown because too many files have changed in this diff Show more