Fixed armor

This commit is contained in:
WBHarry 2025-07-08 01:05:23 +02:00
parent f633c42aa8
commit b3472177da
8 changed files with 86 additions and 46 deletions

View file

@ -478,8 +478,8 @@
"name": "Impenetrable", "name": "Impenetrable",
"description": "Once per short rest, when you would mark your last Hit Point, you can instead mark a Stress." "description": "Once per short rest, when you would mark your last Hit Point, you can instead mark a Stress."
}, },
"magic": { "magical": {
"name": "Magic", "name": "Magical",
"description": "You can't mark an Armor Slot to reduce physical damage." "description": "You can't mark an Armor Slot to reduce physical damage."
}, },
"physical": { "physical": {

View file

@ -3,7 +3,7 @@ import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, actor, damage) { constructor(resolve, reject, actor, damage, damageType) {
super({}); super({});
this.resolve = resolve; this.resolve = resolve;
@ -11,10 +11,13 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.actor = actor; this.actor = actor;
this.damage = damage; this.damage = damage;
const maxArmorMarks = Math.min( const canApplyArmor = actor.system.armorApplicableDamageTypes[damageType];
actor.system.armorScore - actor.system.armor.system.marks.value, const maxArmorMarks = canApplyArmor
actor.system.rules.damageReduction.maxArmorMarked.total ? Math.min(
); actor.system.armorScore - actor.system.armor.system.marks.value,
actor.system.rules.damageReduction.maxArmorMarked.total
)
: 0;
const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => { const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => {
acc[foundry.utils.randomID()] = { selected: false }; acc[foundry.utils.randomID()] = { selected: false };
@ -129,12 +132,15 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
getDamageInfo = () => { getDamageInfo = () => {
const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected);
const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected);
const stressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); const stressReductions = this.availableStressReductions
? Object.values(this.availableStressReductions).filter(red => red.selected)
: [];
const currentMarks = const currentMarks =
this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length; this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length;
const currentDamage = const armorMarkReduction =
this.damage - selectedArmorMarks.length - selectedStressMarks.length - stressReductions.length; selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark;
const currentDamage = this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length;
return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage };
}; };

View file

@ -35,7 +35,7 @@ export default class ArmorSheet extends DHBaseItemSheet {
switch (partId) { switch (partId) {
case 'settings': case 'settings':
context.features = this.document.system.features.map(x => x.value); context.features = this.document.system.armorFeatures.map(x => x.value);
break; break;
} }
@ -47,6 +47,6 @@ export default class ArmorSheet extends DHBaseItemSheet {
* @param {Array<Object>} selectedOptions - The currently selected tag objects. * @param {Array<Object>} selectedOptions - The currently selected tag objects.
*/ */
static async #onFeatureSelect(selectedOptions) { static async #onFeatureSelect(selectedOptions) {
await this.document.update({ 'system.features': selectedOptions.map(x => ({ value: x.value })) }); await this.document.update({ 'system.armorFeatures': selectedOptions.map(x => ({ value: x.value })) });
} }
} }

View file

@ -190,19 +190,19 @@ export const armorFeatures = {
} }
] ]
}, },
magic: { magical: {
label: 'DAGGERHEART.CONFIG.ArmorFeature.magic.name', label: 'DAGGERHEART.CONFIG.ArmorFeature.magical.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.magic.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.magical.description',
effects: [ effects: [
{ {
name: 'DAGGERHEART.CONFIG.ArmorFeature.magic.name', name: 'DAGGERHEART.CONFIG.ArmorFeature.magical.name',
description: 'DAGGERHEART.CONFIG.ArmorFeature.magic.description', description: 'DAGGERHEART.CONFIG.ArmorFeature.magical.description',
img: 'icons/magic/defensive/barrier-shield-dome-blue-purple.webp', img: 'icons/magic/defensive/barrier-shield-dome-blue-purple.webp',
changes: [ changes: [
{ {
key: 'system.rules.damageReduction.magic', key: 'system.rules.damageReduction.magical',
mode: 5, mode: 5,
value: '1' value: 1
} }
] ]
} }
@ -220,7 +220,7 @@ export const armorFeatures = {
{ {
key: 'system.rules.damageReduction.physical', key: 'system.rules.damageReduction.physical',
mode: 5, mode: 5,
value: '1' value: 1
} }
] ]
} }

View file

@ -152,7 +152,15 @@ export default class DhCharacter extends BaseDataActor {
initial: null initial: null
}), }),
weapon: new fields.SchemaField({ 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 }), 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 }) flipMinDiceValue: new fields.BooleanField({ intial: false })
}), }),
runeWard: new fields.BooleanField({ initial: false }) runeWard: new fields.BooleanField({ initial: false })
@ -302,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) { static async unequipBeforeEquip(itemToEquip) {
const primary = this.primaryWeapon, const primary = this.primaryWeapon,
secondary = this.secondaryWeapon; secondary = this.secondaryWeapon;
@ -368,6 +383,7 @@ export default class DhCharacter extends BaseDataActor {
} }
const armor = this.armor; 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 = { this.damageThresholds = {
major: armor major: armor
? armor.system.baseThresholds.major + this.levelData.level.current ? armor.system.baseThresholds.major + this.levelData.level.current
@ -395,7 +411,6 @@ export default class DhCharacter extends BaseDataActor {
this.rules.damageReduction.maxArmorMarked.total = this.rules.damageReduction.maxArmorMarked.total =
this.rules.damageReduction.maxArmorMarked.value + this.rules.damageReduction.maxArmorMarked.bonus; 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.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.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus;
this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.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 }), tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }),
equipped: new fields.BooleanField({ initial: false }), equipped: new fields.BooleanField({ initial: false }),
baseScore: new fields.NumberField({ integer: true, initial: 0 }), baseScore: new fields.NumberField({ integer: true, initial: 0 }),
features: new fields.ArrayField( armorFeatures: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, 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) { async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false; if (allowed === false) return false;
if (changes.system.features) { if (changes.system.armorFeatures) {
const removed = this.features.filter(x => !changes.system.features.includes(x)); const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x));
const added = changes.system.features.filter(x => !this.features.includes(x)); const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x));
const effectIds = [];
const actionIds = [];
for (var feature of removed) { for (var feature of removed) {
for (var effectId of feature.effectIds) { effectIds.push(...feature.effectIds);
await this.parent.effects.get(effectId).delete(); actionIds.push(...feature.actionIds);
}
changes.system.actions = this.actions.filter(x => !feature.actionIds.includes(x._id));
} }
await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds);
changes.system.actions = this.actions.filter(x => !actionIds.includes(x._id));
for (var feature of added) { for (var feature of added) {
const featureData = armorFeatures[feature.value]; const featureData = armorFeatures[feature.value];

View file

@ -3,6 +3,7 @@ import { GMUpdateEvent, socketEvent } from '../systemRegistration/socket.mjs';
import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs';
import { LevelOptionType } from '../data/levelTier.mjs'; import { LevelOptionType } from '../data/levelTier.mjs';
import DHFeature from '../data/item/feature.mjs'; import DHFeature from '../data/item/feature.mjs';
import { damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
export default class DhpActor extends Actor { export default class DhpActor extends Actor {
async _preCreate(data, options, user) { async _preCreate(data, options, user) {
@ -430,12 +431,31 @@ export default class DhpActor extends Actor {
cls.create(msg.toObject()); cls.create(msg.toObject());
} }
async takeDamage(damage, type) { #canReduceDamage(hpDamage, type) {
const availableStress = this.system.resources.stress.maxTotal - this.system.resources.stress.value;
const canUseArmor =
this.system.armor &&
this.system.armor.system.marks.value < this.system.armorScore &&
this.system.armorApplicableDamageTypes[type];
const canUseStress = Object.keys(this.system.rules.damageReduction.stressDamageReduction).reduce((acc, x) => {
const rule = this.system.rules.damageReduction.stressDamageReduction[x];
if (damageKeyToNumber(x) <= hpDamage) return acc || (rule.enabled && availableStress >= rule.cost);
return acc;
}, false);
return canUseArmor || canUseStress;
}
async takeDamage(baseDamage, type) {
if (this.type === 'companion') { if (this.type === 'companion') {
await this.modifyResource([{ value: 1, type: 'stress' }]); await this.modifyResource([{ value: 1, type: 'stress' }]);
return; return;
} }
const flatReduction = this.system.bonuses.damageReduction[type];
const damage = Math.max(baseDamage - (flatReduction ?? 0), 0);
const hpDamage = const hpDamage =
damage >= this.system.damageThresholds.severe damage >= this.system.damageThresholds.severe
? 3 ? 3
@ -445,13 +465,9 @@ export default class DhpActor extends Actor {
? 1 ? 1
: 0; : 0;
if ( if (hpDamage && this.type === 'character' && this.#canReduceDamage(hpDamage, type)) {
this.type === 'character' &&
this.system.armor &&
this.system.armor.system.marks.value < this.system.armorScore
) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
new DamageReductionDialog(resolve, reject, this, hpDamage).render(true); new DamageReductionDialog(resolve, reject, this, hpDamage, type).render(true);
}) })
.then(async ({ modifiedDamage, armorSpent, stressSpent }) => { .then(async ({ modifiedDamage, armorSpent, stressSpent }) => {
const resources = [ const resources = [

View file

@ -190,6 +190,8 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) =>
}); });
tagifyElement.on('add', event => { tagifyElement.on('add', event => {
if (event.detail.data.__isValid === 'not allowed') return;
const input = event.detail.tagify.DOM.originalInput; const input = event.detail.tagify.DOM.originalInput;
const currentList = input.value ? JSON.parse(input.value) : []; const currentList = input.value ? JSON.parse(input.value) : [];
onChange([...currentList, event.detail.data], { option: event.detail.data.value, removed: false }, input); onChange([...currentList, event.detail.data], { option: event.detail.data.value, removed: false }, input);
@ -233,19 +235,23 @@ Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false
return nativeReplaceFormulaData(formula, data, { missing, warn }); return nativeReplaceFormulaData(formula, data, { missing, warn });
}; };
export const getDamageLabel = damage => { export const getDamageKey = damage => {
switch (damage) { switch (damage) {
case 3: case 3:
return game.i18n.localize('DAGGERHEART.GENERAL.Damage.severe'); return 'severe';
case 2: case 2:
return game.i18n.localize('DAGGERHEART.GENERAL.Damage.major'); return 'major';
case 1: case 1:
return game.i18n.localize('DAGGERHEART.GENERAL.Damage.minor'); return 'minor';
case 0: case 0:
return game.i18n.localize('DAGGERHEART.GENERAL.Damage.none'); return 'none';
} }
}; };
export const getDamageLabel = damage => {
return game.i18n.localize(`DAGGERHEART.GENERAL.Damage.${getDamageKey(damage)}`);
};
export const damageKeyToNumber = key => { export const damageKeyToNumber = key => {
switch (key) { switch (key) {
case 'severe': case 'severe':