[V14] Refactor ArmorChange schema and fix some bugs (#1742)

* Refactor ArmorChange schema and fix some bugs

* Add current back to schema

* Fixed so changing armor values and taking damage works again

* Fixed so that scrolltexts for armor changes work again

* Removed old marks on armor.system

* Restored damageReductionDialog armorScore.value

* Use toggle for css class addition/removal

* Fix armor change type choices

* Added ArmorChange DamageThresholds

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
This commit is contained in:
Carlos Fernandez 2026-03-21 08:53:12 -04:00 committed by GitHub
parent 6193153596
commit 50dcbf4396
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 356 additions and 428 deletions

View file

@ -27,11 +27,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
true
);
const armor = orderedArmorEffects.reduce((acc, effect) => {
const { value, max } = effect.system.armorData;
const { current, max } = effect.system.armorData;
acc.push({
effect: effect,
marks: [...Array(max).keys()].reduce((acc, _, index) => {
const spent = index < value;
const spent = index < current;
acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent };
return acc;
}, {})

View file

@ -154,6 +154,10 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
htmlElement
.querySelector('.armor-change-checkbox')
?.addEventListener('change', this.armorChangeToggle.bind(this));
htmlElement
.querySelector('.armor-damage-thresholds-checkbox')
?.addEventListener('change', this.armorDamageThresholdToggle.bind(this));
}
async _prepareContext(options) {
@ -190,23 +194,14 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
}));
break;
case 'changes':
const fields = this.document.system.schema.fields.changes.element.fields;
const singleTypes = ['armor'];
const { base, ...typedChanges } = context.source.changes.reduce((acc, change, index) => {
const type = CONFIG.DH.GENERAL.baseActiveEffectModes[change.type] ? 'base' : change.type;
if (singleTypes.includes(type)) {
acc[type] = { ...change, index };
} else {
if (!acc[type]) acc[type] = [];
acc[type].push({ ...change, index });
const typedChanges = context.source.changes.reduce((acc, change, index) => {
if (singleTypes.includes(change.type)) {
acc[change.type] = { ...change, index };
}
return acc;
}, {});
partContext.changes = await Promise.all(
foundry.utils.deepClone(base ?? []).map(c => this._prepareChangeContext(c, fields))
);
partContext.changes = partContext.changes.filter(c => !!c);
partContext.typedChanges = typedChanges;
break;
}
@ -238,29 +233,51 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return this.submit({ updateData: { system: { changes } } });
}
_prepareChangeContext(change, fields) {
armorDamageThresholdToggle(event) {
const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form));
const changes = Object.values(submitData.system?.changes ?? {});
const index = Number(event.target.dataset.index);
if (event.target.checked) {
changes[index].value.damageThresholds = { major: 0, severe: 0 };
} else {
changes[index].value.damageThresholds = null;
}
return this.submit({ updateData: { system: { changes } } });
}
/** @inheritdoc */
_renderChange(context) {
const { change, index, defaultPriority } = context;
if (!(change.type in CONFIG.DH.GENERAL.baseActiveEffectModes)) return null;
const changeTypesSchema = this.document.system.schema.fields.changes.element.types;
const fields = context.fields ?? (changeTypesSchema[change.type] ?? changeTypesSchema.add).fields;
if (typeof change.value !== 'string') change.value = JSON.stringify(change.value);
const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority;
Object.assign(
change,
['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${change.index}.${fieldName}`;
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
}, {})
);
return (
game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type].render?.(
change,
change.index,
index,
defaultPriority
) ??
foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/activeEffect/change.hbs',
{
change,
index: change.index,
index,
defaultPriority,
fields
fields,
types: Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, key) => {
r[key] = CONFIG.DH.GENERAL.baseActiveEffectModes[key].label;
return r;
}, {})
}
)
);

View file

@ -1003,16 +1003,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
const effect = await foundry.utils.fromUuid(event.target.dataset.uuid);
const armorChange = effect.system.armorChange;
if (!armorChange) return;
const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
const current = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0);
const newChanges = effect.system.changes.map(change => ({
...change,
value: change.type === 'armor' ? value : change.value
value: change.type === 'armor' ? { ...change.value, current } : change.value
}));
event.target.value = value;
event.target.value = current;
const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress');
progressBar.value = value;
progressBar.value = current;
await effect.update({ 'system.changes': newChanges });
}
@ -1023,27 +1023,22 @@ export default class CharacterSheet extends DHBaseActorSheet {
const armorChange = effect.system.armorChange;
if (!armorChange) return;
const { value } = effect.system.armorData;
const { current } = effect.system.armorData;
const inputValue = Number.parseInt(target.dataset.value);
const decreasing = value >= inputValue;
const newValue = decreasing ? inputValue - 1 : inputValue;
const decreasing = current >= inputValue;
const newCurrent = decreasing ? inputValue - 1 : inputValue;
const newChanges = effect.system.changes.map(change => ({
...change,
value: change.type === 'armor' ? newValue : change.value
value: change.type === 'armor' ? { ...change.value, current: newCurrent } : change.value
}));
const container = target.closest('.slot-bar');
for (const armorSlot of container.querySelectorAll('.armor-slot i')) {
const index = Number.parseInt(armorSlot.dataset.index);
if (decreasing && index >= newValue) {
armorSlot.classList.remove('fa-shield');
armorSlot.classList.add('fa-shield-halved');
} else if (!decreasing && index < newValue) {
armorSlot.classList.add('fa-shield');
armorSlot.classList.remove('fa-shield-halved');
}
const marked = !decreasing && Number.parseInt(armorSlot.dataset.index) < newCurrent;
armorSlot.classList.toggle('fa-shield', marked);
armorSlot.classList.toggle('fa-shield-halved', !marked);
}
await effect.update({ 'system.changes': newChanges });

View file

@ -51,8 +51,8 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
context.armorScore = this.document.system.armorData.max;
break;
case 'effects':
context.effects.actives = context.effects.actives.filter(x => x.type !== 'armor');
context.effects.inactives = context.effects.actives.filter(x => x.type !== 'armor');
context.effects.actives = context.effects.actives.filter(x => !x.system.armorData);
context.effects.inactives = context.effects.inactives.filter(x => !x.system.armorData);
break;
}
@ -64,7 +64,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
const armorEffect = this.document.system.armorEffect;
if (Number.isNaN(value) || !armorEffect) return;
await armorEffect.system.armorChange.typeData.updateArmorMax(value);
await armorEffect.system.armorChange.updateArmorMax(value);
this.render();
}

View file

@ -12,29 +12,41 @@
* "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility)
*/
import { getScrollTextData } from '../../helpers/utils.mjs';
import { changeTypes } from './_module.mjs';
export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
const baseChanges = Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, type) => {
r[type] = new fields.SchemaField({
key: new fields.StringField({ required: true }),
type: new fields.StringField({
required: true,
choices: [type],
initial: type,
validate: BaseEffect.#validateType
}),
value: new fields.AnyField({
required: true,
nullable: true,
serializable: true,
initial: ''
}),
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
priority: new fields.NumberField()
});
return r;
}, {});
return {
...super.defineSchema(),
changes: new fields.ArrayField(
new fields.SchemaField({
key: new fields.StringField({ required: true }),
type: new fields.StringField({
required: true,
blank: false,
choices: CONFIG.DH.GENERAL.activeEffectModes,
initial: CONFIG.DH.GENERAL.activeEffectModes.add.id,
validate: BaseEffect.#validateType
}),
value: new fields.AnyField({ required: true, nullable: true, serializable: true, initial: '' }),
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
priority: new fields.NumberField(),
typeData: new fields.TypedSchemaField(changeTypes, { nullable: true, initial: null })
})
new fields.TypedSchemaField(
{ ...changeTypes, ...baseChanges },
{ initial: baseChanges.add.getInitialValue() }
)
),
duration: new fields.SchemaField({
type: new fields.StringField({
@ -89,6 +101,12 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
return true;
}
get isSuppressed() {
for (const change of this.changes) {
if (change.isSuppressed) return true;
}
}
get armorChange() {
return this.changes.find(x => x.type === CONFIG.DH.GENERAL.activeEffectModes.armor.id);
}
@ -97,7 +115,7 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
const armorChange = this.armorChange;
if (!armorChange) return null;
return armorChange.typeData.getArmorData(armorChange);
return armorChange.getArmorData();
}
static getDefaultObject() {
@ -119,4 +137,31 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
}
};
}
async _preUpdate(changed, options, userId) {
const allowed = await super._preUpdate(changed, options, userId);
if (allowed === false) return false;
const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
if (
autoSettings.resourceScrollTexts &&
this.parent.actor?.type === 'character' &&
this.parent.actor.system.resources.armor
) {
const newArmorTotal = (changed.system?.changes ?? []).reduce((acc, change) => {
if (change.type === 'armor') acc += change.value.current;
return acc;
}, 0);
const armorData = getScrollTextData(this.parent.actor, { value: newArmorTotal }, 'armor');
options.scrollingTextData = [armorData];
}
}
_onUpdate(changed, options, userId) {
super._onUpdate(changed, options, userId);
if (this.parent.actor && options.scrollingTextData)
this.parent.actor.queueScrollText(options.scrollingTextData);
}
}

View file

@ -2,39 +2,56 @@ import { itemAbleRollParse } from '../../../helpers/utils.mjs';
const fields = foundry.data.fields;
export default class Armor extends foundry.abstract.DataModel {
export default class ArmorChange extends foundry.abstract.DataModel {
static defineSchema() {
return {
type: new fields.StringField({ required: true, initial: 'armor', blank: false }),
max: new fields.StringField({
required: true,
nullable: false,
initial: '1',
label: 'DAGGERHEART.GENERAL.max'
}),
armorInteraction: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction,
initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id,
label: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.armorInteraction.label',
hint: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.armorInteraction.hint'
type: new fields.StringField({ required: true, choices: ['armor'], initial: 'armor' }),
priority: new fields.NumberField(),
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
value: new fields.SchemaField({
current: new fields.NumberField({ integer: true, min: 0, initial: 0 }),
max: new fields.StringField({
required: true,
nullable: false,
initial: '1',
label: 'DAGGERHEART.GENERAL.max'
}),
damageThresholds: new fields.SchemaField(
{
major: new fields.StringField({
initial: '0',
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
}),
severe: new fields.StringField({
initial: '0',
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
},
{ nullable: true, initial: null }
),
interaction: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction,
initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id,
label: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.label',
hint: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.hint'
})
})
};
}
static changeEffect = {
label: 'Armor',
defaultPriortiy: 20,
defaultPriority: 20,
handler: (actor, change, _options, _field, replacementData) => {
const parsedMax = itemAbleRollParse(change.typeData.max, actor, change.effect.parent);
const parsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent);
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
...change,
key: 'system.armorScore.value',
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
value: change.value
value: change.value.current
},
replacementData
);
@ -48,13 +65,52 @@ export default class Armor extends foundry.abstract.DataModel {
},
replacementData
);
if (change.value.damageThresholds) {
const getThresholdValue = value => {
const parsed = itemAbleRollParse(value, actor, change.effect.parent);
const roll = new Roll(parsed).evaluateSync();
return roll ? (roll.isDeterministic ? roll.total : null) : null;
};
const major = getThresholdValue(change.value.damageThresholds.major);
const severe = getThresholdValue(change.value.damageThresholds.severe);
if (major) {
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
...change,
key: 'system.damageThresholds.major',
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
priority: 50,
value: major
},
replacementData
);
}
if (severe) {
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
...change,
key: 'system.damageThresholds.severe',
type: CONFIG.DH.GENERAL.activeEffectModes.override.id,
priority: 50,
value: severe
},
replacementData
);
}
}
return {};
},
render: null
};
get isSuppressed() {
switch (this.armorInteraction) {
switch (this.value.interaction) {
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id:
return !this.parent.parent?.actor.system.armor;
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id:
@ -64,15 +120,12 @@ export default class Armor extends foundry.abstract.DataModel {
}
}
static getInitialValue(locked) {
static getInitialValue() {
return {
key: 'Armor',
type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
value: 0,
typeData: {
type: 'armor',
max: 0,
locked
value: {
current: 0,
max: 0
},
phase: 'initial',
priority: 20
@ -84,22 +137,22 @@ export default class Armor extends foundry.abstract.DataModel {
name: game.i18n.localize('DAGGERHEART.EFFECTS.ChangeTypes.armor.newArmorEffect'),
img: 'icons/equipment/chest/breastplate-helmet-metal.webp',
system: {
changes: [Armor.getInitialValue(true)]
changes: [ArmorChange.getInitialValue()]
}
};
}
/* Helpers */
getArmorData(parentChange) {
getArmorData() {
const actor = this.parent.parent?.actor?.type === 'character' ? this.parent.parent.actor : null;
const maxParse = actor ? itemAbleRollParse(this.max, actor, this.parent.parent.parent) : null;
const maxParse = actor ? itemAbleRollParse(this.value.max, actor, this.parent.parent.parent) : null;
const maxRoll = maxParse ? new Roll(maxParse).evaluateSync() : null;
const maxEvaluated = maxRoll ? (maxRoll.isDeterministic ? maxRoll.total : null) : null;
return {
value: parentChange.value,
max: maxEvaluated ?? this.max
current: this.value.current,
max: maxEvaluated ?? this.value.max
};
}
@ -107,8 +160,14 @@ export default class Armor extends foundry.abstract.DataModel {
const newChanges = [
...this.parent.changes.map(change => ({
...change,
value: change.type === 'armor' ? Math.min(change.value, newMax) : change.value,
typeData: change.type === 'armor' ? { ...change.typeData, max: newMax } : change.typeData
value:
change.type === 'armor'
? {
...change.value,
current: Math.min(change.value.current, newMax),
max: newMax
}
: change.value
}))
];
await this.parent.parent.update({ 'system.changes': newChanges });

View file

@ -42,15 +42,15 @@ export default class DhCharacter extends DhCreature {
}),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
}),
major: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
}),
severe: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
}),
experiences: new fields.TypedObjectField(
@ -479,14 +479,14 @@ export default class DhCharacter extends DhCreature {
for (const armorEffect of orderedEffects) {
let usedArmorChange = 0;
if (clear) {
usedArmorChange -= armorEffect.system.armorChange.value;
usedArmorChange -= armorEffect.system.armorChange.value.current;
} else {
if (increasing) {
const remainingArmor = armorEffect.system.armorData.max - armorEffect.system.armorData.value;
const remainingArmor = armorEffect.system.armorData.max - armorEffect.system.armorData.current;
usedArmorChange = Math.min(remainingChange, remainingArmor);
remainingChange -= usedArmorChange;
} else {
const changeChange = Math.min(armorEffect.system.armorData.value, remainingChange);
const changeChange = Math.min(armorEffect.system.armorData.current, remainingChange);
usedArmorChange -= changeChange;
remainingChange -= changeChange;
}
@ -503,7 +503,10 @@ export default class DhCharacter extends DhCreature {
...change,
value:
change.type === 'armor'
? armorEffect.system.armorChange.value + usedArmorChange
? {
...change.value,
current: armorEffect.system.armorChange.value.current + usedArmorChange
}
: change.value
}))
});
@ -519,11 +522,12 @@ export default class DhCharacter extends DhCreature {
async updateArmorEffectValue({ uuid, value }) {
const effect = await foundry.utils.fromUuid(uuid);
const effectValue = effect.system.armorChange.value;
await effect.update({
'system.changes': [
{
...effect.system.armorChange,
value: effect.system.armorChange.value + value
value: { ...effectValue, current: effectValue.current + value }
}
]
});

View file

@ -28,9 +28,6 @@ export default class DHArmor extends AttachableItem {
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
})
),
marks: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true })
}),
baseThresholds: new fields.SchemaField({
major: new fields.NumberField({ integer: true, initial: 0 }),
severe: new fields.NumberField({ integer: true, initial: 0 })

View file

@ -220,14 +220,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
addLinkedItemsDiff(changed.system?.features, this.features, options);
const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
const armorChanged =
changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value;
if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') {
const armorData = getScrollTextData(this.parent.parent, changed.system.marks, 'armor');
options.scrollingTextData = [armorData];
}
if (changed.system?.actions) {
const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => {
const action = changed.system.actions[key];

View file

@ -1,5 +1,5 @@
import { itemAbleRollParse } from '../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import { RefreshType } from '../systemRegistration/socket.mjs';
export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/* -------------------------------------------- */
@ -155,11 +155,6 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/* Methods */
/* -------------------------------------------- */
/**@inheritdoc */
static applyChange(actor, change, options) {
super.applyChange(actor, change, options);
}
/**@inheritdoc*/
static applyChangeField(model, change, field) {
change.value = Number.isNumeric(change.value)
@ -168,7 +163,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
super.applyChangeField(model, change, field);
}
_applyChangeUnguided(actor, change, changes, options) {
static _applyChangeUnguided(actor, change, changes, options) {
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
super._applyChangeUnguided(actor, change, changes, options);
}