Merge branch 'development' into fix/use-experience-or-adv-disadv-on-roll

This commit is contained in:
Chris Ryan 2025-09-07 09:07:54 +10:00
commit e540a21bdc
99 changed files with 1741 additions and 796 deletions

View file

@ -28,14 +28,14 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
actionType: new fields.StringField({
choices: CONFIG.DH.ITEM.actionTypes,
initial: 'action',
nullable: true
nullable: false,
required: true
})
};
this.extraSchemas.forEach(s => {
let clsField = this.getActionField(s);
if (clsField)
schemaFields[s] = new clsField();
if (clsField) schemaFields[s] = new clsField();
});
return schemaFields;
@ -43,7 +43,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
/**
* Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property.
*
*
* Each step can be called individually as long as needed config is provided.
* Ex: <action>.workflow.get("damage").execute(config)
* @returns {Map}
@ -53,8 +53,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
this.constructor.extraSchemas.forEach(s => {
let clsField = this.constructor.getActionField(s);
if (clsField?.execute) {
workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) } );
if( s === "damage" ) workflow.set("applyDamage", { order: 75, execute: clsField.applyDamage.bind(this) } );
workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) });
if (s === 'damage')
workflow.set('applyDamage', { order: 75, execute: clsField.applyDamage.bind(this) });
}
});
return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order));
@ -64,9 +65,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* Getter returning the workflow property or creating it the first time the property is called
*/
get workflow() {
if ( this.hasOwnProperty("_workflow") ) return this._workflow;
if (this.hasOwnProperty('_workflow')) return this._workflow;
const workflow = Object.freeze(this.defineWorkflow());
Object.defineProperty(this, "_workflow", {value: workflow, writable: false});
Object.defineProperty(this, '_workflow', { value: workflow, writable: false });
return workflow;
}
@ -117,7 +118,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
/**
* Prepare base data based on Action Type & Parent Type
* @param {object} parent
* @param {object} parent
* @returns {object}
*/
static getSourceConfig(parent) {
@ -167,9 +168,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @param {object} config Config object usually created from prepareConfig method
*/
async executeWorkflow(config) {
for(const [key, part] of this.workflow) {
for (const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
if(await part.execute(config) === false) return;
if ((await part.execute(config)) === false) return;
if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return;
}
}
@ -185,7 +186,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (this.chatDisplay) await this.toChat();
let config = this.prepareConfig(event);
if(!config) return;
if (!config) return;
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
@ -194,7 +195,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
config = await D20RollDialog.configure(null, config);
if (!config) return;
}
// Execute the Action Worflow in order based of schema fields
await this.executeWorkflow(config);
@ -206,7 +207,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
/**
* Create the basic config common to every action type
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
* @returns {object}
*/
prepareBaseConfig(event) {
const config = {
@ -236,13 +237,12 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
/**
* Create the config for that action used for its workflow
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
* @returns {object}
*/
prepareConfig(event) {
const config = this.prepareBaseConfig(event);
for(const clsField of Object.values(this.schema.fields)) {
if (clsField?.prepareConfig)
if(clsField.prepareConfig.call(this, config) === false) return false;
for (const clsField of Object.values(this.schema.fields)) {
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
}
return config;
}
@ -260,11 +260,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* Consume Action configured resources & uses.
* That method is only used when we want those resources to be consumed outside of the use method workflow.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {boolean} successCost
* @param {boolean} successCost
*/
async consume(config, successCost = false) {
await this.workflow.get("cost")?.execute(config, successCost);
await this.workflow.get("uses")?.execute(config, successCost);
await this.workflow.get('cost')?.execute(config, successCost);
await this.workflow.get('uses')?.execute(config, successCost);
if (config.roll && !config.roll.success && successCost) {
setTimeout(() => {
@ -291,13 +291,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
get hasDamage() {
return this.damage?.parts?.length && this.type !== 'healing'
return this.damage?.parts?.length && this.type !== 'healing';
}
get hasHealing() {
return this.damage?.parts?.length && this.type === 'healing'
return this.damage?.parts?.length && this.type === 'healing';
}
get hasSave() {
return !!this.save?.trait;
}

View file

@ -30,4 +30,24 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel {
})
};
}
static getDefaultObject() {
return {
name: 'New Effect',
id: foundry.utils.randomID(),
disabled: false,
img: 'icons/magic/life/heart-cross-blue.webp',
description: '',
statuses: [],
changes: [],
system: {
rangeDependence: {
enabled: false,
type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id,
target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id,
range: CONFIG.DH.GENERAL.range.melee.id
}
}
};
}
}

View file

@ -1,9 +1,9 @@
import DHAbilityUse from "./abilityUse.mjs";
import DHActorRoll from "./actorRoll.mjs";
import DHAbilityUse from './abilityUse.mjs';
import DHActorRoll from './actorRoll.mjs';
export const config = {
abilityUse: DHAbilityUse,
adversaryRoll: DHActorRoll,
damageRoll: DHActorRoll,
dualityRoll: DHActorRoll
abilityUse: DHAbilityUse,
adversaryRoll: DHActorRoll,
damageRoll: DHActorRoll,
dualityRoll: DHActorRoll
};

View file

@ -58,10 +58,8 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel {
const actionActor = this.actionActor,
actionItem = this.actionItem;
if (!this.source.action) return null;
if(actionItem)
return actionItem.system.actionsList?.find(a => a.id === this.source.action);
else if(actionActor?.system.attack?._id === this.source.action)
return actionActor.system.attack
if (actionItem) return actionItem.system.actionsList?.find(a => a.id === this.source.action);
else if (actionActor?.system.attack?._id === this.source.action) return actionActor.system.attack;
return null;
}

View file

@ -1,4 +1,4 @@
import BeastformDialog from "../../../applications/dialogs/beastformDialog.mjs";
import BeastformDialog from '../../../applications/dialogs/beastformDialog.mjs';
const fields = foundry.data.fields;
@ -49,14 +49,14 @@ export default class BeastformField extends fields.SchemaField {
return await BeastformField.transform.call(this, selected, evolved, hybrid);
}
/**
* Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if(this.actor.effects.find(x => x.type === 'beastform')) {
if (this.actor.effects.find(x => x.type === 'beastform')) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied'));
return false;
}
@ -73,10 +73,10 @@ export default class BeastformField extends fields.SchemaField {
/**
* TODO by Harry
* @param {*} selectedForm
* @param {*} evolvedData
* @param {*} hybridData
* @returns
* @param {*} selectedForm
* @param {*} evolvedData
* @param {*} hybridData
* @returns
*/
static async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();

View file

@ -218,7 +218,8 @@ export default class CostField extends fields.ArrayField {
* @returns
*/
static getRealCosts(costs) {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
const cloneCosts = foundry.utils.deepClone(costs),
realCosts = cloneCosts?.length ? cloneCosts.filter(c => c.enabled) : [];
let mergedCosts = [];
realCosts.forEach(c => {
const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key);

View file

@ -30,8 +30,13 @@ export default class DamageField extends fields.SchemaField {
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, messageId = null, force = false) {
if(!this.hasDamage && !this.hasHealing) return;
if((this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id) && !force) return;
if (!this.hasDamage && !this.hasHealing) return;
if (
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),
@ -51,9 +56,10 @@ export default class DamageField extends fields.SchemaField {
};
delete damageConfig.evaluate;
if(DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) damageConfig.dialog.configure = false;
if (DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
damageConfig.dialog.configure = false;
if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod;
damageConfig.source.message = config.message?._id ?? messageId;
damageConfig.directDamage = !!damageConfig.source?.message;
@ -61,7 +67,7 @@ export default class DamageField extends fields.SchemaField {
// await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message);
const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig);
if(!damageResult) return false;
if (!damageResult) return false;
config.damage = damageResult.damage;
config.message ??= damageConfig.message;
}
@ -70,19 +76,15 @@ export default class DamageField extends fields.SchemaField {
* Apply Damage/Healing Action Worflow part.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
* @param {*[]} targets Arrays of targets to bypass pre-selected ones.
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
* @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async applyDamage(config, targets = null, force = false) {
targets ??= config.targets.filter(target => target.hit);
if(!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
for (let target of targets) {
const actor = fromUuidSync(target.actorId);
if(!actor) continue;
if (
!config.hasHealing &&
config.onSave &&
target.saved?.success === true
) {
if (!actor) continue;
if (!config.hasHealing && config.onSave && target.saved?.success === true) {
const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1;
Object.entries(config.damage).forEach(([k, v]) => {
v.total = 0;
@ -97,17 +99,17 @@ export default class DamageField extends fields.SchemaField {
else actor.takeDamage(config.damage, config.isDirect);
}
}
/**
* Return value or valueAlt from damage part
* Must be called within Action context or similar.
* @param {object} part Damage Part
* @param {object} part Damage Part
* @param {object} data Action getRollData
* @returns Formula value object
*/
static getFormulaValue(part, data) {
let formulaValue = part.value;
if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt;
const isAdversary = this.actor.type === 'adversary';
@ -123,8 +125,8 @@ 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 {object} data Action getRollData
* @returns
* @param {object} data Action getRollData
* @returns
*/
static formatFormulas(formulas, data) {
const formattedFormulas = [];
@ -145,16 +147,25 @@ export default class DamageField extends fields.SchemaField {
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players)
);
}
/**
* Return the automation setting for applyDamage method for current user role
* @returns {boolean} If applyDamage should be triggered automatically
*/
static getApplyAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players)
);
}
}
@ -165,7 +176,9 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
multiplier: new fields.StringField({
choices: CONFIG.DH.GENERAL.multiplierTypes,
initial: 'prof',
label: 'DAGGERHEART.ACTIONS.Config.damage.multiplier'
label: 'DAGGERHEART.ACTIONS.Config.damage.multiplier',
nullable: false,
required: true
}),
flatMultiplier: new fields.NumberField({
nullable: true,
@ -175,7 +188,9 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
dice: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: 'd6',
label: 'DAGGERHEART.GENERAL.Dice.single'
label: 'DAGGERHEART.GENERAL.Dice.single',
nullable: false,
required: true
}),
bonus: new fields.NumberField({ nullable: true, initial: null, label: 'DAGGERHEART.GENERAL.bonus' }),
custom: new fields.SchemaField({

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from "../../../systemRegistration/socket.mjs";
import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
const fields = foundry.data.fields;
@ -25,21 +25,16 @@ export default class EffectsField extends fields.ArrayField {
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, targets = null, force = false) {
if(!config.hasEffect) return;
if (!config.hasEffect) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if(!message) {
if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
if(EffectsField.getAutomation() || force) {
if (EffectsField.getAutomation() || force) {
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
await emitAsGM(
GMUpdateEvent.UpdateEffect,
EffectsField.applyEffects.bind(this),
targets,
this.uuid
);
await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid);
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
}
}
@ -53,8 +48,7 @@ export default class EffectsField extends fields.ArrayField {
if (!this.effects?.length || !targets?.length) return;
let effects = this.effects;
targets.forEach(async token => {
if (this.hasSave && token.saved.success === true)
effects = this.effects.filter(e => e.onSave === true);
if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true);
if (!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
@ -96,6 +90,11 @@ export default class EffectsField extends fields.ArrayField {
* @returns {boolean} If execute should be triggered automatically
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players)
);
}
}

View file

@ -8,7 +8,7 @@ export default class MacroField extends fields.DocumentUUIDField {
/** @inheritDoc */
constructor(context = {}) {
super({ type: "Macro" }, context);
super({ type: 'Macro' }, context);
}
/**

View file

@ -1,14 +1,13 @@
const fields = foundry.data.fields;
export default class RangeField extends fields.StringField {
/** @inheritDoc */
constructor(context = {}) {
const options = {
choices: CONFIG.DH.GENERAL.range,
required: false,
blank: true,
label: "DAGGERHEART.GENERAL.range"
label: 'DAGGERHEART.GENERAL.range'
};
super(options, context);
}

View file

@ -5,18 +5,27 @@ export class DHActionRollData extends foundry.abstract.DataModel {
static defineSchema() {
return {
type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities, label: "DAGGERHEART.GENERAL.Trait.single" }),
trait: new fields.StringField({
nullable: true,
initial: null,
choices: CONFIG.DH.ACTOR.abilities,
label: 'DAGGERHEART.GENERAL.Trait.single'
}),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
advState: new fields.StringField({
choices: CONFIG.DH.ACTIONS.advantageState,
initial: 'neutral'
initial: 'neutral',
nullable: false,
required: true
}),
diceRolling: new fields.SchemaField({
multiplier: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceSetNumbers,
initial: 'prof',
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier'
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier',
nullable: false,
required: true
}),
flatMultiplier: new fields.NumberField({
nullable: true,
@ -26,7 +35,9 @@ export class DHActionRollData extends foundry.abstract.DataModel {
dice: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: CONFIG.DH.GENERAL.diceTypes.d6,
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice'
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice',
nullable: false,
required: true
}),
compare: new fields.StringField({
choices: CONFIG.DH.ACTIONS.diceCompare,
@ -86,14 +97,14 @@ export class DHActionRollData extends foundry.abstract.DataModel {
}
get rollTrait() {
if(this.parent?.actor?.type !== "character") return null;
if (this.parent?.actor?.type !== 'character') return null;
switch (this.type) {
case CONFIG.DH.GENERAL.rollTypes.spellcast.id:
return this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility';
case CONFIG.DH.GENERAL.rollTypes.attack.id:
case CONFIG.DH.GENERAL.rollTypes.trait.id:
return this.useDefault || !this.trait
? this.parent.item.system.attack?.roll?.trait ?? 'agility'
? (this.parent.item.system.attack?.roll?.trait ?? 'agility')
: this.trait;
default:
return null;
@ -118,21 +129,21 @@ export default class RollField extends fields.EmbeddedDataField {
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
static async execute(config) {
if(!config.hasRoll) return;
if (!config.hasRoll) return;
config = await this.actor.diceRoll(config);
if(!config) return false;
if (!config) return false;
}
/**
* Update Action Workflow config object.
* Must be called within Action context.
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if(!config.hasRoll) return;
if (!config.hasRoll) return;
config.dialog.configure = RollField.getAutomation() ? !config.dialog.configure : config.dialog.configure;
const roll = {
baseModifiers: this.roll.getModifier(),
label: 'Attack',
@ -152,6 +163,11 @@ export default class RollField extends fields.EmbeddedDataField {
* @returns {boolean} If execute should be triggered automatically
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players)
);
}
}

View file

@ -1,4 +1,4 @@
import { abilities } from "../../../config/actorConfig.mjs";
import { abilities } from '../../../config/actorConfig.mjs';
const fields = foundry.data.fields;
@ -19,7 +19,9 @@ export default class SaveField extends fields.SchemaField {
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
damageMod: new fields.StringField({
initial: CONFIG.DH.ACTIONS.damageOnSave.none.id,
choices: CONFIG.DH.ACTIONS.damageOnSave
choices: CONFIG.DH.ACTIONS.damageOnSave,
nullable: false,
required: true
})
};
super(saveFields, options, context);
@ -33,15 +35,15 @@ export default class SaveField extends fields.SchemaField {
* @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example.
*/
static async execute(config, targets = null, force = false) {
if(!config.hasSave) return;
if (!config.hasSave) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if(!message) {
if (!message) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);
}
if(SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
if (SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) {
targets ??= config.targets.filter(t => !config.hasRoll || t.hit);
await SaveField.rollAllSave.call(this, targets, config.event, message);
} else return false;
@ -52,35 +54,35 @@ export default class SaveField extends fields.SchemaField {
* Must be called within Action context.
* @param {object[]} targets Array of formatted targets.
* @param {Event} event Triggering event
* @param {ChatMessage} message The ChatMessage the triggered button comes from.
* @param {ChatMessage} message The ChatMessage the triggered button comes from.
*/
static async rollAllSave(targets, event, message) {
if(!targets) return;
if (!targets) return;
return new Promise(resolve => {
const aPromise = [];
targets.forEach(target => {
aPromise.push(
new Promise(async subResolve => {
const actor = fromUuidSync(target.actorId);
if(actor) {
const rollSave = game.user === actor.owner ?
SaveField.rollSave.call(this, actor, event)
: actor.owner
.query('reactionRoll', {
actionId: this.uuid,
actorId: actor.uuid,
event,
message
});
if (actor) {
const rollSave =
game.user === actor.owner
? SaveField.rollSave.call(this, actor, event)
: actor.owner.query('reactionRoll', {
actionId: this.uuid,
actorId: actor.uuid,
event,
message
});
const result = await rollSave;
await SaveField.updateSaveMessage.call(this, result, message, target.id);
subResolve();
} else subResolve();
})
)
);
});
Promise.all(aPromise).then(result => resolve());
})
});
}
/**
@ -93,10 +95,10 @@ export default class SaveField extends fields.SchemaField {
static async rollSave(actor, event) {
if (!actor) return;
const title = actor.isNPC
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
}),
? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll')
: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: game.i18n.localize(abilities[this.save.trait]?.label)
}),
rollConfig = {
event,
title,
@ -109,7 +111,8 @@ export default class SaveField extends fields.SchemaField {
hasRoll: true,
data: actor.getRollData()
};
if(SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) rollConfig.dialog = { configure: false };
if (SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id)
rollConfig.dialog = { configure: false };
return actor.diceRoll(rollConfig);
}
@ -117,31 +120,32 @@ export default class SaveField extends fields.SchemaField {
* Update a Roll ChatMessage for a token according to his Reaction Roll result.
* @param {object} result Result from the Reaction Roll
* @param {object} message ChatMessage to update
* @param {string} targetId Token ID
* @param {string} targetId Token ID
*/
static async updateSaveMessage(result, message, targetId) {
if (!result) return;
const updateMsg = async function(message, targetId, result) {
const updateMsg = async function (message, targetId, result) {
// setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id),
changes = {
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]:
{
result: result.roll.total,
success: result.roll.success
}
const chatMessage = ui.chat.collection.get(message._id),
changes = {
flags: {
[game.system.id]: {
reactionRolls: {
[targetId]: {
result: result.roll.total,
success: result.roll.success
}
}
}
};
await chatMessage.update(changes);
}
};
await chatMessage.update(changes);
// }, 100);
};
if (game.modules.get('dice-so-nice')?.active)
game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(async () => await updateMsg(message, targetId, result));
game.dice3d
.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id)
.then(async () => await updateMsg(message, targetId, result));
else await updateMsg(message, targetId, result);
}
@ -150,25 +154,29 @@ export default class SaveField extends fields.SchemaField {
* @returns {string} Id from settingsConfig.mjs actionAutomationChoices
*/
static getAutomation() {
return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players)
return (
(game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) ||
(!game.user.isGM &&
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players)
);
}
/**
* Send a query to an Actor owner to roll a Reaction Roll then send back the result.
* @param {object} param0
* @param {string} param0.actionId Action ID
* @param {string} param0.actorId Actor ID
* @param {Event} param0.event Triggering event
* @param {ChatMessage} param0.message Chat Message to update
* @returns
* @param {object} param0
* @param {string} param0.actionId Action ID
* @param {string} param0.actorId Actor ID
* @param {Event} param0.event Triggering event
* @param {ChatMessage} param0.message Chat Message to update
* @returns
*/
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);
if (!actor || !actor?.isOwner) reject();
SaveField.rollSave.call(action, actor, event, message)
.then(result => resolve(result));
SaveField.rollSave.call(action, actor, event, message).then(result => resolve(result));
});
}
}

View file

@ -1,14 +1,14 @@
const fields = foundry.data.fields;
export default class TargetField extends fields.SchemaField {
/** @inheritDoc */
constructor(options = {}, context = {}) {
const targetFields = {
type: new fields.StringField({
choices: CONFIG.DH.GENERAL.targetTypes,
initial: CONFIG.DH.GENERAL.targetTypes.any.id,
nullable: true
nullable: true,
blank: true
}),
amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
};
@ -21,7 +21,7 @@ export default class TargetField extends fields.SchemaField {
* @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.
*/
prepareConfig(config) {
if (!this.target?.type) return config.targets = [];
if (!this.target?.type) return (config.targets = []);
config.hasTarget = true;
let targets;
// If the Action is configured as self-targeted, set targets as the owner.
@ -62,15 +62,11 @@ export default class TargetField extends fields.SchemaField {
* @returns {boolean} If both actors respect the provided type.
*/
static isTargetFriendly(actor, target, type) {
const actorDisposition = actor.token
? actor.token.disposition
: actor.prototypeToken.disposition,
const actorDisposition = actor.token ? actor.token.disposition : actor.prototypeToken.disposition,
targetDisposition = target.document.disposition;
return (
(type === CONFIG.DH.GENERAL.targetTypes.friendly.id &&
actorDisposition === targetDisposition) ||
(type === CONFIG.DH.GENERAL.targetTypes.hostile.id &&
actorDisposition + targetDisposition === 0)
(type === CONFIG.DH.GENERAL.targetTypes.friendly.id && actorDisposition === targetDisposition) ||
(type === CONFIG.DH.GENERAL.targetTypes.hostile.id && actorDisposition + targetDisposition === 0)
);
}

View file

@ -7,7 +7,7 @@ export default class UsesField extends fields.SchemaField {
* Action Workflow order
*/
static order = 160;
/** @inheritDoc */
constructor(options = {}, context = {}) {
const usesFields = {
@ -62,7 +62,7 @@ export default class UsesField extends fields.SchemaField {
/**
* Prepare Uses object for Action Workflow
* Must be called within Action context.
* @param {object} uses
* @param {object} uses
* @returns {object}
*/
static calcUses(uses) {
@ -77,7 +77,7 @@ export default class UsesField extends fields.SchemaField {
/**
* Check if the Action still get atleast one unspent uses.
* Must be called within Action context.
* @param {*} uses
* @param {*} uses
* @returns {boolean}
*/
static hasUses(uses) {

View file

@ -1,5 +1,4 @@
import AttachableItem from './attachableItem.mjs';
import { armorFeatures } from '../../config/itemConfig.mjs';
export default class DHArmor extends AttachableItem {
/** @inheritDoc */
@ -25,7 +24,7 @@ export default class DHArmor extends AttachableItem {
new fields.SchemaField({
value: new fields.StringField({
required: true,
choices: CONFIG.DH.ITEM.armorFeatures,
choices: CONFIG.DH.ITEM.allArmorFeatures,
blank: true
}),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
@ -60,13 +59,14 @@ export default class DHArmor extends AttachableItem {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
const changedArmorFeatures = changes.system?.armorFeatures ?? [];
const removedFeatures = this.armorFeatures.filter(x => changedArmorFeatures.every(y => y.value !== x.value));
if (changes.system?.armorFeatures) {
const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x));
const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x));
const added = changedArmorFeatures.filter(x => this.armorFeatures.every(y => y.value !== x.value));
const effectIds = [];
const actionIds = [];
for (var feature of removed) {
for (var feature of removedFeatures) {
effectIds.push(...feature.effectIds);
actionIds.push(...feature.actionIds);
}
@ -76,8 +76,9 @@ export default class DHArmor extends AttachableItem {
return acc;
}, {});
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
for (const feature of added) {
const featureData = armorFeatures[feature.value];
const featureData = allFeatures[feature.value];
if (featureData.effects?.length > 0) {
const embeddedItems = await this.parent.createEmbeddedDocuments(
'ActiveEffect',
@ -91,7 +92,7 @@ export default class DHArmor extends AttachableItem {
}
const newActions = {};
if (featureData.actions?.length > 0) {
if (featureData.actions?.length > 0 || featureData.actions?.size > 0) {
for (let action of featureData.actions) {
const embeddedEffects = await this.parent.createEmbeddedDocuments(
'ActiveEffect',
@ -110,10 +111,12 @@ export default class DHArmor extends AttachableItem {
{
...cls.getSourceConfig(this),
...action,
type: action.type,
_id: actionId,
name: game.i18n.localize(action.name),
description: game.i18n.localize(action.description),
effects: embeddedEffects.map(x => ({ _id: x.id }))
effects: embeddedEffects.map(x => ({ _id: x.id })),
systemPath: 'actions'
},
{ parent: this }
);
@ -126,6 +129,10 @@ export default class DHArmor extends AttachableItem {
}
}
_onUpdate(a, b, c) {
super._onUpdate(a, b, c);
}
/**
* Generates a list of localized tags based on this item's type-specific properties.
* @returns {string[]} An array of localized tag strings.
@ -145,7 +152,8 @@ export default class DHArmor extends AttachableItem {
*/
_getLabels() {
const labels = [];
if(this.baseScore) labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`)
if (this.baseScore)
labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`);
return labels;
}

View file

@ -39,7 +39,7 @@ export default class DHWeapon extends AttachableItem {
new fields.SchemaField({
value: new fields.StringField({
required: true,
choices: CONFIG.DH.ITEM.weaponFeatures,
choices: CONFIG.DH.ITEM.allWeaponFeatures,
blank: true
}),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
@ -116,13 +116,14 @@ export default class DHWeapon extends AttachableItem {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
const changedWeaponFeatures = changes.system?.weaponFeatures ?? [];
const removedFeatures = this.weaponFeatures.filter(x => changedWeaponFeatures.every(y => y.value !== x.value));
if (changes.system?.weaponFeatures) {
const removed = this.weaponFeatures.filter(x => !changes.system.weaponFeatures.includes(x));
const added = changes.system.weaponFeatures.filter(x => !this.weaponFeatures.includes(x));
const added = changedWeaponFeatures.filter(x => this.weaponFeatures.every(y => y.value !== x.value));
const removedEffectsUpdate = [];
const removedActionsUpdate = [];
for (let weaponFeature of removed) {
for (let weaponFeature of removedFeatures) {
removedEffectsUpdate.push(...weaponFeature.effectIds);
removedActionsUpdate.push(...weaponFeature.actionIds);
}
@ -133,8 +134,9 @@ export default class DHWeapon extends AttachableItem {
return acc;
}, {});
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
for (let weaponFeature of added) {
const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value];
const featureData = allFeatures[weaponFeature.value];
if (featureData.effects?.length > 0) {
const embeddedItems = await this.parent.createEmbeddedDocuments(
'ActiveEffect',
@ -148,7 +150,7 @@ export default class DHWeapon extends AttachableItem {
}
const newActions = {};
if (featureData.actions?.length > 0) {
if (featureData.actions?.length > 0 || featureData.actions?.size > 0) {
for (let action of featureData.actions) {
const embeddedEffects = await this.parent.createEmbeddedDocuments(
'ActiveEffect',
@ -170,10 +172,12 @@ export default class DHWeapon extends AttachableItem {
{
...cls.getSourceConfig(this),
...action,
type: action.type,
_id: actionId,
name: game.i18n.localize(action.name),
description: game.i18n.localize(action.description),
effects: embeddedEffects.map(x => ({ _id: x.id }))
effects: embeddedEffects.map(x => ({ _id: x.id })),
systemPath: 'actions'
},
{ parent: this }
);

View file

@ -169,7 +169,7 @@ export const defaultLevelTiers = {
tiers: {
2: {
tier: 2,
name: 'Tier 2',
name: 'DAGGERHEART.APPLICATIONS.Levelup.tier2.name',
levels: {
start: 2,
end: 4
@ -232,7 +232,7 @@ export const defaultLevelTiers = {
},
3: {
tier: 3,
name: 'Tier 3',
name: 'DAGGERHEART.APPLICATIONS.Levelup.tier3.name',
levels: {
start: 5,
end: 7
@ -313,7 +313,7 @@ export const defaultLevelTiers = {
},
4: {
tier: 4,
name: 'Tier 4',
name: 'DAGGERHEART.APPLICATIONS.Levelup.tier4.name',
levels: {
start: 8,
end: 10

View file

@ -234,7 +234,7 @@ export class DhLevelup extends foundry.abstract.DataModel {
const subclassInTier = subclasses.some(x => x.tier === Number(tierKey));
return {
name: tier.name,
name: game.i18n.localize(tier.name),
active: this.currentLevel >= Math.min(...tier.belongingLevels),
groups: Object.keys(tier.options).map(optionKey => {
const option = tier.options[optionKey];

View file

@ -1,100 +1,46 @@
import { fearDisplay } from '../../config/generalConfig.mjs';
export default class DhAppearance extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.SETTINGS.Appearance'];
static defineSchema() {
const fields = foundry.data.fields;
const { StringField, ColorField, BooleanField, SchemaField } = foundry.data.fields;
// helper to create dice style schema
const diceStyle = ({ fg, bg, outline, edge }) =>
new SchemaField({
foreground: new ColorField({ required: true, initial: fg }),
background: new ColorField({ required: true, initial: bg }),
outline: new ColorField({ required: true, initial: outline }),
edge: new ColorField({ required: true, initial: edge }),
texture: new StringField({ initial: 'astralsea', required: true, blank: false }),
colorset: new StringField({ initial: 'inspired', required: true, blank: false }),
material: new StringField({ initial: 'metal', required: true, blank: false }),
system: new StringField({ initial: 'standard', required: true, blank: false })
});
return {
displayFear: new fields.StringField({
displayFear: new StringField({
required: true,
choices: fearDisplay,
initial: fearDisplay.token.value,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.displayFear.label'
choices: CONFIG.DH.GENERAL.fearDisplay,
initial: CONFIG.DH.GENERAL.fearDisplay.token.value
}),
diceSoNice: new fields.SchemaField({
hope: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#ffffff' }),
background: new fields.ColorField({ required: true, initial: '#ffe760' }),
outline: new fields.ColorField({ required: true, initial: '#000000' }),
edge: new fields.ColorField({ required: true, initial: '#ffffff' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
}),
fear: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#000000' }),
background: new fields.ColorField({ required: true, initial: '#0032b1' }),
outline: new fields.ColorField({ required: true, initial: '#ffffff' }),
edge: new fields.ColorField({ required: true, initial: '#000000' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
}),
advantage: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#ffffff' }),
background: new fields.ColorField({ required: true, initial: '#008000' }),
outline: new fields.ColorField({ required: true, initial: '#000000' }),
edge: new fields.ColorField({ required: true, initial: '#ffffff' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
}),
disadvantage: new fields.SchemaField({
foreground: new fields.ColorField({ required: true, initial: '#000000' }),
background: new fields.ColorField({ required: true, initial: '#b30000' }),
outline: new fields.ColorField({ required: true, initial: '#ffffff' }),
edge: new fields.ColorField({ required: true, initial: '#000000' }),
texture: new fields.StringField({ initial: 'astralsea' }),
colorset: new fields.StringField({ initial: 'inspired' }),
material: new fields.StringField({ initial: 'metal' }),
system: new fields.StringField({ initial: 'standard' })
})
diceSoNice: new SchemaField({
hope: diceStyle({ fg: '#ffffff', bg: '#ffe760', outline: '#000000', edge: '#ffffff' }),
fear: diceStyle({ fg: '#000000', bg: '#0032b1', outline: '#ffffff', edge: '#000000' }),
advantage: diceStyle({ fg: '#ffffff', bg: '#008000', outline: '#000000', edge: '#ffffff' }),
disadvantage: diceStyle({ fg: '#000000', bg: '#b30000', outline: '#ffffff', edge: '#000000' })
}),
showGenericStatusEffects: new fields.BooleanField({
initial: true,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.showGenericStatusEffects.label'
extendCharacterDescriptions: new BooleanField(),
extendAdversaryDescriptions: new BooleanField(),
extendEnvironmentDescriptions: new BooleanField(),
extendItemDescriptions: new BooleanField(),
expandRollMessage: new SchemaField({
desc: new BooleanField(),
roll: new BooleanField(),
damage: new BooleanField(),
target: new BooleanField()
}),
extendCharacterDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendCharacterDescriptions.label'
}),
extendAdversaryDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendAdversaryDescriptions.label'
}),
extendEnvironmentDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendEnvironmentDescriptions.label'
}),
extendItemDescriptions: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.extendItemDescriptions.label'
}),
expandRollMessage: new fields.SchemaField({
desc: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageDesc.label'
}),
roll: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageRoll.label'
}),
damage: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageDamage.label'
}),
target: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.expandRollMessageTarget.label'
})
}),
hideAttribution: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Appearance.FIELDS.hideAttribution.label'
})
hideAttribution: new BooleanField(),
showGenericStatusEffects: new BooleanField({ initial: true })
};
}
}

View file

@ -97,13 +97,13 @@ export default class DhAutomation extends foundry.abstract.DataModel {
damage: new fields.SchemaField({
gm: new fields.StringField({
required: true,
initial: "never",
initial: 'never',
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
label: 'DAGGERHEART.GENERAL.gm'
}),
players: new fields.StringField({
required: true,
initial: "never",
initial: 'never',
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
label: 'DAGGERHEART.GENERAL.player.plurial'
})
@ -111,13 +111,13 @@ export default class DhAutomation extends foundry.abstract.DataModel {
save: new fields.SchemaField({
gm: new fields.StringField({
required: true,
initial: "never",
initial: 'never',
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
label: 'DAGGERHEART.GENERAL.gm'
}),
players: new fields.StringField({
required: true,
initial: "never",
initial: 'never',
choices: CONFIG.DH.SETTINGS.actionAutomationChoices,
label: 'DAGGERHEART.GENERAL.player.plurial'
})

View file

@ -115,7 +115,35 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
label: new fields.StringField({ required: true, label: 'DAGGERHEART.GENERAL.label' }),
description: new fields.StringField()
})
)
),
itemFeatures: new fields.SchemaField({
weaponFeatures: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField(),
effects: new fields.ArrayField(new fields.ObjectField())
})
),
armorFeatures: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
img: new fields.FilePathField({
initial: 'icons/magic/life/cross-worn-green.webp',
categories: ['IMAGE'],
base64: false
}),
description: new fields.HTMLField(),
actions: new ActionsField(),
effects: new fields.ArrayField(new fields.ObjectField())
})
)
})
};
}
}