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
110 changed files with 1374 additions and 2983 deletions

22
daggerheart.d.ts vendored
View file

@ -11,11 +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 DhAutomation from './module/data/settings/Automation.mjs';
import FearTracker from './module/applications/ui/fearTracker.mjs';
import DhCountdowns from './module/applications/ui/countdowns.mjs';
import DhEffectsDisplay from './module/applications/ui/effectsDisplay.mjs';
// Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such
@ -108,20 +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;
}
}
// Add to global ui object
declare module '@client/ui.mjs' {
const countdowns: DhCountdowns;
const resources: FearTracker;
const effectsDisplay: DhEffectsDisplay;
}

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,15 +124,6 @@
"damageOnSave": "Damage on Save",
"useDefaultItemValues": "Use default Item values"
},
"Reload": {
"checkReload": "Check Reload",
"reloadRequired": "Reload Required!",
"notRolled": "Not Rolled",
"checkFailed": "Check Failed",
"checkPassed": "Check Passed",
"rerollConfirmationTitle": "Reroll Reload Check",
"rerollConfirmationText": "Are you sure you want to reroll the reload check?"
},
"RollField": {
"diceRolling": {
"compare": "Should be",
@ -1331,11 +1320,6 @@
"short": "V. Far"
}
},
"ReloadChoices": {
"off": { "label": "Don't Use" },
"manual": { "label": "Manual" },
"auto": { "label": "Automatic" }
},
"RollTypes": {
"trait": {
"name": "Trait"
@ -2235,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",
@ -2501,6 +2483,7 @@
"reroll": "Reroll",
"rerolled": "Rerolled",
"rerollThing": "Reroll {thing}",
"resource": "Resource",
"result": {
"single": "Result",
"plural": "Results"
@ -2567,9 +2550,6 @@
},
"identifier": {
"label": "Identifier"
},
"gmNotes": {
"label": "GM Notes"
}
},
"Ancestry": {
@ -2585,9 +2565,6 @@
"severe": "Severe Threshold"
}
},
"Base": {
"addGMNote": "Add GM Note"
},
"Beastform": {
"FIELDS": {
"beastformType": { "label": "Beastform Type" },
@ -2704,9 +2681,6 @@
"displayFear": {
"label": "Display Fear"
},
"fearPosition": {
"label": "Fear Position"
},
"displayCountdownUI": {
"label": "Display Countdown UI"
},
@ -2758,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": {
@ -2823,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 automatically roll reload checks or wait for the manual press of a button in chat"
},
"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."
@ -3288,9 +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",
"comboDiceOnlyTwoDiceError": "Combo dice functionality only works on a single pair of two dice",
"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."
@ -3331,6 +3292,7 @@
"rangeAndTarget": "Range & Target",
"dragApplyEffect": "Drag effect to apply it to an actor",
"appliedEvenIfSuccessful": "Applied even if save succeeded",
"diceIsRerolled": "The dice has been rerolled (x{times})",
"pendingSaves": "Pending Reaction Rolls",
"openSheetSettings": "Open Settings",
"compendiumBrowser": "Compendium Browser",

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;
@ -541,9 +541,16 @@ 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,35 +574,81 @@ 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);
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) {
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);
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;
}
}
@ -600,7 +657,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
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({}, {
}).toFormGroup(
{},
{
name: 'type',
localize: true,
nameAttr: 'value',
labelAttr: 'label'
}).outerHTML;
}
).outerHTML;
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
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: [
foundry.utils.mergeObject(
{
action: 'ok',
label: 'Confirm',
icon: 'fas fa-check',
default: true,
callback
}
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

@ -110,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] });
@ -126,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);
@ -152,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() {
@ -182,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) {
@ -246,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-type]');
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];
@ -267,8 +286,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
isReaction: message.system.roll.options.actionType === 'reaction'
}
});
rerollDice.results[rerollDice.results.length - 1].rerolled = true;
await message.update({
rolls: [message.system.roll.toJSON()]
});
@ -280,27 +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)
ui.resources.handleOffset();
}
async onRollReloadCheck(_event, messageData) {
const message = game.messages.get(messageData._id);
if (message.system.reloadCheckValue) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: _loc('DAGGERHEART.ACTIONS.Reload.rerollConfirmationTitle')
},
content: _loc('DAGGERHEART.ACTIONS.Reload.rerollConfirmationText')
});
if (!confirmed) return;
}
const { rollValue } = await message.system.action.handleReload?.({ awaitRoll: true });
await message.update({ 'system.reloadCheckValue': rollValue });
}
}

View file

@ -153,6 +153,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
action: socketEvent.Refresh,
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

@ -130,7 +130,5 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
if (options?.force) {
document.getElementById('ui-right-column-1')?.appendChild(this.element);
}
ui.resources.handleOffset();
}
}

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) this.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 =
async _preFirstRender(context, options) {
options.position =
game.user.getFlag(CONFIG.DH.id, 'app.resources.position') ?? FearTracker.DEFAULT_OPTIONS.position;
this.setPosition(position);
break;
}
}
/** @override */
@ -126,51 +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);
}
handleOffset() {
const fearTracker = document.getElementById('resources');
const hotbar = document.getElementById('hotbar');
const countdowns = document.getElementById('countdowns');
if (!fearTracker) return;
let offset = Math.max(0, Number(hotbar.style.getPropertyValue('--offset').replace(/px$/, '')) || 0);
offset -= 13;
// If the countdowns overlaps the fear, offset some more. Countdown based offsets only need to be halved (due to centering)
if (this.fearPosition === 'topCenter') {
const effectsDisplay = document.getElementById('effects-display');
const top = document.getElementById('ui-top');
// Logic derived from the internal code's ChatLog#offsetHotbar(). Since the sidebar might be mid animation, we can't really check directly.
const { uiScale } = game.settings.get('core', 'uiConfig');
const sidebarWidth = (348 * ui.sidebar.expanded) / uiScale;
const countdownsWidth = countdowns.getBoundingClientRect().width;
const effectsWidth = effectsDisplay.getBoundingClientRect().width;
const rightWidth = sidebarWidth + countdownsWidth + 16 + (effectsWidth ? 62 : 0);
const countdownLeftEdge = window.innerWidth - rightWidth - 16; // w/ extra padding
const topBounds = top.getBoundingClientRect();
const fearTrackerWidth = fearTracker.clientWidth;
const currentRightEdge = topBounds.right - (topBounds.width - fearTrackerWidth) / 2;
const countdownShift = countdownLeftEdge < currentRightEdge ? countdownLeftEdge - currentRightEdge : null
offset = Math.min(offset, countdownShift ?? Infinity);
}
fearTracker.style.setProperty('--offset', `${offset}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);
@ -192,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'
},
manual: {
id: 'manual',
label: 'DAGGERHEART.CONFIG.ReloadChoices.manual.label'
},
auto: {
id: 'auto',
label: 'DAGGERHEART.CONFIG.ReloadChoices.auto.label'
}
};

View file

@ -12,7 +12,6 @@ import * as HOOKS from './hooksConfig.mjs';
import * as TRIGGER from './triggerConfig.mjs';
import * as 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

@ -1,23 +0,0 @@
import { DHDamageData, DHResourceData } from '../fields/action/damageField.mjs';
declare module './baseAction.mjs' {
export default interface DHBaseAction {
_id: string;
systemPath: string;
type?: string;
baseAction: boolean;
name?: string;
description: string;
img?: string;
chatDisplay: boolean;
originItem: object;
actionType: string;
targetUuid?: string;
damage: {
main: DHDamageData;
/** An iterable record of items (todo: type the iterable record) */
resources: Record<string, DHResourceData>;
}
}
}

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 needsReload = roll.total === 1;
if (needsReload) {
this.item.update({ 'system.resource.value': 0 });
}
return { needsReload, rollValue: roll.total };
}
/**
* Generate a localized label array for this item subtype.
* @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) || Object.keys(this.damage?.resources ?? {}).length);
return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing';
}
get hasHealing() {
return this.type === 'healing' && Object.keys(this.damage?.resources ?? {}).length;
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,7 +84,8 @@ export default class DhpAdversary extends DhCreature {
type: 'attack'
},
damage: {
main: {
parts: {
hitPoints: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
@ -93,6 +94,7 @@ export default class DhpAdversary extends DhCreature {
}
}
}
}
}),
experiences: new fields.TypedObjectField(
new fields.SchemaField({

View file

@ -104,7 +104,8 @@ export default class DhCharacter extends DhCreature {
trait: 'strength'
},
damage: {
main: {
parts: {
hitPoints: {
type: ['physical'],
applyTo: 'hitPoints',
value: {
@ -116,6 +117,7 @@ export default class DhCharacter extends DhCreature {
}
}
}
}
}),
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
@ -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 }),
reloadCheckValue: new fields.NumberField({ integer: true, nullable: true, initial: null }),
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 })
@ -76,14 +74,10 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
return fromUuidSync(this.source.actor);
}
get item() {
get actionItem() {
const actionActor = this.actionActor;
if (!actionActor || !this.source.item) return null;
return actionActor.items.get(this.source.item);
}
get actionItem() {
switch (this.source.originItem.type) {
case CONFIG.DH.ITEM.originItemType.restMove:
const restMoves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves;
@ -91,18 +85,11 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
this.source.originItem.actionIndex
];
default:
return this.item?.system.actionsList?.find(a => a.id === this.source.action);
const item = actionActor.items.get(this.source.item);
return item ? item.system.actionsList?.find(a => a.id === this.source.action) : null;
}
}
get hasReload() {
return this.item?.system.hasReload;
}
get reloadCheckFailed() {
return this.reloadCheckValue === 1;
}
get action() {
const { actionActor, actionItem: itemAction } = this;
if (!this.source.action) return null;
@ -145,21 +132,28 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
});
}
/* TODO: Change how damage data is stored somehow to enable better rerolling */
async getRerolledDamage() {
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);
@ -204,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,57 +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];
const rerolledResult = await rerollDice.rerollResult(resultIndex);
await reroll._evaluate();
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

@ -6,24 +6,27 @@ export default class IterableTypedObjectField extends foundry.data.fields.TypedO
#elementClass;
/** Initializes an object with an iterator, where foundry.utils.getType() returns "Object" */
/** Initializes an object with an iterator. This modifies the prototype instead of */
initialize(values) {
const object = {};
const object = Object.create(IterableObjectPrototype);
for (const [key, value] of Object.entries(values)) {
object[key] = new this.#elementClass(value);
}
object[Symbol.iterator] = function* () {
for (const value of Object.values(this)) {
yield value;
}
}
Object.defineProperties(object, {
map: {
value: function (func) {
return Array.from(this, func);
}
}
});
return object;
}
}
/**
* The prototype of an iterable object.
* This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown".
*/
const IterableObjectPrototype = {
[Symbol.iterator]: function* () {
for (const value of Object.values(this)) {
yield value;
}
},
map: function (func) {
return Array.from(this, func);
}
};

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,7 +67,8 @@ export default class DHWeapon extends AttachableItem {
type: 'attack'
},
damage: {
main: {
parts: {
hitPoints: {
type: ['physical'],
value: {
multiplier: 'prof',
@ -76,6 +77,7 @@ export default class DHWeapon extends AttachableItem {
}
}
}
}
}),
rules: new fields.SchemaField({
attack: new fields.SchemaField({
@ -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(' | ');
@ -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' }),
@ -123,12 +118,10 @@ export default class DhAppearance extends foundry.abstract.DataModel {
/** Invoked by the setting when data changes */
handleChange() {
if (this.displayFear) {
if (ui.resources) {
if (this.displayFear === 'hide') {
ui.resources.close({ allowed: true });
} else {
ui.resources.render({ force: true });
ui.resources.handleOffset();
if (this.displayFear === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({ force: true });
}
}

View file

@ -196,13 +196,6 @@ export default class DhAutomation extends foundry.abstract.DataModel {
})
})
}),
reload: new fields.StringField({
required: true,
choices: CONFIG.DH.SETTINGS.reloadChoices,
initial: CONFIG.DH.SETTINGS.reloadChoices.manual.id,
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.label',
hint: 'DAGGERHEART.SETTINGS.Automation.FIELDS.reload.hint'
}),
autoExpireActiveEffects: new fields.BooleanField({
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,5 +4,4 @@ export { default as DamageRoll } from './damageRoll.mjs';
export { default as DHRoll } from './dhRoll.mjs';
export { default as DualityRoll } from './dualityRoll.mjs';
export { default as FateRoll } from './fateRoll.mjs';
export { BaseDie } from './die/_module.mjs';
export { diceTypes } from './die/_module.mjs';

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,9 +130,11 @@ 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?.() ?? []) {
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) {
@ -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(config) {
this.options.isCritical = config.isCritical;
for (const [index, part] of this.options.roll.entries()) {
const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id;
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
part.modifiers = this.applyBaseBonus(part);
this.addModifiers(part);
part.modifiers?.forEach(m => {
part.roll.terms.push(...this.formatModifier(m.value));
});
}
constructFormula(formulaData, config, isDamage) {
if (!formulaData) return null;
this.options.isCritical = config.isCritical;
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
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(
if (part.extraFormula) {
part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(formulaData.extraFormula, this.options.data)
...this.constructor.parse(part.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);
}
/* 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 (config.damageOptions.groupAttack?.numAttackers > 1) {
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);
/* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
return formulaData;
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 reloadResult = useReload ? await action?.handleReload?.() : {};
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: this.messageType,
@ -148,11 +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,
reloadCheckValue: reloadResult.rollValue
},
system: { ...config, actionDescription },
rolls: [roll]
};
@ -174,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,193 +0,0 @@
import { adjustDice, triggerChatRollFx } from '../../helpers/utils.mjs';
export default class BaseDie extends foundry.dice.terms.Die {
static MODIFIERS = {
...foundry.dice.terms.Die.MODIFIERS,
cc: 'compoundComboDice',
c: 'comboDice'
};
async rerollResult(resultToReroll) {
const resultIndex = Number(resultToReroll);
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);
if (['c', 'cc'].some(x => this.modifiers.includes(x))) {
await this.handleComboDiceReroll(resultIndex, result);
}
rerolledResult.rerolled = true;
return rerolledResult;
}
/** @inheritDoc */
getResultCSS(result) {
// Accomodating ComboDie as a result can have a different denomination than the die as a whole
const css = super.getResultCSS(result);
const idx = css.findIndex(c => /d\d+/.test(c));
css[idx] = result.denomination ?? this.denomination;
return css;
}
/* -------------------------------------------- */
/* Modifier Logic */
/* -------------------------------------------- */
async compoundComboDice() {
return this.handleComboDice({ maxIncreasesDiceSize: true});
}
async comboDice() {
return this.handleComboDice({ maxIncreasesDiceSize: false });
}
async handleComboDice(maxIncreasesDiceSize) {
/* ComboDice only works with exactly two dice and both have to be the same denomination */
if (this.number !== 2) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.comboDiceOnlyTwoDiceError'));
return false;
}
return this.rollComboDice(maxIncreasesDiceSize);
}
async rollComboDice(options) {
const { maxIncreasesDiceSize, rerollStartIndex } = options;
const initialResultsLength = this.results.filter(x => x.active).length;
const result = await this.continueCombo(maxIncreasesDiceSize);
/* The flow of DiceSoNice has no way of knowing that some of the results of a Die should be a different denomination
We solve this by marking the results as hidden so they're not picked up by the auto roll of DiceSoNice.
The actual rolls are done here in place so every dice gets the correct denomination.
*/
if (game.modules.get('dice-so-nice')?.active) {
const resultsToRoll = this.results.filter((x, index) =>
x.active && (!rerollStartIndex || index === rerollStartIndex || index > initialResultsLength - 1));
const rolls = [];
for (const result of resultsToRoll) {
const roll = await (new Roll(`1${result.denomination ?? this.denomination}`)).evaluate();
roll.terms[0].results = [result];
roll._evaluateTotal();
rolls.push(roll);
}
/* If there are other dice that will be rolled we cannot await here. The other dice will be awaited in the normal flow */
const promises = rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true));
if (rerollStartIndex !== undefined || this._root.dice.length <= 1) {
await Promise.allSettled(promises);
}
}
for (const result of this.results)
result.hidden = true;
const modifier = maxIncreasesDiceSize ? 'cc' : 'c';
if (!this.modifiers.includes(modifier))
this.modifiers.push(modifier);
return result;
}
async continueCombo(maxIncreasesDiceSize) {
const activeResults = this.results.filter(x => x.active);
const lastIndex = activeResults.length - 1;
const lastResult = activeResults[lastIndex];
/* The Combo only continues if the latest roll was higher or equal to the previous */
if (lastResult.result < activeResults[lastIndex - 1].result) return false;
const lastFaces = lastResult.denomination ?
Number(lastResult.denomination.slice(1)) : this.faces;
const currentDenomination = `d${lastFaces}`
const increaseDiceSize =
maxIncreasesDiceSize && lastFaces < 12 &&
lastResult.result === lastFaces;
const denomination = increaseDiceSize ? adjustDice(currentDenomination, false) : currentDenomination;
const newRoll = await (new Roll(`1${denomination}`)).evaluate();
this.results.push({ result: newRoll.total, denomination: denomination, active: true });
this.number += 1;
return this.continueCombo(maxIncreasesDiceSize);
}
async handleComboDiceReroll(rerolledIndex, originalResult) {
/* Potentially sliced when correcting compound dice at (1) */
let resultGroupingIndexes = this.results.map((x, index) => ({ index, active: x.active }))
.filter(x => x.active).map(x => x.index);
if (resultGroupingIndexes.length <= 1) return;
const rerolledResult = this.results[rerolledIndex];
const compoundCombo = this.modifiers.includes('cc');
const rerollGroupingIndex = resultGroupingIndexes.indexOf(rerolledIndex);
const previousIndex = resultGroupingIndexes[rerollGroupingIndex - 1];
const previousResult = this.results[previousIndex];
const nextIndex = resultGroupingIndexes[rerollGroupingIndex + 1];
const nextResult = this.results[nextIndex];
const dropDice = preceeding => {
const cutoffIndex = preceeding ? resultGroupingIndexes[rerollGroupingIndex + 1] + 1 : rerolledIndex + 1;
this.results = this.results.slice(0, cutoffIndex);
this.number = this.results.filter(x => x.active).length;
};
/* (1) If subsequent size increases from the compound effect are now incorrect, drop subsequent dice */
const lastFaces = Number((originalResult.denomination ?? this.denomination).slice(1));
if (
compoundCombo &&
(
(originalResult.result === lastFaces &&
rerolledResult.result !== lastFaces)
||
(originalResult.result !== lastFaces &&
rerolledResult.result === lastFaces)
)
) {
dropDice(false);
resultGroupingIndexes = resultGroupingIndexes.slice(0, this.number);
}
/* (2) Rerolling any of the last two dice might introduce new results */
const isFinalHigher =
rerollGroupingIndex === resultGroupingIndexes.length - 1 && rerolledResult.result >= previousResult?.result;
const isSemifinalLower =
rerollGroupingIndex === resultGroupingIndexes.length - 2 && rerolledResult.result < nextResult?.result;
if (isFinalHigher || isSemifinalLower) {
return await this.rollComboDice({
maxIncreasesDiceSize: compoundCombo,
rerollStartIndex: rerollGroupingIndex
});
}
/* (3) Rerolling a subsequent dice might invalidate later dice which should then be dropped */
else if (rerolledResult.result < previousResult?.result){
dropDice(false);
}
/* (4) Rerolling a preceeding dice might invalidate later dice which should then be dropped */
else if (rerolledResult.result >= nextResult?.result) {
dropDice(true);
}
const fakeRollFaces =
rerolledResult.denomination ? rerolledResult.denomination.slice(1) : this.faces;
const fakeRoll = {
_evaluated: true,
dice: [new foundry.dice.terms.Die({
...this,
results: [rerolledResult],
total: rerolledResult.result,
faces: fakeRollFaces
})],
options: { appearance: {} }
};
await triggerChatRollFx([fakeRoll]);
rerolledResult.hidden = true;
}
}

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,17 +1,12 @@
import { updateResourcesForDualityReroll } from '../helpers.mjs';
import BaseDie from './baseDie.mjs';
export default class DualityDie extends BaseDie {
export default class DualityDie extends foundry.dice.terms.Die {
constructor(options) {
super(options);
this.modifiers = [];
}
get isRerolled() {
return this.results.some(x => x.rerolled);
}
#getDualityState(roll) {
if (!roll) return null;
return roll.withHope ? 1 : roll.withFear ? -1 : 0;

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;
}
}
}
@ -736,9 +730,11 @@ export default class DhpActor extends Actor {
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,10 +4,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
targetHook = null;
static #EXPAND_SECTIONS = [
{ selector: '.roll-section [data-action="expandRoll"]', key: 'roll' },
{ selector: '.damage-section', key: 'damage' },
{ selector: '.target-section', key: 'target' },
{ selector: '.description-section', key: 'desc' }
{ selector: 'roll-section [data-action="expandRoll"]', key: 'roll' },
{ selector: 'damage-section', key: 'damage' },
{ selector: 'target-section', key: 'target' },
{ selector: 'description-section', key: 'desc' }
];
async renderHTML() {

View file

@ -1,9 +1,4 @@
export default class DhpCombat extends Combat {
/** @inheritDoc */
get nextCombatant() {
return null;
}
async startCombat() {
this._playCombatSound('startEncounter');
const updateData = { round: 1, turn: null };

View file

@ -4,55 +4,15 @@ 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;
}
this.noOffset = options.noOffset;
super.activate(element, { ...options, html });
}
async #activateBattlepoints(element, options) {
if (element.dataset.tooltip?.startsWith('#battlepoints#')) {
this.#wide = true;
this.#bordered = true;
const html = await this.getBattlepointHTML(element.dataset.combatId);
html = await this.getBattlepointHTML(element.dataset.combatId);
options.direction = this._determineItemTooltipDirection(element);
super.activate(element, { ...options, html: html });
@ -60,9 +20,13 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => {
element.addEventListener('input', this.toggleModifier.bind(this));
});
return;
} else {
this.#wide = false;
this.#bordered = false;
}
async #activateEffectDisplay(element, options) {
if (element.dataset.tooltip === '#effect-display#') {
this.#bordered = true;
let effect = {};
if (element.dataset.uuid) {
@ -111,7 +75,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
};
}
const html = await foundry.applications.handlebars.renderTemplate(
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/effect-display.hbs`,
{
effect
@ -120,11 +84,11 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
return html;
} else {
this.#bordered = false;
}
async #activateItem(element, options) {
if (element.dataset.tooltip?.startsWith('#item#')) {
const itemUuid = element.dataset.tooltip.slice(6);
const item = await foundry.utils.fromUuid(itemUuid);
if (item) {
@ -133,7 +97,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
await this.enrichText(item);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
const html = await foundry.applications.handlebars.renderTemplate(
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/${type}.hbs`,
{
item: item,
@ -144,20 +108,17 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
);
this.tooltip.innerHTML = html;
options.direction ??= this._determineItemTooltipDirection(element);
return html;
options.direction = this._determineItemTooltipDirection(element);
}
return null;
}
async #activateAttack(element, options) {
} 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 foundry.applications.ux.TextEditor.enrichHTML(attack.description);
const html = await foundry.applications.handlebars.renderTemplate(
const description = await TextEditor.enrichHTML(attack.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/attack.hbs`,
{
attack: attack,
@ -168,57 +129,19 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
);
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);
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);
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(
const description = await TextEditor.enrichHTML(move.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move,
@ -231,7 +154,49 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
return html;
}
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: html });
}
_setAnchor(direction) {

View file

@ -4,20 +4,19 @@ export default class RegisterHandlebarsHelpers {
static registerHelpers() {
Handlebars.registerHelper({
add: this.add,
coalesce: this.coalesce,
damageFormula: this.damageFormula,
damageSymbols: this.damageSymbols,
empty: this.empty,
formulaValue: this.formulaValue,
getProperty: foundry.utils.getProperty,
hasProperty: foundry.utils.hasProperty,
includes: this.includes,
isNullish: this.isNullish,
times: this.times,
damageFormula: this.damageFormula,
formulaValue: this.formulaValue,
damageSymbols: this.damageSymbols,
rollParsed: this.rollParsed,
hasProperty: foundry.utils.hasProperty,
getProperty: foundry.utils.getProperty,
setVar: this.setVar,
empty: this.empty,
pluralize: this.pluralize,
positive: this.positive,
rollParsed: this.rollParsed,
setVar: this.setVar,
times: this.times
isNullish: this.isNullish
});
}
static add(a, b) {
@ -26,10 +25,6 @@ export default class RegisterHandlebarsHelpers {
return (Number.isNaN(aNum) ? 0 : aNum) + (Number.isNaN(bNum) ? 0 : bNum);
}
static coalesce(...args) {
return args.find(a => a !== undefined && a !== null);
}
static includes(list, item) {
return list.includes(item);
}
@ -53,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

@ -138,9 +138,12 @@
"src": "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg",
"anchorX": 0.5,
"anchorY": 0.5,
"offsetX": 0,
"offsetY": 0,
"fit": "contain",
"scaleX": 1,
"scaleY": 1,
"rotation": 0,
"tint": "#ffffff",
"alphaThreshold": 0.75
},
@ -191,7 +194,7 @@
"saturation": 0,
"contrast": 0
},
"detectionModes": {},
"detectionModes": [],
"occludable": {
"radius": 0
},
@ -217,8 +220,7 @@
"flags": {},
"randomImg": false,
"appendNumber": false,
"prependAdjective": false,
"depth": 1
"prependAdjective": false
},
"items": [
{
@ -334,10 +336,13 @@
},
"disabled": false,
"duration": {
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p>You have disadvantage on Agility Rolls until you clear at least 1 HP.</p>",
"tint": "#ffffff",
@ -347,9 +352,6 @@
"_stats": {
"compendiumSource": null
},
"start": null,
"showIcon": 1,
"folder": null,
"_key": "!actors.items.effects!JRhrrEg5UroURiAD.DMtd1EXQPlPaoRmV.wGuxOLokMqdxVSOo"
}
],

View file

@ -6,71 +6,33 @@
"description": "<p>When you deal Severe damage to a target within Melee range, you can <strong>mark a Stress</strong> to force them to mark an additional Hit Point.</p>",
"resource": null,
"actions": {
"EvdZLuP0wunF5cmq": {
"type": "damage",
"_id": "EvdZLuP0wunF5cmq",
"Il2C9zALU6a1gGrg": {
"type": "effect",
"_id": "Il2C9zALU6a1gGrg",
"systemPath": "actions",
"baseAction": false,
"description": "",
"description": "<p>When you deal Severe damage to a target within Melee range, you can <strong>mark a Stress</strong> to force them to mark an additional Hit Point.</p>",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [],
"areas": [],
"cost": [
{
"scalable": false,
"key": "stress",
"value": 1,
"itemId": null,
"step": null,
"consumeOnSuccess": false
"step": null
}
],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"damage": {
"main": null,
"resources": {
"hitPoints": {
"base": false,
"applyTo": "hitPoints",
"resultBased": false,
"value": {
"multiplier": "flat",
"flatMultiplier": 0,
"dice": "d6",
"bonus": 1,
"custom": {
"enabled": false,
"formula": ""
}
},
"valueAlt": {
"multiplier": "flat",
"flatMultiplier": 1,
"dice": "d6",
"bonus": null,
"custom": {
"enabled": false,
"formula": ""
}
}
}
}
},
"target": {
"type": "any",
"amount": null
"max": null,
"recovery": null
},
"effects": [],
"target": {
"type": null,
"amount": null
},
"name": "Mark Stress",
"img": "icons/skills/melee/blood-slash-foam-red.webp",
"range": ""
}
},

View file

@ -10,25 +10,18 @@
"level": 10,
"type": "ability",
"actions": {
"Zztzsl3sPdss7xDe": {
"type": "damage",
"_id": "Zztzsl3sPdss7xDe",
"lekCIrTCQ2FhwGd1": {
"type": "effect",
"_id": "lekCIrTCQ2FhwGd1",
"systemPath": "actions",
"baseAction": false,
"description": "",
"description": "<p class=\"Body-Foundation\">When you make a successful attack against an adversary, you can <strong>mark 4 Stress</strong> to force the target to mark a number of Hit Points equal to the number of Hit Points you currently have marked instead of rolling for damage.</p>",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [],
"areas": [],
"cost": [
{
"scalable": false,
"key": "stress",
"value": 4,
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
@ -39,44 +32,14 @@
"recovery": null,
"consumeOnSuccess": false
},
"damage": {
"main": null,
"resources": {
"hitPoints": {
"base": false,
"applyTo": "hitPoints",
"resultBased": false,
"value": {
"multiplier": "flat",
"flatMultiplier": 1,
"dice": "d6",
"bonus": null,
"custom": {
"enabled": true,
"formula": "@system.resources.hitPoints.value"
}
},
"valueAlt": {
"multiplier": "flat",
"flatMultiplier": 1,
"dice": "d6",
"bonus": null,
"custom": {
"enabled": false,
"formula": ""
}
}
}
}
},
"effects": [],
"target": {
"type": "any",
"amount": null
},
"effects": [],
"name": "Mark Stress",
"range": "",
"img": "icons/skills/melee/strike-axe-red.webp"
"name": "Mark 4 Stress",
"img": "icons/magic/control/silhouette-aura-energy.webp",
"range": ""
}
},
"attribution": {

View file

@ -214,35 +214,7 @@
}
}
},
"includeBase": false,
"main": null,
"resources": {
"hitPoints": {
"base": false,
"applyTo": "hitPoints",
"resultBased": false,
"value": {
"multiplier": "flat",
"flatMultiplier": 0,
"dice": "d6",
"bonus": 1,
"custom": {
"enabled": false,
"formula": ""
}
},
"valueAlt": {
"multiplier": "flat",
"flatMultiplier": 1,
"dice": "d6",
"bonus": null,
"custom": {
"enabled": false,
"formula": ""
}
}
}
}
"includeBase": false
},
"target": {
"type": "any",

View file

@ -14,7 +14,7 @@
"type": "damage",
"_id": "MxaqNvY9IfWnFe5P",
"systemPath": "actions",
"description": "<p>When a creature within your weapons range deals damage to an ally with an attack that doesnt include you, you can <strong>mark a Stress</strong> to force them to make a Reaction Roll (15). On a failure, the target must mark a Hit Point.</p>",
"description": "<p class=\"Body-Foundation\">When you successfully make an attack with your weapon, you never deal damage beneath a targets Major damage threshold (the target always marks a minimum of 2 Hit Points).</p><p class=\"Body-Foundation\">Additionally, when a creature within your weapons range deals damage to an ally with an attack that doesnt include you, you can <strong>mark a Stress</strong> to force them to make a Reaction Roll (15). On a failure, the target must mark a Hit Point.</p>",
"chatDisplay": true,
"actionType": "action",
"cost": [
@ -34,34 +34,7 @@
},
"damage": {
"parts": {},
"includeBase": false,
"resources": {
"hitPoints": {
"base": false,
"applyTo": "hitPoints",
"resultBased": false,
"value": {
"multiplier": "flat",
"flatMultiplier": 0,
"dice": "d6",
"bonus": 1,
"custom": {
"enabled": false,
"formula": ""
}
},
"valueAlt": {
"multiplier": "flat",
"flatMultiplier": 1,
"dice": "d6",
"bonus": null,
"custom": {
"enabled": false,
"formula": ""
}
}
}
}
"includeBase": false
},
"target": {
"type": "any",

View file

@ -34,34 +34,7 @@
},
"damage": {
"parts": {},
"includeBase": false,
"resources": {
"hitPoints": {
"base": false,
"applyTo": "hitPoints",
"resultBased": false,
"value": {
"multiplier": "flat",
"flatMultiplier": 0,
"dice": "d6",
"bonus": 5,
"custom": {
"enabled": false,
"formula": ""
}
},
"valueAlt": {
"multiplier": "flat",
"flatMultiplier": 1,
"dice": "d6",
"bonus": null,
"custom": {
"enabled": false,
"formula": ""
}
}
}
}
"includeBase": false
},
"target": {
"type": "any",

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,20 +70,13 @@
object-fit: cover;
object-position: top center;
}
}
.message-header-main {
display: flex;
align-items: center;
column-gap: 4px;
row-gap: 2px;
.message-sub-header-container {
flex: 1;
overflow: hidden;
display: grid;
grid-template:
"title metadata"
"subtitle subtitle";
display: flex;
flex-direction: column;
justify-content: space-between;
color: @beige;
h4 {
font-size: var(--font-size-16);
@ -76,39 +84,6 @@
margin-bottom: 0;
font-family: @font-subtitle;
color: @golden;
white-space: nowrap;
text-overflow: ellipsis;
flex: 1;
overflow: hidden;
align-self: flex-end;
}
.message-metadata {
font-family: @font-body;
color: @beige;
white-space: nowrap;
align-items: baseline;
.message-timestamp {
font-size: var(--font-size-11);
}
}
.subtitle {
grid-area: subtitle;
flex: 1;
display: flex;
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;
}
}
}

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,18 +34,14 @@
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);
.item-name input[type='text'] {
font-size: var(--font-size-32);
height: 42px;
text-align: center;
width: 100%;
width: 90%;
transition: all 0.3s ease;
outline: 2px solid transparent;
border: 1px solid transparent;
text-overflow: ellipsis;
&:hover[type='text'],
&:focus[type='text'] {
@ -55,7 +49,6 @@
outline: 2px solid light-dark(@dark-blue, @golden);
}
}
}
.item-description {
display: flex;

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 {
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 './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

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

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

View file

@ -1,223 +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 {
position: relative;
width: 300px !important;
max-width: 300px;
left: unset!important;
top: unset!important;
#ui-right:has(#effects-display):has(.effect-container) & {
right: 62px;
}
}
&.leftBottom {
width: 200px !important;
max-width: 200px;
background: transparent;
}
&:not(.performance-low, .noblur) {
backdrop-filter: blur(5px);
}
.window-content {
padding: 0.5rem;
#resource-fear {
position: relative;
&:hover {
.fear-header {
opacity: 1;
height: 18.75px;
visibility: visible;
}
.resize-handle {
opacity: 1;
}
}
.fear-header {
display: flex;
gap: 5px;
pointer-events: all;
margin-bottom: 0.5rem;
opacity: 0;
height: 0;
visibility: hidden;
transition: all 0.3s ease;
.drag-handle {
cursor: grab;
}
.fear-title {
font-size: var(--font-size-13);
}
}
.fear-tokens {
display: flex;
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;
}
.controls,
.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;
border: 2px solid rgb(153 122 79);
background-color: rgb(24 22 46);
}
&::-webkit-progress-value {
background: linear-gradient(90deg, rgba(2, 0, 38, 1) 0%, rgba(199, 1, 252, 1) 100%);
border-radius: 999px;
}
}
.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-self: center;
border-radius: 50%;
align-items: center;
margin-top: 0.5rem;
color: light-dark(@dark, @beige);
.disabled {
justify-content: center;
width: 30px;
height: 30px;
font-size: var(--font-size-20);
cursor: pointer;
&:hover {
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 {
.fear-token {
i {
cursor: pointer;
transition: box-shadow 0.15s ease;
&:hover {
box-shadow: 0 0 8px @primary-color-fear ;
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">
{{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.')}}
{{! 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"}}
{{localize "DAGGERHEART.GENERAL.damage"}}
{{/if}}
{{#unless @root.allDamageTypesUsed}}<a data-action="addDamageResource"><i class="fa-solid fa-plus icon-button"></i></a>{{/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>
{{#each source.resources as |dmg key|}}
<div class="nest-inputs">
<fieldset{{#if dmg.base}} disabled{{/if}} class="one-column">
{{#if @root.hasBaseDamage}}
{{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase name="damage.includeBase" classes="checkbox" localize=true }}
{{/if}}
{{#unless (eq @root.source.type 'healing')}}
{{formField baseFields.direct value=source.direct name=(concat path "damage.direct") localize=true classes="checkbox"}}
{{/unless}}
{{#if (and @root.isNPC (not (eq path 'system.attack.')))}}
{{formField baseFields.groupAttack value=source.groupAttack name=(concat path "damage.groupAttack") localize=true classes="select"}}
{{/if}}
</div>
{{!-- Handlebars uses Symbol.Iterator to produce index|key. This isn't compatible with our parts object, so we instead use applyTo, which is the same value --}}
{{#each source.parts as |dmg key|}}
<div class="nest-inputs">
<fieldset{{#if dmg.base}} disabled{{/if}} class="one-column{{#if ../path}} no-style{{/if}}">
<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>
<a data-action="removeDamage" data-key="{{dmg.applyTo}}"><i class="fas fa-trash"></i></a>
{{/unless}}
</legend>
{{> damageData damage=dmg fields=../fields.resources.element.fields basePath=(concat ../path "damage.resources." dmg.applyTo)}}
{{#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>
{{/unless}}
{{#*inline "formula"}}
{{#unless isBase}}
{{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat basePath ".custom.enabled") classes="checkbox" localize=true}}
{{#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 basePath ".custom.formula") localize=true}}
{{formField fields.custom.fields.formula value=source.custom.formula name=(concat path "damage.parts." key "." target ".custom.formula") localize=true}}
{{else}}
<div class="nest-inputs">
{{#unless @root.isNPC}}
{{formField fields.multiplier value=source.multiplier name=(concat basePath ".multiplier") localize=true}}
{{formField fields.multiplier value=source.multiplier name=(concat path "damage.parts." key "." target ".multiplier") localize=true}}
{{/unless}}
{{#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}}
{{#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="{{basePath}}.{{target}}.multiplier" value="flat">
<input type="hidden" name="{{path}}damage.parts.{{key}}.{{target}}.multiplier" value="flat">
{{/if}}
{{/inline}}
{{#*inline "damageData"}}
{{#if (and (not @root.isNPC) @root.hasRoll (not damage.base))}}
{{formField fields.resultBased value=damage.resultBased name=(concat basePath ".resultBased") localize=true classes="checkbox"}}
{{/if}}
{{#if (and (not @root.isNPC) @root.hasRoll (not damage.base) damage.resultBased)}}
<div class="nest-inputs">
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}}</legend>
{{> formula key=damage.applyTo fields=fields.value.fields type=fields.type isBase=damage.base source=damage.value basePath=(concat basePath ".value")}}
</fieldset>
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}}</legend>
{{> formula key=damage.applyTo fields=fields.valueAlt.fields type=fields.type isBase=damage.base source=damage.valueAlt basePath=(concat basePath ".valueAlt")}}
</fieldset>
</div>
{{else}}
{{> formula key=damage.applyTo fields=fields.value.fields type=fields.type isBase=damage.base source=damage.value basePath=(concat basePath ".value")}}
{{/if}}
<input type="hidden" name="{{concat basePath ".base"}}" value="{{damage.base}}">
{{/inline}}
{{#*inline "hordeDamage"}}
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}}</legend>
<div class="nest-inputs">
<input type="hidden" name="{{basePath}}.valueAlt.multiplier" value="flat">
{{formField fields.valueAlt.fields.flatMultiplier value=source.valueAlt.flatMultiplier name=(concat basePath ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }}
{{formField fields.valueAlt.fields.dice value=source.valueAlt.dice name=(concat basePath ".valueAlt.dice") classes="inline-child" localize=true}}
{{formField fields.valueAlt.fields.bonus value=source.valueAlt.bonus name=(concat basePath ".valueAlt.bonus") localize=true classes="inline-child"}}
</div>
</fieldset>
{{/inline}}

View file

@ -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

@ -16,6 +16,33 @@
</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>
@ -33,45 +60,6 @@
</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">
<legend>{{localize "DAGGERHEART.GENERAL.Modifier.plural"}}</legend>
@ -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>
{{#each damage.parts as |part|}}
<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"}}" />
{{#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>
{{/if}}
{{/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 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}}>
{{#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

@ -39,20 +39,44 @@
</div>
<div class="character-details">
<div>
{{> classHeritage item=document.system.class.value key="classes" label=(localize "TYPES.Item.class")}}
{{#if document.system.class.value}}
<span data-action="editDoc" data-item-uuid="{{document.system.class.value.uuid}}">{{document.system.class.value.name}}</span>
{{else}}
<span class="missing-header-feature" data-action="openPack" data-key="classes">{{localize 'TYPES.Item.class'}}</span>
{{/if}}
<span class="dot">•</span>
{{> classHeritage item=document.system.class.subclass key="subclasses" label=(localize "TYPES.Item.subclass")}}
{{#if document.system.class.subclass}}
<span data-action="editDoc" data-item-uuid="{{document.system.class.subclass.uuid}}">{{document.system.class.subclass.name}}</span>
{{else}}
<span class="missing-header-feature" data-action="openPack" data-key="subclasses">{{localize 'TYPES.Item.subclass'}}</span>
{{/if}}
<span class="dot">•</span>
{{> classHeritage item=document.system.community key="communities" label=(localize "TYPES.Item.community")}}
{{#if document.system.community}}
<span data-action="editDoc" data-item-uuid="{{document.system.community.uuid}}">{{document.system.community.name}}</span>
{{else}}
<span class="missing-header-feature" data-action="openPack" data-key="communities">{{localize 'TYPES.Item.community'}}</span>
{{/if}}
<span class="dot">•</span>
{{> classHeritage item=document.system.ancestry key="ancestries" label=(localize "TYPES.Item.ancestry")}}
{{#if document.system.ancestry}}
<span data-action="editDoc" data-item-uuid="{{document.system.ancestry.uuid}}">{{document.system.ancestry.name}}</span>
{{else}}
<span class="missing-header-feature" data-action="openPack" data-key="ancestries">{{localize 'TYPES.Item.ancestry'}}</span>
{{/if}}
</div>
{{#if (or document.system.multiclass.value document.system.multiclass.subclass)}}
<div class="multiclass">
{{> classHeritage item=document.system.multiclass.value key="classes" label=(localize "DAGGERHEART.GENERAL.multiclass")}}
{{#if document.system.multiclass.value}}
<span data-action="editDoc"data-item-uuid="{{document.system.multiclass.value.uuid}}">{{document.system.multiclass.value.name}}</span>
{{else}}
<span data-action="openPack" data-key="classes">{{localize 'DAGGERHEART.GENERAL.multiclass'}}</span>
{{/if}}
<span class="dot">•</span>
{{> classHeritage item=document.system.multiclass.subclass key="subclasses" label=(localize "TYPES.Item.subclass")}}
{{#if document.system.multiclass.subclass}}
<span data-action="editDoc" data-item-uuid="{{document.system.multiclass.subclass.uuid}}">{{document.system.multiclass.subclass.name}}</span>
{{else}}
<span class="missing-header-feature" data-action="openPack" data-key="subclasses">{{localize 'TYPES.Item.subclass'}}</span>
{{/if}}
</div>
{{/if}}
</div>
@ -137,11 +161,3 @@
<button type="button" data-action="openSettings" data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.openSheetSettings"}}"><i class="fa-solid fa-wrench"></i></button>
{{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
</header>
{{#*inline "classHeritage"}}
{{#if item}}
<span data-action="editDoc" data-item-uuid="{{item.uuid}}">{{item.name}}</span>
{{else}}
<span class="missing-header-feature" data-action="openPack" data-key="{{key}}">{{label}}</span>
{{/if}}
{{/inline}}

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}}
</div>
{{else if (eq item.system.resource.type 'diceValue')}}
<div class="item-resources">

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