Fixed so that costs can be used

This commit is contained in:
WBHarry 2025-07-12 14:27:38 +02:00
parent d9d7b23838
commit e294c7906c
7 changed files with 177 additions and 102 deletions

View file

@ -111,6 +111,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
context.hasBaseDamage = !!this.action.parent.attack; context.hasBaseDamage = !!this.action.parent.attack;
context.getRealIndex = this.getRealIndex.bind(this); context.getRealIndex = this.getRealIndex.bind(this);
context.getEffectDetails = this.getEffectDetails.bind(this); context.getEffectDetails = this.getEffectDetails.bind(this);
context.costOptions = await this.getCostOptions();
context.disableOption = this.disableOption.bind(this); context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor && this.action.actor.type !== 'character'; context.isNPC = this.action.actor && this.action.actor.type !== 'character';
context.hasRoll = this.action.hasRoll; context.hasRoll = this.action.hasRoll;
@ -129,8 +130,46 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
this.render(true); this.render(true);
} }
disableOption(index, options, choices) { async getCostOptions() {
const filtered = foundry.utils.deepClone(options); const worldItems = this.action.actor ? this.action.actor.items : game.items;
const compendiumItems = this.action.actor
? []
: Array.from(game.packs).flatMap(x =>
x.index.map(x => ({
id: x._id,
uuid: x.uuid,
name: x.name
}))
);
const resourceOptions = [...worldItems, ...compendiumItems].reduce((acc, x) => {
if (['feature', 'domainCard'].includes(x.type) && x.system.resource?.max) {
acc.push({
id: x.id,
uuid: x.uuid,
label: x.name,
group: 'TYPES.Actor.character'
});
}
return acc;
}, []);
const abilityCosts = Object.values(CONFIG.DH.GENERAL.abilityCosts);
const sortedOptions = [...abilityCosts, ...resourceOptions].sort((a, b) => {
const groupSort = game.i18n.localize(a.group).localeCompare(game.i18n.localize(b.group));
return groupSort ? groupSort : game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label));
});
return sortedOptions.reduce((acc, x) => {
acc[x.uuid ?? x.id] = x;
return acc;
}, {});
}
disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => { Object.keys(filtered).forEach(o => {
if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true; if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true;
}); });
@ -146,17 +185,25 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
return this.action.item.effects.get(id); return this.action.item.effects.get(id);
} }
_prepareSubmitData(event, formData) { async _prepareSubmitData(_event, formData) {
const submitData = foundry.utils.expandObject(formData.object); const submitData = foundry.utils.expandObject(formData.object);
for (const keyPath of this.constructor.CLEAN_ARRAYS) { for (const keyPath of this.constructor.CLEAN_ARRAYS) {
const data = foundry.utils.getProperty(submitData, keyPath); const data = foundry.utils.getProperty(submitData, keyPath);
if (data) foundry.utils.setProperty(submitData, keyPath, Object.values(data)); const dataValues = data ? Object.values(data) : [];
if (keyPath === 'cost') {
for (var value of dataValues) {
const item = await foundry.utils.fromUuid(value.key ?? '');
value.keyIsUUID = Boolean(item);
}
}
if (data) foundry.utils.setProperty(submitData, keyPath, dataValues);
} }
return submitData; return submitData;
} }
static async updateForm(event, _, formData) { static async updateForm(event, _, formData) {
const submitData = this._prepareSubmitData(event, formData), const submitData = await this._prepareSubmitData(event, formData),
data = foundry.utils.mergeObject(this.action.toObject(), submitData), data = foundry.utils.mergeObject(this.action.toObject(), submitData),
container = foundry.utils.getProperty(this.action.parent, this.action.systemPath); container = foundry.utils.getProperty(this.action.parent, this.action.systemPath);
let newActions; let newActions;

View file

@ -345,37 +345,6 @@ export const refreshTypes = {
} }
}; };
export const featureTokenTypes = {
tide: {
id: 'tide',
label: 'Tide',
group: 'TYPES.Actor.character'
},
chaos: {
id: 'chaos',
label: 'Chaos',
group: 'TYPES.Actor.character'
}
};
export const featureDiceTypes = {
prayer: {
id: 'prayer',
label: 'Prayer Dice',
group: 'TYPES.Actor.character'
},
favor: {
id: 'favor',
label: 'Favor Points',
group: 'TYPES.Actor.character'
},
slayer: {
id: 'slayer',
label: 'Slayer Dice',
group: 'TYPES.Actor.character'
}
};
export const abilityCosts = { export const abilityCosts = {
hp: { hp: {
id: 'hp', id: 'hp',
@ -401,9 +370,9 @@ export const abilityCosts = {
id: 'fear', id: 'fear',
label: 'Fear', label: 'Fear',
group: 'TYPES.Actor.adversary' group: 'TYPES.Actor.adversary'
}, }
...featureTokenTypes, // ...featureTokenTypes,
...featureDiceTypes // ...featureDiceTypes
}; };
export const countdownTypes = { export const countdownTypes = {

View file

@ -1,6 +1,7 @@
import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField } from './actionDice.mjs'; import { DHActionDiceData, DHActionRollData, DHDamageField } from './actionDice.mjs';
import DhpActor from '../../documents/actor.mjs'; import DhpActor from '../../documents/actor.mjs';
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
import { getResources } from '../../helpers/utils.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -35,12 +36,12 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}), }),
cost: new fields.ArrayField( cost: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
type: new fields.StringField({ key: new fields.StringField({
choices: CONFIG.DH.GENERAL.abilityCosts,
nullable: false, nullable: false,
required: true, required: true,
initial: 'hope' initial: 'hope'
}), }),
keyIsUUID: new fields.BooleanField(),
value: new fields.NumberField({ nullable: true, initial: 1 }), value: new fields.NumberField({ nullable: true, initial: 1 }),
scalable: new fields.BooleanField({ initial: false }), scalable: new fields.BooleanField({ initial: false }),
step: new fields.NumberField({ nullable: true, initial: null }) step: new fields.NumberField({ nullable: true, initial: null })
@ -211,7 +212,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
// Prepare Costs // Prepare Costs
const costsConfig = this.prepareCost(); const costsConfig = this.prepareCost();
if (isFastForward && !this.hasCost(costsConfig)) if (isFastForward && !(await this.hasCost(costsConfig)))
return ui.notifications.warn("You don't have the resources to use that action."); return ui.notifications.warn("You don't have the resources to use that action.");
// Prepare Uses // Prepare Uses
@ -285,7 +286,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
prepareCost() { prepareCost() {
const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : [];
return costs; return this.calcCosts(costs);
} }
prepareUse() { prepareUse() {
@ -334,11 +335,27 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
async consume(config) { async consume(config) {
const usefulResources = foundry.utils.deepClone(this.actor.system.resources);
for (var cost of config.costs) {
if (cost.keyIsUUID) {
const item = await foundry.utils.fromUuid(cost.key);
usefulResources[cost.key] = {
value: item.system.resource.value,
target: item,
keyIsUUID: true
};
}
}
const resources = config.costs const resources = config.costs
.filter(c => c.enabled !== false) .filter(c => c.enabled !== false)
.map(c => { .map(c => {
const resource = this.actor.system.resources[c.type]; const resource = usefulResources[c.key];
return { type: c.type, value: (c.total ?? c.value) * (resource.hasOwnProperty('maxTotal') ? 1 : -1) }; return {
key: c.key,
value: (c.total ?? c.value) * (resource.hasOwnProperty('maxTotal') ? 1 : -1),
target: resource.target,
keyIsUUID: resource.keyIsUUID
};
}); });
await this.actor.modifyResource(resources); await this.actor.modifyResource(resources);
@ -379,9 +396,27 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}); });
} }
hasCost(costs) { async getResources(costs) {
const actorResources = this.actor.system.resources;
const itemResources = {};
for (var itemResource of costs) {
if (itemResource.keyIsUUID) {
const item = await foundry.utils.fromUuid(itemResource.key);
itemResources[itemResource.key] = {
value: item?.system?.resource?.value ?? 0
};
}
}
return {
...actorResources,
...itemResources
};
}
async hasCost(costs) {
const realCosts = this.getRealCosts(costs), const realCosts = this.getRealCosts(costs),
hasFearCost = realCosts.findIndex(c => c.type === 'fear'); hasFearCost = realCosts.findIndex(c => c.key === 'fear');
if (hasFearCost > -1) { if (hasFearCost > -1) {
const fearCost = realCosts.splice(hasFearCost, 1)[0]; const fearCost = realCosts.splice(hasFearCost, 1)[0];
if ( if (
@ -392,12 +427,12 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
} }
/* maxTotal is a sign that the resource is inverted, IE it counts upwards instead of down */ /* maxTotal is a sign that the resource is inverted, IE it counts upwards instead of down */
const resources = this.actor.system.resources; const resources = await this.getResources(realCosts);
return realCosts.reduce( return realCosts.reduce(
(a, c) => (a, c) =>
a && resources[c.type].hasOwnProperty('maxTotal') a && resources[c.key].hasOwnProperty('maxTotal')
? resources[c.type].value + (c.total ?? c.value) <= resources[c.type].maxTotal ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].maxTotal
: resources[c.type]?.value >= (c.total ?? c.value), : resources[c.key]?.value >= (c.total ?? c.value),
true true
); );
} }

View file

@ -42,9 +42,7 @@ export default class DhCharacter extends BaseDataActor {
bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }) bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true })
}), }),
stress: resourceField(6), stress: resourceField(6),
hope: resourceField(6), hope: resourceField(6)
tokens: new fields.ObjectField(),
dice: new fields.ObjectField()
}), }),
traits: new fields.SchemaField({ traits: new fields.SchemaField({
agility: attributeField(), agility: attributeField(),

View file

@ -476,7 +476,7 @@ export default class DhpActor extends Actor {
if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, baseDamage, type) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, baseDamage, type) === false) return null;
if (this.type === 'companion') { if (this.type === 'companion') {
await this.modifyResource([{ value: 1, type: 'stress' }]); await this.modifyResource([{ value: 1, key: 'stress' }]);
return; return;
} }
@ -500,8 +500,8 @@ export default class DhpActor extends Actor {
const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; const { modifiedDamage, armorSpent, stressSpent } = armorStackResult;
updates.find(u => u.type === 'hitPoints').value = modifiedDamage; updates.find(u => u.type === 'hitPoints').value = modifiedDamage;
updates.push( updates.push(
...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), ...(armorSpent ? [{ value: armorSpent, key: 'armorStack' }] : []),
...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) ...(stressSpent ? [{ value: stressSpent, key: 'stress' }] : [])
); );
} }
} }
@ -520,51 +520,77 @@ export default class DhpActor extends Actor {
if (!resources.length) return; if (!resources.length) return;
if (resources.find(r => r.type === 'stress')) this.convertStressDamageToHP(resources); if (resources.find(r => r.type === 'stress')) this.convertStressDamageToHP(resources);
let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } }; let updates = {
actor: { target: this, resources: {} },
armor: { target: this.system.armor, resources: {} },
items: {}
};
resources.forEach(r => { resources.forEach(r => {
switch (r.type) { if (r.keyIsUUID) {
case 'fear': updates.items[r.key] = {
ui.resources.updateFear( uuid: r.key,
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value target: r.target,
); resources: {
break; 'system.resource.value': r.target.system.resource.value + r.value
case 'armorStack': }
updates.armor.resources['system.marks.value'] = Math.max( };
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), } else {
0 switch (r.key) {
); case 'fear':
break; ui.resources.updateFear(
default: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value
updates.actor.resources[`system.resources.${r.type}.value`] = Math.max( );
Math.min( break;
this.system.resources[r.type].value + r.value, case 'armorStack':
this.system.resources[r.type].maxTotal ?? this.system.resources[r.type].max updates.armor.resources['system.marks.value'] = Math.max(
), Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0 0
); );
break; break;
default:
updates.actor.resources[`system.resources.${r.type}.value`] = Math.max(
Math.min(
this.system.resources[r.type].value + r.value,
this.system.resources[r.type].maxTotal ?? this.system.resources[r.type].max
),
0
);
break;
}
} }
}); });
Object.values(updates).forEach(async u => { Object.keys(updates).forEach(async key => {
if (Object.keys(u.resources).length > 0) { const u = updates[key];
await emitAsGM( if (key === 'items') {
GMUpdateEvent.UpdateDocument, Object.values(u).forEach(async item => {
u.target.update.bind(u.target), await emitAsGM(
u.resources, GMUpdateEvent.UpdateDocument,
u.target.uuid item.target.update.bind(item.target),
); item.resources,
/* if (game.user.isGM) { item.target.uuid
await u.target.update(u.resources); );
} else { });
await game.socket.emit(`system.${CONFIG.DH.id}`, { } else {
action: socketEvent.GMUpdate, if (Object.keys(u.resources).length > 0) {
data: { await emitAsGM(
action: GMUpdateEvent.UpdateDocument, GMUpdateEvent.UpdateDocument,
uuid: u.target.uuid, u.target.update.bind(u.target),
update: u.resources u.resources,
} u.target.uuid
}); );
} */ /* if (game.user.isGM) {
await u.target.update(u.resources);
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: u.target.uuid,
update: u.resources
}
});
} */
}
} }
}); });
} }

View file

@ -6,7 +6,7 @@
{{#each source as |cost index|}} {{#each source as |cost index|}}
<div class="nest-inputs"> <div class="nest-inputs">
{{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}} {{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}}
{{formField ../fields.type choices=(@root.disableOption index ../fields.type.choices ../source) label="Resource" value=cost.type name=(concat "cost." index ".type") localize=true}} {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true}}
{{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}} {{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}}
{{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}} {{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}}
<a class="btn" data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeElement" data-index="{{index}}"><i class="fas fa-trash"></i></a> <a class="btn" data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeElement" data-index="{{index}}"><i class="fas fa-trash"></i></a>

View file

@ -4,6 +4,6 @@
data-tab="config" data-tab="config"
> >
{{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}}
{{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost}} {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}}
{{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}}
</section> </section>