Merge branch 'main' into feature/enhance-fear-tracker

This commit is contained in:
Murilo Brito 2026-07-21 15:23:56 -03:00 committed by GitHub
commit 5e28d34729
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 1641 additions and 1058 deletions

7
module/data/_types.d.ts vendored Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import { triggerChatRollFx } from '../../helpers/utils.mjs';
import { ChatDamageData } from './chatDamageData.mjs';
const fields = foundry.data.fields;
@ -41,6 +42,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
hasEffect: new fields.BooleanField({ initial: false }),
hasSave: new fields.BooleanField({ initial: false }),
hasTarget: new fields.BooleanField({ initial: false }),
needsReload: new fields.BooleanField({ initial: false }),
isDirect: new fields.BooleanField({ initial: false }),
onSave: new fields.StringField(),
source: new fields.SchemaField({
@ -49,7 +51,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
originItem: originItemField(),
action: new fields.StringField()
}),
damage: new fields.ObjectField(),
damage: new fields.EmbeddedDataField(ChatDamageData),
damageOptions: new fields.ObjectField(),
costs: new fields.ArrayField(new fields.ObjectField()),
successConsumed: new fields.BooleanField({ initial: false })
@ -132,28 +134,21 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
});
}
/* TODO: Change how damage data is stored somehow to enable better rerolling */
async getRerolledDamage() {
if (!this.damage) return;
if (!this.damage.active) return;
const rerolls = [];
const update = { system: { damage: {} } };
for (const partKey in this.damage) {
const part = this.damage[partKey];
const testRoll = Roll.fromData(part.parts[0].roll);
const rerolled = await testRoll.reroll();
rerolls.push(rerolled);
const update = { system: { damage: { main: null, resources: _replace({}) } } };
if (this.damage.main) {
const reroll = await this.damage.main.reroll();
rerolls.push(reroll);
update.system.damage.main = reroll.toJSON();
}
if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] };
const partData = update.system.damage[partKey].parts[0];
update.system.damage[partKey].total = rerolled.total;
partData.modifierTotal = rerolled.terms.reduce((acc, x) => {
if (x.isDeterministic && !x.operator) acc += x.total;
return acc;
}, 0);
partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination }));
partData.total = rerolled.total;
partData.roll = rerolled.toJSON();
for (const key of Object.keys(this.damage.resources)) {
const reroll = await this.damage.resources[key].reroll();
rerolls.push(reroll);
update.system.damage.resources[key] = reroll.toJSON();
}
await triggerChatRollFx(rerolls);
@ -198,6 +193,43 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
this.isGM = game.user.isGM; //temp
}
static migrateData(source) {
const { main, resources, ...flatDamageKeys } = source.damage ?? {};
if (source.damage && !main && !resources) {
source.damage.main = null;
source.damage.resources = {};
const getRoll = key => {
const damageData = source.damage[key];
const oldRoll = damageData.parts[0]?.roll;
return oldRoll ? JSON.stringify({
...oldRoll,
class: 'BaseRoll',
options: {
...oldRoll.options,
damageTypes: damageData.parts[0].damageTypes ?? []
}
}) : null;
};
for (const key of Object.keys(flatDamageKeys)) {
if (key === 'hitPoints' && source.hasDamage && !source.hasHealing) {
source.damage.main = getRoll('hitPoints');
}
else {
source.damage.resources[key] = getRoll(key);
}
}
}
for (const key of Object.keys(flatDamageKeys)) {
delete source.damage[key];
}
return source;
}
getTargetList() {
const targets =
this.targetMode && this.parent.isAuthor

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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