Feature/443 adversary action roll type (#456)

* Some tests

* Filter types choices

* Resource/Uses max as FormulaField

* Removed isReversed on item resources

* Stuffs

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
This commit is contained in:
Dapoulp 2025-07-29 22:34:09 +02:00 committed by GitHub
parent 2608c4a5ae
commit 8e516df7cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 172 additions and 86 deletions

View file

@ -225,7 +225,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
await super.close({});
}
static async armorStackQuery({ actorId, damage, type }) {
static async armorSlotQuery({ actorId, damage, type }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId);
if (!actor || !actor?.isOwner) reject();

View file

@ -108,9 +108,11 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
context.hasBaseDamage = !!this.action.parent.attack;
context.getEffectDetails = this.getEffectDetails.bind(this);
context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this);
context.isNPC = this.action.actor?.isNPC;
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
@ -131,14 +133,23 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) {
const resource = this.action.parent.resource;
if (resource) {
options[this.action.parent.parent.id] = {
label: this.action.parent.parent.name,
group: 'TYPES.Actor.character'
label: "DAGGERHEART.GENERAL.itemResource",
group: 'Global'
};
}
return options;
}
getRollTypeOptions() {
const types = foundry.utils.deepClone(CONFIG.DH.GENERAL.rollTypes);
if(!this.action.actor) return types;
Object.values(types).forEach(t => {
if(this.action.actor.type !== 'character' && t.playerOnly) delete types[t.id];
})
return types;
}
disableOption(index, costOptions, choices) {
const filtered = foundry.utils.deepClone(costOptions);
Object.keys(filtered).forEach(o => {

View file

@ -85,10 +85,10 @@ export const healingTypes = {
label: 'DAGGERHEART.CONFIG.HealingType.hope.name',
abbreviation: 'DAGGERHEART.CONFIG.HealingType.hope.abbreviation'
},
armorStack: {
id: 'armorStack',
label: 'DAGGERHEART.CONFIG.HealingType.armorStack.name',
abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorStack.abbreviation'
armorSlot: {
id: 'armorSlot',
label: 'DAGGERHEART.CONFIG.HealingType.armorSlot.name',
abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorSlot.abbreviation'
},
fear: {
id: 'fear',
@ -199,7 +199,7 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
healing: {
applyTo: healingTypes.armorStack.id,
applyTo: healingTypes.armorSlot.id,
value: {
custom: {
enabled: true,
@ -287,7 +287,7 @@ export const defaultRestOptions = {
actionType: 'action',
chatDisplay: false,
healing: {
applyTo: healingTypes.armorStack.id,
applyTo: healingTypes.armorSlot.id,
value: {
custom: {
enabled: true,
@ -425,8 +425,8 @@ export const refreshTypes = {
};
export const abilityCosts = {
hp: {
id: 'hp',
hitPoints: {
id: 'hitPoints',
label: 'DAGGERHEART.CONFIG.HealingType.hitPoints.name',
group: 'Global'
},
@ -473,11 +473,13 @@ export const rollTypes = {
},
spellcast: {
id: 'spellcast',
label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name'
label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name',
playerOnly: true
},
trait: {
id: 'trait',
label: 'DAGGERHEART.CONFIG.RollTypes.trait.name'
label: 'DAGGERHEART.CONFIG.RollTypes.trait.name',
playerOnly: true
},
diceSet: {
id: 'diceSet',

View file

@ -604,7 +604,7 @@ export const weaponFeatures = {
img: 'icons/skills/melee/hand-grip-sword-strike-orange.webp',
cost: [
{
type: 'armorStack',
type: 'armorSlot',
value: 1
}
],

View file

@ -185,13 +185,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
prepareRoll() {
const roll = {
modifiers: this.modifiers,
trait: this.roll?.trait,
baseModifiers: this.roll.getModifier(),
label: 'Attack',
type: this.actionType,
difficulty: this.roll?.difficulty,
formula: this.roll.getFormula(),
bonus: this.roll.bonus,
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
};
if (this.roll?.type === 'diceSet') roll.lite = true;
@ -205,6 +203,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
async consume(config) {
const usefulResources = foundry.utils.deepClone(this.actor.system.resources);
for (var cost of config.costs) {
if (cost.keyIsID) {
usefulResources[cost.key] = {
@ -225,19 +224,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
keyIsID: resource.keyIsID
};
});
console.log(resources)
await this.actor.modifyResource(resources);
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 });
}
if (config.uses?.enabled)
this.update({ 'uses.value': this.uses.value + 1 });
}
/* */
/* ROLL */
get hasRoll() {
return !!this.roll?.type || !!this.roll?.bonus;
return !!this.roll?.type;
}
get modifiers() {
@ -301,17 +297,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
/* SAVE */
async rollSave(actor, event, message) {
if (!actor) return;
return actor
.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
type: 'reaction'
},
data: actor.getRollData()
});
return actor.diceRoll({
event,
title: 'Roll Save',
roll: {
trait: this.save.trait,
difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty,
type: 'reaction'
},
data: actor.getRollData()
});
}
updateSaveMessage(result, message, targetId) {
@ -324,7 +319,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
else updateMsg();
}
static rollSaveQuery({ actionId, actorId, event, message }) {
static rollSaveQuery({ actionId, actorId, event, message }) {
return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId),
action = await fromUuid(actionId);

View file

@ -563,6 +563,12 @@ export default class DhCharacter extends BaseDataActor {
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
this.resources.armor = {
value: this.armor.system.marks.value,
max: this.armorScore,
isReversed: true
};
this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
}

View file

@ -26,11 +26,19 @@ export default class CostField extends fields.ArrayField {
}
static calcCosts(costs) {
console.log(costs, CostField.getResources.call(this, costs))
const resources = CostField.getResources.call(this, costs);
return costs.map(c => {
c.scale = c.scale ?? 1;
c.step = c.step ?? 1;
c.total = c.value * c.scale * c.step;
c.total = c.value + ((c.scale - 1) * c.step);
c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true;
c.max = c.key === 'fear'
? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
: resources[c.key].isReversed
? resources[c.key].max
: resources[c.key].value
if(c.scalable) c.maxStep = Math.floor(c.max / c.step);
return c;
});
}
@ -51,9 +59,11 @@ export default class CostField extends fields.ArrayField {
const resources = CostField.getResources.call(this, realCosts);
return realCosts.reduce(
(a, c) =>
a && resources[c.key].isReversed
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value),
!resources[c.key]
? a
: a && resources[c.key].isReversed
? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max
: resources[c.key]?.value >= (c.total ?? c.value),
true
);
}
@ -61,10 +71,11 @@ export default class CostField extends fields.ArrayField {
static getResources(costs) {
const actorResources = this.actor.system.resources;
const itemResources = {};
for (var itemResource of costs) {
for (let itemResource of costs) {
if (itemResource.keyIsID) {
itemResources[itemResource.key] = {
value: this.parent.resource.value ?? 0
value: this.parent.resource.value ?? 0,
max: CostField.formatMax.call(this, this.parent?.resource?.max)
};
}
}
@ -79,4 +90,13 @@ export default class CostField extends fields.ArrayField {
const realCosts = costs?.length ? costs.filter(c => c.enabled) : [];
return realCosts;
}
static formatMax(max) {
max ??= 0;
if (isNaN(max)) {
const roll = Roll.replaceFormulaData(max, this.getRollData());
max = roll.total;
}
return Number(max);
}
}

View file

@ -66,6 +66,43 @@ export class DHActionRollData extends foundry.abstract.DataModel {
}
return formula;
}
getModifier() {
const modifiers = [];
if(!this.parent?.actor) return modifiers;
switch (this.parent.actor.type) {
case 'character':
const trait = this.useDefault || !this.trait ? (this.parent.item.system.attack.roll.trait ?? 'agility') : this.trait;
if(this.type === CONFIG.DH.GENERAL.rollTypes.attack.id || this.type === CONFIG.DH.GENERAL.rollTypes.trait.id)
modifiers.push(
{
label: `DAGGERHEART.CONFIG.Traits.${trait}.name`,
value: this.parent.actor.system.traits[trait].value
}
)
else if(this.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id)
modifiers.push(
{
label: `DAGGERHEART.CONFIG.RollTypes.spellcast.name`,
value: this.parent.actor.system.spellcastModifier
}
)
break;
case 'companion':
case 'adversary':
if(this.type === CONFIG.DH.GENERAL.rollTypes.attack.id)
modifiers.push(
{
label: 'Bonus to Hit',
value: this.bonus ?? this.parent.actor.system.attack.roll.bonus
}
)
break;
default:
break;
}
return modifiers;
}
}
export default class RollField extends fields.EmbeddedDataField {

View file

@ -1,10 +1,12 @@
import FormulaField from "../formulaField.mjs";
const fields = foundry.data.fields;
export default class UsesField extends fields.SchemaField {
constructor(options = {}, context = {}) {
const usesFields = {
value: new fields.NumberField({ nullable: true, initial: null }),
max: new fields.NumberField({ nullable: true, initial: null }),
max: new FormulaField({ nullable: true, initial: null, deterministic: true }),
recovery: new fields.StringField({
choices: CONFIG.DH.GENERAL.refreshTypes,
initial: null,
@ -33,6 +35,11 @@ export default class UsesField extends fields.SchemaField {
static hasUses(uses) {
if (!uses) return true;
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max;
let max = uses.max ?? 0;
if(isNaN(max)) {
const roll = new Roll(Roll.replaceFormulaData(uses.max, this.getRollData())).evaluateSync();
max = roll.total;
}
return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= max;
}
}

View file

@ -10,6 +10,7 @@
import { addLinkedItemsDiff, updateLinkedItemApps } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs';
import FormulaField from '../fields/formulaField.mjs';
const fields = foundry.data.fields;
@ -48,7 +49,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
initial: CONFIG.DH.ITEM.itemResourceTypes.simple
}),
value: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
max: new fields.StringField({ nullable: true, initial: null }),
max: new FormulaField({ nullable: true, initial: null, deterministic: true }),
icon: new fields.StringField(),
recovery: new fields.StringField({
choices: CONFIG.DH.GENERAL.refreshTypes,

View file

@ -124,13 +124,7 @@ export default class D20Roll extends DHRoll {
}
applyBaseBonus() {
const modifiers = [];
if (this.options.roll.bonus)
modifiers.push({
label: 'Bonus to Hit',
value: this.options.roll.bonus
});
const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? [];
modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type?.capitalize()} Bonus`));
modifiers.push(

View file

@ -481,7 +481,7 @@ export default class DhpActor extends Actor {
this.system.armor &&
this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)
) {
const armorStackResult = await this.owner.query('armorStack', {
const armorSlotResult = await this.owner.query('armorSlot', {
actorId: this.uuid,
damage: hpDamage.value,
type: [...hpDamage.damageTypes]
@ -490,11 +490,11 @@ export default class DhpActor extends Actor {
timeout: 30000
}
);
if (armorStackResult) {
const { modifiedDamage, armorSpent, stressSpent } = armorStackResult;
if (armorSlotResult) {
const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult;
updates.find(u => u.key === 'hitPoints').value = modifiedDamage;
updates.push(
...(armorSpent ? [{ value: armorSpent, key: 'armorStack' }] : []),
...(armorSpent ? [{ value: armorSpent, key: 'armor' }] : []),
...(stressSpent ? [{ value: stressSpent, key: 'stress' }] : [])
);
}
@ -569,6 +569,7 @@ export default class DhpActor extends Actor {
armor: { target: this.system.armor, resources: {} },
items: {}
};
resources.forEach(r => {
if (r.keyIsID) {
updates.items[r.key] = {
@ -584,7 +585,7 @@ export default class DhpActor extends Actor {
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value
);
break;
case 'armorStack':
case 'armor':
updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0

View file

@ -7,6 +7,7 @@ export default class RegisterHandlebarsHelpers {
includes: this.includes,
times: this.times,
damageFormula: this.damageFormula,
formulaValue: this.formulaValue,
damageSymbols: this.damageSymbols,
rollParsed: this.rollParsed,
hasProperty: foundry.utils.hasProperty,
@ -39,6 +40,15 @@ export default class RegisterHandlebarsHelpers {
return instances.join(traitTotal > 0 ? ' + ' : ' - ');
}
static formulaValue(formula, item) {
if(isNaN(formula)) {
const data = item.getRollData.bind(item)(),
roll = new Roll(Roll.replaceFormulaData(formula, data)).evaluateSync();
formula = roll.total;
}
return formula;
}
static damageSymbols(damageParts) {
const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map(
p => CONFIG.DH.GENERAL.damageTypes[p].icon

View file

@ -77,7 +77,7 @@ export const registerSocketHooks = () => {
};
export const registerUserQueries = () => {
CONFIG.queries.armorStack = DamageReductionDialog.armorStackQuery;
CONFIG.queries.armorSlot = DamageReductionDialog.armorSlotQuery;
CONFIG.queries.reactionRoll = game.system.api.models.actions.actionsTypes.base.rollSaveQuery;
}