Compare commits

..

12 commits
main ... 2.5.4

Author SHA1 Message Date
WBHarry
bc9e11c0b5 Merge branch 'main' into v14 2026-07-14 14:37:20 +02:00
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
96 changed files with 1251 additions and 2393 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 * as dice from './module/dice/_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
@ -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 * as collections from './module/documents/collections/_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 RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
@ -24,7 +23,7 @@ import TokenManager from './module/documents/tokenManager.mjs';
CONFIG.DH = SYSTEM;
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 = {
DHRoll: DHRoll,
DualityRoll: DualityRoll,
@ -39,8 +38,6 @@ CONFIG.RegionBehavior.dataModels = {
};
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.dataModels = models.actors.config;
@ -270,10 +267,6 @@ Hooks.on('i18nInit', () => {
});
Hooks.on('setup', () => {
if (game.user.isGM) {
document.body.dataset.gm = true;
}
CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({

View file

@ -104,10 +104,8 @@
"startCountdown": "Start Countdown"
},
"damage": {
"addResource": "Add Resource",
"multiplier": "Multiplier",
"flatMultiplier": "Flat Multiplier",
"markResources": "Mark Resources"
"flatMultiplier": "Flat Multiplier"
},
"general": {
"customFormula": "Custom Formula",
@ -126,10 +124,6 @@
"damageOnSave": "Damage on Save",
"useDefaultItemValues": "Use default Item values"
},
"Reload": {
"checkReload": "Check Reload",
"reloadRequired": "Reload Required!"
},
"RollField": {
"diceRolling": {
"compare": "Should be",
@ -1326,11 +1320,6 @@
"short": "V. Far"
}
},
"ReloadChoices": {
"off": { "label": "Don't Use" },
"button": { "label": "Use button" },
"auto": { "label": "Automatic" }
},
"RollTypes": {
"trait": {
"name": "Trait"
@ -2230,9 +2219,7 @@
},
"Resource": {
"single": "Resource",
"plural": "Resources",
"unloaded": "The weapon is not loaded",
"loaded": "The weapon is loaded"
"plural": "Resources"
},
"Roll": {
"attack": "Attack Roll",
@ -2496,6 +2483,7 @@
"reroll": "Reroll",
"rerolled": "Rerolled",
"rerollThing": "Reroll {thing}",
"resource": "Resource",
"result": {
"single": "Result",
"plural": "Results"
@ -2562,9 +2550,6 @@
},
"identifier": {
"label": "Identifier"
},
"gmNotes": {
"label": "GM Notes"
}
},
"Ancestry": {
@ -2580,9 +2565,6 @@
"severe": "Severe Threshold"
}
},
"Base": {
"addGMNote": "Add GM Note"
},
"Beastform": {
"FIELDS": {
"beastformType": { "label": "Beastform Type" },
@ -2699,9 +2681,6 @@
"displayFear": {
"label": "Display Fear"
},
"fearPosition": {
"label": "Fear Position"
},
"displayCountdownUI": {
"label": "Display Countdown UI"
},
@ -2753,13 +2732,6 @@
"token": "Tokens",
"bar": "Bar",
"hide": "Hide"
},
"fearPosition": {
"free": "Free",
"topCenter": "Top + Center",
"bottomCenter": "Bottom + Center",
"rightTop": "Right + Top",
"leftBottom": "Left + Bottom"
}
},
"Automation": {
@ -2818,10 +2790,6 @@
"hint": "Effects with defined range dependency will automatically turn on/off depending on range"
}
},
"reload": {
"label": "Reload Checking",
"hint": "If the system should present a button for checking if the character will need to reload or do it automatically"
},
"resourceScrollTexts": {
"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."
@ -3283,8 +3251,7 @@
"knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"noTokenTargeted": "No token is targeted",
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM",
"reloadRequired": "The {weapon} must be reloaded to be used!"
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM"
},
"Progress": {
"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);
context.config = CONFIG.DH;
context.title = this.config.title ?? this.title;
const { damageFormula, resourceFormulas } = this.roll.constructFormulas(this.config);
context.damageFormula = damageFormula;
context.resourceFormulas = resourceFormulas;
context.formula = this.roll.constructFormula(this.config);
context.hasHealing = this.config.hasHealing;
context.directDamage = this.config.directDamage;
context.selectedMessageMode = this.config.selectedMessageMode;
@ -77,11 +73,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
static updateRollConfiguration(_event, _, formData) {
const data = foundry.utils.expandObject(formData.object);
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.roll, data.roll);
foundry.utils.mergeObject(this.config.modifiers, data.modifiers);
this.config.selectedMessageMode = data.selectedMessageMode;

View file

@ -1,5 +1,4 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
import { ChatDamageData } from '../../data/chat-message/chatDamageData.mjs';
import { MemberData } from '../../data/tagTeamData.mjs';
import { getCritDamageBonus, shouldUseHopeFearAutomation } from '../../helpers/utils.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);
if (!hasRolled) return false;
return !data.rollData.options.hasDamage || data.damageRollData.active;
return !data.rollData.options.hasDamage || Boolean(data.rollData.options.damage);
});
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 critSelected = !selectedRoll
? undefined
: (selectedRoll?.roll?.isCritical ?? false);
: (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members);
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 critSelected = !selectedRoll ? undefined : (selectedRoll?.roll?.isCritical ?? false);
const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
const damage = data.rollData?.options?.damage;
return {
...data,
@ -247,9 +247,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
hasRolled: Boolean(data.rollData),
rollOptions,
damageRollOptions,
damage: data.damageRollData,
critDamage: await this.getCriticalDamage(data.damageRollData),
useCritDamage: critSelected || (critSelected === undefined && data.roll?.isCritical)
damage: damage,
critDamage: await this.getCriticalDamage(damage),
useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
};
}
@ -379,7 +379,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
let rollIsSelected = false;
for (const member of Object.values(members)) {
const rollFinished = Boolean(member.rollData);
const damageFinished = member.rollData?.options?.hasDamage ? member.damageRollData.active : true;
const damageFinished = member.rollData?.options?.hasDamage ? Boolean(member.rollData.options.damage) : true;
rollsAreFinished = rollsAreFinished && rollFinished && damageFinished;
rollIsSelected = rollIsSelected || member.selected;
@ -540,10 +540,17 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
await action.workflow.get('damage').execute(config, null, true);
if (!config.damage) return;
const current = this.party.system.tagTeam.members[memberKey].rollData;
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)
);
@ -551,11 +558,15 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
static async #removeDamageRoll(_, button) {
const { memberKey } = button.dataset;
const current = this.party.system.tagTeam.members[memberKey].rollData;
this.updatePartyData(
{
[`system.tagTeam.members.${memberKey}.damageRollData`]: {
main: null,
resources: _replace({})
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: null
}
}
},
this.getUpdatingParts(button)
@ -563,44 +574,89 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
}
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];
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(
{
[updatePath]: updateValue.toJSON()
[`system.tagTeam.members.${memberKey}.rollData`]: rollData
},
this.getUpdatingParts(button)
);
}
async getCriticalDamage(origDamage) {
const newDamage = origDamage ? ChatDamageData.fromJSON(JSON.stringify(origDamage)) : null;
if (newDamage?.main) {
const criticalDamage = await getCritDamageBonus(newDamage.main.formula);
if (criticalDamage) {
const criticalTerm = new foundry.dice.terms.NumericTerm({ number: criticalDamage, evaluated: true });
criticalTerm.evaluate();
newDamage.main = await Roll.fromTerms([
...origDamage.main.terms,
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
criticalTerm
]);
newDamage.main.options = foundry.utils.deepClone(origDamage.main.options);
async getCriticalDamage(damage) {
const newDamage = foundry.utils.deepClone(damage);
for (let key in newDamage) {
var damage = newDamage[key];
damage.formula = '';
damage.total = 0;
for (let part of damage.parts) {
const criticalDamage = await getCritDamageBonus(part.formula);
if (criticalDamage) {
part.modifierTotal += criticalDamage;
part.total += criticalDamage;
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;
}
static async #selectRoll(_, button) {
const { memberKey } = button.dataset;
this.updatePartyData(
{
[`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 */
[
...Object.keys(this.party.system.tagTeam.members),
this.constructor.PARTS.rollSelection.id,
this.constructor.PARTS.result.id
]
this.getUpdatingParts(button)
);
}
@ -634,65 +685,29 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null;
const mainRoll = new MemberData(baseMainRoll.toObject());
mainRoll.damageRollData = baseMainRoll.damageRollData ?
ChatDamageData.fromJSON(JSON.stringify(baseMainRoll.damageRollData)) : null;
const secondaryRoll = new MemberData(baseSecondaryRoll.toObject());
secondaryRoll.damageRollData = baseSecondaryRoll.damageRollData ?
ChatDamageData.fromJSON(JSON.stringify(baseSecondaryRoll.damageRollData)) : null;
const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData;
const systemData = mainRoll.rollData.options;
const isCritical = overrideIsCritical ?? systemData.roll.isCritical;
if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage);
const isCritical = overrideIsCritical ?? mainRoll.roll.isCritical;
if (isCritical) mainRoll.damageRollData = await this.getCriticalDamage(mainRoll.damageRollData);
if (secondaryRoll.damageRollData) {
if (secondaryRollData?.options.hasDamage) {
const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical)
? await this.getCriticalDamage(secondaryRoll.damageRollData)
: secondaryRoll.damageRollData;
if (mainRoll.damageRollData) {
if (secondaryDamage.main) {
if (mainRoll.damageRollData.main) {
mainRoll.damageRollData.main = Roll.fromTerms([
...baseMainRoll.damageRollData.main.terms,
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...baseSecondaryRoll.damageRollData.main.terms
]);
/* 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]
};
? await this.getCriticalDamage(secondaryRollData.options.damage)
: secondaryRollData.options.damage;
if (systemData.damage) {
for (const [key, damage] of Object.entries(secondaryDamage ?? {})) {
if (key in systemData.damage) {
systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula]
.filter(x => x)
.join(' + ');
systemData.damage[key].total += damage.total;
systemData.damage[key].parts.push(...damage.parts);
} else {
mainRoll.damageRollData.main = secondaryDamage.main;
}
}
for (const [key, damage] of Object.entries(secondaryDamage.resources ?? {})) {
if (key in mainRoll.damageRollData.resources) {
mainRoll.damageRollData.resources[key] = Roll.fromTerms([
...baseMainRoll.damageRollData.resources[key].terms,
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...baseSecondaryRoll.damageRollData.resources[key].terms
]);
/* Joining the roll.options of both rolls */
const joinedDamageTypes = new Set([
...baseMainRoll.damageRollData.resources[key].options.damageTypes,
...baseSecondaryRoll.damageRollData.resources[key].options.damageTypes
]);
mainRoll.damageRollData.resources[key].options = {
...baseMainRoll.damageRollData.resources[key].options,
damageTypes: [...joinedDamageTypes]
};
} else {
mainRoll.damageRollData.resources[key] = damage;
systemData.damage[key] = damage;
}
}
} 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);
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'),
msgData = {
type: 'dualityRoll',
user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: mainActor }),
system: systemData,
system: mainRoll.options,
rolls: [JSON.stringify(joinedRoll.roll)],
sound: null,
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';
const { ApplicationV2 } = foundry.applications.api;
@ -31,10 +31,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
removeElement: this.removeElement,
removeTransformActor: this.removeTransformActor,
editEffect: this.editEffect,
addDamage: this.#onAddDamage,
removeDamage: this.#onRemoveDamage,
addDamageResource: this.#onAddDamageResource,
removeDamageResource: this.#onRemoveDamageResource,
addDamage: this.addDamage,
removeDamage: this.removeDamage,
editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
@ -159,9 +157,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
if (this.action.damage) {
const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes);
context.allDamageTypesUsed = allKeys.every(k => k in this.action._source.damage.resources);
if (this.action.damage?.main?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === '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 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.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 DHActionBaseConfig */
static #onAddDamage() {
if (!this.action.damage || this.action.damage?.main) return;
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject();
data.damage.main = {
...DHDamageData.schema.getInitialValue(),
applyTo: 'hitPoints',
type: 'physical'
};
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** @this DHActionBaseConfig */
static #onRemoveDamage() {
if (!this.action.damage?.main) return;
const data = this.action.toObject();
data.damage.main = null;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** @this DHActionBaseConfig */
static #onAddDamageResource(_event) {
if (!this.action.damage) return;
const allKeys = Object.keys(CONFIG.DH.GENERAL.healingTypes);
const unused = allKeys.filter(k => !(k in this.action._source.damage.resources));
const choices = unused.map(k => ({ value: k, label: _loc(CONFIG.DH.GENERAL.healingTypes[k].label) }));
const choices = getUnusedDamageTypes(this.action._source.damage.parts);
const content = new foundry.data.fields.StringField({
label: _loc('DAGGERHEART.GENERAL.Resource.single'),
label: game.i18n.localize('Damage Type'),
choices,
required: true
}).toFormGroup({}, {
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}).outerHTML;
}).toFormGroup(
{},
{
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}
).outerHTML;
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
data.damage.resources[type] = {
...this.action.schema.fields.damage.fields.resources.element.getInitialValue(),
applyTo: type
};
const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue();
part.applyTo = type;
if (type === CONFIG.DH.GENERAL.healingTypes.hitPoints.id)
part.type = this.action.schema.fields.damage.fields.parts.element.fields.type.element.initial;
data.damage.parts[type] = part;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
};
const typeDialog = new foundry.applications.api.DialogV2({
buttons: [
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true,
callback
}
foundry.utils.mergeObject(
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true
},
{ callback: callback }
)
],
content: content,
rejectClose: false,
modal: false,
window: {
title: _loc('DAGGERHEART.ACTIONS.Config.damage.addResource')
title: game.i18n.localize('Add Damage')
},
position: { width: 300 }
});
@ -372,12 +353,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
typeDialog.render(true);
}
/** @this DHActionBaseConfig */
static #onRemoveDamageResource(_event, button) {
if (!this.action.damage?.resources) return;
static removeDamage(_event, button) {
if (!this.action.damage.parts) return;
const data = this.action.toObject();
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) });
}

View file

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

View file

@ -3,7 +3,7 @@ import DhDeathMove from '../../dialogs/deathMove.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.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 */
@ -29,7 +29,6 @@ export default class CharacterSheet extends DHBaseActorSheet {
toggleResourceDice: CharacterSheet.#toggleResourceDice,
handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie,
toggleItemReload: CharacterSheet.#onToggleItemReload,
cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime,
@ -955,21 +954,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
/** */
static #advanceResourceDie(_, target) {
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) {
event.preventDefault();
event.stopPropagation();

View file

@ -30,8 +30,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
addFeature: DHBaseItemSheet.#addFeature,
deleteFeature: DHBaseItemSheet.#deleteFeature,
addResource: DHBaseItemSheet.#addResource,
removeResource: DHBaseItemSheet.#removeResource,
editGMNote: DHBaseItemSheet.#onEditGMNote
removeResource: DHBaseItemSheet.#removeResource
},
dragDrop: [
{ dragSelector: null, dropSelector: '.drop-section' },
@ -77,16 +76,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options);
const TextEditor = foundry.applications.ux.TextEditor.implementation;
switch (partId) {
case 'description':
context.enrichedDescription = await this.document.system.getEnrichedDescription({ gmNotes: false });
context.enrichedGMNotes = await TextEditor.implementation.enrichHTML(this.item.system.gmNotes, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
})
context.enrichedDescription = await this.document.system.getEnrichedDescription();
break;
case 'effects':
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 { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs';
import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs';
import FearTracker from './fearTracker.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) {
@ -111,7 +110,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const message = game.messages.get(li.dataset.messageId);
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 reroll = await message.rolls[0].reroll({ liveRoll: true });
message.update({ rolls: [reroll] });
@ -127,7 +126,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
: false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage;
},
onClick: async (_event, li) => {
callback: async li => {
const message = game.messages.get(li.dataset.messageId);
const update = await message.system.getRerolledDamage();
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 =>
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() {
@ -183,15 +179,28 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
}
async onRollSimple(event, message) {
const buttonType = event.target.dataset.type ?? 'damage';
const total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0);
const targets = Array.from(game.user.targets);
const buttonType = event.target.dataset.type ?? 'damage',
total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0),
damages = {
hitPoints: {
parts: [
{
applyTo: 'hitPoints',
damageTypes: [],
total
}
]
}
},
targets = Array.from(game.user.targets);
if (targets.length === 0)
return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected'));
for (const target of targets) {
if (buttonType === 'healing') target.actor.takeHealing({ hitPoints: total });
else target.actor.takeDamage({ total });
}
targets.forEach(target => {
if (buttonType === 'healing') target.actor.takeHealing(damages);
else target.actor.takeDamage(damages);
});
}
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 target = event.target.closest('[data-result]');
const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') {
const { isResource, damageType, dice, result } = target.dataset;
await message.system.damage.rerollDamageDie(isResource, damageType, dice, result);
const updatePath = isResource ? `system.damage.resources.${damageType}` : 'system.damage.main';
const updateValue = isResource ?
message.system.damage.resources[damageType] : message.system.damage.main;
await message.update({
[updatePath]: updateValue.toJSON()
const { damageType, part, dice, result } = target.dataset;
const damagePart = message.system.damage[damageType].parts[part];
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result);
const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => {
if (index !== Number(part)) return damagePart;
return {
...damagePart,
total: parsedRoll.total,
dice: rerolledDice
};
});
const updateMessage = game.messages.get(message._id);
await updateMessage.update({
[`system.damage.${damageType}`]: {
total: parsedRoll.total,
parts: damageParts
}
});
} else {
const rerollDice = message.system.roll.dice[target.dataset.dieIndex];
@ -279,15 +297,4 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const actor = game.actors.get(event.target.dataset.actorId);
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);
const needsReload = await message.system.action.handleReload?.({ awaitRoll: true });
await message.update({ 'system.needsReload': needsReload });
}
}

View file

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

View file

@ -12,11 +12,14 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
previousCountdownData = null;
changedCountdownsForAnimation = new Set();
constructor(options = {}) {
super(options);
this.previousCountdownData =
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).countdowns;
this.setupHooks();
}
/** @inheritDoc */
@ -77,15 +80,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
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) {
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 */
const typesToAnimate = new Set();
for (const countdownKey of options.animate ?? []) {
for (const countdownKey of this.changedCountdownsForAnimation) {
const shimmerAnimation = [
{ backgroundPositionX: '98%' },
{ 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}"]`);
element?.animate(pulseAnimation, pulseTiming);
}
this.changedCountdownsForAnimation.clear();
}
/** 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]) => ({
key,
countdown,
ownership: countdown.getUserLevel(game.user)
ownership: DhCountdowns.#getPlayerOwnership(game.user, setting, countdown)
}));
return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE);
}
_getCountdownData() {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
return this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => {
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) {
acc.push(user);
}
@ -214,6 +213,19 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
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() {
if (game.user.isGM) return true;
@ -311,12 +323,18 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
action: socketEvent.Refresh,
data: { refreshType: RefreshType.Countdown }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
setupHooks() {
Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
}
async close(options) {
/* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */
if (options.closeKey) return;
Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
return super.close(options);
}
@ -359,30 +377,4 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
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) {
constructor(options = {}) {
super(options);
this._dragData = {
isDragging: false,
startX: 0,
startY: 0,
startLeft: 0,
startTop: 0
}
}
/** @inheritDoc */
@ -29,20 +21,19 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
classes: [],
tag: 'div',
window: {
frame: false,
frame: true,
title: 'DAGGERHEART.GENERAL.fear',
positioned: true,
resizable: true,
minimizable: false
},
classes: ['daggerheart', 'dh-style', 'fear-tracker'],
actions: {
setFear: FearTracker.setFear,
increaseFear: FearTracker.increaseFear
},
position: {
width: 540,
height: 'auto'
width: 222,
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;
}
get fearPosition() {
return game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).fearPosition;
}
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
@ -76,48 +63,15 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
current = this.currentFear,
max = this.maxFear,
percent = (current / max) * 100,
isGM = game.user.isGM,
locked = false,
isFree = this.fearPosition == 'free';
isGM = game.user.isGM;
return { display, current, max, percent, isGM, locked, isFree };
return { display, current, max, percent, isGM };
}
/** @override */
async _onRender(context, options) {
await super._onRender(context, options);
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;
}
async _preFirstRender(context, options) {
options.position =
game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position;
}
/** @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);
}
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) {
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) {
if (!game.user.isGM) return;
const fearCount = Number(target.dataset.index ?? 0);
@ -172,119 +110,4 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
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 = {
resource: {
id: 'resource',
label: 'DAGGERHEART.GENERAL.Resource.single',
label: 'DAGGERHEART.GENERAL.resource',
group: 'Global'
},
quantity: {
@ -924,14 +924,6 @@ export const fearDisplay = {
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 = {
0: { value: 0, label: 'OWNERSHIP.NONE' },
2: { value: 2, label: 'OWNERSHIP.OBSERVER' },

View file

@ -27,10 +27,8 @@ export const menu = {
};
export const gameSettings = {
/** @type {'Automation'} */
Automation: 'Automation',
Metagaming: 'Metagaming',
/** @type {'Homebrew'} */
Homebrew: 'Homebrew',
appearance: 'Appearance',
GlobalOverrides: 'GlobalOverrides',
@ -39,7 +37,6 @@ export const gameSettings = {
Fear: 'ResourcesFear'
},
LevelTiers: 'LevelTiers',
/** @type {'Countdowns'} */
Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion',
SpotlightRequestQueue: 'SpotlightRequestQueue',
@ -62,18 +59,3 @@ export const actionAutomationChoices = {
label: 'DAGGERHEART.CONFIG.ActionAutomationChoices.always'
}
};
export const reloadChoices = {
off: {
id: 'off',
label: 'DAGGERHEART.CONFIG.ReloadChoices.off.label'
},
button: {
id: 'button',
label: 'DAGGERHEART.CONFIG.ReloadChoices.button.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 ITEMBROWSER from './itemBrowserConfig.mjs';
/** @type {"daggerheart"} */
export const SYSTEM_ID = 'daggerheart';
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) {
const baseDamage = this.getParentHitPointDamage();
if (baseDamage) {
if (!this.damage.main) {
this.damage.main = baseDamage;
if (!this.damage.parts.hitPoints) {
this.damage.parts.hitPoints = baseDamage;
} 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,
formula: `${baseDamage.value.getFormula()} + ${this.damage.main.value.getFormula()}`
formula: `${baseDamage.value.getFormula()} + ${this.damage.parts.hitPoints.value.getFormula()}`
};
}
}
}
if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait;
this.roll.type = 'attack';
@ -34,28 +33,24 @@ export default class DHAttackAction extends DHDamageAction {
}
getParentHitPointDamage() {
return this.item?.system?.attack.damage.main;
return this.item?.system?.attack.damage.parts.hitPoints;
}
get damageFormula() {
const hitPointsPart = this.damage.main;
const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula();
}
get altDamageFormula() {
const hitPointsPart = this.damage.main;
const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula();
}
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);
if (result?.message?.system.action?.roll?.type === 'attack') {
@ -66,23 +61,6 @@ export default class DHAttackAction extends DHDamageAction {
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 needsToReload = roll.total === 1;
if (needsToReload) {
this.item.update({ 'system.resource.value': 0 });
}
return needsToReload;
}
/**
* Generate a localized label array for this item subtype.
* @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`));
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 damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
const str = damageString
@ -104,7 +82,7 @@ export default class DHAttackAction extends DHDamageAction {
x: game.i18n.localize('DAGGERHEART.GENERAL.damage')
});
const icons = Array.from(type ?? [])
const icons = Array.from(type)
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
.filter(Boolean);

View file

@ -289,6 +289,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasEffect: this.hasEffect,
hasSave: this.hasSave,
onSave: this.save?.damageMod,
isDirect: !!this.damage?.direct,
selectedMessageMode: game.settings.get('core', 'messageMode'),
data: this.getRollData(),
evaluate: this.hasRoll,
@ -306,20 +307,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
};
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(
this.actor.id,
this.damage.main.groupAttack
this.damage.groupAttack
)
: null;
config.damageOptions = {
groupAttack: this.damage.main?.groupAttack
groupAttack: this.damage.groupAttack
? {
numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.main.groupAttack
range: this.damage.groupAttack
}
: null
};
@ -429,11 +430,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
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() {
return this.type === 'healing' && !foundry.utils.isEmpty(this.damage.resources);
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing';
}
get hasSave() {
@ -469,25 +470,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
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
*/
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'
},
damage: {
main: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
multiplier: 'flat'
parts: {
hitPoints: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
multiplier: 'flat'
}
}
}
}

View file

@ -104,13 +104,15 @@ export default class DhCharacter extends DhCreature {
trait: 'strength'
},
damage: {
main: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
custom: {
enabled: true,
formula: '@profd4'
parts: {
hitPoints: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
custom: {
enabled: true,
formula: '@profd4'
}
}
}
}
@ -836,7 +838,7 @@ export default class DhCharacter extends DhCreature {
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)
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"
const initialAttack = {
type: source.system.attack.damage?.main?.type?.toSorted(),
value: getFormula(source.system.attack.damage?.main?.value)
type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(),
value: getFormula(source.system.attack.damage?.parts.hitPoints?.value)
};
// Update damage of base attack.
try {
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']) {
const data = damage.main[property];
const data = damage.parts.hitPoints[property];
const previousFormula = getFormula(data);
const value = calculateAdjustedDamage(previousFormula, 'attack', damageMeta);
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
for (const action of Object.values(item.system.actions)) {
if (!action.damage?.main) continue;
if (!action.damage?.parts.hitPoints) continue;
try {
// Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead
const result = [];
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 isActuallyAttack =
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
*/
function getFormula(data) {

View file

@ -1,5 +1,4 @@
import { triggerChatRollFx } from '../../helpers/utils.mjs';
import { ChatDamageData } from './chatDamageData.mjs';
const fields = foundry.data.fields;
@ -42,7 +41,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
hasEffect: new fields.BooleanField({ initial: false }),
hasSave: new fields.BooleanField({ initial: false }),
hasTarget: new fields.BooleanField({ initial: false }),
needsReload: new fields.BooleanField({ initial: false }),
isDirect: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(),
source: new fields.SchemaField({
@ -51,7 +49,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
originItem: originItemField(),
action: new fields.StringField()
}),
damage: new fields.EmbeddedDataField(ChatDamageData),
damage: new fields.ObjectField(),
damageOptions: new fields.ObjectField(),
costs: new fields.ArrayField(new fields.ObjectField()),
successConsumed: new fields.BooleanField({ initial: false })
@ -134,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() {
if (!this.damage.active) return;
if (!this.damage) return;
const rerolls = [];
const update = { system: { damage: { main: null, resources: _replace({}) } } };
if (this.damage.main) {
const reroll = await this.damage.main.reroll();
rerolls.push(reroll);
update.system.damage.main = reroll.toJSON();
}
const update = { system: { damage: {} } };
for (const partKey in this.damage) {
const part = this.damage[partKey];
const testRoll = Roll.fromData(part.parts[0].roll);
const rerolled = await testRoll.reroll();
rerolls.push(rerolled);
for (const key of Object.keys(this.damage.resources)) {
const reroll = await this.damage.resources[key].reroll();
rerolls.push(reroll);
update.system.damage.resources[key] = reroll.toJSON();
if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
const partData = update.system.damage[partKey].parts[0];
update.system.damage[partKey].total = rerolled.total;
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);
@ -193,43 +198,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
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() {
const targets =
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';
export default class DhCountdowns extends foundry.abstract.DataModel {
@ -15,36 +14,19 @@ export default class DhCountdowns extends foundry.abstract.DataModel {
};
}
/** @inheritdoc */
_initialize(options) {
super._initialize(options);
for (const [id, countdown] of Object.entries(this.countdowns)) {
countdown.id = id;
}
}
async handleChange() {
handleChange() {
const previousCountdowns = foundry.ui.countdowns.previousCountdownData;
const changedCountdowns = Object.entries(this.countdowns).reduce((acc, [key, countdown]) => {
const previous = previousCountdowns[key];
const currentChanged = !previous || (previous.progress.current !== countdown.progress.current);
if (currentChanged && previous?.progress.start === countdown.progress.start) {
const previousCountdown = previousCountdowns[key];
if (!previousCountdown || (previousCountdown.progress.current !== countdown.progress.current)) {
acc.push(key);
}
return acc;
}, []);
// Re-render countdowns applications. When the change is due to an actual update, resync the editor
if (!foundry.utils.equals(previousCountdowns, this.countdowns)) {
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 });
for (const countdownKey of changedCountdowns)
foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey);
}
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 */
static migrateData(source) {
if (source.progress.max) {
@ -196,29 +170,4 @@ export class DhCountdown extends foundry.abstract.DataModel {
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 */
constructor(options, context = {}) {
super({
main: new fields.EmbeddedDataField(DHDamageData, { nullable: true }),
resources: new IterableTypedObjectField(DHResourceData)
}, options, context);
const damageFields = {
parts: new IterableTypedObjectField(DHDamageData),
includeBase: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
}),
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }),
groupAttack: new fields.StringField({
choices: CONFIG.DH.GENERAL.groupAttackRange,
blank: true,
label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label'
})
};
super(damageFields, options, context);
}
/**
@ -31,23 +41,25 @@ export default class DamageField extends fields.SchemaField {
this.hasRoll &&
DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id &&
!force
) {
)
return;
}
const damageFormula = this.damage.main ?
DamageField.formatFormulas.call(this, [this.damage.main], config)[0] : null;
const resourceFormulas = DamageField.formatFormulas.call(this, this.damage.resources, config);
let formulas = this.damage.parts.map(p => ({
formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor),
damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type,
applyTo: p.applyTo
}));
if (!damageFormula && !resourceFormulas.length) return false;
if (!formulas.length) return false;
formulas = DamageField.formatFormulas.call(this, formulas, config);
messageId = config.message?._id ?? messageId;
const message = game.messages.get(messageId);
const damageConfig = {
dialog: {},
...config,
damageFormula,
resourceFormulas,
roll: formulas,
data: this.getRollData(),
isCritical: Boolean(message?.system.roll?.isCritical)
};
@ -80,7 +92,7 @@ export default class DamageField extends fields.SchemaField {
const targetDamage = [];
const damagePromises = [];
for (const target of targets) {
for (let target of targets) {
const actor = foundry.utils.fromUuidSync(target.actorId);
if (!actor) continue;
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 }))
);
else {
const configDamage = config.damage.clone();
configDamage.main &&= configDamage.main.toJSON();
if (configDamage.main) {
const multiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
const takenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
configDamage.main.total = Math.ceil(configDamage.main.total * multiplier * takenMultiplier);
const configDamage = foundry.utils.deepClone(config.damage);
const hpDamageMultiplier = config.actionActor?.system.rules?.attack?.damage?.hpDamageMultiplier ?? 1;
const hpDamageTakenMultiplier = actor.system.rules?.attack?.damage?.hpDamageTakenMultiplier;
if (configDamage.hitPoints) {
for (const part of configDamage.hitPoints.parts) {
part.total = Math.ceil(part.total * hpDamageMultiplier * hpDamageTakenMultiplier);
}
}
damagePromises.push(
@ -170,19 +183,13 @@ export default class DamageField extends fields.SchemaField {
/**
* Prepare formulas for Damage Roll
* 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
* @returns
*/
static formatFormulas(damageData, data) {
const formulas = damageData.map(x => ({
formula: DamageField.getFormulaValue.call(this, x, data).getFormula(this.actor),
damageTypes: x.type ?? new Set(),
applyTo: x.applyTo
}));
static formatFormulas(formulas, data) {
const formattedFormulas = [];
for (const formula of formulas) {
formulas.forEach(formula => {
if (isNaN(formula.formula))
formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data));
const same = formattedFormulas.find(
@ -190,8 +197,7 @@ export default class DamageField extends fields.SchemaField {
);
if (same) same.formula += ` + ${formula.formula}`;
else formattedFormulas.push(formula);
}
});
return formattedFormulas;
}
@ -269,18 +275,10 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
};
}
/**
* @returns {string} the formula associated with this damage field
*/
getFormula() {
if (this.custom.enabled) return this.custom.formula;
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`;
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;
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`,
bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : '';
return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`;
}
}
@ -288,7 +286,6 @@ export class DHResourceData extends foundry.abstract.DataModel {
/** @override */
static defineSchema() {
return {
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
applyTo: new fields.StringField({
choices: CONFIG.DH.GENERAL.healingTypes,
required: true,
@ -311,16 +308,7 @@ export class DHDamageData extends DHResourceData {
static defineSchema() {
return {
...super.defineSchema(),
includeBase: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label'
}),
direct: new fields.BooleanField({ initial: false, label: 'DAGGERHEART.CONFIG.DamageType.direct.name' }),
groupAttack: new fields.StringField({
choices: CONFIG.DH.GENERAL.groupAttackRange,
blank: true,
label: 'DAGGERHEART.ACTIONS.Settings.groupAttack.label'
}),
base: new fields.BooleanField({ initial: false, readonly: true, label: 'Base' }),
type: new fields.SetField(
new fields.StringField({
choices: CONFIG.DH.GENERAL.damageTypes,

View file

@ -238,7 +238,6 @@ export function ActionMixin(Base) {
result = this.parent;
} else {
result = await this.item.update({ [path]: updates }, options);
if (!result) return result;
}
return this.inCollection

View file

@ -49,10 +49,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
})
};
if (this.metadata.hasDescription) {
schema.description = new fields.HTMLField({ required: true, nullable: true });
schema.gmNotes = new fields.HTMLField({ required: true, nullable: true });
}
if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResource) {
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.
* @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) {
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' }
* @returns {Promise<string>}
*/
async getEnrichedDescription({ gmNotes = true } = {}) {
async getEnrichedDescription() {
if (!this.metadata.hasDescription) return '';
const { prefix, value, suffix } = await this.getDescriptionData();
let 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;
}
const fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n<hr>\n');
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this.parent,
relativeTo: this,
rollData: this.getRollData(),
secrets: this.parent.isOwner
});

View file

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

View file

@ -41,11 +41,6 @@ export default class DhAppearance extends foundry.abstract.DataModel {
choices: CONFIG.DH.GENERAL.fearDisplay,
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 }),
diceSoNice: new SchemaField({
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.button.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.label',
hint: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.hint'
}),
autoExpireActiveEffects: new fields.BooleanField({
required: true,
initial: true,

View file

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

View file

@ -4,8 +4,4 @@ export default class BaseRoll extends Roll {
/** @inheritdoc */
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 createRollInstance(config) {
return new this(undefined, config.data, config);
}
/** @inheritdoc */
static async buildEvaluate(roll, config = {}) {
if (config.dialog.configure === false) roll.constructFormulas(config);
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);
}
static async buildEvaluate(roll, config = {}, message = {}) {
if (config.dialog.configure === false) roll.constructFormula(config);
for (const roll of config.roll) await roll.roll.evaluate();
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) {
@ -51,10 +53,9 @@ export default class DamageRoll extends DHRoll {
const diceRolls = [];
if (game.modules.get('dice-so-nice')?.active) {
config.mute = true;
const pool = foundry.dice.terms.PoolTerm.fromRolls([
...(config.damage.main ? [config.damage.main] : []),
...Object.values(config.damage.resources)
]);
const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
);
diceRolls.push(Roll.fromTerms([pool]));
}
@ -65,14 +66,22 @@ export default class DamageRoll extends DHRoll {
await super.buildPost(roll, config, message);
if (config.source?.message) {
chatMessage.update({ 'system.damage': {
...config.damage.toObject(),
main: config.damage.main,
resources: config.damage.resources
}});
chatMessage.update({ 'system.damage': config.damage });
}
}
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) {
let formula, total;
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 changeKeys = [];
for (const damageType of this.options.damageFormula?.damageTypes?.values?.() ?? []) {
changeKeys.push(`system.bonuses.${type}.${damageType}`);
for (const roll of this.options.roll) {
for (const damageType of roll.damageTypes?.values?.() ?? []) {
changeKeys.push(`system.bonuses.${type}.${damageType}`);
}
}
const item = this.data.parent?.items?.get(this.options.source.item);
if (item) {
switch (item.type) {
@ -140,69 +151,62 @@ export default class DamageRoll extends DHRoll {
return changeKeys;
}
constructFormulas(config) {
return {
damageFormula: this.constructFormula(this.options.damageFormula, config, true),
resourceFormulas: this.options.resourceFormulas.map(x => this.constructFormula(x, config))
};
}
constructFormula(formulaData, config, isDamage) {
if (!formulaData) return null;
constructFormula(config) {
this.options.isCritical = config.isCritical;
formulaData.roll = new Roll(Roll.replaceFormulaData(formulaData.formula, config.data));
formulaData.roll.terms = Roll.parse(formulaData.roll.formula, config.data);
if (formulaData.extraFormula) {
formulaData.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(formulaData.extraFormula, this.options.data)
);
}
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);
for (const [index, part] of this.options.roll.entries()) {
const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id;
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
part.modifiers = this.applyBaseBonus(part);
this.addModifiers(part);
part.modifiers?.forEach(m => {
part.roll.terms.push(...this.formatModifier(m.value));
});
}
/* To Remove When Reaction System */
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(formulaData);
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
if (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];
for (const term of formulaData.roll.terms) {
for (const term of part.roll.terms) {
if (damageTypes.some(type => term instanceof type)) {
term.number *= config.damageOptions.groupAttack.numAttackers;
}
}
}
if (config.isCritical) {
const total = formulaData.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
if (config.isCritical && isHitpointPart) {
const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 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);
return formulaData;
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
part.roll._formula = this.constructor.getFormula(part.roll.terms);
}
return this.options.roll;
}
/* To Remove When Reaction System */
@ -295,4 +299,76 @@ export default class DamageRoll extends DHRoll {
config.modifiers = 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 { triggerChatRollFx } from '../helpers/utils.mjs';
import BaseRoll from './baseRoll.mjs';
export default class DHRoll extends BaseRoll {
export default class DHRoll extends Roll {
baseTerms = [];
constructor(formula, data = {}, options = {}) {
super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false }));
@ -41,10 +40,6 @@ export default class DHRoll extends BaseRoll {
return config;
}
static createRollInstance(config) {
return new this(config.roll.formula, config.data, config);
}
/**
* @param {Partial<RollConfig>} config
* @returns {Promise<RollConfig>}
@ -62,7 +57,7 @@ export default class DHRoll extends BaseRoll {
this.temporaryModifierBuilder(config);
let roll = this.createRollInstance(config);
let roll = new this(config.roll.formula, config.data, config);
if (config.dialog.configure !== false) {
// Open Roll Dialog
const DialogClass = config.dialog?.class ?? this.DefaultDialog;
@ -117,11 +112,7 @@ export default class DHRoll extends BaseRoll {
static async toMessage(roll, config) {
const item = config.data.parent?.items?.get?.(config.source.item) ?? null;
const actions = item ? [
...item.system.actions,
...(item.system.attack?.id === config.source.action ? [item.system.attack] : [])
] : [];
const action = actions.find(x => x.id === config.source.action);
const action = item ? item.system.actions.get(config.source.action) : null;
let actionDescription = null;
if (action?.chatDisplay) {
actionDescription = action
@ -133,14 +124,6 @@ export default class DHRoll extends BaseRoll {
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 needsReload = useReload ? await action?.handleReload?.() : false;
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: this.messageType,
@ -148,7 +131,7 @@ export default class DHRoll extends BaseRoll {
title: roll.title,
speaker: cls.getSpeaker({ actor: roll.data?.parent }),
sound: config.mute ? null : CONFIG.sounds.dice,
system: { ...config, actionDescription, needsReload },
system: { ...config, actionDescription },
rolls: [roll]
};
@ -170,17 +153,14 @@ export default class DHRoll extends BaseRoll {
if (!this._evaluated) return;
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 });
return foundry.applications.handlebars.renderTemplate(template, {
roll: this,
...chatData,
action: chatData.action,
parent: chatData.parent,
targetMode: chatData.targetMode,
areas: chatData.action?.areas,
metagamingSettings,
automationSettings
metagamingSettings
});
}

View file

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

View file

@ -1,6 +1,4 @@
import BaseDie from './baseDie.mjs';
export default class AdvantageDie extends BaseDie {
export default class AdvantageDie extends foundry.dice.terms.Die {
constructor(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 BaseDie {
export default class DisadvantageDie extends foundry.dice.terms.Die {
constructor(options) {
super(options);

View file

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

View file

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

View file

@ -622,11 +622,6 @@ export default class DhpActor extends Actor {
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) {
const { stressDamageReduction, disabledArmor, reduceSeverity, thresholdImmunities } =
this.system.rules.damageReduction;
@ -653,32 +648,35 @@ export default class DhpActor extends Actor {
return canUseArmor || canUseStress || hasReduceSeverity || hasThresholdImmunity;
}
async takeDamage(args, isDirect = false) {
args = this.#parseDamageArgs(args);
if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, args) === false) return null;
async takeDamage(damages, isDirect = false) {
if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, damages) === false) return null;
if (this.type === 'companion') {
await this.modifyResource([{ value: 1, key: 'stress' }]);
return;
}
const updates = args.resourceUpdates;
if (args.main) {
// todo: avoid side effects, but hook currently requires it
args.main.value = this.calculateDamage(args.main.value, args.main.damageTypes);
}
const updates = [];
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 (!updates.some(u => u.value) && !args.main) return;
if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null;
if (args.main) {
const hpDamage = {
value: this.convertDamageToThreshold(args.main.value),
damageTypes: new Set(args.main.damageTypes),
key: CONFIG.DH.GENERAL.healingTypes.hitPoints.id
};
if (!updates.length) return;
const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (hpDamage?.value) {
hpDamage.value = this.convertDamageToThreshold(hpDamage.value);
if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) {
const armorSlotResult = await this.owner.query(
'armorSlot',
@ -693,7 +691,7 @@ export default class DhpActor extends Actor {
);
if (armorSlotResult) {
const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult;
hpDamage.value = modifiedDamage;
updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
for (const armorChange of armorChanges) {
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 if (this.type === 'adversary') {
}
if (this.type === 'adversary') {
const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => {
return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value);
}, 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
const existing = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);
if (existing) {
existing.value += hpDamage.value;
existing.damageTypes = hpDamage.damageTypes;
} else {
updates.push(hpDamage);
if (
hpDamage.value &&
this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]
) {
hpDamage.value -= 1;
}
}
}
@ -735,10 +729,12 @@ export default class DhpActor extends Actor {
for (var result of results) resourceMap.addResources(result.updates);
resourceMap.updateResources();
}
for (const u of updates) {
u.value = u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value;
}
updates.forEach(
u =>
(u.value =
u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value)
);
await this.modifyResource(updates);
@ -747,36 +743,6 @@ export default class DhpActor extends Actor {
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) {
if (this.canResist(type, 'immunity')) return 0;
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;
}
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
* quick succession.

View file

@ -4,234 +4,199 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
#wide = false;
#bordered = false;
/** @inheritdoc */
async activate(element, options = {}) {
this.#wide = false;
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;
}
const { TextEditor } = foundry.applications.ux;
let html = options.html;
const key = element.dataset.tooltip?.match(/^#([\w-]+)#/)?.[1];
switch (key) {
case 'battlepoints':
return this.#activateBattlepoints(element, options);
case 'effect-display':
html = await this.#activateEffectDisplay(element, options);
break;
case 'item':
html = await this.#activateItem(element, options);
break;
case 'attack':
html = await this.#activateAttack(element, options);
break;
case 'shortRest':
case 'longRest':
html = await this.#activateRest(element, options);
break;
case 'advantage':
case 'disadvantage':
html = await this.#activateAdvantageDisadvantage(element, options);
break;
case 'deathMove':
html = await this.#activateDeathMove(element, options);
break;
if (element.dataset.tooltip?.startsWith('#battlepoints#')) {
this.#wide = true;
this.#bordered = true;
html = await this.getBattlepointHTML(element.dataset.combatId);
options.direction = this._determineItemTooltipDirection(element);
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));
});
return;
} else {
this.#wide = false;
this.#bordered = false;
}
if (element.dataset.tooltip === '#effect-display#') {
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
};
}
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;
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 });
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) {

View file

@ -48,8 +48,9 @@ export default class RegisterHandlebarsHelpers {
return formula;
}
static damageSymbols(damageData) {
const symbols = damageData.type.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon);
static damageSymbols(damageParts) {
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>`));
}

View file

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

View file

@ -7,8 +7,7 @@ export class Migration_2_5_2 extends MigrationHandlerBase {
async updateActiveEffectSource(effectSource, item) {
let shouldUpdate = false;
const newChanges = [];
const srdItem = item?._stats?.compendiumSource ?
const srdItem = item?._stats.compendiumSource ?
await foundry.utils.fromUuid(item?._stats.compendiumSource) :
null;
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 DhActor from "../../documents/actor.mjs";
*/
/**
/**
* The base class of an async migration.
* 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.
@ -23,16 +22,6 @@ export class MigrationHandlerBase {
return null;
}
/**
* Update a world actor
* @param {DhActor} actor
* @returns {Promise<object>}
* @protected
*/
async updateActorSource(actor) {
return null;
}
async migrate() {
// todo: handle more than just migrating effects. Right now this can only migrate effects
// NOTE: the preload is hardcoded, we should not hardcode it
@ -71,39 +60,10 @@ export class MigrationHandlerBase {
}
};
const updateActor = async actor => {
const actorUpdate = await this.updateActorSource(actor);
if (actorUpdate) {
batch.push({
action: 'update',
documentName: 'Actor',
updates: [actorUpdate]
});
}
const aeUpdates = [];
for (const actor of game.actors) {
for (const item of actor.items) {
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();
}
for (const item of game.items) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -128,15 +128,6 @@
"source": "Daggerheart SRD",
"page": 51,
"artist": ""
},
"resource": {
"type": "simple",
"value": 1,
"max": "1",
"recovery": null,
"progression": "decreasing",
"dieFaces": "d4",
"icon": "fa-solid fa-gun"
}
},
"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 {
gap: 4px;
.critical-chip {

View file

@ -7,12 +7,12 @@
.chat-log .chat-message {
background-image: url('../assets/parchments/dh-parchment-light.png');
.message-header .message-header-main .message-metadata,
.message-header .message-header-main .name {
.message-header .message-header-metadata .message-metadata,
.message-header .message-header-main .message-sub-header-container {
color: @dark;
}
.message-header .message-header-main h4 {
.message-header .message-header-main .message-sub-header-container h4 {
color: @dark-blue;
}
@ -42,12 +42,27 @@
.message-header {
display: flex;
gap: 8px;
gap: 4px;
padding: 8px;
align-items: center;
.portrait {
flex: 0 0 auto;
.message-header-metadata {
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 {
border-radius: 50%;
width: 40px;
@ -55,60 +70,20 @@
object-fit: cover;
object-position: top center;
}
}
.message-header-main {
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;
.message-sub-header-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
color: @beige;
gap: 4px;
align-items: baseline;
line-height: 1;
.name {
flex: 1;
}
.whisper-to {
color: @color-text-subtle;
flex: 0;
white-space: nowrap;
h4 {
font-size: var(--font-size-16);
font-weight: bold;
margin-bottom: 0;
font-family: @font-subtitle;
color: @golden;
}
}
}

View file

@ -595,37 +595,7 @@
margin-top: 4px;
color: light-dark(#14142599, #efe6d850);
font-size: var(--font-size-12);
padding-left: 16px;
}
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;
padding-left: 3px;
}
secret-block {
@ -896,8 +866,4 @@
right: 2px;
}
}
.gm-notes {
font-style: italic;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -6,80 +6,11 @@
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
padding: 0;
margin: 0;
overflow-y: hidden !important;
padding-top: 10px;
.description-section {
flex: 1;
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;
}
prose-mirror.active + .artist-attribution {
display: none;
}
}
}

View file

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

View file

@ -1,6 +1,4 @@
.application.sheet.daggerheart.dh-style.beastform {
--portrait-size: 130px;
.settings.tab {
.advantage-on-section {
display: flex;
@ -11,11 +9,4 @@
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';
.application.sheet.daggerheart.dh-style.feature {
--portrait-size: 130px;
.item-sheet-header {
display: flex;
.profile {
height: 130px;
width: 130px;
}
}
section.tab {
height: 400px;
overflow-y: auto;
}
}

View file

@ -1,6 +1,6 @@
@import './item-sheet-shared.less';
@import './beastform.less';
@import './class.less';
@import './domain-card.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 {
.attribution-header-label {
display: none;
@ -14,22 +14,4 @@
button.plain.inline-control {
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;
flex: 1 1 calc(50% - 5px);
span {
font-family: @font-body;
font-weight: 700;
}
&:nth-last-child(1):nth-child(odd) {
flex-basis: 100%;
}

View file

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

View file

@ -271,28 +271,6 @@
}
}
.roll-reload-container {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
.reload-warning {
display: flex;
align-items: center;
border-radius: 5px;
width: fit-content;
gap: 5px;
cursor: pointer;
padding: 5px;
transition: all 0.3s ease;
color: @beige;
background: @red-40;
outline: 1px solid @red;
}
}
.roll-part-extra {
display: flex;
justify-content: center;
@ -644,19 +622,15 @@
.roll-buttons {
display: flex;
flex-wrap: wrap;
gap: 5px;
width: 100%;
margin-top: 8px;
button {
height: 36px;
flex: 1 1 calc(50% - 5px);
font-family: @font-body;
font-weight: 700;
height: 32px;
flex: 1;
&:nth-last-child(1):nth-child(odd) {
flex-basis: 100%;
&.end-button {
flex: 0;
}
}
}

View file

@ -32,7 +32,7 @@
background: var(--background);
border-radius: 4px;
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) {
@ -41,7 +41,6 @@
&:hover::before {
opacity: 1;
transition: opacity var(--ui-fade-duration);
}
#ui-right:has(#effects-display .effect-container) & {

View file

@ -1,221 +1,119 @@
:root {
--hotbar-size: 60px;
}
#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;
}
}
--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;
}
#resources {
position: static;
min-height: calc(var(--header-height) + 4rem);
min-width: 4rem;
color: #d3d3d3;
pointer-events: all;
padding: var(--spacer-8);
max-width: 540px;
min-width: 100px;
&::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);
transition: var(--fear-animation);
header,
.controls,
.window-resize-handle {
transition: var(--fear-animation);
}
&:hover::before {
opacity: 1;
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 {
.window-content {
padding: 0.5rem;
#resource-fear {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem 0.25rem;
.fear-token {
font-size: var(--font-size-16);
border: 1.5px double light-dark(@dark-15, @dark-golden-80);
flex-wrap: wrap;
i {
font-size: var(--font-size-18);
border: 1px solid rgba(0, 0, 0, 0.5);
border-radius: 50%;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
width: 2.5rem;
width: 3rem;
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;
text-shadow: none;
&.inactive {
filter: grayscale(1) !important;
opacity: 0.5;
}
}
}
.resize-handle {
position: absolute;
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,
.resource-bar {
border: 2px solid rgb(153 122 79);
background-color: rgb(24 22 46);
}
.label {
margin: auto 0;
z-index: 2;
height: auto;
text-align: center;
font-size: var(--font-size-18);
color: @beige;
}
}
.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 {
.controls {
display: flex;
align-self: center;
border-radius: 50%;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: var(--font-size-20);
cursor: pointer;
transition: box-shadow 0.15s ease;
&: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,
ol {
margin: 0.5rem 0;
margin: 1rem 0;
padding: 0 0 0 1.25rem;
li {

View file

@ -5,7 +5,7 @@
"version": "2.5.4",
"compatibility": {
"minimum": "14.364",
"verified": "14.365",
"verified": "14.364",
"maximum": "14"
},
"url": "https://github.com/Foundryborne/daggerheart",
@ -256,34 +256,34 @@
},
"Item": {
"ancestry": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"community": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"class": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"subclass": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"feature": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"domainCard": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"loot": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"consumable": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"weapon": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"armor": {
"htmlFields": ["description", "gmNotes"]
"htmlFields": ["description"]
},
"beastform": {}
},

View file

@ -9,7 +9,7 @@
{{/if}}
<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.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.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>

View file

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

View file

@ -1,7 +1,7 @@
<fieldset class="action-category">
<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>
<div class="action-category-data open">
<fieldset>

View file

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

View file

@ -1,36 +1,25 @@
{{#if damage.main}}
{{> damage roll=damage.main label=(localize "DAGGERHEART.GENERAL.damage") memberKey=key}}
{{/if}}
{{#each damage.resources as |roll key|}}
{{> damage roll=roll label=(localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")) memberKey=../key isResource="true" }}
{{/each}}
{{#*inline "damage"}}
{{#each damage as |damage key|}}
<div class="roll-data {{#if isCritical}}critical{{/if}}">
<div class="duality-label">
<span>{{label}}:</span>
<span>{{roll.total}}</span>
<span>{{localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")}}:</span>
<span>{{damage.total}}</span>
</div>
<div class="roll-dice-container">
{{#each roll.dice}}
{{#each results as |result index|}}
{{#if result.active}}
<a class="roll-dice" data-action="rerollDamageDice" data-member-key="{{../../memberKey}}" data-damage-key="{{@../../key}}" {{#if ../../isResource}}data-is-resource="true"{{/if}} data-dice-index="{{@../key}}" data-result-index="{{index}}">
<span class="dice-label">{{result.result}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" ../denomination ".svg"}}" />
</a>
{{/if}}
{{#each damage.parts as |part|}}
<div class="roll-dice-container">
{{#each part.dice as |dice index|}}
<a class="roll-dice" data-action="rerollDamageDice" data-member-key="{{../../../key}}" data-damage-key="{{@../../key}}" data-part="{{@../index}}" data-dice="{{index}}">
<span class="dice-label">{{dice.total}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" dice.dice ".svg"}}" />
</a>
{{#unless @last}}
<span class="roll-operator">+</span>
{{/unless}}
{{/each}}
{{#unless @last}}
<span class="roll-operator">+</span>
{{/unless}}
{{/each}}
{{#if roll.modifierTotal}}
{{#if roll.dice.length}}<span class="roll-operator">{{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}}</span>{{/if}}
<span class="roll-value">{{positive roll.modifierTotal}}</span>
{{/if}}
</div>
{{#if part.modifierTotal}}
{{#if part.dice.length}}<span class="roll-operator">{{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}}</span>{{/if}}
<span class="roll-value">{{positive part.modifierTotal}}</span>
{{/if}}
</div>
{{/each}}
</div>
{{/inline}}
{{/each}}

View file

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

View file

@ -64,10 +64,7 @@
{{#if roll}}
<div class="roll-data {{#if roll.withHope}}hope{{else if roll.withFear}}fear{{else}}critical{{/if}}">
<div class="duality-label">
{{roll.total}}
{{#if roll.isCritical}}{{roll.totalLabel}}{{else}}{{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}{{/if}}
</div>
<div class="duality-label">{{roll.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}</div>
<div class="roll-dice-container">
<a class="roll-dice" data-action="rerollDice" data-member="{{@root.partId}}" data-dice-type="hope">
<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}}>
<img src="systems/daggerheart/assets/icons/dice/hope/d20.svg" />
</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>
</a>
{{#if damage}}
<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>
</span>
{{#if damage.active}}
{{#if damage}}
{{#if useCritDamage}}
{{> "systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs" damage=critDamage isCritical=true }}
{{else}}

View file

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

View file

@ -18,7 +18,6 @@
<p class="hint">{{localize (concat "DAGGERHEART.SETTINGS.Automation.FIELDS.roll." field.name ".hint")}}</p>
</div>
{{/each}}
{{formGroup settingFields.schema.fields.reload value=settingFields.reload localize=true}}
</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.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.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}}
{{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}}

View file

@ -22,5 +22,5 @@
</div>
{{formGroup systemFields.criticalThreshold value=document._source.system.criticalThreshold label="DAGGERHEART.ACTIONS.Settings.criticalThreshold" name="system.criticalThreshold" localize=true}}
</fieldset>
{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields 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>

View file

@ -1,13 +1,7 @@
{{#if (eq item.system.resource.type 'simple')}}
<div class="item-resource">
{{#if item.system.hasReload}}
<a data-action="toggleItemReload" title="{{localize (ifThen item.system.needsReload 'DAGGERHEART.GENERAL.Resource.unloaded' 'DAGGERHEART.GENERAL.Resource.loaded')}}">
<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}}
<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}}" />
</div>
{{else if (eq item.system.resource.type 'diceValue')}}
<div class="item-resources">

View file

@ -1,25 +1,11 @@
<section
class='tab {{tabs.description.cssClass}} {{tabs.description.id}}'
data-tab='{{tabs.description.id}}'
data-group='{{tabs.description.group}}'
>
<div class="description-section">
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
{{#if (and systemFields.gmNotes @root.user.isGM)}}
<section class="gm-notes-section">
{{#if enrichedGMNotes}}
<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
class='tab {{tabs.description.cssClass}} {{tabs.description.id}}'
data-tab='{{tabs.description.id}}'
data-group='{{tabs.description.group}}'
>
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
{{#if (and showAttribution document.system.attribution.artist)}}
<label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label>
{{/if}}
</section>

View file

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

View file

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

View file

@ -1,21 +1,18 @@
<div class="roll-buttons">
{{#if (eq automationSettings.reload 'button')}}
<button class="roll-reload-check" {{#if needsReload}}disabled{{/if}}>{{localize "DAGGERHEART.ACTIONS.Reload.checkReload"}}</button>
{{/if}}
{{#if areas.length}}<button class="action-areas end-button"><i class="fa-solid fa-crosshairs"></i></button>{{/if}}
{{#if hasDamage}}
{{#if damage.active}}
{{#unless (empty damage)}}
<button class="duality-action damage-button">{{localize "DAGGERHEART.UI.Chat.damageRoll.dealDamage"}}</button>
{{else}}
<button class="duality-action duality-action-damage">{{localize "DAGGERHEART.UI.Chat.attackRoll.rollDamage"}}</button>
{{/if}}
{{/unless}}
{{/if}}
{{#if hasHealing}}
{{#if damage.active}}
{{#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>
{{/if}}
{{/unless}}
{{/if}}
{{#if (and hasEffect)}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}}
</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>
{{#if hasHealing}}
@ -10,79 +10,60 @@
</div>
<div class="roll-part-extra on-reduced">
<div class="wrapper">
{{#if damage.main}}
{{> formula roll=damage.main label=(localize "DAGGERHEART.GENERAL.damage") }}
{{/if}}
{{#each damage.resources as | roll index | }}
{{> formula roll=roll label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll'))) }}
{{#each damage as | roll index | }}
<div class="roll-formula">{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}: {{total}}</div>
{{/each}}
</div>
</div>
<div class="roll-part-content dice-result">
<div class="dice-tooltip">
<div class="wrapper">
{{#if damage.main}}
{{> damage label=(localize "DAGGERHEART.GENERAL.damage") roll=damage.main isDirect=isDirect }}
{{/if}}
{{#each damage.resources as | roll index | }}
{{> damage
label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')))
roll=roll
isResource=true
}}
{{#each damage as | roll index | }}
<fieldset>
<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}}
</legend>
{{#each roll.parts}}
{{#if (and (not @root.hasHealing) damageTypes.length)}}
<label class="roll-part-header"><span>
{{#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}}
</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}}
</div>

View file

@ -1,16 +1,5 @@
<div class="chat-roll">
<div class="roll-part-title"><span>{{title}}</span></div>
{{#if (eq action.type 'attack')}}
{{#if needsReload}}
<div class="roll-reload-container">
{{#if needsReload}}
<p class='reload-warning'>{{localize "DAGGERHEART.ACTIONS.Reload.reloadRequired"}}</p>
{{/if}}
</div>
{{/if}}
{{/if}}
{{#if actionDescription}}{{> 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs'}}{{/if}}
{{#if hasRoll}}
<div class="roll-part-header"><span>{{localize "Result"}}</span></div>

View file

@ -1,48 +1,16 @@
<div>
<div id="resource-fear" class="fear-tracker {{#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}}
<div id="resource-fear" class="{{#if isGM}}isGM{{/if}}">
{{#if (eq display 'token')}}
<div class="fear-tokens">
{{#times max}}
<a
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>
{{#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>
{{/times}}
{{/if}}
{{#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}}%">
<progress
class='progress-bar stress-color'
value='{{current}}'
max='{{max}}'
></progress>
<h4 class="label">{{current}} / {{max}}</h4>
<span>{{current}}/{{max}}</span>
</div>
{{#if isGM}}
<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 isGM}}<div class="controls control-plus {{#if (gte current max)}} disabled{{/if}}" data-increment="+1" data-action="increaseFear">+</div>{{/if}}
{{/if}}
</div>
</div>

View file

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

View file

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