[V14] 1354 - Armor Effect (#1652)

* Initial

* progress

* Working armor application

* .

* Added a updateArmorValue function that updates armoreffects according to an auto order

* .

* Added createDialog

* .

* Updated Armor SRD

* .

* Fixed character sheet armor update

* Updated itemconfig

* Actions now use createDialog for effects

* .

* .

* Fixed ArmorEffect max being a string

* Fixed SRD armor effects

* Finally finished the migration ._.

* SRD finalization

* Added ArmoreEffect.armorInteraction option

* Added ArmorManagement menu

* Fixed DamageReductionDialog

* Fixed ArmorManagement pip syle

* feat: add style to armors tooltip, add a style to make armor slot label more clear that was a button and add a tooltip location

* .

* Removed tooltip on manageArmor

* Fixes

* Fixed Downtime armor repair

* Removed ArmorScore from character data model and instead adding it in basePrep

* [Feature] ArmorEffect reworked into ChangeType on BaseEffect (#1739)

* Initial

* .

* Single armor rework start

* More fixes

* Fixed DamageReductionDialog

* Removed last traces of ArmorEffect

* .

* Corrected the SRD to use base effects again

* Removed bare bones armor item

* [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>

* [V14] Armor System ArmorScore (#1744)

* Readded so that armor items have their system defined armor instead of using an ActiveEffect

* Consolidate armor source retrieval

* Fix regression with updating armor when sources are disabled

* Simplify armor pip update

* Use helper in damage reduction dialog

* .

* Corrected SRD Armor Items

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>

* Updated migrations

* Migrations are now not horrible =D

---------

Co-authored-by: Murilo Brito <dev.murilobrito@gmail.com>
Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
This commit is contained in:
WBHarry 2026-03-22 01:57:46 +01:00 committed by GitHub
parent a3f515cf6d
commit ef53a7c561
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 1961 additions and 545 deletions

View file

@ -1,6 +1,7 @@
import BaseEffect from './baseEffect.mjs';
import BeastformEffect from './beastformEffect.mjs';
import HordeEffect from './hordeEffect.mjs';
export { changeTypes, changeEffects } from './changeTypes/_module.mjs';
export { BaseEffect, BeastformEffect, HordeEffect };

View file

@ -12,26 +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()
})
new fields.TypedSchemaField(
{ ...changeTypes, ...baseChanges },
{ initial: baseChanges.add.getInitialValue() }
)
),
duration: new fields.SchemaField({
type: new fields.StringField({
@ -86,6 +101,23 @@ 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);
}
get armorData() {
const armorChange = this.armorChange;
if (!armorChange) return null;
return armorChange.getArmorData();
}
static getDefaultObject() {
return {
name: 'New Effect',
@ -105,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;
}, this.parent.actor.system.armor?.system?.armor?.current ?? 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

@ -0,0 +1,9 @@
import Armor from './armor.mjs';
export const changeEffects = {
armor: Armor.changeEffect
};
export const changeTypes = {
armor: Armor
};

View file

@ -0,0 +1,206 @@
import { itemAbleRollParse } from '../../../helpers/utils.mjs';
const fields = foundry.data.fields;
export default class ArmorChange extends foundry.abstract.DataModel {
static defineSchema() {
return {
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',
defaultPriority: 20,
handler: (actor, change, _options, _field, replacementData) => {
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.current
},
replacementData
);
game.system.api.documents.DhActiveEffect.applyChange(
actor,
{
...change,
key: 'system.armorScore.max',
type: CONFIG.DH.GENERAL.activeEffectModes.add.id,
value: parsedMax
},
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.value.interaction) {
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id:
return !this.parent.parent?.actor.system.armor;
case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id:
return Boolean(this.parent.parent?.actor.system.armor);
default:
return false;
}
}
static getInitialValue() {
return {
type: CONFIG.DH.GENERAL.activeEffectModes.armor.id,
value: {
current: 0,
max: 0
},
phase: 'initial',
priority: 20
};
}
static getDefaultArmorEffect() {
return {
name: game.i18n.localize('DAGGERHEART.EFFECTS.ChangeTypes.armor.newArmorEffect'),
img: 'icons/equipment/chest/breastplate-helmet-metal.webp',
system: {
changes: [ArmorChange.getInitialValue()]
}
};
}
/* Helpers */
getArmorData() {
const actor = this.parent.parent?.actor?.type === 'character' ? this.parent.parent.actor : 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 {
current: this.value.current,
max: maxEvaluated ?? this.value.max
};
}
async updateArmorMax(newMax) {
const newChanges = [
...this.parent.changes.map(change => ({
...change,
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 });
}
static orderEffectsForAutoChange(armorEffects, increasing) {
const getEffectWeight = effect => {
switch (effect.parent.type) {
case 'class':
case 'subclass':
case 'ancestry':
case 'community':
case 'feature':
case 'domainCard':
return 2;
case 'armor':
return 3;
case 'loot':
case 'consumable':
return 4;
case 'weapon':
return 5;
case 'character':
return 6;
default:
return 1;
}
};
return armorEffects
.filter(x => !x.disabled && !x.isSuppressed)
.sort((a, b) =>
increasing ? getEffectWeight(b) - getEffectWeight(a) : getEffectWeight(a) - getEffectWeight(b)
);
}
}