284 - Armor/Weapon Feature Improvements (#292)

* Added parsing of effect values from Item data model. Almost finished with itemConfig.

* Added the last to itemConfig

* Fixed armor

* ContextMenu localization fixes

* Better tooltips for tagify

* Corrected resource logic
This commit is contained in:
WBHarry 2025-07-09 13:06:49 +02:00 committed by GitHub
parent eae4f12910
commit b3e7c6b9b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 3043 additions and 2310 deletions

View file

@ -14,12 +14,12 @@ export default class DHAttackAction extends DHDamageAction {
prepareData() {
super.prepareData();
if(!!this.item?.system?.attack) {
if (!!this.item?.system?.attack) {
if (this.damage.includeBase) {
const baseDamage = this.getParentDamage();
this.damage.parts.unshift(new DHDamageData(baseDamage));
}
if(this.roll.useDefault) {
if (this.roll.useDefault) {
this.roll.trait = this.item.system.attack.roll.trait;
this.roll.type = 'weapon';
}

View file

@ -161,7 +161,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
updateSource['range'] = parent?.system?.attack?.range;
updateSource['roll'] = {
useDefault: true
}
};
} else {
if (parent?.system?.trait) {
updateSource['roll'] = {
@ -295,7 +295,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
}
prepareTarget() {
if(!this.target?.type) return [];
if (!this.target?.type) return [];
let targets;
if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id)
targets = this.constructor.formatTarget(this.actor.token ?? this.actor.prototypeToken);
@ -337,7 +337,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
const resources = config.costs
.filter(c => c.enabled !== false)
.map(c => {
return { type: c.type, value: (c.total ?? c.value) * -1 };
const resource = this.actor.system.resources[c.type];
return { type: c.type, value: (c.total ?? c.value) * (resource.hasOwnProperty('maxTotal') ? 1 : -1) };
});
await this.actor.modifyResource(resources);
@ -382,15 +383,21 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
const realCosts = this.getRealCosts(costs),
hasFearCost = realCosts.findIndex(c => c.type === 'fear');
if (hasFearCost > -1) {
const fearCost = realCosts.splice(hasFearCost, 1);
const fearCost = realCosts.splice(hasFearCost, 1)[0];
if (
!game.user.isGM ||
fearCost[0].total > game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
fearCost.total > game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear)
)
return false;
}
/* maxTotal is a sign that the resource is inverted, IE it counts upwards instead of down */
const resources = this.actor.system.resources;
return realCosts.reduce(
(a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value),
(a, c) =>
a && resources[c.type].hasOwnProperty('maxTotal')
? resources[c.type].value + (c.total ?? c.value) <= resources[c.type].maxTotal
: resources[c.type]?.value >= (c.total ?? c.value),
true
);
}

View file

@ -100,12 +100,19 @@ export default class DhCharacter extends BaseDataActor {
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
armorScore: new fields.NumberField({ integer: true, initial: 0 }),
damageReduction: new fields.SchemaField({
physical: new fields.NumberField({ integer: true, initial: 0 }),
magical: new fields.NumberField({ integer: true, initial: 0 })
}),
damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({ integer: true, initial: 0 }),
major: new fields.NumberField({ integer: true, initial: 0 })
}),
roll: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }),
primaryWeapon: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 })
}),
spellcast: new fields.NumberField({ integer: true, initial: 0 }),
action: new fields.NumberField({ integer: true, initial: 0 }),
hopeOrFear: new fields.NumberField({ integer: true, initial: 0 })
@ -113,20 +120,29 @@ export default class DhCharacter extends BaseDataActor {
damage: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }),
physical: new fields.NumberField({ integer: true, initial: 0 }),
magic: new fields.NumberField({ integer: true, initial: 0 })
magic: new fields.NumberField({ integer: true, initial: 0 }),
primaryWeapon: new fields.SchemaField({
bonus: new fields.NumberField({ integer: true }),
extraDice: new fields.NumberField({ integer: true })
})
})
}),
companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }),
stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 })
}),
stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule(),
major: stressDamageReductionRule(),
minor: stressDamageReductionRule()
damageReduction: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }),
stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0 })
}),
stressDamageReduction: new fields.SchemaField({
severe: stressDamageReductionRule(),
major: stressDamageReductionRule(),
minor: stressDamageReductionRule()
}),
increasePerArmorMark: new fields.NumberField({ integer: true, initial: 1 }),
magical: new fields.BooleanField({ initial: false }),
physical: new fields.BooleanField({ initial: false })
}),
strangePatterns: new fields.NumberField({
integer: true,
@ -135,6 +151,18 @@ export default class DhCharacter extends BaseDataActor {
nullable: true,
initial: null
}),
weapon: new fields.SchemaField({
/* Unimplemented
-> Should remove the lowest damage dice from weapon damage
-> Reflect this in the chat message somehow so players get feedback that their choice is helping them.
*/
dropLowestDamageDice: new fields.BooleanField({ initial: false }),
/* Unimplemented
-> Should flip any lowest possible dice rolls for weapon damage to highest
-> Reflect this in the chat message somehow so players get feedback that their choice is helping them.
*/
flipMinDiceValue: new fields.BooleanField({ intial: false })
}),
runeWard: new fields.BooleanField({ initial: false })
})
};
@ -282,6 +310,13 @@ export default class DhCharacter extends BaseDataActor {
);
}
get armorApplicableDamageTypes() {
return {
physical: !this.rules.damageReduction.magical,
magical: !this.rules.damageReduction.physical
};
}
static async unequipBeforeEquip(itemToEquip) {
const primary = this.primaryWeapon,
secondary = this.secondaryWeapon;
@ -348,6 +383,7 @@ export default class DhCharacter extends BaseDataActor {
}
const armor = this.armor;
this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0; // Bonuses to armorScore won't have been applied yet. Need to solve in documentPreparation somehow
this.damageThresholds = {
major: armor
? armor.system.baseThresholds.major + this.levelData.level.current
@ -372,9 +408,9 @@ export default class DhCharacter extends BaseDataActor {
experience.total = experience.value + experience.bonus;
}
this.rules.maxArmorMarked.total = this.rules.maxArmorMarked.value + this.rules.maxArmorMarked.bonus;
this.rules.damageReduction.maxArmorMarked.total =
this.rules.damageReduction.maxArmorMarked.value + this.rules.damageReduction.maxArmorMarked.bonus;
this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0;
this.resources.hitPoints.maxTotal = (this.class.value?.system?.hitPoints ?? 0) + this.resources.hitPoints.bonus;
this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus;

View file

@ -22,7 +22,7 @@ export default class DHArmor extends BaseDataItem {
tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
equipped: new fields.BooleanField({ initial: false }),
baseScore: new fields.NumberField({ integer: true, initial: 0 }),
features: new fields.ArrayField(
armorFeatures: new fields.ArrayField(
new fields.SchemaField({
value: new fields.StringField({
required: true,
@ -44,25 +44,22 @@ export default class DHArmor extends BaseDataItem {
};
}
get featureInfo() {
return this.feature ? CONFIG.DH.ITEM.armorFeatures[this.feature] : null;
}
async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.system.features) {
const removed = this.features.filter(x => !changes.system.features.includes(x));
const added = changes.system.features.filter(x => !this.features.includes(x));
if (changes.system.armorFeatures) {
const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x));
const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x));
const effectIds = [];
const actionIds = [];
for (var feature of removed) {
for (var effectId of feature.effectIds) {
await this.parent.effects.get(effectId).delete();
}
changes.system.actions = this.actions.filter(x => !feature.actionIds.includes(x._id));
effectIds.push(...feature.effectIds);
actionIds.push(...feature.actionIds);
}
await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds);
changes.system.actions = this.actions.filter(x => !actionIds.includes(x._id));
for (var feature of added) {
const featureData = armorFeatures[feature.value];

View file

@ -10,7 +10,7 @@ export default class DHWeapon extends BaseDataItem {
type: 'weapon',
hasDescription: true,
isQuantifiable: true,
isInventoryItem: true,
isInventoryItem: true
// hasInitialAction: true
});
}
@ -26,8 +26,7 @@ export default class DHWeapon extends BaseDataItem {
//SETTINGS
secondary: new fields.BooleanField({ initial: false }),
burden: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.burden, initial: 'oneHanded' }),
features: new fields.ArrayField(
weaponFeatures: new fields.ArrayField(
new fields.SchemaField({
value: new fields.StringField({
required: true,
@ -59,7 +58,7 @@ export default class DHWeapon extends BaseDataItem {
{
value: {
multiplier: 'prof',
dice: "d8"
dice: 'd8'
}
}
]
@ -78,18 +77,20 @@ export default class DHWeapon extends BaseDataItem {
const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false;
if (changes.system?.features) {
const removed = this.features.filter(x => !changes.system.features.includes(x));
const added = changes.system.features.filter(x => !this.features.includes(x));
if (changes.system?.weaponFeatures) {
const removed = this.weaponFeatures.filter(x => !changes.system.weaponFeatures.includes(x));
const added = changes.system.weaponFeatures.filter(x => !this.weaponFeatures.includes(x));
const removedEffectsUpdate = [];
const removedActionsUpdate = [];
for (let weaponFeature of removed) {
for (var effectId of weaponFeature.effectIds) {
await this.parent.effects.get(effectId).delete();
}
changes.system.actions = this.actions.filter(x => !weaponFeature.actionIds.includes(x._id));
removedEffectsUpdate.push(...weaponFeature.effectIds);
removedActionsUpdate.push(...weaponFeature.actionIds);
}
await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate);
changes.system.actions = this.actions.filter(x => !removedActionsUpdate.includes(x._id));
for (let weaponFeature of added) {
const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value];
if (featureData.effects?.length > 0) {
@ -102,17 +103,37 @@ export default class DHWeapon extends BaseDataItem {
]);
weaponFeature.effectIds = embeddedItems.map(x => x.id);
}
const newActions = [];
if (featureData.actions?.length > 0) {
const newActions = featureData.actions.map(action => {
const cls = actionsTypes[action.type];
return new cls(
{ ...action, _id: foundry.utils.randomID(), name: game.i18n.localize(action.name) },
{ parent: this }
for (let action of featureData.actions) {
const embeddedEffects = await this.parent.createEmbeddedDocuments(
'ActiveEffect',
(action.effects ?? []).map(effect => ({
...effect,
transfer: false,
name: game.i18n.localize(effect.name),
description: game.i18n.localize(effect.description)
}))
);
});
changes.system.actions = [...this.actions, ...newActions];
weaponFeature.actionIds = newActions.map(x => x._id);
const cls = actionsTypes[action.type];
newActions.push(
new cls(
{
...action,
_id: foundry.utils.randomID(),
name: game.i18n.localize(action.name),
description: game.i18n.localize(action.description),
effects: embeddedEffects.map(x => ({ _id: x.id }))
},
{ parent: this }
)
);
}
}
changes.system.actions = [...this.actions, ...newActions];
weaponFeature.actionIds = newActions.map(x => x._id);
}
}
}