migrate trigger flag from monster features to Active Effect for use on items

This commit is contained in:
Florian Zumpe 2026-05-25 13:19:44 +02:00
parent 3c68bf2216
commit fd2947e0cd
8 changed files with 626 additions and 132 deletions

View File

@ -2,7 +2,7 @@
"id": "adaptive-damage-resistance", "id": "adaptive-damage-resistance",
"title": "Adaptive Damage Resistance", "title": "Adaptive Damage Resistance",
"description": "Adaptive resistance and immunity features for dnd5e actors. Adds a compendium with draggable feature items and updates actor defenses based on the last valid damage type.", "description": "Adaptive resistance and immunity features for dnd5e actors. Adds a compendium with draggable feature items and updates actor defenses based on the last valid damage type.",
"version": "1.0.0", "version": "1.1.0",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "14" "verified": "14"
@ -54,6 +54,6 @@
} }
], ],
"url": "https://github.com/fzumpe/foundry-dnd5e-adaptive-resistances", "url": "https://github.com/fzumpe/foundry-dnd5e-adaptive-resistances",
"manifest": "https://raw.githubusercontent.com/fzumpe/foundry-dnd5e-adaptive-resistances/refs/tags/V1.0.0/module.json", "manifest": "https://raw.githubusercontent.com/fzumpe/foundry-dnd5e-adaptive-resistances/main/module.json",
"download": "https://github.com/fzumpe/foundry-dnd5e-adaptive-resistances/archive/refs/tags/V1.0.0.zip" "download": "https://github.com/fzumpe/foundry-dnd5e-adaptive-resistances/archive/refs/tags/V1.1.0.zip"
} }

View File

@ -1,11 +1,40 @@
export const MODULE_ID = "adaptive-damage-resistance"; export const MODULE_ID = "adaptive-damage-resistance";
export const PACK_NAME = "adaptive-features"; export const PACK_NAME = "adaptive-features";
export const PACK_ID = `${MODULE_ID}.${PACK_NAME}`; export const PACK_ID = `${MODULE_ID}.${PACK_NAME}`;
/**
* Dieses Flag bleibt auf den Feature-Items erhalten.
* Es dient weiterhin zur Identifikation beim Erzeugen und Migrieren
* der Kompendium- und Actor-Items.
*
* Für die Laufzeitlogik wird es ab Version 1.1.0 nicht mehr verwendet.
*/
export const FEATURE_FLAG = "adaptiveResistanceFeature"; export const FEATURE_FLAG = "adaptiveResistanceFeature";
/**
* Dieses Flag liegt auf einem Active Effect.
* Nur tatsächlich angewendete Active Effects mit diesem Flag aktivieren
* die adaptive Resistenz- oder Immunitätslogik.
*/
export const TRIGGER_FLAG = "adaptiveResistanceTrigger";
/**
* Dieses Flag markiert den dynamisch auf dem Actor erzeugten Effekt,
* der die konkrete Resistenz oder Immunität gewährt.
*/
export const EFFECT_FLAG = "adaptiveResistanceEffect"; export const EFFECT_FLAG = "adaptiveResistanceEffect";
export const PROFANE_DAMAGE_TYPES = Object.freeze(["bludgeoning", "piercing", "slashing"]); export const PROFANE_DAMAGE_TYPES = Object.freeze([
export const PROFANE_BYPASSES = Object.freeze(["ada", "mgc", "sil"]); "bludgeoning",
"piercing",
"slashing"
]);
export const PROFANE_BYPASSES = Object.freeze([
"ada",
"mgc",
"sil"
]);
export const ADAPTATION_TYPES = Object.freeze({ export const ADAPTATION_TYPES = Object.freeze({
RESISTANCE: "resistance", RESISTANCE: "resistance",
@ -22,6 +51,7 @@ export const ADAPTATION_CONFIG = Object.freeze({
itemNamePrefixKey: "ADR.Features.Prefix.Resistance", itemNamePrefixKey: "ADR.Features.Prefix.Resistance",
icon: "icons/magic/defensive/shield-barrier-flaming-diamond-orange.webp" icon: "icons/magic/defensive/shield-barrier-flaming-diamond-orange.webp"
}, },
[ADAPTATION_TYPES.IMMUNITY]: { [ADAPTATION_TYPES.IMMUNITY]: {
id: ADAPTATION_TYPES.IMMUNITY, id: ADAPTATION_TYPES.IMMUNITY,
priority: 20, priority: 20,
@ -54,104 +84,161 @@ export const DAMAGE_SETS = Object.freeze({
id: "elemental", id: "elemental",
nameKey: "ADR.Features.Set.Elemental.Name", nameKey: "ADR.Features.Set.Elemental.Name",
descriptionKey: "ADR.Features.Set.Elemental.Description", descriptionKey: "ADR.Features.Set.Elemental.Description",
damageTypes: ["acid", "cold", "fire", "lightning", "thunder"] damageTypes: [
"acid",
"cold",
"fire",
"lightning",
"thunder"
]
}, },
magical: { magical: {
id: "magical", id: "magical",
nameKey: "ADR.Features.Set.Magical.Name", nameKey: "ADR.Features.Set.Magical.Name",
descriptionKey: "ADR.Features.Set.Magical.Description", descriptionKey: "ADR.Features.Set.Magical.Description",
damageTypes: ["force", "necrotic", "psychic", "radiant"] damageTypes: [
"force",
"necrotic",
"psychic",
"radiant"
]
}, },
profane: { profane: {
id: "profane", id: "profane",
nameKey: "ADR.Features.Set.Profane.Name", nameKey: "ADR.Features.Set.Profane.Name",
descriptionKey: "ADR.Features.Set.Profane.Description", descriptionKey: "ADR.Features.Set.Profane.Description",
damageTypes: ["bludgeoning", "piercing", "slashing"] damageTypes: [
"bludgeoning",
"piercing",
"slashing"
]
}, },
nonProfane: { nonProfane: {
id: "nonProfane", id: "nonProfane",
nameKey: "ADR.Features.Set.NonProfane.Name", nameKey: "ADR.Features.Set.NonProfane.Name",
descriptionKey: "ADR.Features.Set.NonProfane.Description", descriptionKey: "ADR.Features.Set.NonProfane.Description",
damageTypes: ["acid", "cold", "fire", "force", "lightning", "necrotic", "poison", "psychic", "radiant", "thunder"] damageTypes: [
"acid",
"cold",
"fire",
"force",
"lightning",
"necrotic",
"poison",
"psychic",
"radiant",
"thunder"
]
}, },
all: { all: {
id: "all", id: "all",
nameKey: "ADR.Features.Set.All.Name", nameKey: "ADR.Features.Set.All.Name",
descriptionKey: "ADR.Features.Set.All.Description", descriptionKey: "ADR.Features.Set.All.Description",
damageTypes: ["acid", "bludgeoning", "cold", "fire", "force", "lightning", "necrotic", "piercing", "poison", "psychic", "radiant", "slashing", "thunder"] damageTypes: [
"acid",
"bludgeoning",
"cold",
"fire",
"force",
"lightning",
"necrotic",
"piercing",
"poison",
"psychic",
"radiant",
"slashing",
"thunder"
]
}, },
acid: { acid: {
id: "acid", id: "acid",
nameKey: "ADR.Features.Set.Single.acid.Name", nameKey: "ADR.Features.Set.Single.acid.Name",
descriptionKey: "ADR.Features.Set.Single.acid.Description", descriptionKey: "ADR.Features.Set.Single.acid.Description",
damageTypes: ["acid"] damageTypes: ["acid"]
}, },
bludgeoning: { bludgeoning: {
id: "bludgeoning", id: "bludgeoning",
nameKey: "ADR.Features.Set.Single.bludgeoning.Name", nameKey: "ADR.Features.Set.Single.bludgeoning.Name",
descriptionKey: "ADR.Features.Set.Single.bludgeoning.Description", descriptionKey: "ADR.Features.Set.Single.bludgeoning.Description",
damageTypes: ["bludgeoning"] damageTypes: ["bludgeoning"]
}, },
cold: { cold: {
id: "cold", id: "cold",
nameKey: "ADR.Features.Set.Single.cold.Name", nameKey: "ADR.Features.Set.Single.cold.Name",
descriptionKey: "ADR.Features.Set.Single.cold.Description", descriptionKey: "ADR.Features.Set.Single.cold.Description",
damageTypes: ["cold"] damageTypes: ["cold"]
}, },
fire: { fire: {
id: "fire", id: "fire",
nameKey: "ADR.Features.Set.Single.fire.Name", nameKey: "ADR.Features.Set.Single.fire.Name",
descriptionKey: "ADR.Features.Set.Single.fire.Description", descriptionKey: "ADR.Features.Set.Single.fire.Description",
damageTypes: ["fire"] damageTypes: ["fire"]
}, },
force: { force: {
id: "force", id: "force",
nameKey: "ADR.Features.Set.Single.force.Name", nameKey: "ADR.Features.Set.Single.force.Name",
descriptionKey: "ADR.Features.Set.Single.force.Description", descriptionKey: "ADR.Features.Set.Single.force.Description",
damageTypes: ["force"] damageTypes: ["force"]
}, },
lightning: { lightning: {
id: "lightning", id: "lightning",
nameKey: "ADR.Features.Set.Single.lightning.Name", nameKey: "ADR.Features.Set.Single.lightning.Name",
descriptionKey: "ADR.Features.Set.Single.lightning.Description", descriptionKey: "ADR.Features.Set.Single.lightning.Description",
damageTypes: ["lightning"] damageTypes: ["lightning"]
}, },
necrotic: { necrotic: {
id: "necrotic", id: "necrotic",
nameKey: "ADR.Features.Set.Single.necrotic.Name", nameKey: "ADR.Features.Set.Single.necrotic.Name",
descriptionKey: "ADR.Features.Set.Single.necrotic.Description", descriptionKey: "ADR.Features.Set.Single.necrotic.Description",
damageTypes: ["necrotic"] damageTypes: ["necrotic"]
}, },
piercing: { piercing: {
id: "piercing", id: "piercing",
nameKey: "ADR.Features.Set.Single.piercing.Name", nameKey: "ADR.Features.Set.Single.piercing.Name",
descriptionKey: "ADR.Features.Set.Single.piercing.Description", descriptionKey: "ADR.Features.Set.Single.piercing.Description",
damageTypes: ["piercing"] damageTypes: ["piercing"]
}, },
poison: { poison: {
id: "poison", id: "poison",
nameKey: "ADR.Features.Set.Single.poison.Name", nameKey: "ADR.Features.Set.Single.poison.Name",
descriptionKey: "ADR.Features.Set.Single.poison.Description", descriptionKey: "ADR.Features.Set.Single.poison.Description",
damageTypes: ["poison"] damageTypes: ["poison"]
}, },
psychic: { psychic: {
id: "psychic", id: "psychic",
nameKey: "ADR.Features.Set.Single.psychic.Name", nameKey: "ADR.Features.Set.Single.psychic.Name",
descriptionKey: "ADR.Features.Set.Single.psychic.Description", descriptionKey: "ADR.Features.Set.Single.psychic.Description",
damageTypes: ["psychic"] damageTypes: ["psychic"]
}, },
radiant: { radiant: {
id: "radiant", id: "radiant",
nameKey: "ADR.Features.Set.Single.radiant.Name", nameKey: "ADR.Features.Set.Single.radiant.Name",
descriptionKey: "ADR.Features.Set.Single.radiant.Description", descriptionKey: "ADR.Features.Set.Single.radiant.Description",
damageTypes: ["radiant"] damageTypes: ["radiant"]
}, },
slashing: { slashing: {
id: "slashing", id: "slashing",
nameKey: "ADR.Features.Set.Single.slashing.Name", nameKey: "ADR.Features.Set.Single.slashing.Name",
descriptionKey: "ADR.Features.Set.Single.slashing.Description", descriptionKey: "ADR.Features.Set.Single.slashing.Description",
damageTypes: ["slashing"] damageTypes: ["slashing"]
}, },
thunder: { thunder: {
id: "thunder", id: "thunder",
nameKey: "ADR.Features.Set.Single.thunder.Name", nameKey: "ADR.Features.Set.Single.thunder.Name",
@ -161,4 +248,4 @@ export const DAMAGE_SETS = Object.freeze({
}); });
// Backwards compatible export for older module-internal imports or local overrides. // Backwards compatible export for older module-internal imports or local overrides.
export const RESISTANCE_SETS = DAMAGE_SETS; export const RESISTANCE_SETS = DAMAGE_SETS;

87
scripts/effects.js vendored
View File

@ -6,13 +6,58 @@ import {
PROFANE_DAMAGE_TYPES, PROFANE_DAMAGE_TYPES,
PROFANE_BYPASSES PROFANE_BYPASSES
} from "./constants.js"; } from "./constants.js";
import { getDamageTypeLabel } from "./utils.js";
import {
getAdaptiveTriggerEffects,
getDamageTypeLabel
} from "./utils.js";
/**
* Entfernt sämtliche dynamisch erzeugten adaptiven Schutzwirkungen.
* Eine neu ausgelöste Anpassung ersetzt damit immer die vorherige.
*/
export async function removeOldAdaptiveEffects(actor) { export async function removeOldAdaptiveEffects(actor) {
const oldEffects = actor.effects.filter(effect => effect.getFlag(MODULE_ID, EFFECT_FLAG)); const oldEffects = actor.effects.filter(
effect => effect.getFlag(MODULE_ID, EFFECT_FLAG)
);
if (!oldEffects.length) return; if (!oldEffects.length) return;
await actor.deleteEmbeddedDocuments("ActiveEffect", oldEffects.map(effect => effect.id)); await actor.deleteEmbeddedDocuments(
"ActiveEffect",
oldEffects.map(effect => effect.id)
);
}
/**
* Entfernt adaptive Schutzwirkungen, deren auslösende Quelle nicht mehr
* auf dem Actor angewendet wird.
*
* Beispiel:
* Ein getragenes Amulett verleiht adaptive Feuerresistenz. Wird das Amulett
* abgelegt, verschwindet sein Marker-Effect aus actor.appliedEffects und die
* zuvor erzeugte Feuerresistenz wird ebenfalls entfernt.
*/
export async function removeOrphanedAdaptiveEffects(actor) {
if (!actor) return;
const appliedSourceUuids = new Set(
getAdaptiveTriggerEffects(actor).map(effect => effect.uuid)
);
const orphanedEffects = actor.effects.filter(effect => {
const data = effect.getFlag(MODULE_ID, EFFECT_FLAG);
return data?.sourceEffectUuid
&& !appliedSourceUuids.has(data.sourceEffectUuid);
});
if (!orphanedEffects.length) return;
await actor.deleteEmbeddedDocuments(
"ActiveEffect",
orphanedEffects.map(effect => effect.id)
);
} }
function getAdaptiveChanges(config, damageType) { function getAdaptiveChanges(config, damageType) {
@ -41,29 +86,51 @@ function getAdaptiveChanges(config, damageType) {
return changes; return changes;
} }
export async function createAdaptiveEffect(actor, damageType, adaptationType = ADAPTATION_TYPES.RESISTANCE) { /**
const config = ADAPTATION_CONFIG[adaptationType] ?? ADAPTATION_CONFIG[ADAPTATION_TYPES.RESISTANCE]; * Erzeugt die konkrete adaptive Resistenz oder Immunität auf dem Actor.
*/
export async function createAdaptiveEffect(
actor,
damageType,
adaptationType = ADAPTATION_TYPES.RESISTANCE,
sourceEffectUuid = null
) {
const config = ADAPTATION_CONFIG[adaptationType]
?? ADAPTATION_CONFIG[ADAPTATION_TYPES.RESISTANCE];
const label = getDamageTypeLabel(damageType); const label = getDamageTypeLabel(damageType);
await actor.createEmbeddedDocuments("ActiveEffect", [ await actor.createEmbeddedDocuments("ActiveEffect", [
{ {
name: game.i18n.format(config.effectNameKey, { type: label }), name: game.i18n.format(config.effectNameKey, { type: label }),
icon: config.icon, img: config.icon,
disabled: false, disabled: false,
transfer: false, transfer: false,
flags: { flags: {
[MODULE_ID]: { [MODULE_ID]: {
[EFFECT_FLAG]: { [EFFECT_FLAG]: {
adaptationType: config.id, adaptationType: config.id,
damageType damageType,
sourceEffectUuid
} }
} }
}, },
changes: getAdaptiveChanges(config, damageType) changes: getAdaptiveChanges(config, damageType)
} }
]); ]);
} }
export async function createAdaptiveResistanceEffect(actor, damageType) { export async function createAdaptiveResistanceEffect(
return createAdaptiveEffect(actor, damageType, ADAPTATION_TYPES.RESISTANCE); actor,
} damageType,
sourceEffectUuid = null
) {
return createAdaptiveEffect(
actor,
damageType,
ADAPTATION_TYPES.RESISTANCE,
sourceEffectUuid
);
}

View File

@ -2,12 +2,12 @@ import {
MODULE_ID, MODULE_ID,
PACK_ID, PACK_ID,
FEATURE_FLAG, FEATURE_FLAG,
TRIGGER_FLAG,
DAMAGE_SETS, DAMAGE_SETS,
ADAPTATION_CONFIG, ADAPTATION_CONFIG,
ADAPTATION_TYPES ADAPTATION_TYPES
} from "./constants.js"; } from "./constants.js";
const ENGLISH_DAMAGE_TYPE_LABELS = Object.freeze({ const ENGLISH_DAMAGE_TYPE_LABELS = Object.freeze({
acid: "Acid", acid: "Acid",
bludgeoning: "Bludgeoning", bludgeoning: "Bludgeoning",
@ -29,70 +29,87 @@ const ENGLISH_SET_TEXT = Object.freeze({
name: "Elemental", name: "Elemental",
description: "The creature adapts to elemental force: caustic acid, biting cold, searing fire, crackling lightning, and concussive thunder." description: "The creature adapts to elemental force: caustic acid, biting cold, searing fire, crackling lightning, and concussive thunder."
}, },
magical: { magical: {
name: "Magical", name: "Magical",
description: "The creature adapts to supernatural harm born of raw force, necrotic decay, psychic pressure, or radiant power." description: "The creature adapts to supernatural harm born of raw force, necrotic decay, psychic pressure, or radiant power."
}, },
profane: { profane: {
name: "Profane", name: "Profane",
description: "The creature adapts to bodily violence from bludgeoning, piercing, and slashing harm. This follows the dnd5e damage types and does not reliably distinguish magical from nonmagical weapons." description: "The creature adapts to bodily violence from bludgeoning, piercing, and slashing harm. This follows the dnd5e damage types and does not reliably distinguish magical from nonmagical weapons."
}, },
nonProfane: { nonProfane: {
name: "Non-Profane", name: "Non-Profane",
description: "The creature adapts to damage that is not simple bodily violence: acid, cold, fire, force, lightning, necrotic, poison, psychic, radiant, and thunder." description: "The creature adapts to damage that is not simple bodily violence: acid, cold, fire, force, lightning, necrotic, poison, psychic, radiant, and thunder."
}, },
all: { all: {
name: "All Damage", name: "All Damage",
description: "The creature adapts to any registered dnd5e damage type as long as the damage truly pierces its defenses." description: "The creature adapts to any registered dnd5e damage type as long as the damage truly pierces its defenses."
}, },
acid: { acid: {
name: "Acid", name: "Acid",
description: "The creature adapts to acid damage once it truly pierces its defenses." description: "The creature adapts to acid damage once it truly pierces its defenses."
}, },
bludgeoning: { bludgeoning: {
name: "Bludgeoning", name: "Bludgeoning",
description: "The creature adapts to bludgeoning damage once it truly pierces its defenses." description: "The creature adapts to bludgeoning damage once it truly pierces its defenses."
}, },
cold: { cold: {
name: "Cold", name: "Cold",
description: "The creature adapts to cold damage once it truly pierces its defenses." description: "The creature adapts to cold damage once it truly pierces its defenses."
}, },
fire: { fire: {
name: "Fire", name: "Fire",
description: "The creature adapts to fire damage once it truly pierces its defenses." description: "The creature adapts to fire damage once it truly pierces its defenses."
}, },
force: { force: {
name: "Force", name: "Force",
description: "The creature adapts to force damage once it truly pierces its defenses." description: "The creature adapts to force damage once it truly pierces its defenses."
}, },
lightning: { lightning: {
name: "Lightning", name: "Lightning",
description: "The creature adapts to lightning damage once it truly pierces its defenses." description: "The creature adapts to lightning damage once it truly pierces its defenses."
}, },
necrotic: { necrotic: {
name: "Necrotic", name: "Necrotic",
description: "The creature adapts to necrotic damage once it truly pierces its defenses." description: "The creature adapts to necrotic damage once it truly pierces its defenses."
}, },
piercing: { piercing: {
name: "Piercing", name: "Piercing",
description: "The creature adapts to piercing damage once it truly pierces its defenses." description: "The creature adapts to piercing damage once it truly pierces its defenses."
}, },
poison: { poison: {
name: "Poison", name: "Poison",
description: "The creature adapts to poison damage once it truly pierces its defenses." description: "The creature adapts to poison damage once it truly pierces its defenses."
}, },
psychic: { psychic: {
name: "Psychic", name: "Psychic",
description: "The creature adapts to psychic damage once it truly pierces its defenses." description: "The creature adapts to psychic damage once it truly pierces its defenses."
}, },
radiant: { radiant: {
name: "Radiant", name: "Radiant",
description: "The creature adapts to radiant damage once it truly pierces its defenses." description: "The creature adapts to radiant damage once it truly pierces its defenses."
}, },
slashing: { slashing: {
name: "Slashing", name: "Slashing",
description: "The creature adapts to slashing damage once it truly pierces its defenses." description: "The creature adapts to slashing damage once it truly pierces its defenses."
}, },
thunder: { thunder: {
name: "Thunder", name: "Thunder",
description: "The creature adapts to thunder damage once it truly pierces its defenses." description: "The creature adapts to thunder damage once it truly pierces its defenses."
@ -104,6 +121,7 @@ const ENGLISH_ADAPTATION_TEXT = Object.freeze({
prefix: "Adaptive Resistance", prefix: "Adaptive Resistance",
rules: "After a hit, the creature remembers the damage type if the damage truly pierced its defenses. It then gains limited resistance to that exact damage type. If another matching damage type harms it later, the new adaptation replaces the old one. If resistance or immunity already softened or stopped the damage, the adaptation does not change." rules: "After a hit, the creature remembers the damage type if the damage truly pierced its defenses. It then gains limited resistance to that exact damage type. If another matching damage type harms it later, the new adaptation replaces the old one. If resistance or immunity already softened or stopped the damage, the adaptation does not change."
}, },
[ADAPTATION_TYPES.IMMUNITY]: { [ADAPTATION_TYPES.IMMUNITY]: {
prefix: "Adaptive Immunity", prefix: "Adaptive Immunity",
rules: "After a hit, the creature remembers the damage type if the damage truly pierced its defenses. It then gains limited immunity to that exact damage type. If another matching damage type harms it later, the new adaptation replaces the old one. If resistance or immunity already softened or stopped the damage, the adaptation does not change." rules: "After a hit, the creature remembers the damage type if the damage truly pierced its defenses. It then gains limited immunity to that exact damage type. If another matching damage type harms it later, the new adaptation replaces the old one. If resistance or immunity already softened or stopped the damage, the adaptation does not change."
@ -115,68 +133,124 @@ function getEnglishDamageTypeLabel(type) {
} }
function featureName(set, adaptationType) { function featureName(set, adaptationType) {
const text = ENGLISH_ADAPTATION_TEXT[adaptationType] ?? ENGLISH_ADAPTATION_TEXT[ADAPTATION_TYPES.RESISTANCE]; const text = ENGLISH_ADAPTATION_TEXT[adaptationType]
?? ENGLISH_ADAPTATION_TEXT[ADAPTATION_TYPES.RESISTANCE];
const setText = ENGLISH_SET_TEXT[set.id]; const setText = ENGLISH_SET_TEXT[set.id];
return `${text.prefix}: ${setText?.name ?? set.id}`; return `${text.prefix}: ${setText?.name ?? set.id}`;
} }
function featureDescription(set, adaptationType) { function featureDescription(set, adaptationType) {
const text = ENGLISH_ADAPTATION_TEXT[adaptationType] ?? ENGLISH_ADAPTATION_TEXT[ADAPTATION_TYPES.RESISTANCE]; const text = ENGLISH_ADAPTATION_TEXT[adaptationType]
?? ENGLISH_ADAPTATION_TEXT[ADAPTATION_TYPES.RESISTANCE];
const setText = ENGLISH_SET_TEXT[set.id]; const setText = ENGLISH_SET_TEXT[set.id];
const damageTypes = set.damageTypes.map(getEnglishDamageTypeLabel).join(", "); const damageTypes = set.damageTypes.map(getEnglishDamageTypeLabel).join(", ");
return ` return `<p>${setText?.description ?? "The creature adapts to the selected damage type once it truly pierces its defenses."}</p>
<p>${setText?.description ?? "The creature adapts to the selected damage type once it truly pierces its defenses."}</p> <p><strong>Reacts to:</strong> ${damageTypes}</p>
<p><strong>Reacts to:</strong> ${damageTypes}</p> <p>${text.rules}</p>`;
<p>${text.rules}</p>
`;
} }
function featureSourceId(setId, adaptationType) { function featureSourceId(setId, adaptationType) {
return `${MODULE_ID}.${adaptationType}.${setId}`; return `${MODULE_ID}.${adaptationType}.${setId}`;
} }
export function buildFeatureItemData(set, adaptationType = ADAPTATION_TYPES.RESISTANCE) { /**
const config = ADAPTATION_CONFIG[adaptationType] ?? ADAPTATION_CONFIG[ADAPTATION_TYPES.RESISTANCE]; * Erstellt den passiven Marker-Effekt, nach dem die Laufzeitlogik sucht.
*
* Der Effect verändert selbst keine Werte. Er markiert nur eine aktuell
* wirksame Quelle für adaptive Resistenz oder Immunität.
*
* Da transfer=true gesetzt ist, kann derselbe Effect auch später an
* Ausrüstung oder attuned Items verwendet werden.
*/
export function buildTriggerEffectData(
set,
adaptationType = ADAPTATION_TYPES.RESISTANCE
) {
const config = ADAPTATION_CONFIG[adaptationType]
?? ADAPTATION_CONFIG[ADAPTATION_TYPES.RESISTANCE];
return {
name: featureName(set, config.id),
img: config.icon,
disabled: false,
transfer: true,
changes: [],
flags: {
[MODULE_ID]: {
[TRIGGER_FLAG]: {
enabled: true,
adaptationType: config.id,
setId: set.id,
damageTypes: [...set.damageTypes]
}
}
}
};
}
/**
* Erstellt ein neues Kompendium-Feature einschließlich seines Marker-Effects.
*/
export function buildFeatureItemData(
set,
adaptationType = ADAPTATION_TYPES.RESISTANCE
) {
const config = ADAPTATION_CONFIG[adaptationType]
?? ADAPTATION_CONFIG[ADAPTATION_TYPES.RESISTANCE];
return { return {
name: featureName(set, config.id), name: featureName(set, config.id),
type: "feat", type: "feat",
img: config.icon, img: config.icon,
system: { system: {
description: { description: {
value: featureDescription(set, config.id), value: featureDescription(set, config.id),
chat: "" chat: ""
}, },
source: { source: {
book: "Adaptive Damage Resistance", book: "Adaptive Damage Resistance",
page: "", page: "",
custom: "" custom: ""
}, },
activation: { activation: {
type: "special", type: "special",
cost: null, cost: null,
condition: "" condition: ""
}, },
uses: { uses: {
spent: 0, spent: 0,
max: "", max: "",
recovery: [] recovery: []
}, },
type: { type: {
value: "monster", value: "monster",
subtype: "" subtype: ""
} }
}, },
effects: [
buildTriggerEffectData(set, config.id)
],
flags: { flags: {
[MODULE_ID]: { [MODULE_ID]: {
[FEATURE_FLAG]: { [FEATURE_FLAG]: {
enabled: true, enabled: true,
adaptationType: config.id, adaptationType: config.id,
setId: set.id, setId: set.id,
damageTypes: set.damageTypes damageTypes: [...set.damageTypes]
} }
}, },
core: { core: {
sourceId: `Item.${featureSourceId(set.id, config.id)}` sourceId: `Item.${featureSourceId(set.id, config.id)}`
} }
@ -184,48 +258,84 @@ export function buildFeatureItemData(set, adaptationType = ADAPTATION_TYPES.RESI
}; };
} }
function getItemFeatureFlag(item) {
return item?.getFlag?.(MODULE_ID, FEATURE_FLAG)
?? foundry.utils.getProperty(item, `flags.${MODULE_ID}.${FEATURE_FLAG}`);
}
function getTriggerFlag(effect) {
return effect?.getFlag?.(MODULE_ID, TRIGGER_FLAG)
?? foundry.utils.getProperty(effect, `flags.${MODULE_ID}.${TRIGGER_FLAG}`);
}
export function getFeatureKey(entry) {
const flag = getItemFeatureFlag(entry);
if (!flag?.setId) return null;
return `${flag.adaptationType ?? ADAPTATION_TYPES.RESISTANCE}.${flag.setId}`;
}
/**
* Ergänzt bei einem bereits existierenden Feature-Item den neuen Marker-Effect,
* falls das Item aus Version 1.0.0 stammt und diesen noch nicht besitzt.
*
* Name, Beschreibung und sonstige manuelle Änderungen des Items bleiben erhalten.
*/
export async function ensureTriggerEffectOnFeatureItem(item) {
const feature = getItemFeatureFlag(item);
const adaptationType = feature?.adaptationType ?? ADAPTATION_TYPES.RESISTANCE;
const set = DAMAGE_SETS[feature?.setId];
if (!feature?.enabled || !set || !ADAPTATION_CONFIG[adaptationType]) {
return false;
}
const hasTrigger = Array.from(item.effects ?? []).some(effect => {
const trigger = getTriggerFlag(effect);
return trigger?.enabled === true
&& trigger.setId === set.id
&& (trigger.adaptationType ?? ADAPTATION_TYPES.RESISTANCE) === adaptationType;
});
if (hasTrigger) return false;
await item.createEmbeddedDocuments("ActiveEffect", [
buildTriggerEffectData(set, adaptationType)
]);
return true;
}
async function getOrCreatePack() { async function getOrCreatePack() {
const pack = game.packs.get(PACK_ID); const pack = game.packs.get(PACK_ID);
if (!pack) { if (!pack) {
console.error(`${MODULE_ID} | Compendium ${PACK_ID} wurde nicht gefunden.`); console.error(`${MODULE_ID} | Compendium ${PACK_ID} was not found.`);
return null; return null;
} }
return pack; return pack;
} }
async function unlockPack(pack) { async function unlockPack(pack) {
if (!pack.locked) return false; if (!pack.locked) return false;
await pack.configure({ locked: false }); await pack.configure({ locked: false });
return true; return true;
} }
async function restorePackLock(pack, wasLocked) { async function restorePackLock(pack, wasLocked) {
if (!wasLocked) return; if (!wasLocked) return;
await pack.configure({ locked: true }); await pack.configure({ locked: true });
} }
function getFeatureKey(entry) { /**
const flag = foundry.utils.getProperty(entry, `flags.${MODULE_ID}.${FEATURE_FLAG}`); * Erzeugt fehlende Features und ergänzt bestehende v1.0.0-Features um den
if (!flag?.setId) return null; * ActiveEffect-Marker für die neue Architektur.
return `${flag.adaptationType ?? ADAPTATION_TYPES.RESISTANCE}.${flag.setId}`; */
}
async function deleteDuplicatePackItems(pack, featureKey, keepId) {
const index = await pack.getIndex({
fields: [
`flags.${MODULE_ID}.${FEATURE_FLAG}.setId`,
`flags.${MODULE_ID}.${FEATURE_FLAG}.adaptationType`
]
});
const duplicates = index
.filter(entry => entry._id !== keepId)
.filter(entry => getFeatureKey(entry) === featureKey)
.map(entry => entry._id);
if (duplicates.length) await Item.deleteDocuments(duplicates, { pack: PACK_ID });
}
export async function seedFeatureCompendium() { export async function seedFeatureCompendium() {
if (!game.user?.isGM) return; if (!game.user?.isGM) return;
@ -235,32 +345,44 @@ export async function seedFeatureCompendium() {
const wasLocked = await unlockPack(pack); const wasLocked = await unlockPack(pack);
try { try {
const index = await pack.getIndex({
fields: [
`flags.${MODULE_ID}.${FEATURE_FLAG}.setId`,
`flags.${MODULE_ID}.${FEATURE_FLAG}.adaptationType`
]
});
for (const adaptationType of Object.values(ADAPTATION_TYPES)) { for (const adaptationType of Object.values(ADAPTATION_TYPES)) {
for (const set of Object.values(DAMAGE_SETS)) { for (const set of Object.values(DAMAGE_SETS)) {
const itemData = buildFeatureItemData(set, adaptationType);
const featureKey = `${adaptationType}.${set.id}`; const featureKey = `${adaptationType}.${set.id}`;
const index = await pack.getIndex({
fields: [
`flags.${MODULE_ID}.${FEATURE_FLAG}.setId`,
`flags.${MODULE_ID}.${FEATURE_FLAG}.adaptationType`
]
});
const existing = index.find(entry => getFeatureKey(entry) === featureKey);
if (existing) { const existingEntry = index.find(
// Existing feature items are intentionally left untouched. entry => getFeatureKey(entry) === featureKey
// This preserves manual edits and prevents GM client language changes );
// from rewriting already seeded compendium entries.
if (existingEntry) {
const existingItem = await pack.getDocument(existingEntry._id);
await ensureTriggerEffectOnFeatureItem(existingItem);
continue; continue;
} }
await Item.create(itemData, { pack: PACK_ID }); await Item.create(
buildFeatureItemData(set, adaptationType),
{ pack: PACK_ID }
);
} }
} }
} catch (error) { } catch (error) {
console.error(`${MODULE_ID} | Fehler beim Befüllen des Feature-Kompendiums.`, error); console.error(
ui.notifications?.error(game.i18n.localize("ADR.Notifications.SeedFailed")); `${MODULE_ID} | Failed to seed or upgrade the feature compendium.`,
error
);
ui.notifications?.error(
game.i18n.localize("ADR.Notifications.SeedFailed")
);
} finally { } finally {
await restorePackLock(pack, wasLocked); await restorePackLock(pack, wasLocked);
} }
} }

View File

@ -1,70 +1,173 @@
import { MODULE_ID } from "./constants.js"; import {
MODULE_ID
} from "./constants.js";
import { import {
actorAlreadyProtected, actorAlreadyProtected,
actorVulnerableTo, actorVulnerableTo,
getAdaptiveFeatureItems, getAdaptiveTriggerEffects,
getCandidatesForActor, getCandidatesForActor,
getDominantDamageCandidate, getDominantDamageCandidate,
isActiveGM isActiveGM,
isAdaptiveTriggerApplied
} from "./utils.js"; } from "./utils.js";
import { createAdaptiveEffect, removeOldAdaptiveEffects } from "./effects.js";
function hasAdaptiveFeature(actor) { import {
return getAdaptiveFeatureItems(actor).length > 0; createAdaptiveEffect,
} removeOldAdaptiveEffects,
removeOrphanedAdaptiveEffects
} from "./effects.js";
function storeDamageSelection(options, data) { function storeDamageSelection(options, data) {
options[MODULE_ID] = data; options[MODULE_ID] = data;
} }
function actorFromParentDocument(document) {
if (!document) return null;
if (document.documentName === "Actor") {
return document;
}
if (
document.documentName === "Item"
&& document.parent?.documentName === "Actor"
) {
return document.parent;
}
return null;
}
function queueOrphanCleanup(actor) {
if (!actor || !isActiveGM()) return;
queueMicrotask(() => removeOrphanedAdaptiveEffects(actor));
}
/**
* Vor der Schadensberechnung wird ausschließlich ermittelt,
* welcher adaptive Schutz nach dem Treffer entstehen dürfte.
*
* Der neue Schutz wird hier bewusst noch nicht gesetzt, damit er den
* auslösenden Treffer nicht bereits selbst reduziert.
*/
Hooks.on("dnd5e.preCalculateDamage", (actor, damages, options = {}) => { Hooks.on("dnd5e.preCalculateDamage", (actor, damages, options = {}) => {
if (!isActiveGM()) return; if (!isActiveGM()) return;
if (!hasAdaptiveFeature(actor)) return; if (!getAdaptiveTriggerEffects(actor).length) return;
const candidates = getCandidatesForActor(actor, damages); const candidates = getCandidatesForActor(actor, damages);
if (!candidates.length) return; if (!candidates.length) return;
const alreadyProtected = candidates.some(candidate => actorAlreadyProtected(actor, candidate.type)); const alreadyProtected = candidates.some(
candidate => actorAlreadyProtected(actor, candidate.type)
);
if (alreadyProtected) { if (alreadyProtected) {
storeDamageSelection(options, { storeDamageSelection(options, {
skip: true, skip: true,
reason: "damage-already-reduced-or-prevented" reason: "damage-already-reduced-or-prevented"
}); });
return; return;
} }
const vulnerable = candidates.some(candidate => actorVulnerableTo(actor, candidate.type)); const vulnerable = candidates.some(
candidate => actorVulnerableTo(actor, candidate.type)
);
if (vulnerable) { if (vulnerable) {
storeDamageSelection(options, { storeDamageSelection(options, {
skip: true, skip: true,
reason: "damage-has-vulnerability" reason: "damage-has-vulnerability"
}); });
return; return;
} }
const candidate = getDominantDamageCandidate(candidates); const candidate = getDominantDamageCandidate(candidates);
if (!candidate?.type || !candidate?.adaptationType) return;
if (
!candidate?.type
|| !candidate?.adaptationType
|| !candidate?.sourceEffectUuid
) {
return;
}
storeDamageSelection(options, { storeDamageSelection(options, {
skip: false, skip: false,
damageType: candidate.type, damageType: candidate.type,
adaptationType: candidate.adaptationType adaptationType: candidate.adaptationType,
sourceEffectUuid: candidate.sourceEffectUuid
}); });
}); });
/**
* Erst nachdem dnd5e tatsächlichen Schaden angewendet hat, wird die neue
* Resistenz oder Immunität erzeugt.
*/
Hooks.on("dnd5e.applyDamage", async (actor, amount, options = {}) => { Hooks.on("dnd5e.applyDamage", async (actor, amount, options = {}) => {
if (!isActiveGM()) return; if (!isActiveGM()) return;
if (!hasAdaptiveFeature(actor)) return;
const data = options[MODULE_ID]; const data = options[MODULE_ID];
if (!data || data.skip) return;
if (!data.damageType || !data.adaptationType) return;
// Der Effekt soll nur entstehen, wenn nach Berechnung wirklich Schaden am Actor ankommt. if (!data || data.skip) return;
if (typeof amount !== "number" || amount <= 0) return;
if (
!data.damageType
|| !data.adaptationType
|| !data.sourceEffectUuid
) {
return;
}
// Kein tatsächlich angewendeter Schaden: keine Anpassung.
if (typeof amount !== "number" || amount <= 0) {
return;
}
// Die Quelle muss beim Anwenden des Schadens noch aktiv sein.
if (!isAdaptiveTriggerApplied(actor, data.sourceEffectUuid)) {
return;
}
await removeOldAdaptiveEffects(actor); await removeOldAdaptiveEffects(actor);
await createAdaptiveEffect(actor, data.damageType, data.adaptationType);
console.debug(`${MODULE_ID} | ${actor.name} gains adaptive ${data.adaptationType} against ${data.damageType}.`); await createAdaptiveEffect(
actor,
data.damageType,
data.adaptationType,
data.sourceEffectUuid
);
console.debug(
`${MODULE_ID} | ${actor.name} gains adaptive ${data.adaptationType} against ${data.damageType}.`
);
}); });
/**
* Wird ein Gegenstand abgelegt, seine Einstimmung aufgehoben oder ein
* Marker-Effect deaktiviert beziehungsweise entfernt, muss eine daraus
* entstandene adaptive Schutzwirkung ebenfalls verschwinden.
*/
Hooks.on("updateItem", item => {
queueOrphanCleanup(actorFromParentDocument(item.parent));
});
Hooks.on("deleteItem", item => {
queueOrphanCleanup(actorFromParentDocument(item.parent));
});
Hooks.on("createActiveEffect", effect => {
queueOrphanCleanup(actorFromParentDocument(effect.parent));
});
Hooks.on("updateActiveEffect", effect => {
queueOrphanCleanup(actorFromParentDocument(effect.parent));
});
Hooks.on("deleteActiveEffect", effect => {
queueOrphanCleanup(actorFromParentDocument(effect.parent));
});

View File

@ -1,5 +1,15 @@
import { MODULE_ID } from "./constants.js"; import {
import { seedFeatureCompendium } from "./features.js"; MODULE_ID
} from "./constants.js";
import {
seedFeatureCompendium
} from "./features.js";
import {
migrateWorldActorFeatureItems
} from "./migrations.js";
import "./hooks.js"; import "./hooks.js";
Hooks.once("init", () => { Hooks.once("init", () => {
@ -8,4 +18,5 @@ Hooks.once("init", () => {
Hooks.once("ready", async () => { Hooks.once("ready", async () => {
await seedFeatureCompendium(); await seedFeatureCompendium();
}); await migrateWorldActorFeatureItems();
});

39
scripts/migrations.js Normal file
View File

@ -0,0 +1,39 @@
import {
MODULE_ID,
FEATURE_FLAG
} from "./constants.js";
import {
ensureTriggerEffectOnFeatureItem
} from "./features.js";
/**
* Ergänzt bei Feature-Items, die bereits in Version 1.0.0 auf Welt-Actoren
* gezogen wurden, den neuen transferierenden ActiveEffect-Marker.
*
* Das bisherige Item-Flag wird nicht entfernt. Es bleibt als Identifikator
* für Migrationen und Kompendium-Abgleich erhalten.
*/
export async function migrateWorldActorFeatureItems() {
if (!game.user?.isGM) return;
let upgraded = 0;
for (const actor of game.actors ?? []) {
for (const item of actor.items ?? []) {
const legacyFeature = item.getFlag(MODULE_ID, FEATURE_FLAG);
if (!legacyFeature?.enabled) continue;
if (await ensureTriggerEffectOnFeatureItem(item)) {
upgraded += 1;
}
}
}
if (upgraded > 0) {
console.info(
`${MODULE_ID} | Added ActiveEffect triggers to ${upgraded} existing actor feature item(s).`
);
}
}

View File

@ -1,6 +1,6 @@
import { import {
MODULE_ID, MODULE_ID,
FEATURE_FLAG, TRIGGER_FLAG,
DAMAGE_SETS, DAMAGE_SETS,
ADAPTATION_TYPES, ADAPTATION_TYPES,
ADAPTATION_CONFIG ADAPTATION_CONFIG
@ -16,8 +16,13 @@ export function localize(key) {
export function getDamageTypeLabel(type) { export function getDamageTypeLabel(type) {
const configured = CONFIG.DND5E?.damageTypes?.[type]; const configured = CONFIG.DND5E?.damageTypes?.[type];
if (!configured) return type; if (!configured) return type;
if (typeof configured === "string") return game.i18n.localize(configured);
if (typeof configured === "string") {
return game.i18n.localize(configured);
}
return game.i18n.localize(configured.label ?? type); return game.i18n.localize(configured.label ?? type);
} }
@ -35,22 +40,40 @@ export function getDamageValue(damage) {
]; ];
for (const value of candidates) { for (const value of candidates) {
if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
} }
return 0; return 0;
} }
export function getDamageType(damage) { export function getDamageType(damage) {
return damage?.type ?? damage?.damageType ?? damage?.application?.type ?? null; return damage?.type
?? damage?.damageType
?? damage?.application?.type
?? null;
} }
export function toArrayValue(value) { export function toArrayValue(value) {
if (!value) return []; if (!value) return [];
if (Array.isArray(value)) return value;
if (value instanceof Set) return Array.from(value); if (Array.isArray(value)) {
if (typeof value === "object") return Object.keys(value).filter(key => value[key]); return value;
if (typeof value === "string") return [value]; }
if (value instanceof Set) {
return Array.from(value);
}
if (typeof value === "object") {
return Object.keys(value).filter(key => value[key]);
}
if (typeof value === "string") {
return [value];
}
return []; return [];
} }
@ -79,32 +102,63 @@ export function actorAlreadyImmune(actor, damageType) {
} }
export function actorAlreadyProtected(actor, damageType) { export function actorAlreadyProtected(actor, damageType) {
return actorAlreadyResists(actor, damageType) || actorAlreadyImmune(actor, damageType); return actorAlreadyResists(actor, damageType)
|| actorAlreadyImmune(actor, damageType);
} }
export function getAdaptiveFeatureItems(actor) { /**
if (!actor?.items) return []; * Liefert alle aktuell auf den Actor angewendeten Marker-Effects.
*
* Hierdurch funktionieren sowohl:
* - Feature-Items auf dem Actor,
* - transferierende Effekte von Ausrüstung,
* - transferierende Effekte von attuned Items,
* - direkt auf den Actor gesetzte Marker-Effects.
*/
export function getAdaptiveTriggerEffects(actor) {
if (!actor) return [];
return actor.items.filter(item => { return Array.from(actor.appliedEffects ?? []).filter(effect => {
const flag = item.getFlag(MODULE_ID, FEATURE_FLAG); const trigger = effect.getFlag(MODULE_ID, TRIGGER_FLAG);
const adaptationType = flag?.adaptationType ?? ADAPTATION_TYPES.RESISTANCE; const adaptationType =
return flag?.enabled === true && DAMAGE_SETS[flag?.setId] && ADAPTATION_CONFIG[adaptationType]; trigger?.adaptationType ?? ADAPTATION_TYPES.RESISTANCE;
return trigger?.enabled === true
&& DAMAGE_SETS[trigger?.setId]
&& ADAPTATION_CONFIG[adaptationType];
}); });
} }
/**
* Prüft, ob eine konkrete adaptive Quelle weiterhin auf dem Actor angewendet wird.
*/
export function isAdaptiveTriggerApplied(actor, effectUuid) {
if (!effectUuid) return false;
return getAdaptiveTriggerEffects(actor).some(
effect => effect.uuid === effectUuid
);
}
/**
* Bereitet aus den aktuell angewendeten Marker-Effects alle erlaubten
* Schadenstyp-/Adaptationstyp-Kombinationen auf.
*/
export function getAllowedAdaptationsForActor(actor) { export function getAllowedAdaptationsForActor(actor) {
const result = []; const result = [];
for (const item of getAdaptiveFeatureItems(actor)) { for (const effect of getAdaptiveTriggerEffects(actor)) {
const flag = item.getFlag(MODULE_ID, FEATURE_FLAG); const trigger = effect.getFlag(MODULE_ID, TRIGGER_FLAG);
const set = DAMAGE_SETS[flag.setId]; const set = DAMAGE_SETS[trigger.setId];
const adaptationType = flag.adaptationType ?? ADAPTATION_TYPES.RESISTANCE; const adaptationType =
trigger.adaptationType ?? ADAPTATION_TYPES.RESISTANCE;
for (const damageType of set.damageTypes) { for (const damageType of set.damageTypes) {
result.push({ result.push({
damageType, damageType,
adaptationType, adaptationType,
priority: ADAPTATION_CONFIG[adaptationType].priority priority: ADAPTATION_CONFIG[adaptationType].priority,
sourceEffectUuid: effect.uuid
}); });
} }
} }
@ -112,38 +166,49 @@ export function getAllowedAdaptationsForActor(actor) {
return result; return result;
} }
/**
* Ermittelt die tatsächlich für den Treffer in Frage kommenden
* adaptiven Schadensreaktionen.
*/
export function getCandidatesForActor(actor, damages) { export function getCandidatesForActor(actor, damages) {
const allowed = getAllowedAdaptationsForActor(actor); const allowed = getAllowedAdaptationsForActor(actor);
if (!allowed.length || !Array.isArray(damages)) return [];
if (!allowed.length || !Array.isArray(damages)) {
return [];
}
return damages return damages
.map(damage => ({ .map(damage => ({
type: getDamageType(damage), type: getDamageType(damage),
value: getDamageValue(damage) value: getDamageValue(damage)
})) }))
.filter(damage => { .filter(damage => damage.type && damage.value > 0)
if (!damage.type) return false; .flatMap(damage => allowed
if (damage.value <= 0) return false; .filter(entry => entry.damageType === damage.type)
return true; .map(entry => ({
}) type: damage.type,
.flatMap(damage => { value: damage.value,
return allowed adaptationType: entry.adaptationType,
.filter(entry => entry.damageType === damage.type) priority: entry.priority,
.map(entry => ({ sourceEffectUuid: entry.sourceEffectUuid
type: damage.type, }))
value: damage.value, );
adaptationType: entry.adaptationType,
priority: entry.priority
}));
});
} }
/**
* Bei mehreren passenden Schadenstypen gewinnt zunächst der höchste Schaden.
* Bei Gleichstand gewinnt Immunität vor Resistenz über deren höhere Priorität.
*/
export function getDominantDamageCandidate(candidates) { export function getDominantDamageCandidate(candidates) {
if (!candidates?.length) return null; if (!candidates?.length) return null;
return [...candidates].sort((a, b) => { return [...candidates].sort((a, b) => {
const valueDifference = b.value - a.value; const valueDifference = b.value - a.value;
if (valueDifference !== 0) return valueDifference;
if (valueDifference !== 0) {
return valueDifference;
}
return b.priority - a.priority; return b.priority - a.priority;
})[0]; })[0];
} }
@ -155,4 +220,4 @@ export function getDominantDamageType(candidates) {
// Backwards-compatible alias used by older local patches of this module. // Backwards-compatible alias used by older local patches of this module.
export function getElementalCandidatesForActor(actor, damages) { export function getElementalCandidatesForActor(actor, damages) {
return getCandidatesForActor(actor, damages); return getCandidatesForActor(actor, damages);
} }