merged with main

This commit is contained in:
WBHarry 2025-06-29 23:05:14 +02:00
commit 0aa08deaa3
41 changed files with 805 additions and 446 deletions

View file

@ -1,5 +1,5 @@
import CostSelectionDialog from '../../applications/costSelectionDialog.mjs';
import { DHActionDiceData, DHDamageData, DHDamageField } from './actionDice.mjs';
import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField } from './actionDice.mjs';
import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../dialogs/d20RollDialog.mjs';
@ -69,16 +69,14 @@ export class DHBaseAction extends foundry.abstract.DataModel {
static defineExtraSchema() {
const extraFields = {
damage: new DHDamageField(),
roll: new fields.SchemaField({
type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 })
}),
roll: new fields.EmbeddedDataField(DHActionRollData),
save: new fields.SchemaField({
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }),
damageMod: new fields.StringField({ initial: SYSTEM.ACTIONS.damageOnSave.none.id, choices: SYSTEM.ACTIONS.damageOnSave })
damageMod: new fields.StringField({
initial: SYSTEM.ACTIONS.damageOnSave.none.id,
choices: SYSTEM.ACTIONS.damageOnSave
})
}),
target: new fields.SchemaField({
type: new fields.StringField({
@ -103,9 +101,12 @@ export class DHBaseAction extends foundry.abstract.DataModel {
initial: SYSTEM.GENERAL.healingTypes.hitPoints.id,
label: 'Healing'
}),
resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }),
resultBased: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.Actions.Settings.ResultBased.label'
}),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData)
})
},
extraSchemas = {};
@ -158,19 +159,23 @@ export class DHBaseAction extends foundry.abstract.DataModel {
return updateSource;
}
getRollData() {
getRollData(data = {}) {
const actorData = this.actor.getRollData(false);
// Remove when included directly in Actor getRollData
actorData.prof = actorData.proficiency?.value ?? 1,
actorData.cast = actorData.spellcast?.value ?? 1,
actorData.scale = this.cost.length
? this.cost.reduce((a, c) => {
actorData.prof = actorData.proficiency?.value ?? 1;
actorData.cast = actorData.spellcast?.value ?? 1;
actorData.result = data.roll?.total ?? 1;
/* actorData.scale = data.costs?.length
? data.costs.reduce((a, c) => {
a[c.type] = c.value;
return a;
}, {})
: 1,
actorData.roll = {}
: 1; */
actorData.scale = data.costs?.length // Right now only return the first scalable cost.
? (data.costs.find(c => c.scalable)?.total ?? 1)
: 1;
actorData.roll = {};
return actorData;
}
@ -192,12 +197,14 @@ export class DHBaseAction extends foundry.abstract.DataModel {
// Prepare Costs
const costsConfig = this.prepareCost();
if(isFastForward && !this.hasCost(costsConfig)) return ui.notifications.warn("You don't have the resources to use that action.");
if (isFastForward && !this.hasCost(costsConfig))
return ui.notifications.warn("You don't have the resources to use that action.");
// config = this.prepareUseCost(config)
// Prepare Uses
const usesConfig = this.prepareUse();
if(isFastForward && !this.hasUses(usesConfig)) return ui.notifications.warn("That action doesn't have remaining uses.");
if (isFastForward && !this.hasUses(usesConfig))
return ui.notifications.warn("That action doesn't have remaining uses.");
// config = this.prepareUseCost(config)
// Prepare Roll Data
@ -210,24 +217,24 @@ export class DHBaseAction extends foundry.abstract.DataModel {
costs: costsConfig,
uses: usesConfig,
data: actorData
}
if ( Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false ) return;
};
if (Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary
if ( config.dialog.configure && this.requireConfigurationDialog(config) ) {
if (config.dialog?.configure && this.requireConfigurationDialog(config)) {
config = await D20RollDialog.configure(config);
if (!config) return;
}
if ( this.hasRoll ) {
if (this.hasRoll) {
const rollConfig = this.prepareRoll(config);
config.roll = rollConfig;
config = await this.actor.diceRoll(config);
if (!config) return;
}
if( this.hasSave ) {
if (this.hasSave) {
/* config.targets.forEach((t) => {
if(t.hit) {
const target = game.canvas.tokens.get(t.id),
@ -259,16 +266,16 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}) */
}
if ( this.doFollowUp() ) {
if(this.rollDamage) await this.rollDamage(event, config);
if(this.rollHealing) await this.rollHealing(event, config);
if(this.trigger) await this.trigger(event, config);
if (this.doFollowUp()) {
if (this.rollDamage) await this.rollDamage(event, config);
if (this.rollHealing) await this.rollHealing(event, config);
if (this.trigger) await this.trigger(event, config);
}
// Consume resources
await this.consume(config);
if ( Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false ) return;
if (Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false) return;
return config;
}
@ -281,21 +288,18 @@ export class DHBaseAction extends foundry.abstract.DataModel {
source: {
item: this.item._id,
action: this._id
// action: this
},
dialog: {
configure: true
},
dialog: {},
type: this.type,
hasDamage: !!this.damage?.parts?.length,
hasHealing: !!this.healing,
hasEffect: !!this.effects?.length,
hasSave: this.hasSave
}
};
}
requireConfigurationDialog(config) {
return !config.event.shiftkey && !this.hasRoll && (config.costs?.length || config.uses);
return !config.event.shiftKey && !this.hasRoll && (config.costs?.length || config.uses);
}
prepareCost() {
@ -321,7 +325,6 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
targets = targets.map(t => this.formatTarget(t));
return targets;
}
prepareRange() {
@ -330,13 +333,16 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
prepareRoll() {
const roll = {
const roll = {
modifiers: [],
trait: this.roll?.trait,
label: 'Attack',
type: this.actionType,
difficulty: this.roll?.difficulty
difficulty: this.roll?.difficulty,
formula: this.roll.getFormula()
};
if (this.roll?.type === 'diceSet') roll.lite = true;
return roll;
}
@ -345,11 +351,14 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
async consume(config) {
const resources = config.costs.filter(c => c.enabled !== false).map(c => {
return { type: c.type, value: c.total * -1 };
});
const resources = config.costs
.filter(c => c.enabled !== false)
.map(c => {
return { type: c.type, value: (c.total ?? c.value) * -1 };
});
await this.actor.modifyResource(resources);
if(config.uses?.enabled) {
if (config.uses?.enabled) {
const newActions = foundry.utils.getProperty(this.item.system, this.systemPath).map(x => x.toObject());
newActions[this.index].uses.value++;
await this.item.update({ [`system.${this.systemPath}`]: newActions });
@ -388,13 +397,16 @@ export class DHBaseAction extends foundry.abstract.DataModel {
hasCost(costs) {
const realCosts = this.getRealCosts(costs);
return realCosts.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true);
return realCosts.reduce(
(a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value),
true
);
}
/* COST */
/* USES */
calcUses(uses) {
if(!uses) return null;
if (!uses) return null;
return {
...uses,
enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true
@ -402,7 +414,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
hasUses(uses) {
if(!uses) return true;
if (!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max;
}
/* USES */
@ -432,7 +444,7 @@ export class DHBaseAction extends foundry.abstract.DataModel {
/* TARGET */
/* RANGE */
/* RANGE */
/* EFFECTS */
@ -441,10 +453,10 @@ export class DHBaseAction extends foundry.abstract.DataModel {
let effects = this.effects;
data.system.targets.forEach(async token => {
if (!token.hit && !force) return;
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;
if (!effects.length) return;
effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor,
effect = this.item.effects.get(e._id);
@ -479,35 +491,42 @@ export class DHBaseAction extends foundry.abstract.DataModel {
/* SAVE */
async rollSave(target, event, message) {
if(!target?.actor) return;
target.actor.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty,
type: "reaction"
},
data: target.actor.getRollData()
}).then(async (result) => {
this.updateChatMessage(message, target.id, {result: result.roll.total, success: result.roll.success});
})
if (!target?.actor) return;
target.actor
.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty,
type: 'reaction'
},
data: target.actor.getRollData()
})
.then(async result => {
if (result)
this.updateChatMessage(message, target.id, {
result: result.roll.total,
success: result.roll.success
});
});
}
async updateChatMessage(message, targetId, changes, chain=true) {
async updateChatMessage(message, targetId, changes, chain = true) {
setTimeout(async () => {
const chatMessage = ui.chat.collection.get(message._id),
msgTargets = chatMessage.system.targets,
msgTarget = msgTargets.find(mt => mt.id === targetId);
msgTarget.saved = changes;
await chatMessage.update({'system.targets': msgTargets});
},100);
if(chain) {
if(message.system.source.message) this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
await chatMessage.update({ 'system.targets': msgTargets });
}, 100);
if (chain) {
if (message.system.source.message)
this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false);
const relatedChatMessages = ui.chat.collection.filter(c => c.system.source.message === message._id);
relatedChatMessages.forEach(c => {
this.updateChatMessage(c, targetId, changes, false);
})
});
}
}
/* SAVE */
@ -525,7 +544,7 @@ export class DHDamageAction extends DHBaseAction {
getFormulaValue(part, data) {
let formulaValue = part.value;
if(this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt;
return formulaValue;
}
@ -535,16 +554,19 @@ export class DHDamageAction extends DHBaseAction {
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula },
bonusDamage = [];
if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data));
const config = {
title: game.i18n.format('DAGGERHEART.Chat.DamageRoll.Title', { damage: this.name }),
formula,
targets: (data.system?.targets.filter(t => t.hit) ?? data.targets),
roll: { formula },
targets: data.system?.targets.filter(t => t.hit) ?? data.targets,
hasSave: this.hasSave,
source: data.system?.source
source: data.system?.source,
event
};
if(this.hasSave) config.onSave = this.save.damageMod;
if(data.system) {
if (this.hasSave) config.onSave = this.save.damageMod;
if (data.system) {
config.source.message = data._id;
config.directDamage = false;
}
@ -575,7 +597,7 @@ export class DHAttackAction extends DHDamageAction {
getParentDamage() {
return {
value: {
multiplier: 'proficiency',
multiplier: 'prof',
dice: this.item?.system?.damage.value,
bonus: this.item?.system?.damage.bonus ?? 0
},
@ -594,7 +616,8 @@ export class DHHealingAction extends DHBaseAction {
getFormulaValue(data) {
let formulaValue = this.healing.value;
if(this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1) return this.healing.valueAlt;
if (this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1)
return this.healing.valueAlt;
return formulaValue;
}
@ -605,15 +628,16 @@ export class DHHealingAction extends DHBaseAction {
if (!formula || formula == '') return;
let roll = { formula: formula, total: formula },
bonusDamage = [];
const config = {
title: game.i18n.format('DAGGERHEART.Chat.HealingRoll.Title', {
healing: game.i18n.localize(SYSTEM.GENERAL.healingTypes[this.healing.type].label)
}),
formula,
roll: { formula },
targets: (data.system?.targets ?? data.targets).filter(t => t.hit),
messageType: 'healing',
type: this.healing.type
type: this.healing.type,
event
};
roll = CONFIG.Dice.daggerheart.DamageRoll.build(config);

View file

@ -2,13 +2,63 @@ import FormulaField from '../fields/formulaField.mjs';
const fields = foundry.data.fields;
/* Roll Field */
export class DHActionRollData extends foundry.abstract.DataModel {
/** @override */
static defineSchema() {
return {
type: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.GENERAL.rollTypes }),
trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
diceRolling: new fields.SchemaField({
multiplier: new fields.StringField({
choices: SYSTEM.GENERAL.diceSetNumbers,
initial: 'prof',
label: 'Dice Number'
}),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
dice: new fields.StringField({ choices: SYSTEM.GENERAL.diceTypes, initial: 'd6', label: 'Dice Type' }),
compare: new fields.StringField({
choices: SYSTEM.ACTIONS.diceCompare,
initial: 'above',
label: 'Should be'
}),
treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' })
})
};
}
getFormula() {
if (!this.type) return;
let formula = '';
switch (this.type) {
case 'diceSet':
const multiplier =
this.diceRolling.multiplier === 'flat'
? this.diceRolling.flatMultiplier
: `@${this.diceRolling.multiplier}`;
formula = `${multiplier}${this.diceRolling.dice}cs${SYSTEM.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`;
break;
default:
// formula = `${(!!this.parent?.actor?.system?.attack ? `@attackBonus` : `@traits.${this.trait}.total`)}`;
formula = '';
break;
}
return formula;
}
}
/* Damage & Healing Field */
export class DHActionDiceData extends foundry.abstract.DataModel {
/** @override */
static defineSchema() {
return {
multiplier: new fields.StringField({
choices: SYSTEM.GENERAL.multiplierTypes,
initial: 'proficiency',
initial: 'prof',
label: 'Multiplier'
}),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
@ -22,10 +72,13 @@ export class DHActionDiceData extends foundry.abstract.DataModel {
}
getFormula(actor) {
const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : actor.system[this.multiplier]?.total;
/* const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : actor.system[this.multiplier]?.total;
return this.custom.enabled
? this.custom.formula
: `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`;
: `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; */
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}`;
}
}
@ -53,9 +106,12 @@ export class DHDamageData extends foundry.abstract.DataModel {
nullable: false,
required: true
}),
resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }),
resultBased: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.Actions.Settings.ResultBased.label'
}),
value: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData),
valueAlt: new fields.EmbeddedDataField(DHActionDiceData)
};
}
}

View file

@ -77,4 +77,8 @@ export default class DhpAdversary extends BaseDataActor {
})
};
}
get attackBonus() {
return this.attack.roll.bonus;
}
}

View file

@ -62,7 +62,7 @@ export default class DhCharacter extends BaseDataActor {
}),
experiences: new fields.TypedObjectField(
new fields.SchemaField({
description: new fields.StringField({}),
name: new fields.StringField(),
value: new fields.NumberField({ integer: true, initial: 0 }),
bonus: new fields.NumberField({ integer: true, initial: 0 })
})

View file

@ -26,6 +26,7 @@ export default class DHDualityRoll extends foundry.abstract.TypeDataModel {
})
})
),
costs: new fields.ArrayField(new fields.ObjectField()),
hasDamage: new fields.BooleanField({ initial: false }),
hasHealing: new fields.BooleanField({ initial: false }),
hasEffect: new fields.BooleanField({ initial: false }),

View file

@ -9,7 +9,8 @@ export default class DHArmor extends BaseDataItem {
label: 'TYPES.Item.armor',
type: 'armor',
hasDescription: true,
isQuantifiable: true
isQuantifiable: true,
isInventoryItem: true
});
}

View file

@ -7,6 +7,7 @@ import { actionsTypes } from '../action/_module.mjs';
* @property {string} type - The system type that this data model represents.
* @property {boolean} hasDescription - Indicates whether items of this type have description field
* @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
*/
const fields = foundry.data.fields;
@ -18,7 +19,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
label: 'Base Item',
type: 'base',
hasDescription: false,
isQuantifiable: false
isQuantifiable: false,
isInventoryItem: false
};
}
@ -52,9 +54,9 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const data = { ...actorRollData, item: { ...this } };
return data;
}
async _preCreate(data, options, user) {
if(!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return;
if (!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return;
const actionType = {
weapon: 'attack'
}[this.constructor.metadata.type],
@ -70,6 +72,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
parent: this.parent
}
);
this.updateSource({actions: [action]});
this.updateSource({ actions: [action] });
}
}

View file

@ -8,7 +8,8 @@ export default class DHConsumable extends BaseDataItem {
label: 'TYPES.Item.consumable',
type: 'consumable',
hasDescription: true,
isQuantifiable: true
isQuantifiable: true,
isInventoryItem: true
});
}

View file

@ -8,7 +8,8 @@ export default class DHMiscellaneous extends BaseDataItem {
label: 'TYPES.Item.miscellaneous',
type: 'miscellaneous',
hasDescription: true,
isQuantifiable: true
isQuantifiable: true,
isInventoryItem: true
});
}

View file

@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs';
import FormulaField from '../fields/formulaField.mjs';
import ActionField from '../fields/actionField.mjs';
import { weaponFeatures } from '../../config/itemConfig.mjs';
import { actionsTypes } from '../action/_module.mjs';
import { actionsTypes } from '../action/_module.mjs';
export default class DHWeapon extends BaseDataItem {
/** @inheritDoc */
@ -12,9 +12,7 @@ export default class DHWeapon extends BaseDataItem {
type: 'weapon',
hasDescription: true,
isQuantifiable: true,
embedded: {
feature: 'featureTest'
},
isInventoryItem: true,
hasInitialAction: true
});
}

View file

@ -16,6 +16,38 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
traitArray: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }), {
initial: () => [2, 1, 1, 0, 0, -1]
}),
currency: new fields.SchemaField({
enabled: new fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.Settings.Homebrew.Currency.enabled'
}),
title: new fields.StringField({
required: true,
initial: 'Gold',
label: 'DAGGERHEART.Settings.Homebrew.Currency.currencyName'
}),
coins: new fields.StringField({
required: true,
initial: 'Coins',
label: 'DAGGERHEART.Settings.Homebrew.Currency.coinName'
}),
handfulls: new fields.StringField({
required: true,
initial: 'Handfulls',
label: 'DAGGERHEART.Settings.Homebrew.Currency.handfullName'
}),
bags: new fields.StringField({
required: true,
initial: 'Bags',
label: 'DAGGERHEART.Settings.Homebrew.Currency.bagName'
}),
chests: new fields.StringField({
required: true,
initial: 'Chests',
label: 'DAGGERHEART.Settings.Homebrew.Currency.chestName'
})
}),
restMoves: new fields.SchemaField({
longRest: new fields.SchemaField({
nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }),