From f01e78c1f11a08ecc14f97fcfe05dc859613ba85 Mon Sep 17 00:00:00 2001 From: fretuks Date: Thu, 27 Aug 2026 09:35:42 +0200 Subject: [PATCH] adjust API --- README.md | 30 +++- .../net/fretux/ailments/api/AilmentApi.java | 167 ++++++++++++++++-- .../fretux/ailments/effect/BleedEffect.java | 5 + .../fretux/ailments/effect/SoulRotEffect.java | 4 +- .../fretux/ailments/event/AilmentEvents.java | 5 +- .../ailments/util/ControlEffectHelper.java | 24 ++- .../ailments/util/HemorrhageTracker.java | 19 +- .../ailments/util/ModEntityTypeTags.java | 2 + 8 files changed, 236 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c72f2de..2c72218 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,29 @@ int appliedCount = AilmentApi.applyEffects(target, source, Bleed/Soul Rot stacking and `timed` for an explicit duration and amplifier. `AilmentApi.getEffect(type)` exposes the registered `MobEffect` when an integration needs to inspect an active effect without depending on registry internals. +Other mods can use the same source-safe machinery for their own registered effects: + +```java +MobEffect frost = MyEffects.FROST.get(); + +// Raw duration, vanilla merging, real Forge source, and persistent latest-source tracking. +AilmentApi.applyEffect(target, source, frost, 100, 0); + +// Add one stack per call, cap at 4 stacks, and refresh to 100 ticks. +AilmentApi.applyStackingEffect(target, source, frost, 100, 4); + +// Add one stack, extend remaining duration, and Arcane-scale each added duration chunk once. +AilmentApi.applyStackingEffect(target, source, frost, 100, 4, true, true); + +LivingEntity loadedSource = AilmentApi.getEffectSource(target, frost); +UUID sourceId = AilmentApi.getEffectSourceUuid(target, frost); +``` + +`applyScaledEffect` provides the non-stacking Arcane-scaled variant. Generic applications are server-only, reject +invalid or cross-dimension sources, and honor team/friendly-fire rules for harmful player-on-player effects. Source +metadata is keyed by the external effect's registry ID and is cleared lazily when queried after the effect expires; +`clearEffectSource` can clear it explicitly without removing the effect. + Fracture stacks up to three times. Each stack removes 2 armor and 1 armor toughness; reapplication adds a stack and refreshes its duration. Arcane scales that refreshed duration but never changes the fixed attribute penalties. @@ -73,10 +96,15 @@ snapshotted on application so source unloading does not alter an active effect. Entity type tags: - `ascend_ailments:bleed_immune` +- `ascend_ailments:soulless` (immune to Soul Rot) +- `ascend_ailments:sturdy` (immune to Fracture) - `ascend_ailments:mental_control_resistant` - `ascend_ailments:mental_control_immune` -Players remain Bleed-compatible and non-player undead are automatically rejected independently of tags. +Vanilla undead are included in `soulless`, and any vanilla or modded entity classified with `MobType.UNDEAD` is +also rejected independently of the tag. `sturdy` includes iron golems by default. Non-player undead remain naturally +Bleed-immune; `bleed_immune` takes precedence even for players and also prevents and clears Hemorrhage buildup. +Integrations can query `AilmentApi.canSoulRot`, `canFracture`, and `canBleed` before presenting or applying abilities. Item tags: diff --git a/src/main/java/net/fretux/ailments/api/AilmentApi.java b/src/main/java/net/fretux/ailments/api/AilmentApi.java index e105ec7..0616db9 100644 --- a/src/main/java/net/fretux/ailments/api/AilmentApi.java +++ b/src/main/java/net/fretux/ailments/api/AilmentApi.java @@ -8,14 +8,17 @@ import net.fretux.ailments.util.MentalControlUtil; import net.fretux.ailments.util.ModEntityTypeTags; import net.fretux.ailments.util.HemorrhageTracker; import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectCategory; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.MobType; import net.minecraft.world.entity.monster.EnderMan; import net.minecraft.world.entity.player.Player; +import net.minecraftforge.registries.ForgeRegistries; import javax.annotation.Nullable; import java.util.Objects; +import java.util.UUID; /** Stable, source-aware entry point for applying and querying Ascend: Ailments effects. */ public final class AilmentApi { @@ -85,6 +88,97 @@ public final class AilmentApi { return applied; } + /** + * Applies an effect owned by another mod with standard Forge source propagation and persistent source tracking. + * The requested duration is used exactly and is not Arcane-scaled. Vanilla effect-merging rules apply. + */ + public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int requestedTicks, int amplifier) { + return applyExternalEffect(target, source, effect, requestedTicks, amplifier, false); + } + + /** Applies an amplifier-zero external effect with source propagation and persistent source tracking. */ + public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int requestedTicks) { + return applyEffect(target, source, effect, requestedTicks, 0); + } + + /** + * Applies an external effect and scales its requested duration once using the optional Ascend Arcane curve. + * Use this overload only when the owning mod wants Ailments to be the duration-scaling authority. + */ + public static boolean applyScaledEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int requestedTicks, int amplifier) { + return applyExternalEffect(target, source, effect, requestedTicks, amplifier, true); + } + + /** Applies an amplifier-zero external effect with one-time Arcane duration scaling. */ + public static boolean applyScaledEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int requestedTicks) { + return applyScaledEffect(target, source, effect, requestedTicks, 0); + } + + /** Adds one amplifier stack to an external effect, refreshes its duration, and tracks the latest source. */ + public static boolean applyStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int requestedTicks, int maximumStacks) { + return applyStackingEffect(target, source, effect, requestedTicks, maximumStacks, false, false); + } + + /** + * Adds one stack to an external effect with configurable duration behavior and optional Arcane scaling. + * {@code maximumStacks} uses a human-facing one-based count; the applied amplifier is capped at one less. + * When {@code extendDuration} is true, the scaled requested duration is added to the remaining duration. + */ + public static boolean applyStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int requestedTicks, int maximumStacks, boolean extendDuration, + boolean scaleDurationWithArcane) { + Objects.requireNonNull(effect, "effect"); + if (!externalApplicationValid(target, source, effect, requestedTicks, 0) + || maximumStacks < 1 || maximumStacks > 256) return false; + MobEffectInstance current = target.getEffect(effect); + if (current != null && current.isInfiniteDuration()) return false; + int amplifier = current == null ? 0 : current.getAmplifier() >= maximumStacks - 1 + ? maximumStacks - 1 : current.getAmplifier() + 1; + int duration = scaleDurationWithArcane ? AilmentScaling.scaleDuration(requestedTicks, source) : requestedTicks; + if (extendDuration && current != null) duration = safeAdd(current.getDuration(), duration); + boolean applied = addStackingEffect(target, source, effect, duration, amplifier, false, true, true); + if (applied) trackExternalSource(target, source, effect); + return applied; + } + + /** Returns the UUID of the latest successful source tracked for an active external effect. */ + @Nullable public static UUID getEffectSourceUuid(LivingEntity target, MobEffect effect) { + Objects.requireNonNull(target, "target"); + Objects.requireNonNull(effect, "effect"); + String key = externalSourceKey(effect); + if (key == null) return null; + if (!target.hasEffect(effect)) { + EffectSourceUtil.clear(target, key); + return null; + } + return EffectSourceUtil.getSourceUuid(target, key); + } + + /** Returns the latest tracked source when it is currently loaded, or {@code null}. */ + @Nullable public static LivingEntity getEffectSource(LivingEntity target, MobEffect effect) { + if (getEffectSourceUuid(target, effect) == null) return null; + return EffectSourceUtil.getSource(target, externalSourceKey(effect)); + } + + /** Resolves the latest tracked source only when it is loaded in the target's current level. */ + @Nullable public static LivingEntity getEffectSourceInLevel(LivingEntity target, MobEffect effect) { + if (getEffectSourceUuid(target, effect) == null) return null; + return EffectSourceUtil.getSourceInLevel(target, externalSourceKey(effect)); + } + + /** Clears source metadata created by the generic external-effect API without removing the effect. */ + public static void clearEffectSource(LivingEntity target, MobEffect effect) { + Objects.requireNonNull(target, "target"); + Objects.requireNonNull(effect, "effect"); + String key = externalSourceKey(effect); + if (key != null) EffectSourceUtil.clear(target, key); + } + /** Returns the registered MobEffect represented by a stable public ailment identifier. */ public static MobEffect getEffect(AilmentType type) { Objects.requireNonNull(type, "type"); @@ -108,7 +202,8 @@ public final class AilmentApi { /** Applies one Soul Rot stack; disabling extension always refreshes the configured base duration. */ public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source, boolean extendWithStacks) { - if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false; + if (!serverValid(target) || !canSoulRot(target) + || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false; MobEffect effect = ModEffects.SOUL_ROT.get(); MobEffectInstance current = target.getEffect(effect); int currentStacks = current == null ? 0 : current.getAmplifier() + 1; @@ -130,7 +225,8 @@ public final class AilmentApi { /** Applies an explicit, capped Soul Rot amplifier and source-scaled requested duration. */ public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source, int requestedTicks, int amplifier) { - if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false; + if (!serverValid(target) || !canSoulRot(target) + || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false; int capped = Math.min(Math.max(0, amplifier), AilmentsConfig.value(AilmentsConfig.SOUL_ROT_MAX_STACKS) - 1); return finishApplication(applyDot(target, source, ModEffects.SOUL_ROT.get(), EffectSourceUtil.SOUL_ROT, @@ -183,7 +279,7 @@ public final class AilmentApi { */ public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source, int requestedTicks, int amplifier) { - if (!serverValid(target) || requestedTicks < 1 || amplifier < 0 + if (!serverValid(target) || !canFracture(target) || requestedTicks < 1 || amplifier < 0 || !pvpProcReady(target, source, AilmentType.FRACTURE)) return false; MobEffect effect = ModEffects.FRACTURE.get(); MobEffectInstance current = target.getEffect(effect); @@ -246,8 +342,16 @@ public final class AilmentApi { } public static boolean canBleed(LivingEntity target) { - if (target instanceof Player) return true; - return target.getMobType() != MobType.UNDEAD && !target.getType().is(ModEntityTypeTags.BLEED_IMMUNE); + if (target.getType().is(ModEntityTypeTags.BLEED_IMMUNE)) return false; + return target instanceof Player || target.getMobType() != MobType.UNDEAD; + } + /** Returns false for the Soulless tag and for any entity classified as undead by Minecraft or another mod. */ + public static boolean canSoulRot(LivingEntity target) { + return target.getMobType() != MobType.UNDEAD && !target.getType().is(ModEntityTypeTags.SOULLESS); + } + /** Returns false for entity types in the Sturdy tag. */ + public static boolean canFracture(LivingEntity target) { + return !target.getType().is(ModEntityTypeTags.STURDY); } public static boolean isMentalControlResistant(LivingEntity target) { return MentalControlUtil.isMentalControlResistant(target); @@ -319,12 +423,30 @@ public final class AilmentApi { } return applied; } + private static boolean applyExternalEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int requestedTicks, int amplifier, boolean scaleDurationWithArcane) { + Objects.requireNonNull(effect, "effect"); + if (!externalApplicationValid(target, source, effect, requestedTicks, amplifier)) return false; + int duration = scaleDurationWithArcane ? AilmentScaling.scaleDuration(requestedTicks, source) : requestedTicks; + boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, false, true, true); + if (applied) trackExternalSource(target, source, effect); + return applied; + } private static MobEffectInstance instance(MobEffect effect, int duration, int amplifier, boolean icon) { return new MobEffectInstance(effect, duration, amplifier, false, false, icon); } + private static MobEffectInstance instance(MobEffect effect, int duration, int amplifier, boolean ambient, + boolean visible, boolean icon) { + return new MobEffectInstance(effect, duration, amplifier, ambient, visible, icon); + } private static boolean addSourcedEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, int duration, int amplifier, boolean icon) { - boolean applied = target.addEffect(instance(effect, duration, amplifier, icon), source); + return addSourcedEffect(target, source, effect, duration, amplifier, false, false, icon); + } + private static boolean addSourcedEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int duration, int amplifier, boolean ambient, boolean visible, + boolean icon) { + boolean applied = target.addEffect(instance(effect, duration, amplifier, ambient, visible, icon), source); if (applied && AscendCompat.isAscendLoaded()) { // Ascend observes the standard Forge source and scales duration during addEffect. Ailments already // applied its source curve, so restore the requested duration to prevent double scaling. @@ -335,19 +457,44 @@ public final class AilmentApi { } private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, int duration, int amplifier, boolean icon) { + return addStackingEffect(target, source, effect, duration, amplifier, false, false, icon); + } + private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, + int duration, int amplifier, boolean ambient, boolean visible, + boolean icon) { MobEffectInstance previous = target.getEffect(effect); if (previous == null || previous.getAmplifier() >= amplifier) - return addSourcedEffect(target, source, effect, duration, amplifier, icon); + return addSourcedEffect(target, source, effect, duration, amplifier, ambient, visible, icon); - // Vanilla preserves a lower-amplifier effect as a hidden fallback when upgrading. These ailments refresh - // to one authoritative stack state, so suppress that fallback without emitting a remove/add lifecycle. + // Vanilla preserves a lower-amplifier effect as a hidden fallback when upgrading. Stacking applications use + // one authoritative stack state, so suppress that fallback without emitting a remove/add lifecycle. int previousDuration = previous.getDuration(); previous.mapDuration(ignored -> 0); - boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, icon); + boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, ambient, visible, icon); if (!applied && target.getEffect(effect) == previous) previous.mapDuration(ignored -> previousDuration); return applied; } + private static boolean externalApplicationValid(LivingEntity target, @Nullable LivingEntity source, + MobEffect effect, int requestedTicks, int amplifier) { + if (!serverValid(target) || requestedTicks < 1 || amplifier < 0) return false; + if ((effect == ModEffects.BLEED.get() && !canBleed(target)) + || (effect == ModEffects.SOUL_ROT.get() && !canSoulRot(target)) + || (effect == ModEffects.FRACTURE.get() && !canFracture(target))) return false; + if (source != null && (!source.isAlive() || source.isSpectator() + || source.level().isClientSide || source.level() != target.level())) return false; + return effect.getCategory() != MobEffectCategory.HARMFUL + || !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer) + || source == target || sourcePlayer.canHarmPlayer(targetPlayer); + } + private static void trackExternalSource(LivingEntity target, @Nullable LivingEntity source, MobEffect effect) { + String key = externalSourceKey(effect); + if (key != null) EffectSourceUtil.setSource(target, key, source); + } + @Nullable private static String externalSourceKey(MobEffect effect) { + var id = ForgeRegistries.MOB_EFFECTS.getKey(effect); + return id == null ? null : "ascend_ailments.external." + id; + } private static boolean serverValid(LivingEntity target) { return target != null && !target.level().isClientSide && target.isAlive() && (!(target instanceof Player player) || !player.isSpectator()); diff --git a/src/main/java/net/fretux/ailments/effect/BleedEffect.java b/src/main/java/net/fretux/ailments/effect/BleedEffect.java index 7540daa..dabe3a6 100644 --- a/src/main/java/net/fretux/ailments/effect/BleedEffect.java +++ b/src/main/java/net/fretux/ailments/effect/BleedEffect.java @@ -1,5 +1,6 @@ package net.fretux.ailments.effect; +import net.fretux.ailments.api.AilmentApi; import net.fretux.ailments.config.AilmentsConfig; import net.fretux.ailments.damage.ModDamageSources; import net.fretux.ailments.util.EffectSourceUtil; @@ -20,6 +21,10 @@ public final class BleedEffect extends MobEffect { @Override public void applyEffectTick(LivingEntity entity, int amplifier) { if (entity.level().isClientSide) return; + if (!AilmentApi.canBleed(entity)) { + HemorrhageTracker.clearIfPresent(entity); + return; + } double base = amplifier <= 0 ? AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_0) : amplifier == 1 ? AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_1) : AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_2); diff --git a/src/main/java/net/fretux/ailments/effect/SoulRotEffect.java b/src/main/java/net/fretux/ailments/effect/SoulRotEffect.java index c6edefb..1fcdb18 100644 --- a/src/main/java/net/fretux/ailments/effect/SoulRotEffect.java +++ b/src/main/java/net/fretux/ailments/effect/SoulRotEffect.java @@ -1,5 +1,6 @@ package net.fretux.ailments.effect; +import net.fretux.ailments.api.AilmentApi; import net.fretux.ailments.config.AilmentsConfig; import net.fretux.ailments.util.EffectSourceUtil; import net.minecraft.world.effect.MobEffect; @@ -13,7 +14,8 @@ public final class SoulRotEffect extends MobEffect { @Override public boolean isDurationEffectTick(int duration, int amplifier) { return duration % 20 == 0; } @Override public void applyEffectTick(LivingEntity entity, int amplifier) { - if (entity.level().isClientSide || !(entity instanceof Player player)) return; + if (entity.level().isClientSide || !AilmentApi.canSoulRot(entity) + || !(entity instanceof Player player)) return; float base = (float) (AilmentsConfig.value(AilmentsConfig.SOUL_ROT_EXHAUSTION_BASE) + AilmentsConfig.value(AilmentsConfig.SOUL_ROT_EXHAUSTION_PER_AMP) * amplifier); player.causeFoodExhaustion(base * EffectSourceUtil.getPotency(entity, EffectSourceUtil.SOUL_ROT)); diff --git a/src/main/java/net/fretux/ailments/event/AilmentEvents.java b/src/main/java/net/fretux/ailments/event/AilmentEvents.java index 6ebe147..83a953a 100644 --- a/src/main/java/net/fretux/ailments/event/AilmentEvents.java +++ b/src/main/java/net/fretux/ailments/event/AilmentEvents.java @@ -58,7 +58,8 @@ public final class AilmentEvents { @SubscribeEvent public static void onHeal(LivingHealEvent event) { if (!event.getEntity().level().isClientSide && !event.isCanceled() - && event.getEntity().hasEffect(ModEffects.SOUL_ROT.get())) + && event.getEntity().hasEffect(ModEffects.SOUL_ROT.get()) + && AilmentApi.canSoulRot(event.getEntity())) event.setAmount((float) (event.getAmount() * AilmentsConfig.value(AilmentsConfig.SOUL_ROT_HEALING))); } @@ -97,7 +98,7 @@ public final class AilmentEvents { float amount = event.getAmount(); // 1. Victim vulnerability. - if (victim.hasEffect(ModEffects.SOUL_ROT.get())) + if (victim.hasEffect(ModEffects.SOUL_ROT.get()) && AilmentApi.canSoulRot(victim)) amount *= (float) AilmentsConfig.value(AilmentsConfig.SOUL_ROT_DAMAGE_TAKEN); // 2. Charm victim and attacker rules. diff --git a/src/main/java/net/fretux/ailments/util/ControlEffectHelper.java b/src/main/java/net/fretux/ailments/util/ControlEffectHelper.java index 7726a3f..1b40099 100644 --- a/src/main/java/net/fretux/ailments/util/ControlEffectHelper.java +++ b/src/main/java/net/fretux/ailments/util/ControlEffectHelper.java @@ -26,8 +26,8 @@ public final class ControlEffectHelper { public static void handleLivingTick(LivingEntity entity) { if (entity.level().isClientSide) return; - handleSoulRotFood(entity); cleanDotData(entity); + handleSoulRotFood(entity); if (entity.hasEffect(ModEffects.FEAR.get())) tickFear(entity); else { EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR); @@ -181,9 +181,25 @@ public final class ControlEffectHelper { } private static void cleanDotData(LivingEntity entity) { - if (!entity.hasEffect(ModEffects.SOUL_ROT.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT); - if (!entity.hasEffect(ModEffects.BLEED.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED); - if (!entity.hasEffect(ModEffects.FRACTURE.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE); + if (!AilmentApi.canSoulRot(entity)) { + if (entity.hasEffect(ModEffects.SOUL_ROT.get())) entity.removeEffect(ModEffects.SOUL_ROT.get()); + EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT); + } else if (!entity.hasEffect(ModEffects.SOUL_ROT.get())) { + EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT); + } + if (!AilmentApi.canBleed(entity)) { + if (entity.hasEffect(ModEffects.BLEED.get())) entity.removeEffect(ModEffects.BLEED.get()); + EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED); + HemorrhageTracker.clearIfPresent(entity); + } else if (!entity.hasEffect(ModEffects.BLEED.get())) { + EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED); + } + if (!AilmentApi.canFracture(entity)) { + if (entity.hasEffect(ModEffects.FRACTURE.get())) entity.removeEffect(ModEffects.FRACTURE.get()); + EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE); + } else if (!entity.hasEffect(ModEffects.FRACTURE.get())) { + EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE); + } } private ControlEffectHelper() {} } diff --git a/src/main/java/net/fretux/ailments/util/HemorrhageTracker.java b/src/main/java/net/fretux/ailments/util/HemorrhageTracker.java index ea210fe..19429c2 100644 --- a/src/main/java/net/fretux/ailments/util/HemorrhageTracker.java +++ b/src/main/java/net/fretux/ailments/util/HemorrhageTracker.java @@ -1,6 +1,7 @@ package net.fretux.ailments.util; import net.fretux.ailments.config.AilmentsConfig; +import net.fretux.ailments.api.AilmentApi; import net.fretux.ailments.damage.ModDamageSources; import net.fretux.ailments.network.AilmentNetwork; import net.minecraft.nbt.CompoundTag; @@ -16,8 +17,12 @@ public final class HemorrhageTracker { * Arcane potency is snapshotted when Bleed is applied, so an unloaded source cannot change an active meter. */ public static boolean recordBleedDamage(LivingEntity target, int amplifier, double arcanePotency) { - if (target.level().isClientSide - || amplifier + 1 < AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_MINIMUM_STACKS)) return false; + if (target.level().isClientSide) return false; + if (!AilmentApi.canBleed(target)) { + clear(target); + return false; + } + if (amplifier + 1 < AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_MINIMUM_STACKS)) return false; double threshold = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_THRESHOLD); double added = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_FILL_PER_TICK) * Math.max(0.0, arcanePotency); @@ -41,6 +46,10 @@ public final class HemorrhageTracker { } public static double getProgress(LivingEntity target) { + if (!AilmentApi.canBleed(target)) { + clearIfPresent(target); + return 0.0; + } CompoundTag data = target.getPersistentData(); return data.contains(PROGRESS) ? Math.max(0.0, data.getDouble(PROGRESS)) : 0.0; } @@ -55,6 +64,12 @@ public final class HemorrhageTracker { hideDisplay(target); } + /** Clears stored buildup and its HUD only when buildup actually exists. */ + public static void clearIfPresent(LivingEntity target) { + if (!target.getPersistentData().contains(PROGRESS)) return; + clear(target); + } + /** Synchronizes the compact client HUD after login or a meter update. */ public static void syncDisplay(LivingEntity target) { if (!(target instanceof ServerPlayer player)) return; diff --git a/src/main/java/net/fretux/ailments/util/ModEntityTypeTags.java b/src/main/java/net/fretux/ailments/util/ModEntityTypeTags.java index 929fb18..d9df20e 100644 --- a/src/main/java/net/fretux/ailments/util/ModEntityTypeTags.java +++ b/src/main/java/net/fretux/ailments/util/ModEntityTypeTags.java @@ -8,6 +8,8 @@ import net.minecraft.world.entity.EntityType; public final class ModEntityTypeTags { public static final TagKey> BLEED_IMMUNE = tag("bleed_immune"); + public static final TagKey> SOULLESS = tag("soulless"); + public static final TagKey> STURDY = tag("sturdy"); public static final TagKey> MENTAL_CONTROL_RESISTANT = tag("mental_control_resistant"); public static final TagKey> MENTAL_CONTROL_IMMUNE = tag("mental_control_immune"); private static TagKey> tag(String path) {