This commit is contained in:
WBHarry 2026-02-09 19:38:29 +01:00
parent 514e0260eb
commit fb9f89fa9d
8 changed files with 127 additions and 57 deletions

View file

@ -1849,6 +1849,9 @@
"Attachments": { "Attachments": {
"attachHint": "Drop items here to attach them", "attachHint": "Drop items here to attach them",
"transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one." "transferHint": "If checked, this effect will be applied to any actor that owns this Effect's parent Item. The effect is always applied if this Item is attached to another one."
},
"Armor": {
"newArmorEffect": "Armor Effect"
} }
}, },
"GENERAL": { "GENERAL": {
@ -2975,7 +2978,9 @@
"tokenActorMissing": "{name} is missing an Actor", "tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors", "tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token" "knowTheTide": "Know The Tide gained a token",
"cannotAlterArmorEffectChanges": "You cannot alter the changes length of an armor effect",
"cannotAlterArmorEffectType": "You cannot alter the type of armor effect changes"
}, },
"Sidebar": { "Sidebar": {
"actorDirectory": { "actorDirectory": {

View file

@ -10,7 +10,6 @@ export default class ArmorActiveEffectConfig extends HandlebarsApplicationMixin(
closeOnSubmit: false closeOnSubmit: false
}, },
actions: { actions: {
addEffect: ArmorActiveEffectConfig.#addEffect,
finish: ArmorActiveEffectConfig.#finish finish: ArmorActiveEffectConfig.#finish
} }
}; };
@ -54,11 +53,6 @@ export default class ArmorActiveEffectConfig extends HandlebarsApplicationMixin(
this.render(); this.render();
} }
static #addEffect() {
this.document.update({ 'system.changes': [...this.document.system.changes, {}] });
this.render();
}
static #finish() { static #finish() {
this.close(); this.close();
} }

View file

@ -48,7 +48,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) {
switch (partId) { switch (partId) {
case 'settings': case 'settings':
context.features = this.document.system.armorFeatures.map(x => x.value); context.features = this.document.system.armorFeatures.map(x => x.value);
context.armorScore = this.document.system.armorEffect?.system.armorData?.max; context.armorScore = this.document.system.armorData.max;
break; break;
} }

View file

@ -1,3 +1,7 @@
/**
* ArmorEffects are ActiveEffects that have a static changes field of length 1. It includes current and maximum armor.
* When applied to a character, it adds to their currently marked and maximum armor.
*/
export default class ArmorEffect extends foundry.data.ActiveEffectTypeDataModel { export default class ArmorEffect extends foundry.data.ActiveEffectTypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -14,7 +18,7 @@ export default class ArmorEffect extends foundry.data.ActiveEffectTypeDataModel
}), }),
phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }), phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }),
priority: new fields.NumberField({ integer: true, initial: 20 }), priority: new fields.NumberField({ integer: true, initial: 20 }),
marked: new fields.NumberField({ value: new fields.NumberField({
required: true, required: true,
integer: true, integer: true,
initial: 0, initial: 0,
@ -28,28 +32,35 @@ export default class ArmorEffect extends foundry.data.ActiveEffectTypeDataModel
min: 1, min: 1,
label: 'DAGGERHEART.GENERAL.max' label: 'DAGGERHEART.GENERAL.max'
}) })
}) }),
{
initial: [
{
type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
phase: 'initial',
priority: 20,
value: 0,
max: 1
}
]
}
) )
}; };
} }
get armorData() { /* Type Functions */
if (this.changes.length !== 1) return { value: 0, max: 0 };
return { value: this.changes[0].value, max: this.changes[0].max };
}
async updateArmorMax(newMax) { /**
if (this.changes.length !== 1) return; * Validate that an {@link EffectChangeData#type} string is well-formed.
const newChanges = this.changes.map(change => ({ * @param {string} type The string to be validated
...change, * @returns {true}
max: newMax, * @throws {Error} An error if the type string is malformed
marked: Math.min(change.marked, newMax) */
})); static #validateType(type) {
await this.parent.update({ 'system.changes': newChanges }); if (type !== CONFIG.DH.GENERAL.activeEffectModes.armor.id)
} throw new Error('An armor effect must have change.type "armor"');
static applyChangeField(model, change, field) { return true;
return [model, change, field];
} }
static armorChangeEffect = { static armorChangeEffect = {
@ -81,23 +92,71 @@ export default class ArmorEffect extends foundry.data.ActiveEffectTypeDataModel
render: null render: null
}; };
/* Helpers */
get armorChange() {
if (this.changes.length !== 1)
throw new Error('Unexpected error. An armor effect should have a changes field of length 1.');
return this.changes[0];
}
get armorData() {
return { value: this.armorChange.value, max: this.armorChange.max };
}
async updateArmorMax(newMax) {
const newChanges = [
{
...this.armorChange,
max: newMax,
value: Math.min(this.armorChange.value, newMax)
}
];
await this.parent.update({ 'system.changes': newChanges });
}
/* Overrides */
prepareBaseData() { prepareBaseData() {
for (const change of this.changes) { const armorChange = this.armorChange;
change.key = 'system.armorScore'; armorChange.key = 'system.armorScore';
change.value = Math.min(change.max - change.marked, change.max); }
static getDefaultEffectData() {
return {
type: 'armor',
name: game.i18n.localize('DAGGERHEART.EFFECTS.Armor.newArmorEffect'),
img: 'icons/equipment/chest/breastplate-helmet-metal.webp'
};
}
async _preCreate(data, options, user) {
const allowed = await super._preCreate(data, options, user);
if (allowed === false) return;
await this.updateSource({ ...ArmorEffect.getDefaultEffectData(), data });
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.system?.changes) {
const changesChanged = changes.system.changes.length !== this.changes.length;
if (changesChanged) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectChanges')
);
return false;
}
if (
changes.system.changes.length === 1 &&
changes.system.changes[0].type !== CONFIG.DH.GENERAL.activeEffectModes.armor.id
) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.cannotAlterArmorEffectType'));
return false;
}
} }
} }
/**
* Validate that an {@link EffectChangeData#type} string is well-formed.
* @param {string} type The string to be validated
* @returns {true}
* @throws {Error} An error if the type string is malformed
*/
static #validateType(type) {
if (type !== CONFIG.DH.GENERAL.activeEffectModes.armor.id)
throw new Error('An armor effect must have change.type "armor"');
return true;
}
} }

View file

@ -458,6 +458,11 @@ export default class DhCharacter extends BaseDataActor {
return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); return this.parent.items.find(x => x.type === 'armor' && x.system.equipped);
} }
/* TODO: Prep datastructure to be useful when applying automatic armor damage order */
get armorEffects() {
return Array.from(this.parent.allApplicableEffects());
}
get activeBeastform() { get activeBeastform() {
return this.parent.effects.find(x => x.type === 'beastform'); return this.parent.effects.find(x => x.type === 'beastform');
} }

View file

@ -54,10 +54,16 @@ export default class DHArmor extends AttachableItem {
} }
get armorEffect() { get armorEffect() {
/* TODO: make armors only able to have on armor effect, or handle in some other way */
return this.parent.effects.find(x => x.type === 'armor'); return this.parent.effects.find(x => x.type === 'armor');
} }
get armorData() {
const armorEffect = this.armorEffect;
if (!armorEffect) return { value: 0, max: 0 };
return armorEffect.system.armorData;
}
/**@inheritdoc */ /**@inheritdoc */
async getDescriptionData() { async getDescriptionData() {
const baseDescription = this.description; const baseDescription = this.description;
@ -73,6 +79,17 @@ export default class DHArmor extends AttachableItem {
return { prefix, value: baseDescription, suffix: null }; return { prefix, value: baseDescription, suffix: null };
} }
/**@inheritdoc */
async _onCreate(_data, _options, userId) {
if (userId !== game.user.id) return;
if (!this.parent.effects.some(x => x.type === 'armor')) {
this.parent.createEmbeddedDocuments('ActiveEffect', [
game.system.api.data.activeEffects.ArmorEffect.getDefaultEffectData()
]);
}
}
/**@inheritdoc */ /**@inheritdoc */
async _preUpdate(changes, options, user) { async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);
@ -163,9 +180,8 @@ export default class DHArmor extends AttachableItem {
* @returns {string[]} An array of localized tag strings. * @returns {string[]} An array of localized tag strings.
*/ */
_getTags() { _getTags() {
const baseScore = this.armorEffect?.system.armorData?.value;
const tags = [ const tags = [
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${baseScore}`, `${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armorData.value}`,
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseThresholds.base')}: ${this.baseThresholds.major} / ${this.baseThresholds.severe}` `${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseThresholds.base')}: ${this.baseThresholds.major} / ${this.baseThresholds.severe}`
]; ];
@ -177,11 +193,7 @@ export default class DHArmor extends AttachableItem {
* @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects. * @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects.
*/ */
_getLabels() { _getLabels() {
const labels = []; const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armorData.value}`];
if (this.armorEffect)
labels.push(
`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armorEffect.system.armorData?.value}`
);
return labels; return labels;
} }

View file

@ -146,18 +146,15 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/**@inheritdoc*/ /**@inheritdoc*/
static applyChangeField(model, change, field) { static applyChangeField(model, change, field) {
if (this.system?.applyChangeField)
super.applyChangeField(...this.system.applyChangeField(model, change, field));
change.value = Number.isNumeric(change.value) change.value = Number.isNumeric(change.value)
? change.value ? change.value
: DhActiveEffect.getChangeValue(model, change, change.effect); : DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyChangeField(model, change, field); super.applyChangeField(model, change, field);
} }
_applyLegacy(actor, change, changes) { _applyChangeUnguided(actor, change, changes, options) {
change.value = DhActiveEffect.getChangeValue(actor, change, change.effect); change.value = DhActiveEffect.getChangeValue(actor, change, change.effect);
super._applyLegacy(actor, change, changes); super._applyChangeUnguided(actor, change, changes, options);
} }
static getChangeValue(model, change, effect) { static getChangeValue(model, change, effect) {

View file

@ -1,10 +1,8 @@
<section class="tab{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}"> <section class="tab{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}">
<button data-action="addEffect">Add Effect</button>
<div class="armor-effects-container"> <div class="armor-effects-container">
{{#each source.system.changes as |change index|}} {{#each source.system.changes as |change index|}}
<div class="armor-effect-container"> <div class="armor-effect-container">
{{formGroup @root.systemFields.changes.element.fields.marked name=(concat 'system.changes.' index '.marked') value=change.marked localize=true}} {{formGroup @root.systemFields.changes.element.fields.value name=(concat 'system.changes.' index '.value') value=change.value localize=true}}
{{formGroup @root.systemFields.changes.element.fields.max name=(concat 'system.changes.' index '.max') value=change.max localize=true}} {{formGroup @root.systemFields.changes.element.fields.max name=(concat 'system.changes.' index '.max') value=change.max localize=true}}
</div> </div>
{{/each}} {{/each}}