mirror of
https://github.com/fzumpe/foundry-dnd5e-adaptive-resistances.git
synced 2026-06-06 21:10:02 +02:00
migrate trigger flag from monster features to Active Effect for use on items
This commit is contained in:
parent
3c68bf2216
commit
fd2947e0cd
@ -2,7 +2,7 @@
|
||||
"id": "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.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "14"
|
||||
@ -54,6 +54,6 @@
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"download": "https://github.com/fzumpe/foundry-dnd5e-adaptive-resistances/archive/refs/tags/V1.0.0.zip"
|
||||
}
|
||||
"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.1.0.zip"
|
||||
}
|
||||
@ -1,11 +1,40 @@
|
||||
export const MODULE_ID = "adaptive-damage-resistance";
|
||||
export const PACK_NAME = "adaptive-features";
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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 PROFANE_DAMAGE_TYPES = Object.freeze(["bludgeoning", "piercing", "slashing"]);
|
||||
export const PROFANE_BYPASSES = Object.freeze(["ada", "mgc", "sil"]);
|
||||
export const PROFANE_DAMAGE_TYPES = Object.freeze([
|
||||
"bludgeoning",
|
||||
"piercing",
|
||||
"slashing"
|
||||
]);
|
||||
|
||||
export const PROFANE_BYPASSES = Object.freeze([
|
||||
"ada",
|
||||
"mgc",
|
||||
"sil"
|
||||
]);
|
||||
|
||||
export const ADAPTATION_TYPES = Object.freeze({
|
||||
RESISTANCE: "resistance",
|
||||
@ -22,6 +51,7 @@ export const ADAPTATION_CONFIG = Object.freeze({
|
||||
itemNamePrefixKey: "ADR.Features.Prefix.Resistance",
|
||||
icon: "icons/magic/defensive/shield-barrier-flaming-diamond-orange.webp"
|
||||
},
|
||||
|
||||
[ADAPTATION_TYPES.IMMUNITY]: {
|
||||
id: ADAPTATION_TYPES.IMMUNITY,
|
||||
priority: 20,
|
||||
@ -54,104 +84,161 @@ export const DAMAGE_SETS = Object.freeze({
|
||||
id: "elemental",
|
||||
nameKey: "ADR.Features.Set.Elemental.Name",
|
||||
descriptionKey: "ADR.Features.Set.Elemental.Description",
|
||||
damageTypes: ["acid", "cold", "fire", "lightning", "thunder"]
|
||||
damageTypes: [
|
||||
"acid",
|
||||
"cold",
|
||||
"fire",
|
||||
"lightning",
|
||||
"thunder"
|
||||
]
|
||||
},
|
||||
|
||||
magical: {
|
||||
id: "magical",
|
||||
nameKey: "ADR.Features.Set.Magical.Name",
|
||||
descriptionKey: "ADR.Features.Set.Magical.Description",
|
||||
damageTypes: ["force", "necrotic", "psychic", "radiant"]
|
||||
damageTypes: [
|
||||
"force",
|
||||
"necrotic",
|
||||
"psychic",
|
||||
"radiant"
|
||||
]
|
||||
},
|
||||
|
||||
profane: {
|
||||
id: "profane",
|
||||
nameKey: "ADR.Features.Set.Profane.Name",
|
||||
descriptionKey: "ADR.Features.Set.Profane.Description",
|
||||
damageTypes: ["bludgeoning", "piercing", "slashing"]
|
||||
damageTypes: [
|
||||
"bludgeoning",
|
||||
"piercing",
|
||||
"slashing"
|
||||
]
|
||||
},
|
||||
|
||||
nonProfane: {
|
||||
id: "nonProfane",
|
||||
nameKey: "ADR.Features.Set.NonProfane.Name",
|
||||
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: {
|
||||
id: "all",
|
||||
nameKey: "ADR.Features.Set.All.Name",
|
||||
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: {
|
||||
id: "acid",
|
||||
nameKey: "ADR.Features.Set.Single.acid.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.acid.Description",
|
||||
damageTypes: ["acid"]
|
||||
},
|
||||
|
||||
bludgeoning: {
|
||||
id: "bludgeoning",
|
||||
nameKey: "ADR.Features.Set.Single.bludgeoning.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.bludgeoning.Description",
|
||||
damageTypes: ["bludgeoning"]
|
||||
},
|
||||
|
||||
cold: {
|
||||
id: "cold",
|
||||
nameKey: "ADR.Features.Set.Single.cold.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.cold.Description",
|
||||
damageTypes: ["cold"]
|
||||
},
|
||||
|
||||
fire: {
|
||||
id: "fire",
|
||||
nameKey: "ADR.Features.Set.Single.fire.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.fire.Description",
|
||||
damageTypes: ["fire"]
|
||||
},
|
||||
|
||||
force: {
|
||||
id: "force",
|
||||
nameKey: "ADR.Features.Set.Single.force.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.force.Description",
|
||||
damageTypes: ["force"]
|
||||
},
|
||||
|
||||
lightning: {
|
||||
id: "lightning",
|
||||
nameKey: "ADR.Features.Set.Single.lightning.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.lightning.Description",
|
||||
damageTypes: ["lightning"]
|
||||
},
|
||||
|
||||
necrotic: {
|
||||
id: "necrotic",
|
||||
nameKey: "ADR.Features.Set.Single.necrotic.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.necrotic.Description",
|
||||
damageTypes: ["necrotic"]
|
||||
},
|
||||
|
||||
piercing: {
|
||||
id: "piercing",
|
||||
nameKey: "ADR.Features.Set.Single.piercing.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.piercing.Description",
|
||||
damageTypes: ["piercing"]
|
||||
},
|
||||
|
||||
poison: {
|
||||
id: "poison",
|
||||
nameKey: "ADR.Features.Set.Single.poison.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.poison.Description",
|
||||
damageTypes: ["poison"]
|
||||
},
|
||||
|
||||
psychic: {
|
||||
id: "psychic",
|
||||
nameKey: "ADR.Features.Set.Single.psychic.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.psychic.Description",
|
||||
damageTypes: ["psychic"]
|
||||
},
|
||||
|
||||
radiant: {
|
||||
id: "radiant",
|
||||
nameKey: "ADR.Features.Set.Single.radiant.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.radiant.Description",
|
||||
damageTypes: ["radiant"]
|
||||
},
|
||||
|
||||
slashing: {
|
||||
id: "slashing",
|
||||
nameKey: "ADR.Features.Set.Single.slashing.Name",
|
||||
descriptionKey: "ADR.Features.Set.Single.slashing.Description",
|
||||
damageTypes: ["slashing"]
|
||||
},
|
||||
|
||||
thunder: {
|
||||
id: "thunder",
|
||||
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.
|
||||
export const RESISTANCE_SETS = DAMAGE_SETS;
|
||||
export const RESISTANCE_SETS = DAMAGE_SETS;
|
||||
87
scripts/effects.js
vendored
87
scripts/effects.js
vendored
@ -6,13 +6,58 @@ import {
|
||||
PROFANE_DAMAGE_TYPES,
|
||||
PROFANE_BYPASSES
|
||||
} 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) {
|
||||
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;
|
||||
|
||||
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) {
|
||||
@ -41,29 +86,51 @@ function getAdaptiveChanges(config, damageType) {
|
||||
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);
|
||||
|
||||
await actor.createEmbeddedDocuments("ActiveEffect", [
|
||||
{
|
||||
name: game.i18n.format(config.effectNameKey, { type: label }),
|
||||
icon: config.icon,
|
||||
img: config.icon,
|
||||
disabled: false,
|
||||
transfer: false,
|
||||
|
||||
flags: {
|
||||
[MODULE_ID]: {
|
||||
[EFFECT_FLAG]: {
|
||||
adaptationType: config.id,
|
||||
damageType
|
||||
damageType,
|
||||
sourceEffectUuid
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
changes: getAdaptiveChanges(config, damageType)
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
export async function createAdaptiveResistanceEffect(actor, damageType) {
|
||||
return createAdaptiveEffect(actor, damageType, ADAPTATION_TYPES.RESISTANCE);
|
||||
}
|
||||
export async function createAdaptiveResistanceEffect(
|
||||
actor,
|
||||
damageType,
|
||||
sourceEffectUuid = null
|
||||
) {
|
||||
return createAdaptiveEffect(
|
||||
actor,
|
||||
damageType,
|
||||
ADAPTATION_TYPES.RESISTANCE,
|
||||
sourceEffectUuid
|
||||
);
|
||||
}
|
||||
@ -2,12 +2,12 @@ import {
|
||||
MODULE_ID,
|
||||
PACK_ID,
|
||||
FEATURE_FLAG,
|
||||
TRIGGER_FLAG,
|
||||
DAMAGE_SETS,
|
||||
ADAPTATION_CONFIG,
|
||||
ADAPTATION_TYPES
|
||||
} from "./constants.js";
|
||||
|
||||
|
||||
const ENGLISH_DAMAGE_TYPE_LABELS = Object.freeze({
|
||||
acid: "Acid",
|
||||
bludgeoning: "Bludgeoning",
|
||||
@ -29,70 +29,87 @@ const ENGLISH_SET_TEXT = Object.freeze({
|
||||
name: "Elemental",
|
||||
description: "The creature adapts to elemental force: caustic acid, biting cold, searing fire, crackling lightning, and concussive thunder."
|
||||
},
|
||||
|
||||
magical: {
|
||||
name: "Magical",
|
||||
description: "The creature adapts to supernatural harm born of raw force, necrotic decay, psychic pressure, or radiant power."
|
||||
},
|
||||
|
||||
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."
|
||||
},
|
||||
|
||||
nonProfane: {
|
||||
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."
|
||||
},
|
||||
|
||||
all: {
|
||||
name: "All Damage",
|
||||
description: "The creature adapts to any registered dnd5e damage type as long as the damage truly pierces its defenses."
|
||||
},
|
||||
|
||||
acid: {
|
||||
name: "Acid",
|
||||
description: "The creature adapts to acid damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
bludgeoning: {
|
||||
name: "Bludgeoning",
|
||||
description: "The creature adapts to bludgeoning damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
cold: {
|
||||
name: "Cold",
|
||||
description: "The creature adapts to cold damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
fire: {
|
||||
name: "Fire",
|
||||
description: "The creature adapts to fire damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
force: {
|
||||
name: "Force",
|
||||
description: "The creature adapts to force damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
lightning: {
|
||||
name: "Lightning",
|
||||
description: "The creature adapts to lightning damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
necrotic: {
|
||||
name: "Necrotic",
|
||||
description: "The creature adapts to necrotic damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
piercing: {
|
||||
name: "Piercing",
|
||||
description: "The creature adapts to piercing damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
poison: {
|
||||
name: "Poison",
|
||||
description: "The creature adapts to poison damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
psychic: {
|
||||
name: "Psychic",
|
||||
description: "The creature adapts to psychic damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
radiant: {
|
||||
name: "Radiant",
|
||||
description: "The creature adapts to radiant damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
slashing: {
|
||||
name: "Slashing",
|
||||
description: "The creature adapts to slashing damage once it truly pierces its defenses."
|
||||
},
|
||||
|
||||
thunder: {
|
||||
name: "Thunder",
|
||||
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",
|
||||
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]: {
|
||||
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."
|
||||
@ -115,68 +133,124 @@ function getEnglishDamageTypeLabel(type) {
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
return `${text.prefix}: ${setText?.name ?? set.id}`;
|
||||
}
|
||||
|
||||
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 damageTypes = set.damageTypes.map(getEnglishDamageTypeLabel).join(", ");
|
||||
|
||||
return `
|
||||
<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>${text.rules}</p>
|
||||
`;
|
||||
return `<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>${text.rules}</p>`;
|
||||
}
|
||||
|
||||
function featureSourceId(setId, adaptationType) {
|
||||
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 {
|
||||
name: featureName(set, config.id),
|
||||
type: "feat",
|
||||
img: config.icon,
|
||||
|
||||
system: {
|
||||
description: {
|
||||
value: featureDescription(set, config.id),
|
||||
chat: ""
|
||||
},
|
||||
|
||||
source: {
|
||||
book: "Adaptive Damage Resistance",
|
||||
page: "",
|
||||
custom: ""
|
||||
},
|
||||
|
||||
activation: {
|
||||
type: "special",
|
||||
cost: null,
|
||||
condition: ""
|
||||
},
|
||||
|
||||
uses: {
|
||||
spent: 0,
|
||||
max: "",
|
||||
recovery: []
|
||||
},
|
||||
|
||||
type: {
|
||||
value: "monster",
|
||||
subtype: ""
|
||||
}
|
||||
},
|
||||
|
||||
effects: [
|
||||
buildTriggerEffectData(set, config.id)
|
||||
],
|
||||
|
||||
flags: {
|
||||
[MODULE_ID]: {
|
||||
[FEATURE_FLAG]: {
|
||||
enabled: true,
|
||||
adaptationType: config.id,
|
||||
setId: set.id,
|
||||
damageTypes: set.damageTypes
|
||||
damageTypes: [...set.damageTypes]
|
||||
}
|
||||
},
|
||||
|
||||
core: {
|
||||
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() {
|
||||
const pack = game.packs.get(PACK_ID);
|
||||
|
||||
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 pack;
|
||||
}
|
||||
|
||||
async function unlockPack(pack) {
|
||||
if (!pack.locked) return false;
|
||||
|
||||
await pack.configure({ locked: false });
|
||||
return true;
|
||||
}
|
||||
|
||||
async function restorePackLock(pack, wasLocked) {
|
||||
if (!wasLocked) return;
|
||||
|
||||
await pack.configure({ locked: true });
|
||||
}
|
||||
|
||||
function getFeatureKey(entry) {
|
||||
const flag = foundry.utils.getProperty(entry, `flags.${MODULE_ID}.${FEATURE_FLAG}`);
|
||||
if (!flag?.setId) return null;
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt fehlende Features und ergänzt bestehende v1.0.0-Features um den
|
||||
* ActiveEffect-Marker für die neue Architektur.
|
||||
*/
|
||||
export async function seedFeatureCompendium() {
|
||||
if (!game.user?.isGM) return;
|
||||
|
||||
@ -235,32 +345,44 @@ export async function seedFeatureCompendium() {
|
||||
const wasLocked = await unlockPack(pack);
|
||||
|
||||
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 set of Object.values(DAMAGE_SETS)) {
|
||||
const itemData = buildFeatureItemData(set, adaptationType);
|
||||
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) {
|
||||
// Existing feature items are intentionally left untouched.
|
||||
// This preserves manual edits and prevents GM client language changes
|
||||
// from rewriting already seeded compendium entries.
|
||||
const existingEntry = index.find(
|
||||
entry => getFeatureKey(entry) === featureKey
|
||||
);
|
||||
|
||||
if (existingEntry) {
|
||||
const existingItem = await pack.getDocument(existingEntry._id);
|
||||
|
||||
await ensureTriggerEffectOnFeatureItem(existingItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
await Item.create(itemData, { pack: PACK_ID });
|
||||
await Item.create(
|
||||
buildFeatureItemData(set, adaptationType),
|
||||
{ pack: PACK_ID }
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${MODULE_ID} | Fehler beim Befüllen des Feature-Kompendiums.`, error);
|
||||
ui.notifications?.error(game.i18n.localize("ADR.Notifications.SeedFailed"));
|
||||
console.error(
|
||||
`${MODULE_ID} | Failed to seed or upgrade the feature compendium.`,
|
||||
error
|
||||
);
|
||||
|
||||
ui.notifications?.error(
|
||||
game.i18n.localize("ADR.Notifications.SeedFailed")
|
||||
);
|
||||
} finally {
|
||||
await restorePackLock(pack, wasLocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
scripts/hooks.js
141
scripts/hooks.js
@ -1,70 +1,173 @@
|
||||
import { MODULE_ID } from "./constants.js";
|
||||
import {
|
||||
MODULE_ID
|
||||
} from "./constants.js";
|
||||
|
||||
import {
|
||||
actorAlreadyProtected,
|
||||
actorVulnerableTo,
|
||||
getAdaptiveFeatureItems,
|
||||
getAdaptiveTriggerEffects,
|
||||
getCandidatesForActor,
|
||||
getDominantDamageCandidate,
|
||||
isActiveGM
|
||||
isActiveGM,
|
||||
isAdaptiveTriggerApplied
|
||||
} from "./utils.js";
|
||||
import { createAdaptiveEffect, removeOldAdaptiveEffects } from "./effects.js";
|
||||
|
||||
function hasAdaptiveFeature(actor) {
|
||||
return getAdaptiveFeatureItems(actor).length > 0;
|
||||
}
|
||||
import {
|
||||
createAdaptiveEffect,
|
||||
removeOldAdaptiveEffects,
|
||||
removeOrphanedAdaptiveEffects
|
||||
} from "./effects.js";
|
||||
|
||||
function storeDamageSelection(options, 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 = {}) => {
|
||||
if (!isActiveGM()) return;
|
||||
if (!hasAdaptiveFeature(actor)) return;
|
||||
if (!getAdaptiveTriggerEffects(actor).length) return;
|
||||
|
||||
const candidates = getCandidatesForActor(actor, damages);
|
||||
|
||||
if (!candidates.length) return;
|
||||
|
||||
const alreadyProtected = candidates.some(candidate => actorAlreadyProtected(actor, candidate.type));
|
||||
const alreadyProtected = candidates.some(
|
||||
candidate => actorAlreadyProtected(actor, candidate.type)
|
||||
);
|
||||
|
||||
if (alreadyProtected) {
|
||||
storeDamageSelection(options, {
|
||||
skip: true,
|
||||
reason: "damage-already-reduced-or-prevented"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const vulnerable = candidates.some(candidate => actorVulnerableTo(actor, candidate.type));
|
||||
const vulnerable = candidates.some(
|
||||
candidate => actorVulnerableTo(actor, candidate.type)
|
||||
);
|
||||
|
||||
if (vulnerable) {
|
||||
storeDamageSelection(options, {
|
||||
skip: true,
|
||||
reason: "damage-has-vulnerability"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const candidate = getDominantDamageCandidate(candidates);
|
||||
if (!candidate?.type || !candidate?.adaptationType) return;
|
||||
|
||||
if (
|
||||
!candidate?.type
|
||||
|| !candidate?.adaptationType
|
||||
|| !candidate?.sourceEffectUuid
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
storeDamageSelection(options, {
|
||||
skip: false,
|
||||
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 = {}) => {
|
||||
if (!isActiveGM()) return;
|
||||
if (!hasAdaptiveFeature(actor)) return;
|
||||
|
||||
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 (typeof amount !== "number" || amount <= 0) return;
|
||||
if (!data || data.skip) 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 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));
|
||||
});
|
||||
@ -1,5 +1,15 @@
|
||||
import { MODULE_ID } from "./constants.js";
|
||||
import { seedFeatureCompendium } from "./features.js";
|
||||
import {
|
||||
MODULE_ID
|
||||
} from "./constants.js";
|
||||
|
||||
import {
|
||||
seedFeatureCompendium
|
||||
} from "./features.js";
|
||||
|
||||
import {
|
||||
migrateWorldActorFeatureItems
|
||||
} from "./migrations.js";
|
||||
|
||||
import "./hooks.js";
|
||||
|
||||
Hooks.once("init", () => {
|
||||
@ -8,4 +18,5 @@ Hooks.once("init", () => {
|
||||
|
||||
Hooks.once("ready", async () => {
|
||||
await seedFeatureCompendium();
|
||||
});
|
||||
await migrateWorldActorFeatureItems();
|
||||
});
|
||||
39
scripts/migrations.js
Normal file
39
scripts/migrations.js
Normal 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).`
|
||||
);
|
||||
}
|
||||
}
|
||||
141
scripts/utils.js
141
scripts/utils.js
@ -1,6 +1,6 @@
|
||||
import {
|
||||
MODULE_ID,
|
||||
FEATURE_FLAG,
|
||||
TRIGGER_FLAG,
|
||||
DAMAGE_SETS,
|
||||
ADAPTATION_TYPES,
|
||||
ADAPTATION_CONFIG
|
||||
@ -16,8 +16,13 @@ export function localize(key) {
|
||||
|
||||
export function getDamageTypeLabel(type) {
|
||||
const configured = CONFIG.DND5E?.damageTypes?.[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);
|
||||
}
|
||||
|
||||
@ -35,22 +40,40 @@ export function getDamageValue(damage) {
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) 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];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
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 [];
|
||||
}
|
||||
|
||||
@ -79,32 +102,63 @@ export function actorAlreadyImmune(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 => {
|
||||
const flag = item.getFlag(MODULE_ID, FEATURE_FLAG);
|
||||
const adaptationType = flag?.adaptationType ?? ADAPTATION_TYPES.RESISTANCE;
|
||||
return flag?.enabled === true && DAMAGE_SETS[flag?.setId] && ADAPTATION_CONFIG[adaptationType];
|
||||
return Array.from(actor.appliedEffects ?? []).filter(effect => {
|
||||
const trigger = effect.getFlag(MODULE_ID, TRIGGER_FLAG);
|
||||
const 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) {
|
||||
const result = [];
|
||||
|
||||
for (const item of getAdaptiveFeatureItems(actor)) {
|
||||
const flag = item.getFlag(MODULE_ID, FEATURE_FLAG);
|
||||
const set = DAMAGE_SETS[flag.setId];
|
||||
const adaptationType = flag.adaptationType ?? ADAPTATION_TYPES.RESISTANCE;
|
||||
for (const effect of getAdaptiveTriggerEffects(actor)) {
|
||||
const trigger = effect.getFlag(MODULE_ID, TRIGGER_FLAG);
|
||||
const set = DAMAGE_SETS[trigger.setId];
|
||||
const adaptationType =
|
||||
trigger.adaptationType ?? ADAPTATION_TYPES.RESISTANCE;
|
||||
|
||||
for (const damageType of set.damageTypes) {
|
||||
result.push({
|
||||
damageType,
|
||||
adaptationType,
|
||||
priority: ADAPTATION_CONFIG[adaptationType].priority
|
||||
priority: ADAPTATION_CONFIG[adaptationType].priority,
|
||||
sourceEffectUuid: effect.uuid
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -112,38 +166,49 @@ export function getAllowedAdaptationsForActor(actor) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die tatsächlich für den Treffer in Frage kommenden
|
||||
* adaptiven Schadensreaktionen.
|
||||
*/
|
||||
export function getCandidatesForActor(actor, damages) {
|
||||
const allowed = getAllowedAdaptationsForActor(actor);
|
||||
if (!allowed.length || !Array.isArray(damages)) return [];
|
||||
|
||||
if (!allowed.length || !Array.isArray(damages)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return damages
|
||||
.map(damage => ({
|
||||
type: getDamageType(damage),
|
||||
value: getDamageValue(damage)
|
||||
}))
|
||||
.filter(damage => {
|
||||
if (!damage.type) return false;
|
||||
if (damage.value <= 0) return false;
|
||||
return true;
|
||||
})
|
||||
.flatMap(damage => {
|
||||
return allowed
|
||||
.filter(entry => entry.damageType === damage.type)
|
||||
.map(entry => ({
|
||||
type: damage.type,
|
||||
value: damage.value,
|
||||
adaptationType: entry.adaptationType,
|
||||
priority: entry.priority
|
||||
}));
|
||||
});
|
||||
.filter(damage => damage.type && damage.value > 0)
|
||||
.flatMap(damage => allowed
|
||||
.filter(entry => entry.damageType === damage.type)
|
||||
.map(entry => ({
|
||||
type: damage.type,
|
||||
value: damage.value,
|
||||
adaptationType: entry.adaptationType,
|
||||
priority: entry.priority,
|
||||
sourceEffectUuid: entry.sourceEffectUuid
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!candidates?.length) return null;
|
||||
|
||||
return [...candidates].sort((a, b) => {
|
||||
const valueDifference = b.value - a.value;
|
||||
if (valueDifference !== 0) return valueDifference;
|
||||
|
||||
if (valueDifference !== 0) {
|
||||
return valueDifference;
|
||||
}
|
||||
|
||||
return b.priority - a.priority;
|
||||
})[0];
|
||||
}
|
||||
@ -155,4 +220,4 @@ export function getDominantDamageType(candidates) {
|
||||
// Backwards-compatible alias used by older local patches of this module.
|
||||
export function getElementalCandidatesForActor(actor, damages) {
|
||||
return getCandidatesForActor(actor, damages);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user