Compare commits

..

No commits in common. "e2414b167a9d73553bbd3b63b36d38223ed4686a" and "553941e22f6aadeebfa014cc7d4d6091fc0ca872" have entirely different histories.

496 changed files with 5769 additions and 4656 deletions

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 => ({
@ -397,7 +390,7 @@ const updateActorsRangeDependentEffects = async token => {
).rangeMeasurement; ).rangeMeasurement;
for (let effect of token.actor?.allApplicableEffects() ?? []) { for (let effect of token.actor?.allApplicableEffects() ?? []) {
if (!effect.system.rangeDependence) continue; if (!effect.system.rangeDependence?.enabled) continue;
const { target, range, type } = effect.system.rangeDependence; const { target, range, type } = effect.system.rangeDependence;
// If there are no targets, assume false. Otherwise, start with the effect enabled. // If there are no targets, assume false. Otherwise, start with the effect enabled.

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",
@ -173,7 +171,9 @@
}, },
"ACTIVEEFFECT": { "ACTIVEEFFECT": {
"Config": { "Config": {
"rangeDependence": { "title": "Range Dependence" }, "rangeDependence": {
"title": "Range Dependence"
},
"stacking": { "title": "Stacking" }, "stacking": { "title": "Stacking" },
"targetDispositions": "Affected Dispositions" "targetDispositions": "Affected Dispositions"
}, },
@ -778,7 +778,7 @@
"removeRoll": "Remove Roll", "removeRoll": "Remove Roll",
"resultsHint": "Results will appear when characters roll", "resultsHint": "Results will appear when characters roll",
"selectLeaderHint": "Select one Character to be the leader", "selectLeaderHint": "Select one Character to be the leader",
"selectParticipantsHint": "Select all Characters who will participate in the roll", "selectParticipantsHint": "Select one Character to be the leader",
"startGroupRoll": "Start Group Roll", "startGroupRoll": "Start Group Roll",
"title": "Group Roll" "title": "Group Roll"
}, },
@ -2253,7 +2253,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",
@ -2491,6 +2491,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"
@ -2557,9 +2558,6 @@
}, },
"identifier": { "identifier": {
"label": "Identifier" "label": "Identifier"
},
"gmNotes": {
"label": "GM Notes"
} }
}, },
"Ancestry": { "Ancestry": {
@ -2575,9 +2573,6 @@
"severe": "Severe Threshold" "severe": "Severe Threshold"
} }
}, },
"Base": {
"addGMNote": "Add GM Note"
},
"Beastform": { "Beastform": {
"FIELDS": { "FIELDS": {
"beastformType": { "label": "Beastform Type" }, "beastformType": { "label": "Beastform Type" },
@ -3250,7 +3245,7 @@
"beastformToManyAdvantages": "You cannot select any more advantages.", "beastformToManyAdvantages": "You cannot select any more advantages.",
"beastformToManyFeatures": "You cannot select any more features.", "beastformToManyFeatures": "You cannot select any more features.",
"beastformEquipWeapon": "You cannot use weapons while in a Beastform.", "beastformEquipWeapon": "You cannot use weapons while in a Beastform.",
"loadoutMaxReached": "You've reached the maximum loadout size. To add more, move at least one domain card to the vault.", "loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.",
"domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.", "domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.",
"insufficientResources": "You don't have enough resources to use that action.", "insufficientResources": "You don't have enough resources to use that action.",
"actionNoUsesRemaining": "That action doesn't have remaining uses.", "actionNoUsesRemaining": "That action doesn't have remaining uses.",

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,15 +139,13 @@ 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', mode: 2,
type: 'add', value: 'true'
value: 'true' }
} ]
]
}
} }
]); ]);
@ -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,7 +1,7 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
import { shouldUseHopeFearAutomation } from '../../helpers/utils.mjs'; import { shouldUseHopeFearAutomation } from '../../helpers/utils.mjs';
import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import PartySheet from '../sheets/actors/party.mjs'; import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -11,7 +11,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
this.party = party; this.party = party;
this.partyMembers = party.system.partyMembers this.partyMembers = party.system.partyMembers
.filter(x => PartySheet.DICE_ROLL_ACTOR_TYPES.includes(x.type)) .filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
.map(member => ({ .map(member => ({
...member.toObject(), ...member.toObject(),
uuid: member.uuid, uuid: member.uuid,

View file

@ -1,9 +1,8 @@
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';
import PartySheet from '../sheets/actors/party.mjs'; import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -14,7 +13,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
this.usesTagTeamHopeCost = true; this.usesTagTeamHopeCost = true;
this.party = party; this.party = party;
this.partyMembers = party.system.partyMembers this.partyMembers = party.system.partyMembers
.filter(x => PartySheet.DICE_ROLL_ACTOR_TYPES.includes(x.type)) .filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
.map(member => ({ .map(member => ({
...member.toObject(), ...member.toObject(),
uuid: member.uuid, uuid: member.uuid,
@ -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({
@ -209,16 +208,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const rollOptions = []; const rollOptions = [];
const damageRollOptions = []; const damageRollOptions = [];
if (actor?.system.usedUnarmed) {
damageRollOptions.push({
value: actor.system.attack.uuid,
label: actor.system.usedUnarmed.name,
group: actor.name,
baseAction: actor.system.attack
});
}
for (const item of actor?.items ?? []) { for (const item of actor?.items ?? []) {
if (!item.system.metadata.hasActions) continue; if (!item.system.metadata.hasActions) continue;
const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])]; const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
@ -236,7 +225,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 +237,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 +369,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
let rollIsSelected = false; let rollIsSelected = false;
for (const member of Object.values(members)) { for (const member of Object.values(members)) {
const rollFinished = Boolean(member.rollData); const rollFinished = Boolean(member.rollData);
const damageFinished = member.rollData?.options?.hasDamage ? member.damageRollData.active : true; const damageFinished = member.rollData?.options?.hasDamage ? Boolean(member.rollData.options.damage) : true;
rollsAreFinished = rollsAreFinished && rollFinished && damageFinished; rollsAreFinished = rollsAreFinished && rollFinished && damageFinished;
rollIsSelected = rollIsSelected || member.selected; rollIsSelected = rollIsSelected || member.selected;
@ -540,10 +530,17 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
await action.workflow.get('damage').execute(config, null, true); await action.workflow.get('damage').execute(config, null, true);
if (!config.damage) return; if (!config.damage) return;
const current = this.party.system.tagTeam.members[memberKey].rollData;
await this.updatePartyData( await this.updatePartyData(
{ {
[`system.tagTeam.members.${memberKey}.damageRollData`]: config.damage [`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: config.damage
}
}
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
); );
@ -551,11 +548,15 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
static async #removeDamageRoll(_, button) { static async #removeDamageRoll(_, button) {
const { memberKey } = button.dataset; const { memberKey } = button.dataset;
const current = this.party.system.tagTeam.members[memberKey].rollData;
this.updatePartyData( this.updatePartyData(
{ {
[`system.tagTeam.members.${memberKey}.damageRollData`]: { [`system.tagTeam.members.${memberKey}.rollData`]: {
main: null, ...current,
resources: _replace({}) options: {
...current.options,
damage: null
}
} }
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
@ -563,44 +564,89 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
} }
static async #rerollDamageDice(_, button) { static async #rerollDamageDice(_, button) {
const { isResource, memberKey, damageKey, diceIndex, resultIndex } = button.dataset; const { memberKey, damageKey, part, dice } = button.dataset;
const memberData = this.party.system.tagTeam.members[memberKey]; const memberData = this.party.system.tagTeam.members[memberKey];
await memberData.damageRollData.rerollDamageDie(isResource, damageKey, diceIndex, resultIndex); const partData = memberData.rollData.options.damage[damageKey].parts[part];
const activeDiceResultKey = Object.keys(partData.dice[dice].results).find(
index => partData.dice[dice].results[index].active
);
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(
partData,
dice,
activeDiceResultKey
);
const rollData = this.party.system.tagTeam.members[memberKey].rollData;
rollData.options.damage[damageKey].parts = rollData.options.damage[damageKey].parts.map((damagePart, index) => {
if (index !== Number.parseInt(part)) return damagePart;
return {
...damagePart,
total: parsedRoll.total,
dice: rerolledDice
};
});
rollData.options.damage[damageKey].total = rollData.options.damage[damageKey].parts.reduce((acc, part) => {
acc += part.total;
return acc;
}, 0);
const basePath = `system.tagTeam.members.${memberKey}.damageRollData`;
const updatePath = isResource ? `${basePath}.resources.${damageKey}` : `${basePath}.main`;
const updateValue = isResource ?
memberData.damageRollData.resources[damageKey] : memberData.damageRollData.main;
this.updatePartyData( this.updatePartyData(
{ {
[updatePath]: updateValue.toJSON() [`system.tagTeam.members.${memberKey}.rollData`]: rollData
}, },
this.getUpdatingParts(button) this.getUpdatingParts(button)
); );
} }
async getCriticalDamage(origDamage) { async getCriticalDamage(damage) {
const newDamage = origDamage ? ChatDamageData.fromJSON(JSON.stringify(origDamage)) : null; const newDamage = foundry.utils.deepClone(damage);
if (newDamage?.main) { for (let key in newDamage) {
const criticalDamage = await getCritDamageBonus(newDamage.main.formula); var damage = newDamage[key];
if (criticalDamage) { damage.formula = '';
const criticalTerm = new foundry.dice.terms.NumericTerm({ number: criticalDamage, evaluated: true }); damage.total = 0;
criticalTerm.evaluate();
newDamage.main = await Roll.fromTerms([ for (let part of damage.parts) {
...origDamage.main.terms, const criticalDamage = await getCritDamageBonus(part.formula);
new foundry.dice.terms.OperatorTerm({ operator: '+' }), if (criticalDamage) {
criticalTerm part.modifierTotal += criticalDamage;
]); part.total += criticalDamage;
newDamage.main.options = foundry.utils.deepClone(origDamage.main.options); part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
part.roll = new Roll(part.formula);
}
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
damage.total += part.total;
} }
} }
return newDamage;
}
async getNonCriticalDamage(config) {
const newDamage = foundry.utils.deepClone(config.damage);
for (let key in newDamage) {
var damage = newDamage[key];
damage.formula = '';
damage.total = 0;
for (let part of damage.parts) {
const critDamageBonus = await getCritDamageBonus(part.formula);
part.modifierTotal -= critDamageBonus;
part.total -= critDamageBonus;
part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
part.roll = new Roll(part.formula);
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
damage.total += part.total;
}
}
return newDamage; return newDamage;
} }
static async #selectRoll(_, button) { static async #selectRoll(_, button) {
const { memberKey } = button.dataset; const { memberKey } = button.dataset;
this.updatePartyData( this.updatePartyData(
{ {
[`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce( [`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce(
@ -611,12 +657,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 +675,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 +752,13 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor); const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor);
mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle'); mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
/* This could assumably be done better. For some reason rolls don't get correctly done through rollData.toJSON */
const systemData = {
...mainRoll.options,
damage: joinedRoll.damageRollData?.toJSON()
};
if (joinedRoll.damageRollData.main) {
systemData.damage.main = joinedRoll.damageRollData.toJSON();
}
for (const type of Object.keys(joinedRoll.damageRollData?.resources ?? {})) {
systemData.damage.resources[type] = joinedRoll.damageRollData.resources[type].toJSON();
}
const cls = getDocumentClass('ChatMessage'), const cls = getDocumentClass('ChatMessage'),
msgData = { msgData = {
type: 'dualityRoll', type: 'dualityRoll',
user: game.user.id, user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'), title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: mainActor }), speaker: cls.getSpeaker({ actor: mainActor }),
system: systemData, system: mainRoll.options,
rolls: [JSON.stringify(joinedRoll.roll)], rolls: [JSON.stringify(joinedRoll.roll)],
sound: null, sound: null,
flags: { core: { RollTable: true } } flags: { core: { RollTable: true } }

View file

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

View file

@ -155,10 +155,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
.querySelector('.stacking-change-checkbox') .querySelector('.stacking-change-checkbox')
?.addEventListener('change', this.stackingChangeToggle.bind(this)); ?.addEventListener('change', this.stackingChangeToggle.bind(this));
htmlElement
.querySelector('.range-dependence-change-checkbox')
?.addEventListener('change', this.rangeDependenceChangeToggle.bind(this));
htmlElement htmlElement
.querySelector('.armor-change-checkbox') .querySelector('.armor-change-checkbox')
?.addEventListener('change', this.armorChangeToggle.bind(this)); ?.addEventListener('change', this.armorChangeToggle.bind(this));
@ -228,20 +224,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return this.submit({ updateData: { system: systemData } }); return this.submit({ updateData: { system: systemData } });
} }
rangeDependenceChangeToggle(event) {
const rangeFields = this.document.system.schema.fields.rangeDependence.fields;
const systemData = {
rangeDependence: event.target.checked
? _replace({
type: rangeFields.type.initial,
target: rangeFields.target.initial,
range: rangeFields.range.initial
})
: null
};
return this.submit({ updateData: { system: systemData } });
}
armorChangeToggle(event) { armorChangeToggle(event) {
if (event.target.checked) { if (event.target.checked) {
this.addArmorChange(); this.addArmorChange();

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

@ -1,18 +1,8 @@
import DhCompanion from '../../../data/actor/companion.mjs'; import DhCompanion from '../../../data/actor/companion.mjs';
import DhParty from '../../../data/actor/party.mjs';
import DhpActor from '../../../documents/actor.mjs'; import DhpActor from '../../../documents/actor.mjs';
declare module './companion.mjs' { declare module './companion.mjs' {
export default interface DhCompanionSheet { export default interface DhCompanionSheet {
actor: DhpActor<DhCompanion>; actor: DhpActor<DhCompanion>;
document: DhpActor<DhCompanion>;
} }
} }
declare module './party.mjs' {
export default interface PartySheet {
actor: DhpActor<DhParty>;
document: DhpActor<DhParty>;
}
}

View file

@ -915,7 +915,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
const doc = await getDocFromElement(button); const doc = await getDocFromElement(button);
const { available } = this.document.system.loadoutSlot; const { available } = this.document.system.loadoutSlot;
if (doc.system.inVault && !available && !doc.system.loadoutIgnore) { if (doc.system.inVault && !available && !doc.system.loadoutIgnore) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.loadoutMaxReached', { localize: true }); return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached'));
} }
await doc?.update({ 'system.inVault': !doc.system.inVault }); await doc?.update({ 'system.inVault': !doc.system.inVault });
@ -1216,22 +1216,10 @@ export default class CharacterSheet extends DHBaseActorSheet {
if (!confirmed) return; if (!confirmed) return;
} }
// Check for same actor drag/drop attempts if (this.document.uuid === item.parent?.uuid) {
const isSameActor = this.document.uuid === item.parent?.uuid;
const loadoutFieldset = event.target.closest('[data-in-vault]');
const vaulting = loadoutFieldset?.dataset.inVault === 'true';
if (isSameActor && loadoutFieldset && item.type === 'domainCard' && vaulting !== item.system.inVault) {
// This is likely an attempt to vault or unvault an item
const { available } = this.document.system.loadoutSlot;
if (!vaulting && !available && !item.system.loadoutIgnore) {
return ui.notifications.warn('DAGGERHEART.UI.Notifications.loadoutMaxReached', { localize: true });
}
return item.update({ 'system.inVault': vaulting });
} else if (isSameActor) {
return super._onDropItem(event, item); return super._onDropItem(event, item);
} }
// Handle beastforms
if (item.type === 'beastform') { if (item.type === 'beastform') {
if (this.document.effects.find(x => x.type === 'beastform')) { if (this.document.effects.find(x => x.type === 'beastform')) {
return ui.notifications.warn( return ui.notifications.warn(

View file

@ -6,9 +6,7 @@ import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs';
import DhpActor from '../../../documents/actor.mjs'; import DhpActor from '../../../documents/actor.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ export default class Party extends DHBaseActorSheet {
export default class PartySheet extends DHBaseActorSheet {
constructor(options) { constructor(options) {
super(options); super(options);
@ -26,17 +24,17 @@ export default class PartySheet extends DHBaseActorSheet {
resizable: true resizable: true
}, },
actions: { actions: {
openDocument: PartySheet.#onOpenDocument, openDocument: Party.#openDocument,
deletePartyMember: PartySheet.#onDeletePartyMember, deletePartyMember: Party.#deletePartyMember,
toggleHope: PartySheet.#onToggleHope, toggleHope: Party.#toggleHope,
toggleHitPoints: PartySheet.#onToggleHitPoints, toggleHitPoints: Party.#toggleHitPoints,
toggleStress: PartySheet.#onToggleStress, toggleStress: Party.#toggleStress,
toggleArmorSlot: PartySheet.#onToggleArmorSlot, toggleArmorSlot: Party.#toggleArmorSlot,
tempBrowser: PartySheet.#onTempBrowser, tempBrowser: Party.#tempBrowser,
refreshActions: PartySheet.#onRefreshActions, refeshActions: Party.#refeshActions,
triggerRest: PartySheet.#onTriggerRest, triggerRest: Party.#triggerRest,
tagTeamRoll: PartySheet.#onTagTeamRoll, tagTeamRoll: Party.#tagTeamRoll,
groupRoll: PartySheet.#onGroupRoll groupRoll: Party.#groupRoll
}, },
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }] dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
}; };
@ -74,23 +72,6 @@ export default class PartySheet extends DHBaseActorSheet {
this._createSearchFilter(); this._createSearchFilter();
} }
/** @inheritdoc */
_toggleDisabled(disabled) {
// Overriden to only disable text inputs by default if the user is a member
const isMember = this.actor.system.partyMembers.some(
m => m.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)
);
if (!isMember) return super._toggleDisabled(disabled);
const form = this.form;
for (const input of form.querySelectorAll('input:not([type=search]), .editor.prosemirror')) {
input.disabled = disabled;
}
for (const element of form.querySelectorAll('.input[contenteditable]')) {
element.classList.toggle('disabled', disabled);
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Prepare Context */ /* Prepare Context */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -217,16 +198,8 @@ export default class PartySheet extends DHBaseActorSheet {
}; };
} }
} }
/* -------------------------------------------- */
/* Event handlers */
/* -------------------------------------------- */
/** static async #openDocument(_, target) {
* @this PartySheet
* @type {ApplicationClickAction}
*/
static async #onOpenDocument(_, target) {
const uuid = target.dataset.uuid; const uuid = target.dataset.uuid;
const document = await foundry.utils.fromUuid(uuid); const document = await foundry.utils.fromUuid(uuid);
document?.sheet?.render(true); document?.sheet?.render(true);
@ -234,10 +207,9 @@ export default class PartySheet extends DHBaseActorSheet {
/** /**
* Toggles a hope resource value. * Toggles a hope resource value.
* @this PartySheet
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #onToggleHope(_, target) { static async #toggleHope(_, target) {
const hopeValue = Number.parseInt(target.dataset.value); const hopeValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue; const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue;
@ -247,10 +219,9 @@ export default class PartySheet extends DHBaseActorSheet {
/** /**
* Toggles a hp resource value. * Toggles a hp resource value.
* @this PartySheet
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #onToggleHitPoints(_, target) { static async #toggleHitPoints(_, target) {
const hitPointsValue = Number.parseInt(target.dataset.value); const hitPointsValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue; const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
@ -260,10 +231,9 @@ export default class PartySheet extends DHBaseActorSheet {
/** /**
* Toggles a stress resource value. * Toggles a stress resource value.
* @this PartySheet
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #onToggleStress(_, target) { static async #toggleStress(_, target) {
const stressValue = Number.parseInt(target.dataset.value); const stressValue = Number.parseInt(target.dataset.value);
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue; const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue;
@ -273,10 +243,9 @@ export default class PartySheet extends DHBaseActorSheet {
/** /**
* Toggles a armor slot resource value. * Toggles a armor slot resource value.
* @this PartySheet
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #onToggleArmorSlot(_, target) { static async #toggleArmorSlot(_, target) {
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const actor = await foundry.utils.fromUuid(target.dataset.actorId);
const { value, max } = actor.system.armorScore; const { value, max } = actor.system.armorScore;
const inputValue = Number.parseInt(target.dataset.value); const inputValue = Number.parseInt(target.dataset.value);
@ -289,19 +258,12 @@ export default class PartySheet extends DHBaseActorSheet {
/** /**
* Opens Compedium Browser * Opens Compedium Browser
* @this PartySheet
* @type {ApplicationClickAction}
*/ */
static async #onTempBrowser(_, target) { static async #tempBrowser(_, target) {
new ItemBrowser().render({ force: true }); new ItemBrowser().render({ force: true });
} }
/** static async #refeshActions() {
* @todo Is this unused?
* @this PartySheet
* @type {ApplicationClickAction}
*/
static async #onRefreshActions() {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {
title: 'New Section', title: 'New Section',
@ -319,11 +281,7 @@ export default class PartySheet extends DHBaseActorSheet {
if (!confirmed) return; if (!confirmed) return;
} }
/** static async #triggerRest(_, button) {
* @this PartySheet
* @type {ApplicationClickAction}
*/
static async #onTriggerRest(_, button) {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {
title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`), title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`),
@ -346,30 +304,6 @@ export default class PartySheet extends DHBaseActorSheet {
}); });
} }
/**
* @this PartySheet
* @type {ApplicationClickAction}
*/
static async #onDeletePartyMember(event, target) {
const doc = await foundry.utils.fromUuid(target.closest('[data-uuid]')?.dataset.uuid);
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.title', {
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMembersList });
}
static async downtimeMoveQuery({ actorId, downtimeType }) { static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId); const actor = await foundry.utils.fromUuid(actorId);
if (!actor || !actor?.isOwner) return; if (!actor || !actor?.isOwner) return;
@ -378,19 +312,11 @@ export default class PartySheet extends DHBaseActorSheet {
}); });
} }
/** static async #tagTeamRoll() {
* @this PartySheet
* @type {ApplicationClickAction}
*/
static async #onTagTeamRoll() {
new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true }); new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true });
} }
/** static async #groupRoll(_params) {
* @this PartySheet
* @type {ApplicationClickAction}
*/
static async #onGroupRoll(_params) {
new game.system.api.applications.dialogs.GroupRollDialog(this.document).render({ force: true }); new game.system.api.applications.dialogs.GroupRollDialog(this.document).render({ force: true });
} }
@ -534,10 +460,11 @@ export default class PartySheet extends DHBaseActorSheet {
} }
} }
/** @inheritdoc */ /* -------------------------------------------- */
async _onDropActor(event, document) { async _onDropActor(event, document) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (document instanceof DhpActor && PartySheet.ALLOWED_ACTOR_TYPES.includes(document.type)) { if (document instanceof DhpActor && Party.ALLOWED_ACTOR_TYPES.includes(document.type)) {
const currentMembers = this.document.system.partyMembers.map(x => x.uuid); const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
if (currentMembers.includes(data.uuid)) { if (currentMembers.includes(data.uuid)) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateCharacter')); return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateCharacter'));
@ -550,4 +477,24 @@ export default class PartySheet extends DHBaseActorSheet {
return null; return null;
} }
static async #deletePartyMember(event, target) {
const doc = await foundry.utils.fromUuid(target.closest('[data-uuid]')?.dataset.uuid);
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.title', {
name: doc.name
})
},
content: game.i18n.format('DAGGERHEART.ACTORS.Party.RemoveConfirmation.text', { name: doc.name })
});
if (!confirmed) return;
}
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMembersList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMembersList });
}
} }

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

@ -110,7 +110,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
return message.system.hasRoll && (game.user.isGM || message.isAuthor); return message.system.hasRoll && (game.user.isGM || message.isAuthor);
}, },
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] });
@ -126,7 +126,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
: false; : false;
return (game.user.isGM || message.isAuthor) && hasRolledDamage; return (game.user.isGM || message.isAuthor) && hasRolledDamage;
}, },
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);
@ -182,15 +182,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) {
@ -246,17 +259,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];

View file

@ -90,20 +90,18 @@ 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', mode: 2,
type: 'add', value: '1d4'
value: '1d4' },
}, {
{ key: 'system.bonuses.damage.magical.dice',
key: 'system.bonuses.damage.magical.dice', mode: 2,
type: 'add', 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: {

View file

@ -37,15 +37,13 @@ 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', mode: 2,
type: 'add', value: '1'
value: '1' }
} ]
]
}
} }
] ]
}, },
@ -57,45 +55,43 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'system.traits.strength.value',
key: 'system.traits.strength.value', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'system.traits.finesse.value',
key: 'system.traits.finesse.value', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'system.traits.instinct.value',
key: 'system.traits.instinct.value', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'system.traits.presence.value',
key: 'system.traits.presence.value', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'system.traits.knowledge.value',
key: 'system.traits.knowledge.value', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'system.evasion',
key: 'system.evasion', mode: 2,
type: 'add', value: '-1'
value: '-1' }
} ]
]
}
} }
] ]
}, },
@ -107,15 +103,13 @@ 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', mode: 2,
type: 'add', value: '1'
value: '1' }
} ]
]
}
} }
] ]
}, },
@ -127,15 +121,13 @@ 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', mode: 5,
type: 'override', value: '2'
value: '2' }
} ]
]
}
} }
] ]
}, },
@ -147,15 +139,13 @@ 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', mode: 2,
type: 'add', value: '1'
value: '1' }
} ]
]
}
} }
] ]
}, },
@ -167,15 +157,13 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' }
} ]
]
}
} }
] ]
}, },
@ -224,15 +212,13 @@ 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', mode: 5,
type: 'override', value: 1
value: 1 }
} ]
]
}
} }
] ]
}, },
@ -263,15 +249,13 @@ 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', mode: 5,
type: 'override', value: 1
value: 1 }
} ]
]
}
} }
] ]
}, },
@ -296,20 +280,18 @@ 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', mode: 2,
type: 'add', value: '2'
value: '2' },
}, {
{ key: 'system.bunuses.damageThresholds.severe',
key: 'system.bunuses.damageThresholds.severe', mode: 2,
type: 'add', value: '2'
value: '2' }
} ]
]
}
} }
] ]
}, },
@ -344,20 +326,18 @@ 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', mode: 2,
type: 'add', value: '1d4'
value: '1d4' },
}, {
{ key: 'system.bonuses.damage.secondaryWeapon.dice',
key: 'system.bonuses.damage.secondaryWeapon.dice', mode: 2,
type: 'add', value: '1d4'
value: '1d4' }
} ]
]
}
} }
] ]
}, },
@ -428,20 +408,18 @@ 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', mode: 2,
type: 'add', value: '-2'
value: '-2' },
}, {
{ key: 'system.traits.agility.value',
key: 'system.traits.agility.value', mode: 2,
type: 'add', value: '-1'
value: '-1' }
} ]
]
}
} }
] ]
}, },
@ -453,16 +431,14 @@ 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', mode: 2,
type: 'add', value: '@system.armorScore',
value: '@system.armorScore', priority: 21
priority: 21 }
} ]
]
}
} }
] ]
} }
@ -512,23 +488,21 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'Armor',
key: 'Armor', type: 'armor',
typeData: {
type: 'armor', type: 'armor',
typeData: { max: 'ITEM.@system.tier + 1'
type: 'armor',
max: 'ITEM.@system.tier + 1'
}
} }
] }
} ]
} }
] ]
}, },
@ -540,15 +514,13 @@ 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', mode: 2,
type: 'add', value: '@system.levelData.level.current'
value: '@system.levelData.level.current' }
} ]
]
}
} }
] ]
}, },
@ -581,20 +553,18 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' },
}, {
{ key: 'system.damageThresholds.severe',
key: 'system.damageThresholds.severe', mode: 2,
type: 'add', value: 'ITEM.@system.tier'
value: 'ITEM.@system.tier' }
} ]
]
}
} }
] ]
}, },
@ -648,15 +618,13 @@ 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', mode: 2,
type: 'add', value: '1'
value: '1' }
} ]
]
}
} }
] ]
} }
@ -692,15 +660,13 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' }
} ]
]
}
} }
] ]
}, },
@ -741,16 +707,14 @@ 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', mode: 2,
type: 'add', value: '@system.armorScore',
value: '@system.armorScore', priority: 21
priority: 21 }
} ]
]
}
} }
] ]
} }
@ -790,15 +754,13 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' }
} ]
]
}
} }
] ]
}, },
@ -829,14 +791,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description',
img: 'icons/skills/melee/sword-shield-stylized-white.webp', img: 'icons/skills/melee/sword-shield-stylized-white.webp',
changes: [
{
key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2,
value: '1'
}
],
system: { system: {
changes: [
{
key: 'system.bonuses.damage.primaryWeapon.bonus',
type: 'add',
value: '1'
}
],
rangeDependence: { rangeDependence: {
enabled: true, enabled: true,
range: 'melee', range: 'melee',
@ -940,15 +902,13 @@ 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', mode: 2,
type: 'add', value: '1'
value: '1' }
} ]
]
}
} }
] ]
} }
@ -991,15 +951,13 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' }
} ]
]
}
} }
] ]
}, },
@ -1108,15 +1066,13 @@ 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', mode: 2,
type: 'add', value: '-1'
value: '-1' }
} ]
]
}
} }
] ]
}, },
@ -1147,14 +1103,14 @@ export const weaponFeatures = {
name: 'DAGGERHEART.CONFIG.WeaponFeature.paired.effects.paired.name', name: 'DAGGERHEART.CONFIG.WeaponFeature.paired.effects.paired.name',
description: 'DAGGERHEART.CONFIG.WeaponFeature.paired.effects.paired.description', description: 'DAGGERHEART.CONFIG.WeaponFeature.paired.effects.paired.description',
img: 'icons/skills/melee/weapons-crossed-swords-yellow-teal.webp', img: 'icons/skills/melee/weapons-crossed-swords-yellow-teal.webp',
changes: [
{
key: 'system.bonuses.damage.primaryWeapon.bonus',
mode: 2,
value: 'ITEM.@system.tier + 1'
}
],
system: { system: {
changes: [
{
key: 'system.bonuses.damage.primaryWeapon.bonus',
type: 'add',
value: 'ITEM.@system.tier + 1'
}
],
rangeDependence: { rangeDependence: {
enabled: true, enabled: true,
range: 'melee', range: 'melee',
@ -1202,15 +1158,13 @@ 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', mode: 2,
type: 'add', value: '2'
value: '2' }
} ]
]
}
} }
] ]
} }
@ -1249,20 +1203,17 @@ 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', type: 'armor',
value: 0,
typeData: {
type: 'armor', type: 'armor',
value: 0, max: 'ITEM.@system.tier'
typeData: {
type: 'armor',
max: 'ITEM.@system.tier'
}
} }
] }
} ]
} }
] ]
}, },
@ -1293,15 +1244,13 @@ 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', mode: 2,
type: 'add', value: 1
value: 1 }
} ]
]
}
} }
] ]
}, },
@ -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

@ -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,18 +33,18 @@ 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();
@ -96,7 +95,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
@ -105,7 +104,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

@ -57,6 +57,11 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' }) description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' })
}), }),
rangeDependence: new fields.SchemaField({ rangeDependence: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.GENERAL.enabled'
}),
type: new fields.StringField({ type: new fields.StringField({
required: true, required: true,
choices: CONFIG.DH.GENERAL.rangeInclusion, choices: CONFIG.DH.GENERAL.rangeInclusion,
@ -75,7 +80,7 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
initial: CONFIG.DH.GENERAL.range.melee.id, initial: CONFIG.DH.GENERAL.range.melee.id,
label: 'DAGGERHEART.GENERAL.range' label: 'DAGGERHEART.GENERAL.range'
}) })
}, { nullable: true, initial: null }), }),
stacking: new fields.SchemaField( stacking: new fields.SchemaField(
{ {
value: new fields.NumberField({ value: new fields.NumberField({
@ -141,8 +146,14 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
description: '', description: '',
transfer: options.transfer, transfer: options.transfer,
statuses: [], statuses: [],
changes: [],
system: { system: {
changes: [] rangeDependence: {
enabled: false,
type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id,
target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id,
range: CONFIG.DH.GENERAL.range.melee.id
}
} }
}; };
} }
@ -174,12 +185,4 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
if (this.parent.actor && options.scrollingTextData) if (this.parent.actor && options.scrollingTextData)
this.parent.actor.queueScrollText(options.scrollingTextData); this.parent.actor.queueScrollText(options.scrollingTextData);
} }
static migrateData(source) {
if (source.rangeDependence?.enabled === false) {
source.rangeDependence = null;
}
return super.migrateData(source);
}
} }

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
import { triggerChatRollFx } from '../../helpers/utils.mjs'; import { triggerChatRollFx } from '../../helpers/utils.mjs';
import { ChatDamageData } from './chatDamageData.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -51,7 +50,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 })
@ -134,21 +133,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);
@ -193,43 +199,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

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

@ -8,8 +8,7 @@ export default class DHArmor extends AttachableItem {
type: 'armor', type: 'armor',
hasDescription: true, hasDescription: true,
isInventoryItem: true, isInventoryItem: true,
hasActions: true, hasActions: true
hasResource: true
}); });
} }

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,11 +67,13 @@ export default class DHWeapon extends AttachableItem {
type: 'attack' type: 'attack'
}, },
damage: { damage: {
main: { parts: {
type: ['physical'], hitPoints: {
value: { type: ['physical'],
multiplier: 'prof', value: {
dice: 'd8' multiplier: 'prof',
dice: 'd8'
}
} }
} }
} }
@ -236,12 +238,11 @@ export default class DHWeapon extends AttachableItem {
game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`) game.i18n.localize(`DAGGERHEART.CONFIG.Burden.${burden}`)
]; ];
if (attack.damage.main) { for (const { value, type } of attack.damage.parts) {
const { value, type } = attack.damage.main;
const parts = value.custom.enabled ? [game.i18n.localize('DAGGERHEART.GENERAL.custom')] : [value.dice]; const parts = value.custom.enabled ? [game.i18n.localize('DAGGERHEART.GENERAL.custom')] : [value.dice];
if (!value.custom.enabled && value.bonus) parts.push(value.bonus.signedString()); if (!value.custom.enabled && value.bonus) parts.push(value.bonus.signedString());
if (type?.size) { if (type.size > 0) {
const typeTags = Array.from(type) const typeTags = Array.from(type)
.map(t => game.i18n.localize(`DAGGERHEART.CONFIG.DamageType.${t}.abbreviation`)) .map(t => game.i18n.localize(`DAGGERHEART.CONFIG.DamageType.${t}.abbreviation`))
.join(' | '); .join(' | ');
@ -250,7 +251,7 @@ export default class DHWeapon extends AttachableItem {
tags.push(parts.join('')); tags.push(parts.join(''));
} }
return tags; return tags;
} }
@ -265,10 +266,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

@ -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,10 +124,12 @@ export default class DamageRoll extends DHRoll {
const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'); const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage');
const changeKeys = []; const changeKeys = [];
for (const damageType of this.options.damageFormula?.damageTypes?.values?.() ?? []) { for (const roll of this.options.roll) {
changeKeys.push(`system.bonuses.${type}.${damageType}`); for (const damageType of roll.damageTypes?.values?.() ?? []) {
changeKeys.push(`system.bonuses.${type}.${damageType}`);
}
} }
const item = this.data.parent?.items?.get(this.options.source.item); const item = this.data.parent?.items?.get(this.options.source.item);
if (item) { if (item) {
switch (item.type) { switch (item.type) {
@ -140,69 +145,62 @@ export default class DamageRoll extends DHRoll {
return changeKeys; return changeKeys;
} }
constructFormulas(config) { constructFormula(config) {
return {
damageFormula: this.constructFormula(this.options.damageFormula, config, true),
resourceFormulas: this.options.resourceFormulas.map(x => this.constructFormula(x, config))
};
}
constructFormula(formulaData, config, isDamage) {
if (!formulaData) return null;
this.options.isCritical = config.isCritical; this.options.isCritical = config.isCritical;
for (const [index, part] of this.options.roll.entries()) {
formulaData.roll = new Roll(Roll.replaceFormulaData(formulaData.formula, config.data)); const isHitpointPart = part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id;
formulaData.roll.terms = Roll.parse(formulaData.roll.formula, config.data); part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));
part.roll.terms = Roll.parse(part.roll.formula, config.data);
if (formulaData.extraFormula) { if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
formulaData.roll.terms.push( part.modifiers = this.applyBaseBonus(part);
new foundry.dice.terms.OperatorTerm({ operator: '+' }), this.addModifiers(part);
...this.constructor.parse(formulaData.extraFormula, this.options.data) part.modifiers?.forEach(m => {
); part.roll.terms.push(...this.formatModifier(m.value));
} });
if (isDamage && formulaData.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
formulaData.modifiers = this.applyBaseBonus(formulaData);
this.addModifiers(formulaData);
formulaData.modifiers?.forEach(m => {
formulaData.roll.terms.push(...this.formatModifier(m.value));
});
/* To Remove When Reaction System */
for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (
modifier.beforeCrit === true &&
(modifier.enabled || modifier.value)
) modifier.callback(formulaData);
} }
/* To Remove When Reaction System */ /* To Remove When Reaction System */
for (const mod in config.modifiers) { if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
const modifier = config.modifiers[mod]; for (const mod in config.modifiers) {
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(formulaData); const modifier = config.modifiers[mod];
if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part);
}
} }
if (config.damageOptions.groupAttack?.numAttackers > 1) { if (part.extraFormula) {
part.roll.terms.push(
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(part.extraFormula, this.options.data)
);
}
if (config.damageOptions.groupAttack?.numAttackers > 1 && isHitpointPart) {
const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm]; const damageTypes = [foundry.dice.terms.Die, foundry.dice.terms.NumericTerm];
for (const term of formulaData.roll.terms) { for (const term of part.roll.terms) {
if (damageTypes.some(type => term instanceof type)) { if (damageTypes.some(type => term instanceof type)) {
term.number *= config.damageOptions.groupAttack.numAttackers; term.number *= config.damageOptions.groupAttack.numAttackers;
} }
} }
} }
if (config.isCritical) { if (config.isCritical && isHitpointPart) {
const total = formulaData.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0);
if (total > 0) { if (total > 0) {
formulaData.roll.terms.push(...this.formatModifier(total)); part.roll.terms.push(...this.formatModifier(total));
} }
} }
}
formulaData.roll._formula = this.constructor.getFormula(formulaData.roll.terms); /* To Remove When Reaction System */
if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) {
return formulaData; for (const mod in config.modifiers) {
const modifier = config.modifiers[mod];
if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part);
}
}
part.roll._formula = this.constructor.getFormula(part.roll.terms);
}
return this.options.roll;
} }
/* To Remove When Reaction System */ /* To Remove When Reaction System */
@ -295,4 +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 = {}) {
await roll.evaluate(); if (config.evaluate !== false) {
config.roll = { await roll.evaluate();
...roll.options.roll, config.roll = this.postEvaluate(roll, config);
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,6 +91,20 @@ 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 actions = item ? [

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,63 +622,53 @@ export default class DhpActor extends Actor {
return rollData; return rollData;
} }
/** #canReduceDamage(hpDamage, type) {
* Checks to see if damage can be reduced in one way or another. const { stressDamageReduction, disabledArmor } = this.system.rules.damageReduction;
* @param {number} hpDamage the amount of marked hp that will be marked
* @param {Set<string>} types a list of damage types
*/
#canReduceDamage(hpDamage, types) {
const { stressDamageReduction, disabledArmor, reduceSeverity, thresholdImmunities } =
this.system.rules.damageReduction;
if (disabledArmor) return false; if (disabledArmor) return false;
const availableStress = this.system.resources.stress.max - this.system.resources.stress.value; const availableStress = this.system.resources.stress.max - this.system.resources.stress.value;
const canUseArmor = const canUseArmor =
this.system.armorScore.value < this.system.armorScore.max && this.system.armorScore.value < this.system.armorScore.max &&
types.every(t => this.system.armorApplicableDamageTypes[t] === true); type.every(t => this.system.armorApplicableDamageTypes[t] === true);
const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => { const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => {
const rule = stressDamageReduction[x]; const rule = stressDamageReduction[x];
if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost); if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost);
return acc; return acc;
}, false); }, false);
const hasReduceSeverity = types.some(t => reduceSeverity[t]); return canUseArmor || canUseStress;
const hasThresholdImmunity = Object.entries(thresholdImmunities)
.filter(([key, value]) => Boolean(value) && damageKeyToNumber(key) === hpDamage)
.length;
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 +683,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 +693,19 @@ export default class DhpActor extends Actor {
else updates.push({ value: stressSpent, key: 'stress' }); else updates.push({ value: stressSpent, key: 'stress' });
} }
} }
} else if (this.type === 'adversary') { }
if (this.type === 'adversary') {
const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => { const reducedSeverity = hpDamage.damageTypes.reduce((value, curr) => {
return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value); return Math.max(this.system.rules.damageReduction.reduceSeverity[curr], value);
}, 0); }, 0);
hpDamage.value = Math.max(hpDamage.value - reducedSeverity, 0); hpDamage.value = Math.max(hpDamage.value - reducedSeverity, 0);
if (this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]) {
hpDamage.value = Math.max(0, hpDamage.value - 1);
}
}
// Merge existing hitPoint deduction with finalised damage deduction if (
const existing = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); hpDamage.value &&
if (existing) { this.system.rules.damageReduction.thresholdImmunities[getDamageKey(hpDamage.value)]
existing.value += hpDamage.value; ) {
existing.damageTypes = hpDamage.damageTypes; hpDamage.value -= 1;
} else { }
updates.push(hpDamage);
} }
} }
@ -735,10 +721,12 @@ export default class DhpActor extends Actor {
for (var result of results) resourceMap.addResources(result.updates); for (var result of results) resourceMap.addResources(result.updates);
resourceMap.updateResources(); resourceMap.updateResources();
} }
for (const u of updates) { updates.forEach(
u.value = u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value; u =>
} (u.value =
u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value)
);
await this.modifyResource(updates); await this.modifyResource(updates);
@ -747,36 +735,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 +759,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,234 +4,199 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
#wide = false; #wide = false;
#bordered = false; #bordered = false;
/** @inheritdoc */
async activate(element, options = {}) { async activate(element, options = {}) {
this.#wide = false; const { TextEditor } = foundry.applications.ux;
this.#bordered = false;
const isMacro = document.getElementById('action-bar').contains(element);
const macro = isMacro ? game.macros.get(game.user.hotbar[Number(element.dataset.slot)] ?? null) : null;
const macroItemUuid = macro?.type === 'script' ? macro.command.match(/await game\.system\.api\.applications\.ui\.DhHotbar\.useItem\("([^"]+)"\);/)?.[1] : null;
if (macroItemUuid && await fromUuid(macroItemUuid, { strict: false })) {
element.dataset.tooltip = `#item#${macroItemUuid}`;
options.direction = this.constructor.TOOLTIP_DIRECTIONS.UP;
}
let html = options.html; let html = options.html;
const key = element.dataset.tooltip?.match(/^#([\w-]+)#/)?.[1]; if (element.dataset.tooltip?.startsWith('#battlepoints#')) {
switch (key) { this.#wide = true;
case 'battlepoints': this.#bordered = true;
return this.#activateBattlepoints(element, options);
case 'effect-display': html = await this.getBattlepointHTML(element.dataset.combatId);
html = await this.#activateEffectDisplay(element, options); options.direction = this._determineItemTooltipDirection(element);
break; super.activate(element, { ...options, html: html });
case 'item':
html = await this.#activateItem(element, options); const lockedTooltip = this.lockTooltip();
break; lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => {
case 'attack': element.addEventListener('input', this.toggleModifier.bind(this));
html = await this.#activateAttack(element, options); });
break; return;
case 'shortRest': } else {
case 'longRest': this.#wide = false;
html = await this.#activateRest(element, options); this.#bordered = false;
break; }
case 'advantage':
case 'disadvantage': if (element.dataset.tooltip === '#effect-display#') {
html = await this.#activateAdvantageDisadvantage(element, options); this.#bordered = true;
break; let effect = {};
case 'deathMove': if (element.dataset.uuid) {
html = await this.#activateDeathMove(element, options); const effectItem = await foundry.utils.fromUuid(element.dataset.uuid);
break; const effectData = effectItem.toObject();
effect = {
...effectData,
name: game.i18n.localize(effectData.name)
};
if (effectData.type === 'beastform') {
const beastformData = {
features: [],
advantageOn: effectData.system.advantageOn,
beastformAttackData: game.system.api.data.items.DHBeastform.getBeastformAttackData(effectItem)
};
const features = effectItem.parent.items.filter(x => effectItem.system.featureIds.includes(x.id));
for (const feature of features) {
const featureData = feature.toObject();
featureData.enrichedDescription = await feature.system.getEnrichedDescription();
beastformData.features.push(featureData);
}
effect.description = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs',
{
item: { system: beastformData }
}
);
} else {
effect.description = game.i18n.localize(
effectData.description ?? effectData.parent.system.description
);
}
} else {
const conditions = CONFIG.DH.GENERAL.conditions();
const condition = conditions[element.dataset.condition];
effect = {
...condition,
name: game.i18n.localize(condition.name),
description: game.i18n.localize(condition.description),
appliedBy: element.dataset.appliedBy,
isLockedCondition: true
};
}
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/effect-display.hbs`,
{
effect
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
} else {
this.#bordered = false;
}
if (element.dataset.tooltip?.startsWith('#item#')) {
const itemUuid = element.dataset.tooltip.slice(6);
const item = await foundry.utils.fromUuid(itemUuid);
if (item) {
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
const isEffect = item instanceof ActiveEffect;
await this.enrichText(item);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/${type}.hbs`,
{
item: item,
description: item.system?.enrichedDescription ?? item.enrichedDescription,
config: CONFIG.DH,
allDomains: CONFIG.DH.DOMAIN.allDomains()
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
}
} else {
const attack = element.dataset.tooltip?.startsWith('#attack#');
if (attack) {
const actorUuid = element.dataset.tooltip.slice(8);
const actor = await foundry.utils.fromUuid(actorUuid);
const attack = actor.system.attack;
const description = await TextEditor.enrichHTML(attack.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/attack.hbs`,
{
attack: attack,
description: description,
parent: actor,
config: CONFIG.DH
}
);
this.tooltip.innerHTML = html;
}
const shortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const longRest = element.dataset.tooltip?.startsWith('#longRest#');
if (shortRest || longRest) {
const key = element.dataset.tooltip.slice(shortRest ? 11 : 10);
const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[
element.dataset.restType
].moves;
const move = moves[key];
const description = await TextEditor.enrichHTML(move.description);
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move,
description: description
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
}
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#');
if (isAdvantage || isDisadvantage) {
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
}
}
const deathMove = element.dataset.tooltip?.startsWith('#deathMove#');
if (deathMove) {
const name = element.dataset.deathName;
const img = element.dataset.deathImg;
const description = element.dataset.deathDescription;
html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/death-move.hbs`,
{
move: { name: name, img: img, description: description }
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
}
} }
this.noOffset = options.noOffset; this.noOffset = options.noOffset;
super.activate(element, { ...options, html });
}
async #activateBattlepoints(element, options) {
this.#wide = true;
this.#bordered = true;
const html = await this.getBattlepointHTML(element.dataset.combatId);
options.direction = this._determineItemTooltipDirection(element);
super.activate(element, { ...options, html: html }); super.activate(element, { ...options, html: html });
const lockedTooltip = this.lockTooltip();
lockedTooltip.querySelectorAll('.battlepoint-toggle-container input').forEach(element => {
element.addEventListener('input', this.toggleModifier.bind(this));
});
}
async #activateEffectDisplay(element, options) {
this.#bordered = true;
let effect = {};
if (element.dataset.uuid) {
const effectItem = await foundry.utils.fromUuid(element.dataset.uuid);
const effectData = effectItem.toObject();
effect = {
...effectData,
name: game.i18n.localize(effectData.name)
};
if (effectData.type === 'beastform') {
const beastformData = {
features: [],
advantageOn: effectData.system.advantageOn,
beastformAttackData: game.system.api.data.items.DHBeastform.getBeastformAttackData(effectItem)
};
const features = effectItem.parent.items.filter(x => effectItem.system.featureIds.includes(x.id));
for (const feature of features) {
const featureData = feature.toObject();
featureData.enrichedDescription = await feature.system.getEnrichedDescription();
beastformData.features.push(featureData);
}
effect.description = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/tooltip/parts/beastformData.hbs',
{
item: { system: beastformData }
}
);
} else {
effect.description = game.i18n.localize(
effectData.description ?? effectData.parent.system.description
);
}
} else {
const conditions = CONFIG.DH.GENERAL.conditions();
const condition = conditions[element.dataset.condition];
effect = {
...condition,
name: game.i18n.localize(condition.name),
description: game.i18n.localize(condition.description),
appliedBy: element.dataset.appliedBy,
isLockedCondition: true
};
}
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/effect-display.hbs`,
{
effect
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
return html;
}
async #activateItem(element, options) {
const itemUuid = element.dataset.tooltip.slice(6);
const item = await foundry.utils.fromUuid(itemUuid);
if (item) {
const isAction = item instanceof game.system.api.models.actions.actionsTypes.base;
const isEffect = item instanceof ActiveEffect;
await this.enrichText(item);
const type = isAction ? 'action' : isEffect ? 'effect' : item.type;
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/${type}.hbs`,
{
item: item,
description: item.system?.enrichedDescription ?? item.enrichedDescription,
config: CONFIG.DH,
allDomains: CONFIG.DH.DOMAIN.allDomains()
}
);
this.tooltip.innerHTML = html;
options.direction ??= this._determineItemTooltipDirection(element);
return html;
}
return null;
}
async #activateAttack(element, options) {
const actorUuid = element.dataset.tooltip.slice(8);
const actor = await foundry.utils.fromUuid(actorUuid);
const attack = actor.system.attack;
const description = await foundry.applications.ux.TextEditor.enrichHTML(attack.description);
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/attack.hbs`,
{
attack: attack,
description: description,
parent: actor,
config: CONFIG.DH
}
);
this.tooltip.innerHTML = html;
return html;
}
async #activateAdvantageDisadvantage(element, options) {
const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
const actor = await foundry.utils.fromUuid(actorUuid);
if (actor) {
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
{
sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
}
);
this.tooltip.innerHTML = html;
return html;
}
return null;
}
async #activateDeathMove(element, options) {
const name = element.dataset.deathName;
const img = element.dataset.deathImg;
const description = element.dataset.deathDescription;
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/death-move.hbs`,
{
move: { name: name, img: img, description: description }
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
return html;
}
async #activateRest(element, options) {
const isShortRest = element.dataset.tooltip?.startsWith('#shortRest#');
const key = element.dataset.tooltip.slice(isShortRest ? 11 : 10);
const moves = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves[
element.dataset.restType
].moves;
const move = moves[key];
const description = await foundry.applications.ux.TextEditor.enrichHTML(move.description);
const html = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/downtime.hbs`,
{
move: move,
description: description
}
);
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(
element,
this.constructor.TOOLTIP_DIRECTIONS.RIGHT
);
return html;
} }
_setAnchor(direction) { _setAnchor(direction) {

View file

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

@ -1,5 +1,5 @@
import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs';
import PartySheet from '../applications/sheets/actors/party.mjs'; import Party from '../applications/sheets/actors/party.mjs';
export function handleSocketEvent({ action = null, data = {} } = {}) { export function handleSocketEvent({ action = null, data = {} } = {}) {
switch (action) { switch (action) {
@ -16,7 +16,7 @@ export function handleSocketEvent({ action = null, data = {} } = {}) {
Hooks.call(socketEvent.Refresh, data); Hooks.call(socketEvent.Refresh, data);
break; break;
case socketEvent.DowntimeTrigger: case socketEvent.DowntimeTrigger:
PartySheet.downtimeMoveQuery(data); Party.downtimeMoveQuery(data);
break; break;
case socketEvent.TagTeamStart: case socketEvent.TagTeamStart:
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data); Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data);

View file

@ -403,6 +403,12 @@
"_id": "9PsnogEPsp1OOK64", "_id": "9PsnogEPsp1OOK64",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "act" "type": "act"

View file

@ -322,23 +322,32 @@
"_id": "ia0NUIOxE3RHb45P", "_id": "ia0NUIOxE3RHb45P",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.evasion", "type": "withinRange",
"type": "override", "target": "hostile",
"value": "@system.evasion / 2", "range": "melee"
"priority": 10 }
}
]
},
"duration": {
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
}, },
"changes": [
{
"key": "system.evasion",
"mode": 5,
"value": "@system.evasion / 2",
"priority": 10
}
],
"disabled": false, "disabled": false,
"description": "<p>When the Flickerfly makes an attack, the targets Evasion is halved against the attack.</p>", "duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
},
"description": "<p>When the Flickerfl y makes an attack, the targets Evasion is halved against the attack.</p>",
"tint": "#ffffff", "tint": "#ffffff",
"statuses": [], "statuses": [],
"sort": 0, "sort": 0,

View file

@ -491,6 +491,12 @@
"_id": "KGdf2eqcXkdigg0u", "_id": "KGdf2eqcXkdigg0u",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -325,15 +325,21 @@
"_id": "wGuxOLokMqdxVSOo", "_id": "wGuxOLokMqdxVSOo",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.disadvantageSources", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "Agility Rolls", "range": "melee"
"priority": null }
}
]
}, },
"changes": [
{
"key": "system.disadvantageSources",
"mode": 2,
"value": "Agility Rolls",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,

View file

@ -472,8 +472,14 @@
"disabled": true, "disabled": true,
"_id": "bq8hTzQoCXmA3xad", "_id": "bq8hTzQoCXmA3xad",
"system": { "system": {
"changes": [] "rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
}, },
"changes": [],
"duration": { "duration": {
"startTime": 0, "startTime": 0,
"combat": null "combat": null

View file

@ -270,6 +270,12 @@
"_id": "2iBVUGHtGW3I9VIj", "_id": "2iBVUGHtGW3I9VIj",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",
@ -335,17 +341,23 @@
"name": "Out of Nowhere", "name": "Out of Nowhere",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.advantageSources", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "On attacks if they are Hidden.", "range": "melee"
"priority": null }
}
]
}, },
"_id": "qT5nh7OcQ6aGdiWS", "_id": "qT5nh7OcQ6aGdiWS",
"img": "icons/magic/perception/shadow-stealth-eyes-purple.webp", "img": "icons/magic/perception/shadow-stealth-eyes-purple.webp",
"changes": [
{
"key": "system.advantageSources",
"mode": 2,
"value": "On attacks if they are Hidden.",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -440,6 +452,12 @@
"_id": "yP4ot8VqS56RnxnE", "_id": "yP4ot8VqS56RnxnE",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [] "changes": []
}, },
"disabled": false, "disabled": false,

View file

@ -680,6 +680,12 @@
"_id": "9NQcCXMhjyBReJRd", "_id": "9NQcCXMhjyBReJRd",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",
@ -921,6 +927,12 @@
"_id": "S7kJlhnV8Nexzi8l", "_id": "S7kJlhnV8Nexzi8l",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -363,6 +363,12 @@
"_id": "U50Ccm9emMqAxma6", "_id": "U50Ccm9emMqAxma6",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -371,6 +371,12 @@
"_id": "LmzztuktRkwOCy1a", "_id": "LmzztuktRkwOCy1a",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -447,6 +447,12 @@
"_id": "CjMrSdL6kgD8mKRQ", "_id": "CjMrSdL6kgD8mKRQ",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary" "type": "temporary"

View file

@ -232,17 +232,23 @@
"name": "Levitation", "name": "Levitation",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.rules.conditionImmunities.restrained", "type": "withinRange",
"type": "override", "target": "hostile",
"value": "1", "range": "melee"
"priority": null }
}
]
}, },
"_id": "k6iaQVfMZhrpwYQj", "_id": "k6iaQVfMZhrpwYQj",
"img": "icons/magic/air/fog-gas-smoke-blue-gray.webp", "img": "icons/magic/air/fog-gas-smoke-blue-gray.webp",
"changes": [
{
"key": "system.rules.conditionImmunities.restrained",
"mode": 5,
"value": "1",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -295,17 +301,23 @@
"name": "Wards", "name": "Wards",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.resistance.magical.resistance", "type": "withinRange",
"type": "override", "target": "hostile",
"value": "1", "range": "melee"
"priority": null }
}
]
}, },
"_id": "4rj6PM0AGrrlbxSc", "_id": "4rj6PM0AGrrlbxSc",
"img": "icons/magic/defensive/barrier-shield-dome-blue-purple.webp", "img": "icons/magic/defensive/barrier-shield-dome-blue-purple.webp",
"changes": [
{
"key": "system.resistance.magical.resistance",
"mode": 5,
"value": "1",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,

View file

@ -459,15 +459,21 @@
"_id": "xkDIZk9u2ipDHvOL", "_id": "xkDIZk9u2ipDHvOL",
"img": "icons/creatures/magical/construct-golem-stone-blue.webp", "img": "icons/creatures/magical/construct-golem-stone-blue.webp",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.bonuses.damage.physical.bonus", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "10", "range": "melee"
"priority": null }
}
]
}, },
"changes": [
{
"key": "system.bonuses.damage.physical.bonus",
"mode": 2,
"value": "10",
"priority": null
}
],
"disabled": true, "disabled": true,
"duration": { "duration": {
"startTime": null, "startTime": null,

View file

@ -315,6 +315,12 @@
"_id": "blcRqns0PHqiuPac", "_id": "blcRqns0PHqiuPac",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -331,6 +331,12 @@
"_id": "YNMhgBZW8ndrCjIp", "_id": "YNMhgBZW8ndrCjIp",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "scene" "type": "scene"

View file

@ -415,6 +415,12 @@
"_id": "U9lWz1LgeAiK5L85", "_id": "U9lWz1LgeAiK5L85",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [ "changes": [
{ {
"key": "system.resistance.magical.resistance", "key": "system.resistance.magical.resistance",
@ -540,6 +546,12 @@
"_id": "lNH6srSPyEprXZ4o", "_id": "lNH6srSPyEprXZ4o",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -379,6 +379,12 @@
"_id": "LwWxRz7FTMA80VdA", "_id": "LwWxRz7FTMA80VdA",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary" "type": "temporary"

View file

@ -410,6 +410,12 @@
"_id": "F3E7fiz01AbF2kr5", "_id": "F3E7fiz01AbF2kr5",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -270,17 +270,23 @@
"name": "Numbers Must Go Up", "name": "Numbers Must Go Up",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.bonuses.roll.attack.bonus", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "ITEM.@system.resource.value", "range": "melee"
"priority": 21 }
}
]
}, },
"_id": "iLeiOlUtWvYsgbDQ", "_id": "iLeiOlUtWvYsgbDQ",
"img": "icons/magic/control/fear-fright-monster-grin-red-orange.webp", "img": "icons/magic/control/fear-fright-monster-grin-red-orange.webp",
"changes": [
{
"key": "system.bonuses.roll.attack.bonus",
"mode": 2,
"value": "ITEM.@system.resource.value",
"priority": 21
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,

View file

@ -238,17 +238,23 @@
"name": "Depths Of Despair", "name": "Depths Of Despair",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.rules.attack.damage.hpDamageMultiplier", "type": "withinRange",
"type": "override", "target": "hostile",
"value": "2", "range": "melee"
"priority": null }
}
]
}, },
"_id": "nofxm1vGZ2TmceA2", "_id": "nofxm1vGZ2TmceA2",
"img": "icons/magic/death/skull-horned-worn-fire-blue.webp", "img": "icons/magic/death/skull-horned-worn-fire-blue.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true, "disabled": true,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -350,6 +356,12 @@
"_id": "6WSx03mFbpbPWnOI", "_id": "6WSx03mFbpbPWnOI",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [ "changes": [
{ {
"key": "system.rules.dualityRoll.defaultHopeDice", "key": "system.rules.dualityRoll.defaultHopeDice",

View file

@ -319,6 +319,12 @@
"_id": "gFeHLGgeRoDdd3VG", "_id": "gFeHLGgeRoDdd3VG",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [ "changes": [
{ {
"key": "system.rules.dualityRoll.defaultFearDice", "key": "system.rules.dualityRoll.defaultFearDice",

View file

@ -440,8 +440,14 @@
"disabled": true, "disabled": true,
"_id": "dPZ7PwvTERjtG92P", "_id": "dPZ7PwvTERjtG92P",
"system": { "system": {
"changes": [] "rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
}, },
"changes": [],
"duration": { "duration": {
"startTime": 0, "startTime": 0,
"combat": null "combat": null

View file

@ -238,6 +238,12 @@
"name": "Flying", "name": "Flying",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [ "changes": [
{ {
"key": "system.difficulty", "key": "system.difficulty",

View file

@ -430,6 +430,12 @@
"_id": "YNKHEFQ4ucGr4Rmc", "_id": "YNKHEFQ4ucGr4Rmc",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -358,8 +358,14 @@
"disabled": true, "disabled": true,
"_id": "xoal60Ed3Ih7saBJ", "_id": "xoal60Ed3Ih7saBJ",
"system": { "system": {
"changes": [] "rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
}, },
"changes": [],
"duration": { "duration": {
"startTime": 0, "startTime": 0,
"combat": null "combat": null

View file

@ -240,17 +240,23 @@
"name": "Warped Fortitude", "name": "Warped Fortitude",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.resistance.physical.resistance", "type": "withinRange",
"type": "override", "target": "hostile",
"value": "1", "range": "melee"
"priority": null }
}
]
}, },
"_id": "3QLH2EEtcUEZolFz", "_id": "3QLH2EEtcUEZolFz",
"img": "icons/creatures/magical/humanoid-silhouette-glowing-pink.webp", "img": "icons/creatures/magical/humanoid-silhouette-glowing-pink.webp",
"changes": [
{
"key": "system.resistance.physical.resistance",
"mode": 5,
"value": "1",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -303,17 +309,23 @@
"name": "Overwhelm", "name": "Overwhelm",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.rules.attack.damage.hpDamageMultiplier", "type": "withinRange",
"type": "override", "target": "hostile",
"value": "2", "range": "melee"
"priority": null }
}
]
}, },
"_id": "eGB9G0ljYCcdGbOx", "_id": "eGB9G0ljYCcdGbOx",
"img": "icons/skills/melee/strike-slashes-orange.webp", "img": "icons/skills/melee/strike-slashes-orange.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true, "disabled": true,
"duration": { "duration": {
"startTime": null, "startTime": null,

View file

@ -388,6 +388,12 @@
"_id": "Q99saHj6NOY2PSXf", "_id": "Q99saHj6NOY2PSXf",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",
@ -590,6 +596,12 @@
"_id": "pWDg0MADoohKu40O", "_id": "pWDg0MADoohKu40O",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -656,6 +656,12 @@
"vulnerable" "vulnerable"
], ],
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"description": "", "description": "",

View file

@ -347,6 +347,12 @@
"_id": "vb1sZCOLwDNLsr3j", "_id": "vb1sZCOLwDNLsr3j",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -265,17 +265,23 @@
"name": "Flying", "name": "Flying",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.difficulty", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "3", "range": "melee"
"priority": null }
}
]
}, },
"_id": "odAyKY0BSAkcO3Dd", "_id": "odAyKY0BSAkcO3Dd",
"img": "icons/creatures/birds/raptor-hawk-flying.webp", "img": "icons/creatures/birds/raptor-hawk-flying.webp",
"changes": [
{
"key": "system.difficulty",
"mode": 2,
"value": "3",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -419,6 +425,12 @@
"_id": "m6qqQZxukDPVcGdQ", "_id": "m6qqQZxukDPVcGdQ",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "act" "type": "act"
@ -567,8 +579,14 @@
"_id": "Sd34xywRqiF5w9sX", "_id": "Sd34xywRqiF5w9sX",
"type": "base", "type": "base",
"system": { "system": {
"changes": [] "rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
}, },
"changes": [],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,

View file

@ -269,15 +269,21 @@
"_id": "aATxfjeOzUYtKuU6", "_id": "aATxfjeOzUYtKuU6",
"img": "icons/commodities/biological/wing-insect-green.webp", "img": "icons/commodities/biological/wing-insect-green.webp",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.difficulty", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "2", "range": "melee"
"priority": null }
}
]
}, },
"changes": [
{
"key": "system.difficulty",
"mode": 2,
"value": "2",
"priority": null
}
],
"disabled": true, "disabled": true,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -379,8 +385,14 @@
"disabled": true, "disabled": true,
"_id": "dQgcYTz5vmV25Y6G", "_id": "dQgcYTz5vmV25Y6G",
"system": { "system": {
"changes": [] "rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
}, },
"changes": [],
"duration": { "duration": {
"startTime": 0, "startTime": 0,
"combat": null "combat": null

View file

@ -433,6 +433,12 @@
"_id": "oILkLJlGNZl7KI1R", "_id": "oILkLJlGNZl7KI1R",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "shortRest" "type": "shortRest"

View file

@ -597,6 +597,12 @@
"_id": "Mchd23xNQ4nSWw9X", "_id": "Mchd23xNQ4nSWw9X",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [] "changes": []
}, },
"disabled": false, "disabled": false,

View file

@ -330,6 +330,12 @@
"_id": "bYBrgiSwHwYfQyjn", "_id": "bYBrgiSwHwYfQyjn",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [ "changes": [
{ {
"key": "system.rules.conditionImmunities.hidden", "key": "system.rules.conditionImmunities.hidden",

View file

@ -340,17 +340,23 @@
"name": "Immovable Object", "name": "Immovable Object",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.resistance.physical.reduction", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "7", "range": "melee"
"priority": null }
}
]
}, },
"_id": "7V7qMq9in3AArwiM", "_id": "7V7qMq9in3AArwiM",
"img": "icons/magic/defensive/shield-barrier-glowing-triangle-green.webp", "img": "icons/magic/defensive/shield-barrier-glowing-triangle-green.webp",
"changes": [
{
"key": "system.resistance.physical.reduction",
"mode": 2,
"value": "7",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -504,6 +510,12 @@
"_id": "38MUzfbH64EMLVse", "_id": "38MUzfbH64EMLVse",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -323,15 +323,21 @@
"_id": "WYqjt1tVqPEdVV5l", "_id": "WYqjt1tVqPEdVV5l",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.disadvantageSources", "type": "withinRange",
"type": "add", "target": "hostile",
"value": "Your next action", "range": "melee"
"priority": null }
}
]
}, },
"changes": [
{
"key": "system.disadvantageSources",
"mode": 2,
"value": "Your next action",
"priority": null
}
],
"disabled": false, "disabled": false,
"duration": { "duration": {
"startTime": null, "startTime": null,
@ -456,6 +462,12 @@
"_id": "X8NF2OB23mSpDvlx", "_id": "X8NF2OB23mSpDvlx",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -460,6 +460,12 @@
"_id": "yk5kR5OVLCgDWfgY", "_id": "yk5kR5OVLCgDWfgY",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -234,17 +234,23 @@
"name": "Punish the Guilty", "name": "Punish the Guilty",
"type": "base", "type": "base",
"system": { "system": {
"changes": [ "rangeDependence": {
{ "enabled": false,
"key": "system.rules.attack.damage.hpDamageMultiplier", "type": "withinRange",
"type": "override", "target": "hostile",
"value": "2", "range": "melee"
"priority": null }
}
]
}, },
"_id": "ID85zoIa5GfhNMti", "_id": "ID85zoIa5GfhNMti",
"img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp", "img": "icons/magic/control/buff-flight-wings-runes-red-yellow.webp",
"changes": [
{
"key": "system.rules.attack.damage.hpDamageMultiplier",
"mode": 5,
"value": "2",
"priority": null
}
],
"disabled": true, "disabled": true,
"duration": { "duration": {
"startTime": null, "startTime": null,

View file

@ -386,6 +386,12 @@
"_id": "O8G0oOf9f3qzNOAT", "_id": "O8G0oOf9f3qzNOAT",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -429,6 +429,12 @@
"_id": "EwZ8owroJxFpg81e", "_id": "EwZ8owroJxFpg81e",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [], "changes": [],
"duration": { "duration": {
"type": "temporary", "type": "temporary",

View file

@ -510,6 +510,12 @@
"_id": "iBJ3YhEkVsGKEIVk", "_id": "iBJ3YhEkVsGKEIVk",
"type": "base", "type": "base",
"system": { "system": {
"rangeDependence": {
"enabled": false,
"type": "withinRange",
"target": "hostile",
"range": "melee"
},
"changes": [ "changes": [
{ {
"key": "system.resistance.magical.immunity", "key": "system.resistance.magical.immunity",

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