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

@ -486,14 +486,10 @@
"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."
}, },
"painful": {
"name": "Painful",
"description": "Each time you mark an Armor Slot, you must mark a Stress."
},
"physical": { "physical": {
"name": "Physical", "name": "Physical",
"description": "You can't mark an Armor Slot to reduce magic damage." "description": "You can't mark an Armor Slot to reduce magic damage."
@ -897,6 +893,10 @@
"name": "Scary", "name": "Scary",
"description": "On a successful attack, the target must mark a Stress." "description": "On a successful attack, the target must mark a Stress."
}, },
"selfCorrecting": {
"name": "Self Correcting",
"description": "When you roll a 1 on a damage die, it deals 6 damage instead."
},
"serrated": { "serrated": {
"name": "Serrated", "name": "Serrated",
"description": "When you roll a 1 on a damage die, it deals 8 damage instead." "description": "When you roll a 1 on a damage die, it deals 8 damage instead."

View file

@ -3,7 +3,7 @@ import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs';
const { DialogV2, ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { DialogV2, 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,23 +11,30 @@ 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];
const maxArmorMarks = canApplyArmor
? Math.min(
actor.system.armorScore - actor.system.armor.system.marks.value, actor.system.armorScore - actor.system.armor.system.marks.value,
actor.system.rules.maxArmorMarked.total 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 };
return acc; return acc;
}, {}); }, {});
const stress = [...Array(actor.system.rules.maxArmorMarked.stressExtra ?? 0).keys()].reduce((acc, _) => { const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce(
(acc, _) => {
acc[foundry.utils.randomID()] = { selected: false }; acc[foundry.utils.randomID()] = { selected: false };
return acc; return acc;
}, {}); },
{}
);
this.marks = { armor, stress }; this.marks = { armor, stress };
this.availableStressReductions = Object.keys(actor.system.rules.stressDamageReduction).reduce((acc, key) => { this.availableStressReductions = Object.keys(actor.system.rules.damageReduction.stressDamageReduction).reduce(
const dr = actor.system.rules.stressDamageReduction[key]; (acc, key) => {
const dr = actor.system.rules.damageReduction.stressDamageReduction[key];
if (dr.enabled) { if (dr.enabled) {
if (acc === null) acc = {}; if (acc === null) acc = {};
@ -41,7 +48,9 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
} }
return acc; return acc;
}, null); },
null
);
} }
get title() { get title() {
@ -90,7 +99,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
context.armorScore = this.actor.system.armorScore; context.armorScore = this.actor.system.armorScore;
context.armorMarks = currentMarks; context.armorMarks = currentMarks;
context.basicMarksUsed = selectedArmorMarks.length === this.actor.system.rules.maxArmorMarked.total; context.basicMarksUsed =
selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.total;
const stressReductionStress = this.availableStressReductions const stressReductionStress = this.availableStressReductions
? stressReductions.reduce((acc, red) => acc + red.cost, 0) ? stressReductions.reduce((acc, red) => acc + red.cost, 0)
@ -122,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 };
}; };
@ -216,11 +229,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
await super.close({}); await super.close({});
} }
static async armorStackQuery({actorId, damage}) { static async armorStackQuery({ actorId, damage, type }) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const actor = await fromUuid(actorId); const actor = await fromUuid(actorId);
if (!actor || !actor?.isOwner) reject(); if (!actor || !actor?.isOwner) reject();
new DamageReductionDialog(resolve, reject, actor, damage).render({ force: true }); new DamageReductionDialog(resolve, reject, actor, damage, type).render({ force: true });
}) });
} }
} }

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

@ -33,7 +33,7 @@ export default class WeaponSheet extends DHBaseItemSheet {
super._preparePartContext(partId, context); super._preparePartContext(partId, context);
switch (partId) { switch (partId) {
case 'settings': case 'settings':
context.features = this.document.system.features.map(x => x.value); context.features = this.document.system.weaponFeatures.map(x => x.value);
context.systemFields.attack.fields = this.document.system.attack.schema.fields; context.systemFields.attack.fields = this.document.system.attack.schema.fields;
break; break;
} }
@ -45,6 +45,6 @@ export default class WeaponSheet 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.weaponFeatures': selectedOptions.map(x => ({ value: x.value })) });
} }
} }

View file

@ -59,13 +59,13 @@ export const damageTypes = {
id: 'physical', id: 'physical',
label: 'DAGGERHEART.CONFIG.DamageType.physical.name', label: 'DAGGERHEART.CONFIG.DamageType.physical.name',
abbreviation: 'DAGGERHEART.CONFIG.DamageType.physical.abbreviation', abbreviation: 'DAGGERHEART.CONFIG.DamageType.physical.abbreviation',
icon: ["fa-hand-fist"] icon: ['fa-hand-fist']
}, },
magical: { magical: {
id: 'magical', id: 'magical',
label: 'DAGGERHEART.CONFIG.DamageType.magical.name', label: 'DAGGERHEART.CONFIG.DamageType.magical.name',
abbreviation: 'DAGGERHEART.CONFIG.DamageType.magical.abbreviation', abbreviation: 'DAGGERHEART.CONFIG.DamageType.magical.abbreviation',
icon: ["fa-wand-sparkles"] icon: ['fa-wand-sparkles']
} }
}; };

File diff suppressed because it is too large Load diff

View file

@ -161,7 +161,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
updateSource['range'] = parent?.system?.attack?.range; updateSource['range'] = parent?.system?.attack?.range;
updateSource['roll'] = { updateSource['roll'] = {
useDefault: true useDefault: true
} };
} else { } else {
if (parent?.system?.trait) { if (parent?.system?.trait) {
updateSource['roll'] = { updateSource['roll'] = {
@ -337,7 +337,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
const resources = config.costs const resources = config.costs
.filter(c => c.enabled !== false) .filter(c => c.enabled !== false)
.map(c => { .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); await this.actor.modifyResource(resources);
@ -382,15 +383,21 @@ export default class DHBaseAction extends foundry.abstract.DataModel {
const realCosts = this.getRealCosts(costs), const realCosts = this.getRealCosts(costs),
hasFearCost = realCosts.findIndex(c => c.type === 'fear'); hasFearCost = realCosts.findIndex(c => c.type === 'fear');
if (hasFearCost > -1) { if (hasFearCost > -1) {
const fearCost = realCosts.splice(hasFearCost, 1); const fearCost = realCosts.splice(hasFearCost, 1)[0];
if ( if (
!game.user.isGM || !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; 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( 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 true
); );
} }

View file

@ -100,12 +100,19 @@ export default class DhCharacter extends BaseDataActor {
levelData: new fields.EmbeddedDataField(DhLevelData), levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({ bonuses: new fields.SchemaField({
armorScore: new fields.NumberField({ integer: true, initial: 0 }), 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({ damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({ integer: true, initial: 0 }), severe: new fields.NumberField({ integer: true, initial: 0 }),
major: new fields.NumberField({ integer: true, initial: 0 }) major: new fields.NumberField({ integer: true, initial: 0 })
}), }),
roll: new fields.SchemaField({ roll: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }), 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 }), spellcast: new fields.NumberField({ integer: true, initial: 0 }),
action: new fields.NumberField({ integer: true, initial: 0 }), action: new fields.NumberField({ integer: true, initial: 0 }),
hopeOrFear: new fields.NumberField({ integer: true, initial: 0 }) hopeOrFear: new fields.NumberField({ integer: true, initial: 0 })
@ -113,11 +120,16 @@ export default class DhCharacter extends BaseDataActor {
damage: new fields.SchemaField({ damage: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }), all: new fields.NumberField({ integer: true, initial: 0 }),
physical: 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 }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({ rules: new fields.SchemaField({
damageReduction: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({ maxArmorMarked: new fields.SchemaField({
value: new fields.NumberField({ required: true, integer: true, initial: 1 }), value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }), bonus: new fields.NumberField({ required: true, integer: true, initial: 0 }),
@ -128,6 +140,10 @@ export default class DhCharacter extends BaseDataActor {
major: stressDamageReductionRule(), major: stressDamageReductionRule(),
minor: 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({ strangePatterns: new fields.NumberField({
integer: true, integer: true,
min: 1, min: 1,
@ -135,6 +151,18 @@ export default class DhCharacter extends BaseDataActor {
nullable: true, nullable: true,
initial: null 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 }) 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) { static async unequipBeforeEquip(itemToEquip) {
const primary = this.primaryWeapon, const primary = this.primaryWeapon,
secondary = this.secondaryWeapon; secondary = this.secondaryWeapon;
@ -348,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
@ -372,9 +408,9 @@ export default class DhCharacter extends BaseDataActor {
experience.total = experience.value + experience.bonus; 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.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

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

View file

@ -25,7 +25,11 @@ export default class DhActiveEffect extends ActiveEffect {
} }
static applyField(model, change, field) { static applyField(model, change, field) {
change.value = Roll.safeEval(Roll.replaceFormulaData(change.value, change.effect.parent)); const isItemTarget = change.value.toLowerCase().startsWith('item.');
change.value = isItemTarget ? change.value.slice(5) : change.value;
change.value = Roll.safeEval(
Roll.replaceFormulaData(change.value, isItemTarget ? change.effect.parent : model)
);
super.applyField(model, change, field); super.applyField(model, change, field);
} }

View file

@ -3,6 +3,7 @@ import { emitAsGM, emitAsOwner, GMUpdateEvent, socketEvent } from '../systemRegi
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 {
/** /**
@ -455,14 +456,32 @@ export default class DhpActor extends Actor {
cls.create(msg.toObject()); cls.create(msg.toObject());
} }
async takeDamage(damage, type) { #canReduceDamage(hpDamage, type) {
if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, damage, type) === false) return null; 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 (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, baseDamage, type) === false) return null;
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 = this.convertDamageToThreshold(damage); const hpDamage = this.convertDamageToThreshold(damage);
if (Hooks.call(`${CONFIG.DH.id}.postDamageTreshold`, this, hpDamage, damage, type) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.postDamageTreshold`, this, hpDamage, damage, type) === false) return null;
@ -471,12 +490,12 @@ export default class DhpActor extends Actor {
const updates = [{ value: hpDamage, type: 'hitPoints' }]; const updates = [{ value: hpDamage, type: 'hitPoints' }];
if ( if (this.type === 'character' && this.system.armor && this.#canReduceDamage(hpDamage, type)) {
this.type === 'character' && const armorStackResult = await this.owner.query('armorStack', {
this.system.armor && actorId: this.uuid,
this.system.armor.system.marks.value < this.system.armorScore damage: hpDamage,
) { type: type
const armorStackResult = await this.owner.query('armorStack', { actorId: this.uuid, damage: hpDamage }); });
if (armorStackResult) { if (armorStackResult) {
const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; const { modifiedDamage, armorSpent, stressSpent } = armorStackResult;
updates.find(u => u.type === 'hitPoints').value = modifiedDamage; updates.find(u => u.type === 'hitPoints').value = modifiedDamage;

View file

@ -90,10 +90,12 @@ export default class DHItem extends foundry.documents.Item {
ok: { ok: {
label: title, label: title,
callback: (event, button, dialog) => { callback: (event, button, dialog) => {
Object.defineProperty(prevEvent, "shiftKey", { Object.defineProperty(prevEvent, 'shiftKey', {
get() { return event.shiftKey; }, get() {
return event.shiftKey;
}
}); });
return this.system.actionsList.find(a => a._id === button.form.elements.actionId.value) return this.system.actionsList.find(a => a._id === button.form.elements.actionId.value);
} }
} }
}); });

View file

@ -159,7 +159,8 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) =>
return { return {
value: key, value: key,
name: game.i18n.localize(option.label), name: game.i18n.localize(option.label),
src: option.src src: option.src,
description: option.description
}; };
}), }),
maxTags: maxTags, maxTags: maxTags,
@ -173,11 +174,12 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) =>
}, },
templates: { templates: {
tag(tagData) { tag(tagData) {
return `<tag title="${tagData.title || tagData.value}" return `<tag
contenteditable='false' contenteditable='false'
spellcheck='false' spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}" tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}" class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}"
data-tooltip="${tagData.description || tagData.name}"
${this.getAttributes(tagData)}> ${this.getAttributes(tagData)}>
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x> <x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div> <div>
@ -190,6 +192,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 +237,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':

View file

@ -45,7 +45,13 @@ export const registerSocketHooks = () => {
await game.settings.set( await game.settings.set(
CONFIG.DH.id, CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear,
Math.max(0, Math.min(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear, data.update)) Math.max(
0,
Math.min(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
data.update
)
)
); );
/* Hooks.callAll(socketEvent.DhpFearUpdate); /* Hooks.callAll(socketEvent.DhpFearUpdate);
await game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.DhpFearUpdate }); */ await game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.DhpFearUpdate }); */
@ -74,7 +80,7 @@ export const emitAsGM = async (eventName, callback, update, uuid = null) => {
} }
}); });
} else return callback(update); } else return callback(update);
} };
export const emitAsOwner = (eventName, userId, args) => { export const emitAsOwner = (eventName, userId, args) => {
if (userId === game.user.id) return; if (userId === game.user.id) return;
@ -87,4 +93,4 @@ export const emitAsOwner = (eventName, userId, args) => {
} }
}); });
return false; return false;
} };