mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-07-22 10:29:54 +02:00
Merge branch 'main' into feature/enhance-fear-tracker
This commit is contained in:
commit
5e28d34729
76 changed files with 1641 additions and 1058 deletions
7
module/data/_types.d.ts
vendored
Normal file
7
module/data/_types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { DhCountdown } from './countdowns.mjs'
|
||||
|
||||
declare module './countdowns.mjs' {
|
||||
export default interface DhCountdowns {
|
||||
countdowns: Record<string, DhCountdown>;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() ?? {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
58
module/data/chat-message/chatDamageData.mjs
Normal file
58
module/data/chat-message/chatDamageData.mjs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue