Compare commits

..

9 commits
main ... 2.5.1

Author SHA1 Message Date
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
362 changed files with 2195 additions and 3621 deletions

13
daggerheart.d.ts vendored
View file

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

View file

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

View file

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

View file

@ -315,15 +315,15 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
const beastformEffect = selected.effects.find(x => x.type === 'beastform'); const beastformEffect = selected.effects.find(x => x.type === 'beastform');
for (const traitBonus of app.modifications.traitBonuses) { for (const traitBonus of app.modifications.traitBonuses) {
const existingChange = beastformEffect.system.changes.find( const existingChange = beastformEffect.changes.find(
x => x.key === `system.traits.${traitBonus.trait}.value` x => x.key === `system.traits.${traitBonus.trait}.value`
); );
if (existingChange) { if (existingChange) {
existingChange.value = Number.parseInt(existingChange.value) + traitBonus.bonus; existingChange.value = Number.parseInt(existingChange.value) + traitBonus.bonus;
} else { } else {
beastformEffect.system.changes.push({ beastformEffect.changes.push({
key: `system.traits.${traitBonus.trait}.value`, key: `system.traits.${traitBonus.trait}.value`,
type: 'add', mode: 2,
priority: null, priority: null,
value: traitBonus.bonus value: traitBonus.bonus
}); });

View file

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

View file

@ -139,16 +139,14 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'), name: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.name'),
description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'), description: game.i18n.localize('DAGGERHEART.CONFIG.DeathMoves.blazeOfGlory.description'),
img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img, img: CONFIG.DH.GENERAL.deathMoves.blazeOfGlory.img,
system: {
changes: [ changes: [
{ {
key: 'system.rules.roll.guaranteedCritical', key: 'system.rules.roll.guaranteedCritical',
type: 'add', mode: 2,
value: 'true' value: 'true'
} }
] ]
} }
}
]); ]);
await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id); await this.actor.setDeathMoveDefeated(CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id);
@ -185,6 +183,8 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
if (result === undefined) return; if (result === undefined) return;
const autoExpandDescription = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.expandRollMessage?.desc;
const cls = getDocumentClass('ChatMessage'); const cls = getDocumentClass('ChatMessage');
const msg = { const msg = {
@ -200,6 +200,7 @@ export default class DhDeathMove extends HandlebarsApplicationMixin(ApplicationV
img: this.selectedMove.img, img: this.selectedMove.img,
description: game.i18n.localize(this.selectedMove.description), description: game.i18n.localize(this.selectedMove.description),
result: result, result: result,
open: autoExpandDescription ? 'open' : '',
showRiskItAllButton: this.showRiskItAllButton, showRiskItAllButton: this.showRiskItAllButton,
riskItAllButtonLabel: this.riskItAllButtonLabel, riskItAllButtonLabel: this.riskItAllButtonLabel,
riskItAllHope: this.riskItAllHope riskItAllHope: this.riskItAllHope

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -90,21 +90,19 @@ export const BPModifiers = {
name: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.name', name: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.name',
description: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.description', description: 'DAGGERHEART.CONFIG.BPModifiers.increaseDamage.effect.description',
img: 'icons/magic/control/buff-flight-wings-red.webp', img: 'icons/magic/control/buff-flight-wings-red.webp',
system: {
changes: [ changes: [
{ {
key: 'system.bonuses.damage.physical.dice', key: 'system.bonuses.damage.physical.dice',
type: 'add', mode: 2,
value: '1d4' value: '1d4'
}, },
{ {
key: 'system.bonuses.damage.magical.dice', key: 'system.bonuses.damage.magical.dice',
type: 'add', mode: 2,
value: '1d4' value: '1d4'
} }
] ]
} }
}
] ]
} }
}, },

View file

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

View file

@ -37,16 +37,14 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.channeling.effects.channeling.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.channeling.effects.channeling.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.channeling.effects.channeling.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.channeling.effects.channeling.description',
img: 'icons/magic/symbols/rune-sigil-horned-blue.webp', img: 'icons/magic/symbols/rune-sigil-horned-blue.webp',
system: {
changes: [ changes: [
{ {
key: 'system.bonuses.roll.spellcast', key: 'system.bonuses.roll.spellcast',
type: 'add', mode: 2,
value: '1' value: '1'
} }
] ]
} }
}
] ]
}, },
difficult: { difficult: {
@ -57,46 +55,44 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.difficult.effects.difficult.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.difficult.effects.difficult.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.difficult.effects.difficult.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.difficult.effects.difficult.description',
img: 'icons/magic/control/buff-flight-wings-red.webp', img: 'icons/magic/control/buff-flight-wings-red.webp',
system: {
changes: [ changes: [
{ {
key: 'system.traits.agility.value', key: 'system.traits.agility.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
key: 'system.traits.strength.value', key: 'system.traits.strength.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
key: 'system.traits.finesse.value', key: 'system.traits.finesse.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
key: 'system.traits.instinct.value', key: 'system.traits.instinct.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
key: 'system.traits.presence.value', key: 'system.traits.presence.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
key: 'system.traits.knowledge.value', key: 'system.traits.knowledge.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '-1' value: '-1'
} }
] ]
} }
}
] ]
}, },
flexible: { flexible: {
@ -107,16 +103,14 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.flexible.effects.flexible.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.flexible.effects.flexible.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.flexible.effects.flexible.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.flexible.effects.flexible.description',
img: 'icons/magic/movement/abstract-ribbons-red-orange.webp', img: 'icons/magic/movement/abstract-ribbons-red-orange.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '1' value: '1'
} }
] ]
} }
}
] ]
}, },
fortified: { fortified: {
@ -127,16 +121,14 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.fortified.effects.fortified.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.fortified.effects.fortified.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.fortified.effects.fortified.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.fortified.effects.fortified.description',
img: 'icons/magic/defensive/shield-barrier-glowing-blue.webp', img: 'icons/magic/defensive/shield-barrier-glowing-blue.webp',
system: {
changes: [ changes: [
{ {
key: 'system.rules.damageReduction.increasePerArmorMark', key: 'system.rules.damageReduction.increasePerArmorMark',
type: 'override', mode: 5,
value: '2' value: '2'
} }
] ]
} }
}
] ]
}, },
gilded: { gilded: {
@ -147,16 +139,14 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.gilded.effects.gilded.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.gilded.effects.gilded.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.gilded.effects.gilded.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.gilded.effects.gilded.description',
img: 'icons/magic/control/control-influence-crown-gold.webp', img: 'icons/magic/control/control-influence-crown-gold.webp',
system: {
changes: [ changes: [
{ {
key: 'system.traits.presence.value', key: 'system.traits.presence.value',
type: 'add', mode: 2,
value: '1' value: '1'
} }
] ]
} }
}
] ]
}, },
heavy: { heavy: {
@ -167,16 +157,14 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.heavy.effects.heavy.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.heavy.effects.heavy.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.heavy.effects.heavy.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.heavy.effects.heavy.description',
img: 'icons/commodities/metal/ingot-worn-iron.webp', img: 'icons/commodities/metal/ingot-worn-iron.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '-1' value: '-1'
} }
] ]
} }
}
] ]
}, },
hopeful: { hopeful: {
@ -224,16 +212,14 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.magical.effects.magical.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.magical.effects.magical.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.magical.effects.magical.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.magical.effects.magical.description',
img: 'icons/magic/defensive/barrier-shield-dome-blue-purple.webp', img: 'icons/magic/defensive/barrier-shield-dome-blue-purple.webp',
system: {
changes: [ changes: [
{ {
key: 'system.rules.damageReduction.magical', key: 'system.rules.damageReduction.magical',
type: 'override', mode: 5,
value: 1 value: 1
} }
] ]
} }
}
] ]
}, },
painful: { painful: {
@ -263,16 +249,14 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.physical.effects.physical.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.physical.effects.physical.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.physical.effects.physical.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.physical.effects.physical.description',
img: 'icons/commodities/stone/ore-pile-tan.webp', img: 'icons/commodities/stone/ore-pile-tan.webp',
system: {
changes: [ changes: [
{ {
key: 'system.rules.damageReduction.physical', key: 'system.rules.damageReduction.physical',
type: 'override', mode: 5,
value: 1 value: 1
} }
] ]
} }
}
] ]
}, },
quiet: { quiet: {
@ -296,21 +280,19 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.reinforced.effects.reinforced.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.reinforced.effects.reinforced.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.reinforced.effects.reinforced.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.reinforced.effects.reinforced.description',
img: 'icons/magic/defensive/shield-barrier-glowing-triangle-green.webp', img: 'icons/magic/defensive/shield-barrier-glowing-triangle-green.webp',
system: {
changes: [ changes: [
{ {
key: 'system.bunuses.damageThresholds.major', key: 'system.bunuses.damageThresholds.major',
type: 'add', mode: 2,
value: '2' value: '2'
}, },
{ {
key: 'system.bunuses.damageThresholds.severe', key: 'system.bunuses.damageThresholds.severe',
type: 'add', mode: 2,
value: '2' value: '2'
} }
] ]
} }
}
] ]
}, },
resilient: { resilient: {
@ -344,21 +326,19 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.sharp.effects.sharp.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.sharp.effects.sharp.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.sharp.effects.sharp.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.sharp.effects.sharp.description',
img: 'icons/magic/defensive/shield-barrier-glowing-triangle-green.webp', img: 'icons/magic/defensive/shield-barrier-glowing-triangle-green.webp',
system: {
changes: [ changes: [
{ {
key: 'system.bonuses.damage.primaryWeapon.dice', key: 'system.bonuses.damage.primaryWeapon.dice',
type: 'add', mode: 2,
value: '1d4' value: '1d4'
}, },
{ {
key: 'system.bonuses.damage.secondaryWeapon.dice', key: 'system.bonuses.damage.secondaryWeapon.dice',
type: 'add', mode: 2,
value: '1d4' value: '1d4'
} }
] ]
} }
}
] ]
}, },
shifting: { shifting: {
@ -428,21 +408,19 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.veryHeavy.effects.veryHeavy.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.veryHeavy.effects.veryHeavy.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.veryHeavy.effects.veryHeavy.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.veryHeavy.effects.veryHeavy.description',
img: 'icons/commodities/metal/ingot-stamped-steel.webp', img: 'icons/commodities/metal/ingot-stamped-steel.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '-2' value: '-2'
}, },
{ {
key: 'system.traits.agility.value', key: 'system.traits.agility.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
} }
] ]
} }
}
] ]
}, },
warded: { warded: {
@ -453,17 +431,15 @@ export const armorFeatures = {
name: 'DAGGERHEART.CONFIG.ArmorFeature.warded.effects.warded.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.warded.effects.warded.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.warded.effects.warded.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.warded.effects.warded.description',
img: 'icons/magic/defensive/barrier-shield-dome-pink.webp', img: 'icons/magic/defensive/barrier-shield-dome-pink.webp',
system: {
changes: [ changes: [
{ {
key: 'system.resistance.magical.reduction', key: 'system.resistance.magical.reduction',
type: 'add', mode: 2,
value: '@system.armorScore', value: '@system.armorScore',
priority: 21 priority: 21
} }
] ]
} }
}
] ]
} }
}; };
@ -512,11 +488,10 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description',
img: 'icons/skills/melee/shield-block-bash-blue.webp', img: 'icons/skills/melee/shield-block-bash-blue.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
@ -529,7 +504,6 @@ export const weaponFeatures = {
} }
] ]
} }
}
] ]
}, },
bonded: { bonded: {
@ -540,16 +514,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.bonded.effects.damage.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.bonded.effects.damage.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.bonded.effects.damage.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.bonded.effects.damage.description',
img: 'icons/magic/symbols/chevron-elipse-circle-blue.webp', img: 'icons/magic/symbols/chevron-elipse-circle-blue.webp',
system: {
changes: [ changes: [
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
type: 'add', mode: 2,
value: '@system.levelData.level.current' value: '@system.levelData.level.current'
} }
] ]
} }
}
] ]
}, },
bouncing: { bouncing: {
@ -581,21 +553,19 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.brave.effects.brave.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.brave.effects.brave.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.brave.effects.brave.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.brave.effects.brave.description',
img: 'icons/magic/life/heart-cross-strong-flame-purple-orange.webp', img: 'icons/magic/life/heart-cross-strong-flame-purple-orange.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '-1' value: '-1'
}, },
{ {
key: 'system.damageThresholds.severe', key: 'system.damageThresholds.severe',
type: 'add', mode: 2,
value: 'ITEM.@system.tier' value: 'ITEM.@system.tier'
} }
] ]
} }
}
] ]
}, },
brutal: { brutal: {
@ -648,16 +618,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.charged.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.charged.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.charged.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.charged.description',
img: 'icons/magic/lightning/claws-unarmed-strike-teal.webp', img: 'icons/magic/lightning/claws-unarmed-strike-teal.webp',
system: {
changes: [ changes: [
{ {
key: 'system.proficiency', key: 'system.proficiency',
type: 'add', mode: 2,
value: '1' value: '1'
} }
] ]
} }
}
] ]
} }
] ]
@ -692,16 +660,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.cumbersome.effects.cumbersome.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.cumbersome.effects.cumbersome.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.cumbersome.effects.cumbersome.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.cumbersome.effects.cumbersome.description',
img: 'icons/commodities/metal/mail-plate-steel.webp', img: 'icons/commodities/metal/mail-plate-steel.webp',
system: {
changes: [ changes: [
{ {
key: 'system.traits.finesse.value', key: 'system.traits.finesse.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
} }
] ]
} }
}
] ]
}, },
deadly: { deadly: {
@ -741,17 +707,15 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.effects.deflecting.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.effects.deflecting.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.effects.deflecting.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.deflecting.effects.deflecting.description',
img: 'icons/skills/melee/hand-grip-sword-strike-orange.webp', img: 'icons/skills/melee/hand-grip-sword-strike-orange.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '@system.armorScore', value: '@system.armorScore',
priority: 21 priority: 21
} }
] ]
} }
}
] ]
} }
] ]
@ -790,16 +754,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.effects.agility', description: 'DAGGERHEART.CONFIG.WeaponFeature.destructive.effects.agility',
img: 'icons/skills/melee/strike-flail-spiked-pink.webp', img: 'icons/skills/melee/strike-flail-spiked-pink.webp',
system: {
changes: [ changes: [
{ {
key: 'system.traits.agility.value', key: 'system.traits.agility.value',
type: 'add', mode: 2,
value: '-1' value: '-1'
} }
] ]
} }
}
] ]
}, },
devastating: { devastating: {
@ -833,7 +795,7 @@ export const weaponFeatures = {
changes: [ changes: [
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
type: 'add', mode: 2,
value: '1' value: '1'
} }
], ],
@ -940,16 +902,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.actions.greed.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.actions.greed.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.actions.greed.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.greedy.actions.greed.description',
img: 'icons/commodities/currency/coins-crown-stack-gold.webp', img: 'icons/commodities/currency/coins-crown-stack-gold.webp',
system: {
changes: [ changes: [
{ {
key: 'system.proficiency', key: 'system.proficiency',
type: 'add', mode: 2,
value: '1' value: '1'
} }
] ]
} }
}
] ]
} }
] ]
@ -991,16 +951,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.heavy.effects.heavy.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.heavy.effects.heavy.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.heavy.effects.heavy.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.heavy.effects.heavy.description',
img: 'icons/commodities/metal/ingot-worn-iron.webp', img: 'icons/commodities/metal/ingot-worn-iron.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '-1' value: '-1'
} }
] ]
} }
}
] ]
}, },
hooked: { hooked: {
@ -1108,16 +1066,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.massive.effects.massive.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.massive.effects.massive.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.massive.effects.massive.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.massive.effects.massive.description',
img: 'icons/skills/melee/strike-flail-destructive-yellow.webp', img: 'icons/skills/melee/strike-flail-destructive-yellow.webp',
system: {
changes: [ changes: [
{ {
key: 'system.evasion', key: 'system.evasion',
type: 'add', mode: 2,
value: '-1' value: '-1'
} }
] ]
} }
}
] ]
}, },
painful: { painful: {
@ -1151,7 +1107,7 @@ export const weaponFeatures = {
changes: [ changes: [
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
type: 'add', mode: 2,
value: 'ITEM.@system.tier + 1' value: 'ITEM.@system.tier + 1'
} }
], ],
@ -1202,16 +1158,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.effects.persuasive.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.effects.persuasive.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.effects.persuasive.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.persuasive.effects.persuasive.description',
img: 'icons/magic/control/hypnosis-mesmerism-eye.webp', img: 'icons/magic/control/hypnosis-mesmerism-eye.webp',
system: {
changes: [ changes: [
{ {
key: 'system.traits.presence.value', key: 'system.traits.presence.value',
type: 'add', mode: 2,
value: '2' value: '2'
} }
] ]
} }
}
] ]
} }
] ]
@ -1249,7 +1203,6 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.protective.effects.protective.description',
img: 'icons/skills/melee/shield-block-gray-orange.webp', img: 'icons/skills/melee/shield-block-gray-orange.webp',
system: {
changes: [ changes: [
{ {
key: 'Armor', key: 'Armor',
@ -1262,8 +1215,6 @@ export const weaponFeatures = {
} }
] ]
} }
}
] ]
}, },
quick: { quick: {
@ -1293,16 +1244,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.reliable.effects.reliable.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.reliable.effects.reliable.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.reliable.effects.reliable.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.reliable.effects.reliable.description',
img: 'icons/skills/melee/strike-sword-slashing-red.webp', img: 'icons/skills/melee/strike-sword-slashing-red.webp',
system: {
changes: [ changes: [
{ {
key: 'system.bonuses.roll.primaryWeapon.bonus', key: 'system.bonuses.roll.primaryWeapon.bonus',
type: 'add', mode: 2,
value: 1 value: 1
} }
] ]
} }
}
] ]
}, },
reloading: { reloading: {
@ -1392,7 +1341,7 @@ export const weaponFeatures = {
changes: [ changes: [
{ {
key: 'system.bonuses.damage.primaryWeapon.bonus', key: 'system.bonuses.damage.primaryWeapon.bonus',
type: 'add', mode: 2,
value: '@system.traits.agility.value', value: '@system.traits.agility.value',
priority: 21 priority: 21
} }

View file

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

View file

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

View file

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

View file

@ -13,19 +13,18 @@ export default class DHAttackAction extends DHDamageAction {
if (this.damage.includeBase) { if (this.damage.includeBase) {
const baseDamage = this.getParentHitPointDamage(); const baseDamage = this.getParentHitPointDamage();
if (baseDamage) { if (baseDamage) {
if (!this.damage.main) { if (!this.damage.parts.hitPoints) {
this.damage.main = baseDamage; this.damage.parts.hitPoints = baseDamage;
} else { } else {
for (const type of baseDamage.type) this.damage.main.type.add(type); for (const type of baseDamage.type) this.damage.parts.hitPoints.type.add(type);
this.damage.main.value.custom = { this.damage.parts.hitPoints.value.custom = {
enabled: true, enabled: true,
formula: `${baseDamage.value.getFormula()} + ${this.damage.main.value.getFormula()}` formula: `${baseDamage.value.getFormula()} + ${this.damage.parts.hitPoints.value.getFormula()}`
}; };
} }
} }
} }
if (this.roll.useDefault) { if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait; this.roll.trait = this.item.system.attack.roll.trait;
this.roll.type = 'attack'; this.roll.type = 'attack';
@ -34,28 +33,24 @@ export default class DHAttackAction extends DHDamageAction {
} }
getParentHitPointDamage() { getParentHitPointDamage() {
return this.item?.system?.attack.damage.main; return this.item?.system?.attack.damage.parts.hitPoints;
} }
get damageFormula() { get damageFormula() {
const hitPointsPart = this.damage.main; const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0'; if (!hitPointsPart) return '0';
return hitPointsPart.value.getFormula(); return hitPointsPart.value.getFormula();
} }
get altDamageFormula() { get altDamageFormula() {
const hitPointsPart = this.damage.main; const hitPointsPart = this.damage.parts.hitPoints;
if (!hitPointsPart) return '0'; if (!hitPointsPart) return '0';
return hitPointsPart.valueAlt.getFormula(); return hitPointsPart.valueAlt.getFormula();
} }
async use(event, options) { async use(event, options) {
if (this.item?.system.needsReload) {
return ui.notifications.error(_loc('DAGGERHEART.UI.Notifications.reloadRequired', { weapon: this.item.name }));
}
const result = await super.use(event, options); const result = await super.use(event, options);
if (result?.message?.system.action?.roll?.type === 'attack') { if (result?.message?.system.action?.roll?.type === 'attack') {
@ -66,23 +61,6 @@ export default class DHAttackAction extends DHDamageAction {
return result; return result;
} }
async handleReload(options = { awaitRoll: false }) {
const roll = await new Roll('1d6').evaluate();
if (game.modules.get('dice-so-nice')?.active) {
if (options.awaitRoll)
await game.dice3d.showForRoll(roll, game.user, true);
else
game.dice3d.showForRoll(roll, game.user, true);
}
const needsReload = roll.total === 1;
if (needsReload) {
this.item.update({ 'system.resource.value': 0 });
}
return { needsReload, rollValue: roll.total };
}
/** /**
* Generate a localized label array for this item subtype. * Generate a localized label array for this item subtype.
* @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects. * @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects.
@ -95,7 +73,7 @@ export default class DHAttackAction extends DHDamageAction {
if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`)); if (range) labels.push(game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.short`));
const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active;
for (const { value, valueAlt, type } of [damage.main, ...damage.resources].filter(d => !!d)) { for (const { value, valueAlt, type } of damage.parts) {
const usedValue = useAltDamage ? valueAlt : value; const usedValue = useAltDamage ? valueAlt : value;
const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {});
const str = damageString const str = damageString
@ -104,7 +82,7 @@ export default class DHAttackAction extends DHDamageAction {
x: game.i18n.localize('DAGGERHEART.GENERAL.damage') x: game.i18n.localize('DAGGERHEART.GENERAL.damage')
}); });
const icons = Array.from(type ?? []) const icons = Array.from(type)
.map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon)
.filter(Boolean); .filter(Boolean);

View file

@ -256,8 +256,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat();
await this.toChat(null, config);
return config; return config;
} }
@ -289,6 +288,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasEffect: this.hasEffect, hasEffect: this.hasEffect,
hasSave: this.hasSave, hasSave: this.hasSave,
onSave: this.save?.damageMod, onSave: this.save?.damageMod,
isDirect: !!this.damage?.direct,
selectedMessageMode: game.settings.get('core', 'messageMode'), selectedMessageMode: game.settings.get('core', 'messageMode'),
data: this.getRollData(), data: this.getRollData(),
evaluate: this.hasRoll, evaluate: this.hasRoll,
@ -306,20 +306,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}; };
if (this.damage) { if (this.damage) {
config.isDirect = !!this.damage.main?.direct; config.isDirect = this.damage.direct;
const groupAttackTokens = this.damage.main?.groupAttack const groupAttackTokens = this.damage.groupAttack
? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens( ? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens(
this.actor.id, this.actor.id,
this.damage.main.groupAttack this.damage.groupAttack
) )
: null; : null;
config.damageOptions = { config.damageOptions = {
groupAttack: this.damage.main?.groupAttack groupAttack: this.damage.groupAttack
? { ? {
numAttackers: Math.max(groupAttackTokens.length, 1), numAttackers: Math.max(groupAttackTokens.length, 1),
range: this.damage.main.groupAttack range: this.damage.groupAttack
} }
: null : null
}; };
@ -429,11 +429,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
} }
get hasDamage() { get hasDamage() {
return this.type !== 'healing' && (Boolean(this.damage?.main) || !foundry.utils.isEmpty(this.damage?.resources)); return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing';
} }
get hasHealing() { get hasHealing() {
return this.type === 'healing' && !foundry.utils.isEmpty(this.damage?.resources); return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing';
} }
get hasSave() { get hasSave() {
@ -469,25 +469,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return acc; return acc;
}, {}); }, {});
} }
if (source.damage?.parts && !source.damage.resources && !source.damage.main) {
source.damage.main = null;
source.damage.resources = {};
for (const [partKey, part] of Object.entries(source.damage.parts)) {
if (partKey === 'hitPoints' && source.type !== 'healing') {
source.damage.main = {
...part,
includeBase: source.damage.includeBase,
direct: source.damage.direct,
groupAttack: source.damage.groupAttack
};
} else {
source.damage.resources[partKey] = part;
}
}
delete source.damage.parts;
}
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,58 +0,0 @@
import { triggerChatRollFx } from '../../helpers/utils.mjs';
export class ChatDamageData extends foundry.abstract.DataModel {
constructor(data = {}, options = {}) {
super(data, options);
this._prepareRolls();
}
static defineSchema() {
const fields = foundry.data.fields;
return {
main: new fields.JSONField({ nullable: true, validate: ChatDamageData.#validateRoll}),
resources: new fields.TypedObjectField(new fields.JSONField({validate: ChatDamageData.#validateRoll}))
};
}
get active() {
return !!this.main || Boolean(Object.keys(this.resources).length);
}
static #validateRoll(rollJSON) {
if (rollJSON) {
const roll = JSON.parse(rollJSON);
if (!roll.evaluated) throw new Error('Roll objects added to ChatMessage documents must be evaluated');
}
}
_prepareRolls() {
this.main &&= Roll.fromData(this.main);
for (const key of Object.keys(this.resources)) {
this.resources[key] = Roll.fromData(this.resources[key]);
}
}
async rerollDamageDie(isResource, damageType, dice, resultIndex) {
const reroll = isResource ? this.resources[damageType] : this.main;
const rerollDice = reroll.dice[dice];
await rerollDice.rerollResult(resultIndex);
await reroll._evaluate();
const rerolledResult = rerollDice.results[rerollDice.results.length - 1];
if (rerolledResult) {
const fakeRoll = {
_evaluated: true,
dice: [new foundry.dice.terms.Die({
...rerollDice,
results: [rerolledResult],
total: rerolledResult.value,
faces: rerollDice.faces
})],
options: { appearance: {} }
};
await triggerChatRollFx([fakeRoll]);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -185,10 +185,8 @@ export default class D20Roll extends DHRoll {
return changeKeys; return changeKeys;
} }
static async buildEvaluate(roll, config = {}, message = {}) { static postEvaluate(roll, config = {}) {
await super.buildEvaluate(roll, config, message); const data = super.postEvaluate(roll, config);
const data = config.roll;
data.type = config.actionType; data.type = config.actionType;
data.difficulty = config.roll.difficulty; data.difficulty = config.roll.difficulty;
if (config.targets?.length) { if (config.targets?.length) {
@ -224,6 +222,7 @@ export default class D20Roll extends DHRoll {
}; };
}); });
data.modifierTotal = roll.modifierTotal; data.modifierTotal = roll.modifierTotal;
return data;
} }
resetFormula() { resetFormula() {

View file

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

View file

@ -1,8 +1,7 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import { triggerChatRollFx } from '../helpers/utils.mjs'; import { triggerChatRollFx } from '../helpers/utils.mjs';
import BaseRoll from './baseRoll.mjs';
export default class DHRoll extends BaseRoll { export default class DHRoll extends Roll {
baseTerms = []; baseTerms = [];
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false })); super(formula, data, foundry.utils.mergeObject(options, { roll: [] }, { overwrite: false }));
@ -34,17 +33,11 @@ export default class DHRoll extends BaseRoll {
if (config.skips?.createMessage) config.messageRoll = roll; if (config.skips?.createMessage) config.messageRoll = roll;
if (config.evaluate !== false) {
await this.buildEvaluate(roll, config, (message = {})); await this.buildEvaluate(roll, config, (message = {}));
}
await this.buildPost(roll, config, (message = {})); await this.buildPost(roll, config, (message = {}));
return config; return config;
} }
static createRollInstance(config) {
return new this(config.roll.formula, config.data, config);
}
/** /**
* @param {Partial<RollConfig>} config * @param {Partial<RollConfig>} config
* @returns {Promise<RollConfig>} * @returns {Promise<RollConfig>}
@ -62,7 +55,7 @@ export default class DHRoll extends BaseRoll {
this.temporaryModifierBuilder(config); this.temporaryModifierBuilder(config);
let roll = this.createRollInstance(config); let roll = new this(config.roll.formula, config.data, config);
if (config.dialog.configure !== false) { if (config.dialog.configure !== false) {
// Open Roll Dialog // Open Roll Dialog
const DialogClass = config.dialog?.class ?? this.DefaultDialog; const DialogClass = config.dialog?.class ?? this.DefaultDialog;
@ -79,30 +72,13 @@ export default class DHRoll extends BaseRoll {
return roll; return roll;
} }
/**
* Evaluates the roll and assigns roll data into the config.
* This is only called if config.evaluate is not set to false
* @protected
*/
static async buildEvaluate(roll, config = {}, message = {}) { static async buildEvaluate(roll, config = {}, message = {}) {
if (config.evaluate !== false) {
await roll.evaluate(); await roll.evaluate();
config.roll = { config.roll = this.postEvaluate(roll, config);
...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
}))
};
} }
/**
* Runs any post configuration events that need to happen towards the end, such as hooks and dice so nice
* @protected
*/
static async buildPost(roll, config, message) { static async buildPost(roll, config, message) {
for (const hook of config.hooks) { for (const hook of config.hooks) {
if (Hooks.call(`${CONFIG.DH.id}.postRoll${hook.capitalize()}`, config, message) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.postRoll${hook.capitalize()}`, config, message) === false) return null;
@ -115,13 +91,23 @@ export default class DHRoll extends BaseRoll {
} }
} }
static postEvaluate(roll, config = {}) {
return {
...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
}))
};
}
static async toMessage(roll, config) { static async toMessage(roll, config) {
const item = config.data.parent?.items?.get?.(config.source.item) ?? null; const item = config.data.parent?.items?.get?.(config.source.item) ?? null;
const actions = item ? [ const action = item ? item.system.actions.get(config.source.action) : null;
...item.system.actions,
...(item.system.attack?.id === config.source.action ? [item.system.attack] : [])
] : [];
const action = actions.find(x => x.id === config.source.action);
let actionDescription = null; let actionDescription = null;
if (action?.chatDisplay) { if (action?.chatDisplay) {
actionDescription = action actionDescription = action
@ -133,14 +119,6 @@ export default class DHRoll extends BaseRoll {
config.actionChatMessageHandled = true; config.actionChatMessageHandled = true;
} }
const reloadSetting =
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).reload;
const useReload =
item?.system.hasReload &&
action?.type === 'attack' &&
reloadSetting === CONFIG.DH.SETTINGS.reloadChoices.auto.id;
const reloadResult = useReload ? await action?.handleReload?.() : {};
const cls = getDocumentClass('ChatMessage'), const cls = getDocumentClass('ChatMessage'),
msgData = { msgData = {
type: this.messageType, type: this.messageType,
@ -148,11 +126,7 @@ export default class DHRoll extends BaseRoll {
title: roll.title, title: roll.title,
speaker: cls.getSpeaker({ actor: roll.data?.parent }), speaker: cls.getSpeaker({ actor: roll.data?.parent }),
sound: config.mute ? null : CONFIG.sounds.dice, sound: config.mute ? null : CONFIG.sounds.dice,
system: { system: { ...config, actionDescription },
...config,
actionDescription,
reloadCheckValue: reloadResult.rollValue
},
rolls: [roll] rolls: [roll]
}; };
@ -174,17 +148,14 @@ export default class DHRoll extends BaseRoll {
if (!this._evaluated) return; if (!this._evaluated) return;
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, { return foundry.applications.handlebars.renderTemplate(template, {
roll: this, roll: this,
...chatData, ...chatData,
action: chatData.action,
parent: chatData.parent, parent: chatData.parent,
targetMode: chatData.targetMode, targetMode: chatData.targetMode,
areas: chatData.action?.areas, areas: chatData.action?.areas,
metagamingSettings, metagamingSettings
automationSettings
}); });
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -232,41 +232,9 @@ export default class DualityRoll extends D20Roll {
return changeKeys; return changeKeys;
} }
/** @inheritdoc */
static async buildEvaluate(roll, config = {}, message = {}) { static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message); await super.buildEvaluate(roll, config, message);
config.roll.hope = {
dice: roll.dHope.denomination,
value: this.guaranteedCritical ? 0 : roll.dHope.total,
rerolled: {
any: roll.dHope.results.some(x => x.rerolled),
rerolls: roll.dHope.results.filter(x => x.rerolled)
}
};
config.roll.fear = {
dice: roll.dFear.denomination,
value: this.guaranteedCritical ? 0 : roll.dFear.total,
rerolled: {
any: roll.dFear.results.some(x => x.rerolled),
rerolls: roll.dFear.results.filter(x => x.rerolled)
}
};
config.roll.rally = {
dice: roll.dRally?.denomination,
value: roll.dRally?.total
};
config.roll.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
if (roll._rallyIndex && roll.data?.parent)
roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]);
}
/** @inheritdoc */
static async buildPost(roll, config, message) {
await setDiceSoNiceForDualityRoll( await setDiceSoNiceForDualityRoll(
roll, roll,
config.roll.advantage.type, config.roll.advantage.type,
@ -274,7 +242,44 @@ export default class DualityRoll extends D20Roll {
config.roll.fear.dice, config.roll.fear.dice,
config.roll.advantage.dice config.roll.advantage.dice
); );
}
static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config);
data.hope = {
dice: roll.dHope.denomination,
value: this.guaranteedCritical ? 0 : roll.dHope.total,
rerolled: {
any: roll.dHope.results.some(x => x.rerolled),
rerolls: roll.dHope.results.filter(x => x.rerolled)
}
};
data.fear = {
dice: roll.dFear.denomination,
value: this.guaranteedCritical ? 0 : roll.dFear.total,
rerolled: {
any: roll.dFear.results.some(x => x.rerolled),
rerolls: roll.dFear.results.filter(x => x.rerolled)
}
};
data.rally = {
dice: roll.dRally?.denomination,
value: roll.dRally?.total
};
data.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: this.guaranteedCritical ? 0 : roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
if (roll._rallyIndex && roll.data?.parent)
roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]);
return data;
}
static async buildPost(roll, config, message) {
await super.buildPost(roll, config, message); await super.buildPost(roll, config, message);
await DualityRoll.dualityUpdate(config); await DualityRoll.dualityUpdate(config);
@ -409,4 +414,8 @@ export default class DualityRoll extends D20Roll {
return rerolled; return rerolled;
} }
fromJSON(json) {
return super.fromJSON(json);
}
} }

View file

@ -75,23 +75,25 @@ export default class FateRoll extends D20Roll {
this.terms[0] = new foundry.dice.terms.Die({ faces: 12 }); this.terms[0] = new foundry.dice.terms.Die({ faces: 12 });
} }
/** @inheritdoc */
static async buildEvaluate(roll, config = {}, message = {}) { static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message); await super.buildEvaluate(roll, config, message);
config.roll.fate = {
dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination,
value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total,
fateDie: roll.fateDie
};
}
/** @inheritdoc */
static async buildPost(roll, config, message) {
if (roll.fateDie === 'Hope') { if (roll.fateDie === 'Hope') {
await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice); await setDiceSoNiceForHopeFateRoll(roll, config.roll.fate.dice);
} else { } else {
await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice); await setDiceSoNiceForFearFateRoll(roll, config.roll.fate.dice);
} }
return super.buildPost(roll, config, message); }
static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config);
data.fate = {
dice: roll.fateDie === 'Hope' ? roll.dHope.denomination : roll.dFear.denomination,
value: roll.fateDie === 'Hope' ? roll.dHope.total : roll.dFear.total,
fateDie: roll.fateDie
};
return data;
} }
} }

View file

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

View file

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

View file

@ -208,7 +208,10 @@ export default class DHItem extends foundry.documents.Item {
tags: this._getTags() tags: this._getTags()
}, },
actions: item.system.actionsList, actions: item.system.actionsList,
description: await this.system.getEnrichedDescription() description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.system.description, {
relativeTo: this.parent,
rollData: this.parent?.getRollData() ?? {}
})
}; };
const msg = { const msg = {

View file

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

View file

@ -4,55 +4,15 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
#wide = false; #wide = false;
#bordered = false; #bordered = false;
/** @inheritdoc */
async activate(element, options = {}) { async activate(element, options = {}) {
this.#wide = false; const { TextEditor } = foundry.applications.ux;
this.#bordered = false;
const isMacro = document.getElementById('action-bar').contains(element);
const macro = isMacro ? game.macros.get(game.user.hotbar[Number(element.dataset.slot)] ?? null) : null;
const macroItemUuid = macro?.type === 'script' ? macro.command.match(/await game\.system\.api\.applications\.ui\.DhHotbar\.useItem\("([^"]+)"\);/)?.[1] : null;
if (macroItemUuid && await fromUuid(macroItemUuid, { strict: false })) {
element.dataset.tooltip = `#item#${macroItemUuid}`;
options.direction = this.constructor.TOOLTIP_DIRECTIONS.UP;
}
let html = options.html; let html = options.html;
const key = element.dataset.tooltip?.match(/^#([\w-]+)#/)?.[1]; if (element.dataset.tooltip?.startsWith('#battlepoints#')) {
switch (key) {
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) {
this.#wide = true; this.#wide = true;
this.#bordered = true; this.#bordered = true;
const html = await this.getBattlepointHTML(element.dataset.combatId); html = await this.getBattlepointHTML(element.dataset.combatId);
options.direction = this._determineItemTooltipDirection(element); options.direction = this._determineItemTooltipDirection(element);
super.activate(element, { ...options, html: html }); 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 => { lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => {
element.addEventListener('input', this.toggleModifier.bind(this)); 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; this.#bordered = true;
let effect = {}; let effect = {};
if (element.dataset.uuid) { 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`, `systems/daggerheart/templates/ui/tooltip/effect-display.hbs`,
{ {
effect effect
@ -120,11 +84,11 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
this.tooltip.innerHTML = html; this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element); options.direction = this._determineItemTooltipDirection(element);
} else {
return html; this.#bordered = false;
} }
async #activateItem(element, options) { if (element.dataset.tooltip?.startsWith('#item#')) {
const itemUuid = element.dataset.tooltip.slice(6); const itemUuid = element.dataset.tooltip.slice(6);
const item = await foundry.utils.fromUuid(itemUuid); const item = await foundry.utils.fromUuid(itemUuid);
if (item) { if (item) {
@ -133,7 +97,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
await this.enrichText(item); await this.enrichText(item);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type; 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`, `systems/daggerheart/templates/ui/tooltip/${type}.hbs`,
{ {
item: item, item: item,
@ -144,20 +108,17 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
); );
this.tooltip.innerHTML = html; this.tooltip.innerHTML = html;
options.direction ??= this._determineItemTooltipDirection(element); options.direction = this._determineItemTooltipDirection(element);
return html;
} }
} else {
return null; const attack = element.dataset.tooltip?.startsWith('#attack#');
} if (attack) {
async #activateAttack(element, options) {
const actorUuid = element.dataset.tooltip.slice(8); const actorUuid = element.dataset.tooltip.slice(8);
const actor = await foundry.utils.fromUuid(actorUuid); const actor = await foundry.utils.fromUuid(actorUuid);
const attack = actor.system.attack; const attack = actor.system.attack;
const description = await foundry.applications.ux.TextEditor.enrichHTML(attack.description); const description = await TextEditor.enrichHTML(attack.description);
const html = await foundry.applications.handlebars.renderTemplate( html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/attack.hbs`, `systems/daggerheart/templates/ui/tooltip/attack.hbs`,
{ {
attack: attack, attack: attack,
@ -168,57 +129,19 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
); );
this.tooltip.innerHTML = html; this.tooltip.innerHTML = html;
return html;
} }
async #activateAdvantageDisadvantage(element, options) { const shortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#'); const longRest = element.dataset.tooltip?.startsWith('#longRest#');
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14); if (shortRest || longRest) {
const actor = await foundry.utils.fromUuid(actorUuid); 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[ const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[
element.dataset.restType element.dataset.restType
].moves; ].moves;
const move = moves[key]; const move = moves[key];
const description = await foundry.applications.ux.TextEditor.enrichHTML(move.description); const description = await TextEditor.enrichHTML(move.description);
const html = await foundry.applications.handlebars.renderTemplate( html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`, `systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{ {
move: move, move: move,
@ -231,7 +154,49 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
element, element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT 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) { _setAnchor(direction) {

View file

@ -58,7 +58,7 @@ export const renderMeasuredTemplate = async event => {
if (!type || !range || !game.canvas.scene) return; if (!type || !range || !game.canvas.scene) return;
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({
shapeType: type, shapetype: type,
angle, angle,
range, range,
direction, direction,

View file

@ -35,24 +35,23 @@ export const enricherConfig = [
]; ];
export const enricherRenderSetup = element => { export const enricherRenderSetup = element => {
const clickWrapper = func => event => {
event.stopPropagation();
func(event);
};
element element
.querySelectorAll('.enriched-damage-button') .querySelectorAll('.enriched-damage-button')
.forEach(element => element.addEventListener('click', clickWrapper(renderDamageButton))); .forEach(element => element.addEventListener('click', renderDamageButton));
element element
.querySelectorAll('.duality-roll-button') .querySelectorAll('.duality-roll-button')
.forEach(element => element.addEventListener('click', clickWrapper(renderDualityButton))); .forEach(element => element.addEventListener('click', renderDualityButton));
element element
.querySelectorAll('.fate-roll-button') .querySelectorAll('.fate-roll-button')
.forEach(element => element.addEventListener('click', clickWrapper(renderFateButton))); .forEach(element => element.addEventListener('click', renderFateButton));
element element
.querySelectorAll('.measured-template-button') .querySelectorAll('.measured-template-button')
.forEach(element => element.addEventListener('click', clickWrapper(renderMeasuredTemplate))); .forEach(element => element.addEventListener('click', renderMeasuredTemplate));
// element
// .querySelectorAll('.enriched-effect')
// .forEach(element => element.addEventListener('dragstart', dragEnrichedEffect));
}; };

View file

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

View file

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

View file

@ -1,39 +0,0 @@
import { MigrationHandlerBase } from './base.mjs';
export class Migration_2_5_2 extends MigrationHandlerBase {
version = '2.5.2';
/** @inheritdoc */
async updateActiveEffectSource(effectSource, item) {
let shouldUpdate = false;
const newChanges = [];
const srdItem = item?._stats?.compendiumSource ?
await foundry.utils.fromUuid(item?._stats.compendiumSource) :
null;
for (let i = 0; i < effectSource.system.changes.length; i++) {
const change = effectSource.system.changes[i];
const srdEffect = srdItem?.effects.find(x => x.name === effectSource.name);
if (change.type === 'custom') {
const srdChange = srdEffect ? srdEffect.system.changes[i] : null;
if (srdChange &&
change.key === srdChange.key &&
change.value === srdChange.value &&
change.type !== srdChange.type
) {
shouldUpdate = true;
newChanges.push(srdChange);
}
} else {
newChanges.push(change);
}
}
if (shouldUpdate) {
return {
_id: effectSource._id,
system: { changes: newChanges }
}
}
}
}

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,117 +0,0 @@
/**
* @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.
* Generally a subclass will override the version and the updateXSource() functions.
*/
export class MigrationHandlerBase {
version = null;
/**
* Gets change data for an active effect's source, or null if no changes
* @param {object} effectSource
* @param {DHItem} item
* @returns {Promise<object>}
* @protected
*/
async updateActiveEffectSource(effectSource, item) {
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
const numActors = game.actors.size;
const numItems = game.items.size;
const finalUpdateProgress = 5;
const DhProgress = game.system.api.applications.ui.DhProgress;
const preRunProgress = game.packs.size;
const progress = DhProgress.createMigrationProgress(
preRunProgress + numActors + numItems + finalUpdateProgress
);
// Preload. Avoid hardcoding in the future
for (const pack of game.packs) {
await pack.getDocuments();
progress.advance();
}
const batch = [];
const updateItem = async item => {
const itemUpdates = [];
for (const effect of item.effects) {
const changes = await this.updateActiveEffectSource(effect.toObject(), item);
if (changes) itemUpdates.push(changes);
}
if (itemUpdates.length) {
batch.push({
action: 'update',
documentName: 'ActiveEffect',
updates: itemUpdates,
parent: item
});
}
};
const updateActor = async actor => {
const actorUpdate = await this.updateActorSource(actor);
if (actorUpdate) {
batch.push({
action: 'update',
documentName: 'Actor',
updates: [actorUpdate]
});
}
const aeUpdates = [];
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) {
await updateItem(item);
progress.advance();
}
await foundry.documents.modifyBatch(batch);
progress.advance({ by: finalUpdateProgress });
}
}

View file

@ -1,6 +1,4 @@
import { defaultRestOptions } from '../config/generalConfig.mjs'; import { defaultRestOptions } from '../config/generalConfig.mjs';
import { Migration_2_5_2 } from './migration-handlers/2_5_2.mjs';
import { Migration_2_6_0 } from './migration-handlers/2_6_0.mjs';
export async function runMigrations() { export async function runMigrations() {
let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion); let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion);
@ -322,20 +320,7 @@ export async function runMigrations() {
lastMigrationVersion = '2.1.0'; lastMigrationVersion = '2.1.0';
} }
//#endregion
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion);
/* -------------------------------------------- */
/* New Style migrations below this point */
/* -------------------------------------------- */
const migrations = [
new Migration_2_5_2(),
new Migration_2_6_0()
].filter(m => m.version && foundry.utils.isNewerVersion(m.version, lastMigrationVersion));
for (const handler of migrations) {
await handler.migrate();
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, handler.version);
}
} }

View file

@ -325,7 +325,7 @@
"changes": [ "changes": [
{ {
"key": "system.evasion", "key": "system.evasion",
"type": "override", "mode": 5,
"value": "@system.evasion / 2", "value": "@system.evasion / 2",
"priority": 10 "priority": 10
} }

View file

@ -328,7 +328,7 @@
"changes": [ "changes": [
{ {
"key": "system.disadvantageSources", "key": "system.disadvantageSources",
"type": "add", "mode": 2,
"value": "Agility Rolls", "value": "Agility Rolls",
"priority": null "priority": null
} }

View file

@ -338,7 +338,7 @@
"changes": [ "changes": [
{ {
"key": "system.advantageSources", "key": "system.advantageSources",
"type": "add", "mode": 2,
"value": "On attacks if they are Hidden.", "value": "On attacks if they are Hidden.",
"priority": null "priority": null
} }

View file

@ -235,7 +235,7 @@
"changes": [ "changes": [
{ {
"key": "system.rules.conditionImmunities.restrained", "key": "system.rules.conditionImmunities.restrained",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }
@ -298,7 +298,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.magical.resistance", "key": "system.resistance.magical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -462,7 +462,7 @@
"changes": [ "changes": [
{ {
"key": "system.bonuses.damage.physical.bonus", "key": "system.bonuses.damage.physical.bonus",
"type": "add", "mode": 2,
"value": "10", "value": "10",
"priority": null "priority": null
} }

View file

@ -273,7 +273,7 @@
"changes": [ "changes": [
{ {
"key": "system.bonuses.roll.attack.bonus", "key": "system.bonuses.roll.attack.bonus",
"type": "add", "mode": 2,
"value": "ITEM.@system.resource.value", "value": "ITEM.@system.resource.value",
"priority": 21 "priority": 21
} }

View file

@ -241,7 +241,7 @@
"changes": [ "changes": [
{ {
"key": "system.rules.attack.damage.hpDamageMultiplier", "key": "system.rules.attack.damage.hpDamageMultiplier",
"type": "override", "mode": 5,
"value": "2", "value": "2",
"priority": null "priority": null
} }

View file

@ -243,7 +243,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.resistance", "key": "system.resistance.physical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }
@ -306,7 +306,7 @@
"changes": [ "changes": [
{ {
"key": "system.rules.attack.damage.hpDamageMultiplier", "key": "system.rules.attack.damage.hpDamageMultiplier",
"type": "override", "mode": 5,
"value": "2", "value": "2",
"priority": null "priority": null
} }

View file

@ -268,7 +268,7 @@
"changes": [ "changes": [
{ {
"key": "system.difficulty", "key": "system.difficulty",
"type": "add", "mode": 2,
"value": "3", "value": "3",
"priority": null "priority": null
} }

View file

@ -272,7 +272,7 @@
"changes": [ "changes": [
{ {
"key": "system.difficulty", "key": "system.difficulty",
"type": "add", "mode": 2,
"value": "2", "value": "2",
"priority": null "priority": null
} }

View file

@ -343,7 +343,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.reduction", "key": "system.resistance.physical.reduction",
"type": "add", "mode": 2,
"value": "7", "value": "7",
"priority": null "priority": null
} }

View file

@ -326,7 +326,7 @@
"changes": [ "changes": [
{ {
"key": "system.disadvantageSources", "key": "system.disadvantageSources",
"type": "add", "mode": 2,
"value": "Your next action", "value": "Your next action",
"priority": null "priority": null
} }

View file

@ -237,7 +237,7 @@
"changes": [ "changes": [
{ {
"key": "system.rules.attack.damage.hpDamageMultiplier", "key": "system.rules.attack.damage.hpDamageMultiplier",
"type": "override", "mode": 5,
"value": "2", "value": "2",
"priority": null "priority": null
} }

View file

@ -378,7 +378,7 @@
"changes": [ "changes": [
{ {
"key": "system.advantageSources", "key": "system.advantageSources",
"type": "add", "mode": 2,
"value": "Attacks made while Hidden", "value": "Attacks made while Hidden",
"priority": null "priority": null
} }

View file

@ -349,7 +349,7 @@
"changes": [ "changes": [
{ {
"key": "system.evasion", "key": "system.evasion",
"type": "override", "mode": 5,
"value": "@system.evasion / 2", "value": "@system.evasion / 2",
"priority": 10 "priority": 10
} }

View file

@ -253,7 +253,7 @@
"changes": [ "changes": [
{ {
"key": "system.difficulty", "key": "system.difficulty",
"type": "add", "mode": 2,
"value": "2", "value": "2",
"priority": null "priority": null
} }
@ -316,7 +316,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.reduction", "key": "system.resistance.physical.reduction",
"type": "add", "mode": 2,
"value": "3", "value": "3",
"priority": null "priority": null
} }

View file

@ -238,7 +238,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.magical.resistance", "key": "system.resistance.magical.resistance",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -316,7 +316,7 @@
"changes": [ "changes": [
{ {
"key": "system.disadvantageSources", "key": "system.disadvantageSources",
"type": "add", "mode": 2,
"value": "On your next action roll.", "value": "On your next action roll.",
"priority": null "priority": null
} }

View file

@ -238,7 +238,7 @@
"changes": [ "changes": [
{ {
"key": "system.rules.attack.damage.hpDamageMultiplier", "key": "system.rules.attack.damage.hpDamageMultiplier",
"type": "override", "mode": 5,
"value": "2", "value": "2",
"priority": null "priority": null
} }

View file

@ -240,7 +240,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.resistance", "key": "system.resistance.physical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -273,7 +273,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.resistance", "key": "system.resistance.physical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -273,7 +273,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.resistance", "key": "system.resistance.physical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -273,7 +273,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.resistance", "key": "system.resistance.physical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -300,7 +300,7 @@
"changes": [ "changes": [
{ {
"key": "system.disadvantageSources", "key": "system.disadvantageSources",
"type": "add", "mode": 2,
"value": "On attack rolls while you're within Very Close range of the Sentinel", "value": "On attack rolls while you're within Very Close range of the Sentinel",
"priority": null "priority": null
} }

View file

@ -305,7 +305,7 @@
"changes": [ "changes": [
{ {
"key": "system.evasion", "key": "system.evasion",
"type": "override", "mode": 5,
"value": "@system.evasion / 2", "value": "@system.evasion / 2",
"priority": 10 "priority": 10
} }

View file

@ -379,7 +379,7 @@
"changes": [ "changes": [
{ {
"key": "system.difficulty", "key": "system.difficulty",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -324,7 +324,7 @@
"changes": [ "changes": [
{ {
"key": "system.difficulty", "key": "system.difficulty",
"type": "add", "mode": 2,
"value": "3", "value": "3",
"priority": null "priority": null
} }
@ -387,7 +387,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.resistance", "key": "system.resistance.physical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -262,7 +262,7 @@
"changes": [ "changes": [
{ {
"key": "system.resistance.physical.resistance", "key": "system.resistance.physical.resistance",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -27,13 +27,13 @@
"changes": [ "changes": [
{ {
"key": "system.bonuses.rest.shortRest.shortMoves", "key": "system.bonuses.rest.shortRest.shortMoves",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
}, },
{ {
"key": "system.bonuses.rest.longRest.longMoves", "key": "system.bonuses.rest.longRest.longMoves",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -27,7 +27,7 @@
"changes": [ "changes": [
{ {
"key": "system.advantageSources", "key": "system.advantageSources",
"type": "add", "mode": 2,
"value": "Dread Visage: Rolls to intimidate hostile creatures", "value": "Dread Visage: Rolls to intimidate hostile creatures",
"priority": null "priority": null
} }

View file

@ -27,13 +27,13 @@
"changes": [ "changes": [
{ {
"key": "system.bonuses.rest.shortRest.longMoves", "key": "system.bonuses.rest.shortRest.longMoves",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
}, },
{ {
"key": "system.bonuses.rest.shortRest.shortMoves", "key": "system.bonuses.rest.shortRest.shortMoves",
"type": "add", "mode": 2,
"value": "-1", "value": "-1",
"priority": null "priority": null
} }

View file

@ -27,7 +27,7 @@
"changes": [ "changes": [
{ {
"key": "system.resources.hitPoints.max", "key": "system.resources.hitPoints.max",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -27,7 +27,7 @@
"changes": [ "changes": [
{ {
"key": "system.resources.stress.max", "key": "system.resources.stress.max",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -27,7 +27,7 @@
"changes": [ "changes": [
{ {
"key": "system.advantageSources", "key": "system.advantageSources",
"type": "add", "mode": 2,
"value": "Agility Rolls that involve balancing and climbing", "value": "Agility Rolls that involve balancing and climbing",
"priority": null "priority": null
} }

View file

@ -27,7 +27,7 @@
"changes": [ "changes": [
{ {
"key": "system.evasion", "key": "system.evasion",
"type": "add", "mode": 2,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -27,7 +27,7 @@
"changes": [ "changes": [
{ {
"key": "system.rules.damageReduction.stressDamageReduction.severe.cost", "key": "system.rules.damageReduction.stressDamageReduction.severe.cost",
"type": "override", "mode": 5,
"value": "1", "value": "1",
"priority": null "priority": null
} }

View file

@ -27,13 +27,13 @@
"changes": [ "changes": [
{ {
"key": "system.damageThresholds.major", "key": "system.damageThresholds.major",
"type": "add", "mode": 2,
"value": "@prof", "value": "@prof",
"priority": 21 "priority": 21
}, },
{ {
"key": "system.damageThresholds.severe", "key": "system.damageThresholds.severe",
"type": "add", "mode": 2,
"value": "@prof", "value": "@prof",
"priority": 21 "priority": 21
} }

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