fix more issues

This commit is contained in:
2026-08-27 14:30:33 +02:00
parent f01e78c1f1
commit a464c3c247
8 changed files with 255 additions and 128 deletions
+13 -6
View File
@@ -37,7 +37,7 @@ Other mods can use the same source-safe machinery for their own registered effec
```java ```java
MobEffect frost = MyEffects.FROST.get(); MobEffect frost = MyEffects.FROST.get();
// Raw duration, vanilla merging, real Forge source, and persistent latest-source tracking. // Raw duration, authoritative merging, real Forge source, and persistent latest-source tracking.
AilmentApi.applyEffect(target, source, frost, 100, 0); AilmentApi.applyEffect(target, source, frost, 100, 0);
// Add one stack per call, cap at 4 stacks, and refresh to 100 ticks. // Add one stack per call, cap at 4 stacks, and refresh to 100 ticks.
@@ -50,11 +50,16 @@ LivingEntity loadedSource = AilmentApi.getEffectSource(target, frost);
UUID sourceId = AilmentApi.getEffectSourceUuid(target, frost); UUID sourceId = AilmentApi.getEffectSourceUuid(target, frost);
``` ```
`applyScaledEffect` provides the non-stacking Arcane-scaled variant. Generic applications are server-only, reject `applyScaledEffect` provides the non-stacking Arcane-scaled variant. Source-aware applications reject infinite or
invalid or cross-dimension sources, and honor team/friendly-fire rules for harmful player-on-player effects. Source higher-amplifier active effects instead of creating vanilla hidden fallbacks, ensuring that the visible effect and its
single tracked owner cannot diverge. 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; 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. `clearEffectSource` can clear it explicitly without removing the effect.
Ordinary API applications are not rate-limited. Automatic combat integrations that need the configured PvP proc
cooldown should use `applyProcEffect` with either an `AilmentApplication` descriptor or explicit type/duration values.
Fracture stacks up to three times. Each stack removes 2 armor and 1 armor toughness; reapplication adds a stack and 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. refreshes its duration. Arcane scales that refreshed duration but never changes the fixed attribute penalties.
@@ -83,13 +88,15 @@ The single `LivingHurtEvent` pipeline applies modifiers in this order:
4. Overcharm's pre-hit Charm snapshot and direct-player-melee bonus 4. Overcharm's pre-hit Charm snapshot and direct-player-melee bonus
Each applicable multiplier is applied once. The Overcharm snapshot happens before Charm is refreshed, so its first Each applicable multiplier is applied once. The Overcharm snapshot happens before Charm is refreshed, so its first
hit has no pre-existing-Charm bonus. hit has no pre-existing-Charm bonus. For simultaneous control effects, active Fear takes precedence over Taunt, and
Taunt takes precedence over Charm's prohibition against attacking its owner.
## Optional Ascend integration ## Optional Ascend integration
`compat.AscendCompat` checks for mod ID `ascend` and reflection-isolates its player-stats capability. Missing classes, `compat.AscendCompat` checks for mod ID `ascend` and reflection-isolates its player-stats capability. Missing classes,
missing capabilities, non-player sources, and absent/unloaded source data all fall back to 1.0x. DOT potency is missing capabilities, non-player sources, and absent/unloaded source data all fall back to 1.0x. An incompatible API
snapshotted on application so source unloading does not alter an active effect. is logged and cached instead of retrying reflection during every application. DOT potency is snapshotted on
application so source unloading does not alter an active effect.
## Datapack extension ## Datapack extension
@@ -1,9 +1,9 @@
package net.fretux.ailments.api; package net.fretux.ailments.api;
import net.fretux.ailments.config.AilmentsConfig; import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.compat.AscendCompat;
import net.fretux.ailments.registry.ModEffects; import net.fretux.ailments.registry.ModEffects;
import net.fretux.ailments.util.EffectSourceUtil; import net.fretux.ailments.util.EffectSourceUtil;
import net.fretux.ailments.util.ControlEffectHelper;
import net.fretux.ailments.util.MentalControlUtil; import net.fretux.ailments.util.MentalControlUtil;
import net.fretux.ailments.util.ModEntityTypeTags; import net.fretux.ailments.util.ModEntityTypeTags;
import net.fretux.ailments.util.HemorrhageTracker; import net.fretux.ailments.util.HemorrhageTracker;
@@ -88,9 +88,33 @@ public final class AilmentApi {
return applied; return applied;
} }
/**
* Applies an ailment as an automatic combat proc. Unlike ordinary API applications, successful player-versus-
* player procs observe the configured per-attacker, per-ailment cooldown.
*/
public static boolean applyProcEffect(LivingEntity target, @Nullable LivingEntity source,
AilmentApplication application) {
Objects.requireNonNull(application, "application");
if (!pvpProcReady(target, source, application.type())) return false;
boolean applied = applyEffect(target, source, application);
recordPvpProc(applied, target, source, application.type());
return applied;
}
/** Applies an explicit automatic combat proc with the configured PvP cooldown. */
public static boolean applyProcEffect(LivingEntity target, @Nullable LivingEntity source, AilmentType type,
int requestedTicks, int amplifier) {
Objects.requireNonNull(type, "type");
if (!pvpProcReady(target, source, type)) return false;
boolean applied = applyEffect(target, source, type, requestedTicks, amplifier);
recordPvpProc(applied, target, source, type);
return applied;
}
/** /**
* Applies an effect owned by another mod with standard Forge source propagation and persistent source tracking. * 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. * The requested duration is used exactly and is not Arcane-scaled. Merging is authoritative: a stronger or
* infinite active effect is retained instead of creating an untrackable vanilla hidden fallback.
*/ */
public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks, int amplifier) { int requestedTicks, int amplifier) {
@@ -202,8 +226,7 @@ public final class AilmentApi {
/** Applies one Soul Rot stack; disabling extension always refreshes the configured base duration. */ /** Applies one Soul Rot stack; disabling extension always refreshes the configured base duration. */
public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source, public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source,
boolean extendWithStacks) { boolean extendWithStacks) {
if (!serverValid(target) || !canSoulRot(target) if (!harmfulApplicationValid(target, source) || !canSoulRot(target)) return false;
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
MobEffect effect = ModEffects.SOUL_ROT.get(); MobEffect effect = ModEffects.SOUL_ROT.get();
MobEffectInstance current = target.getEffect(effect); MobEffectInstance current = target.getEffect(effect);
int currentStacks = current == null ? 0 : current.getAmplifier() + 1; int currentStacks = current == null ? 0 : current.getAmplifier() + 1;
@@ -219,23 +242,23 @@ public final class AilmentApi {
EffectSourceUtil.setPotency(target, EffectSourceUtil.SOUL_ROT, EffectSourceUtil.setPotency(target, EffectSourceUtil.SOUL_ROT,
AilmentScaling.getPotencyMultiplier(source, AilmentType.SOUL_ROT)); AilmentScaling.getPotencyMultiplier(source, AilmentType.SOUL_ROT));
} }
return finishApplication(applied, target, source, AilmentType.SOUL_ROT); return applied;
} }
/** Applies an explicit, capped Soul Rot amplifier and source-scaled requested duration. */ /** Applies an explicit, capped Soul Rot amplifier and source-scaled requested duration. */
public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source, public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source,
int requestedTicks, int amplifier) { int requestedTicks, int amplifier) {
if (!serverValid(target) || !canSoulRot(target) if (!harmfulApplicationValid(target, source) || !canSoulRot(target) || requestedTicks < 1 || amplifier < 0)
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false; return false;
int capped = Math.min(Math.max(0, amplifier), int capped = Math.min(Math.max(0, amplifier),
AilmentsConfig.value(AilmentsConfig.SOUL_ROT_MAX_STACKS) - 1); AilmentsConfig.value(AilmentsConfig.SOUL_ROT_MAX_STACKS) - 1);
return finishApplication(applyDot(target, source, ModEffects.SOUL_ROT.get(), EffectSourceUtil.SOUL_ROT, return applyDot(target, source, ModEffects.SOUL_ROT.get(), EffectSourceUtil.SOUL_ROT,
requestedTicks, capped, AilmentType.SOUL_ROT), target, source, AilmentType.SOUL_ROT); requestedTicks, capped, AilmentType.SOUL_ROT);
} }
/** Applies or adds one Bleed stack, returning false for immune targets. */ /** Applies or adds one Bleed stack, returning false for immune targets. */
public static boolean applyBleed(LivingEntity target, @Nullable LivingEntity source) { public static boolean applyBleed(LivingEntity target, @Nullable LivingEntity source) {
if (!serverValid(target) || !canBleed(target) || !pvpProcReady(target, source, AilmentType.BLEED)) return false; if (!harmfulApplicationValid(target, source) || !canBleed(target)) return false;
MobEffect effect = ModEffects.BLEED.get(); MobEffect effect = ModEffects.BLEED.get();
MobEffectInstance current = target.getEffect(effect); MobEffectInstance current = target.getEffect(effect);
int amplifier = current == null ? 0 : Math.min(current.getAmplifier() + 1, int amplifier = current == null ? 0 : Math.min(current.getAmplifier() + 1,
@@ -250,17 +273,18 @@ public final class AilmentApi {
EffectSourceUtil.setPotency(target, EffectSourceUtil.BLEED, EffectSourceUtil.setPotency(target, EffectSourceUtil.BLEED,
AilmentScaling.getPotencyMultiplier(source, AilmentType.BLEED)); AilmentScaling.getPotencyMultiplier(source, AilmentType.BLEED));
} }
return finishApplication(applied, target, source, AilmentType.BLEED); return applied;
} }
/** Applies an explicit, capped Bleed amplifier and source-scaled requested duration. */ /** Applies an explicit, capped Bleed amplifier and source-scaled requested duration. */
public static boolean applyBleed(LivingEntity target, @Nullable LivingEntity source, public static boolean applyBleed(LivingEntity target, @Nullable LivingEntity source,
int requestedTicks, int amplifier) { int requestedTicks, int amplifier) {
if (!serverValid(target) || !canBleed(target) || !pvpProcReady(target, source, AilmentType.BLEED)) return false; if (!harmfulApplicationValid(target, source) || !canBleed(target) || requestedTicks < 1 || amplifier < 0)
return false;
int capped = Math.min(Math.max(0, amplifier), int capped = Math.min(Math.max(0, amplifier),
AilmentsConfig.value(AilmentsConfig.BLEED_MAX_AMPLIFIER)); AilmentsConfig.value(AilmentsConfig.BLEED_MAX_AMPLIFIER));
return finishApplication(applyDot(target, source, ModEffects.BLEED.get(), EffectSourceUtil.BLEED, return applyDot(target, source, ModEffects.BLEED.get(), EffectSourceUtil.BLEED,
requestedTicks, capped, AilmentType.BLEED), target, source, AilmentType.BLEED); requestedTicks, capped, AilmentType.BLEED);
} }
/** Applies one Fracture stack and refreshes its configured, source-scaled duration. */ /** Applies one Fracture stack and refreshes its configured, source-scaled duration. */
@@ -279,8 +303,8 @@ public final class AilmentApi {
*/ */
public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source, public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source,
int requestedTicks, int amplifier) { int requestedTicks, int amplifier) {
if (!serverValid(target) || !canFracture(target) || requestedTicks < 1 || amplifier < 0 if (!harmfulApplicationValid(target, source) || !canFracture(target) || requestedTicks < 1 || amplifier < 0)
|| !pvpProcReady(target, source, AilmentType.FRACTURE)) return false; return false;
MobEffect effect = ModEffects.FRACTURE.get(); MobEffect effect = ModEffects.FRACTURE.get();
MobEffectInstance current = target.getEffect(effect); MobEffectInstance current = target.getEffect(effect);
int stackedAmplifier = current == null ? 0 : Math.min(2, current.getAmplifier() + 1); int stackedAmplifier = current == null ? 0 : Math.min(2, current.getAmplifier() + 1);
@@ -288,57 +312,55 @@ public final class AilmentApi {
int duration = AilmentScaling.scaleDuration(requestedTicks, source); int duration = AilmentScaling.scaleDuration(requestedTicks, source);
boolean applied = addStackingEffect(target, source, effect, duration, appliedAmplifier, true); boolean applied = addStackingEffect(target, source, effect, duration, appliedAmplifier, true);
if (applied) EffectSourceUtil.setSource(target, EffectSourceUtil.FRACTURE, source); if (applied) EffectSourceUtil.setSource(target, EffectSourceUtil.FRACTURE, source);
return finishApplication(applied, target, source, AilmentType.FRACTURE); return applied;
} }
public static boolean applyFear(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) { public static boolean applyFear(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target) if (!hostileControlValid(target, source) || requestedTicks < 1
|| target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_IMMUNITY_HEALTH) || MentalControlUtil.isMentalControlImmune(target)
|| !pvpProcReady(target, source, AilmentType.FEAR)) return false; || target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_IMMUNITY_HEALTH)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source); int duration = AilmentScaling.scaleDuration(requestedTicks, source);
if (target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_REDUCED_HEALTH)) duration /= 2; if (target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_REDUCED_HEALTH)) duration /= 2;
if (MentalControlUtil.isMentalControlResistant(target)) if (MentalControlUtil.isMentalControlResistant(target))
duration = MentalControlUtil.resistantDuration(duration, 3); duration = MentalControlUtil.resistantDuration(duration, 3);
if (target instanceof EnderMan) duration = Math.max(1, duration / 2); if (target instanceof EnderMan) duration = Math.max(1, duration / 2);
return finishApplication(applySourced(target, source, ModEffects.FEAR.get(), EffectSourceUtil.FEAR, return applySourced(target, source, ModEffects.FEAR.get(), EffectSourceUtil.FEAR,
Math.max(1, duration), 0), target, source, AilmentType.FEAR); Math.max(1, duration), 0);
} }
public static boolean applyCharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) { public static boolean applyCharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target) if (!hostileControlValid(target, source) || requestedTicks < 1
|| !pvpProcReady(target, source, AilmentType.CHARM)) return false; || MentalControlUtil.isMentalControlImmune(target)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source); int duration = AilmentScaling.scaleDuration(requestedTicks, source);
if (MentalControlUtil.isMentalControlResistant(target)) if (MentalControlUtil.isMentalControlResistant(target))
duration = MentalControlUtil.resistantDuration(duration, 2); duration = MentalControlUtil.resistantDuration(duration, 2);
return finishApplication(applySourced(target, source, ModEffects.CHARM.get(), EffectSourceUtil.CHARM, return applySourced(target, source, ModEffects.CHARM.get(), EffectSourceUtil.CHARM, duration, 0);
duration, 0), target, source, AilmentType.CHARM);
} }
public static boolean applyTaunt(LivingEntity target, @Nullable LivingEntity source, int requestedTicks, int amplifier) { public static boolean applyTaunt(LivingEntity target, @Nullable LivingEntity source, int requestedTicks, int amplifier) {
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target) if (!hostileControlValid(target, source) || requestedTicks < 1 || amplifier < 0
|| !pvpProcReady(target, source, AilmentType.TAUNT)) return false; || MentalControlUtil.isMentalControlImmune(target)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source); int duration = AilmentScaling.scaleDuration(requestedTicks, source);
if (MentalControlUtil.isMentalControlResistant(target)) if (MentalControlUtil.isMentalControlResistant(target))
duration = MentalControlUtil.resistantDuration(duration, 2); duration = MentalControlUtil.resistantDuration(duration, 2);
return finishApplication(applySourced(target, source, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT, return applySourced(target, source, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT,
duration, Math.max(0, amplifier)), target, source, AilmentType.TAUNT); duration, amplifier);
} }
public static boolean applyOvercharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) { public static boolean applyOvercharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.OVERCHARM)) return false; if (!applicationValid(target, source) || requestedTicks < 1) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source); int duration = AilmentScaling.scaleDuration(requestedTicks, source);
boolean result = addSourcedEffect(target, source, ModEffects.OVERCHARM.get(), duration, 0, true); boolean result = addSourcedEffect(target, source, ModEffects.OVERCHARM.get(), duration, 0, true);
if (result) { if (result) {
EffectSourceUtil.setSource(target, EffectSourceUtil.OVERCHARM, source); EffectSourceUtil.setSource(target, EffectSourceUtil.OVERCHARM, source);
} }
return finishApplication(result, target, source, AilmentType.OVERCHARM); return result;
} }
public static boolean applyWinded(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) { public static boolean applyWinded(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.WINDED)) return false; if (!harmfulApplicationValid(target, source) || requestedTicks < 1) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source); int duration = AilmentScaling.scaleDuration(requestedTicks, source);
return finishApplication(addSourcedEffect(target, source, ModEffects.WINDED.get(), duration, 0, true), return addSourcedEffect(target, source, ModEffects.WINDED.get(), duration, 0, true);
target, source, AilmentType.WINDED);
} }
public static boolean canBleed(LivingEntity target) { public static boolean canBleed(LivingEntity target) {
@@ -370,7 +392,9 @@ public final class AilmentApi {
public static double getHemorrhageFraction(LivingEntity target) { public static double getHemorrhageFraction(LivingEntity target) {
return HemorrhageTracker.getFraction(target); return HemorrhageTracker.getFraction(target);
} }
public static void clearHemorrhage(LivingEntity target) { HemorrhageTracker.clear(target); } public static void clearHemorrhage(LivingEntity target) {
if (target != null && !target.level().isClientSide) HemorrhageTracker.clear(target);
}
@Nullable public static LivingEntity getCharmSource(LivingEntity target) { @Nullable public static LivingEntity getCharmSource(LivingEntity target) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.CHARM); return EffectSourceUtil.getSource(target, EffectSourceUtil.CHARM);
} }
@@ -392,6 +416,7 @@ public final class AilmentApi {
/** Removes all registered ailments and all associated source/potency metadata. */ /** Removes all registered ailments and all associated source/potency metadata. */
public static void clearAll(LivingEntity target) { public static void clearAll(LivingEntity target) {
if (target == null || target.level().isClientSide) return;
target.removeEffect(ModEffects.SOUL_ROT.get()); target.removeEffect(ModEffects.BLEED.get()); target.removeEffect(ModEffects.SOUL_ROT.get()); target.removeEffect(ModEffects.BLEED.get());
target.removeEffect(ModEffects.FEAR.get()); target.removeEffect(ModEffects.CHARM.get()); target.removeEffect(ModEffects.FEAR.get()); target.removeEffect(ModEffects.CHARM.get());
target.removeEffect(ModEffects.TAUNT.get()); target.removeEffect(ModEffects.OVERCHARM.get()); target.removeEffect(ModEffects.TAUNT.get()); target.removeEffect(ModEffects.OVERCHARM.get());
@@ -404,6 +429,7 @@ public final class AilmentApi {
EffectSourceUtil.clear(target, EffectSourceUtil.TAUNT); EffectSourceUtil.clear(target, EffectSourceUtil.TAUNT);
EffectSourceUtil.clear(target, EffectSourceUtil.OVERCHARM); EffectSourceUtil.clear(target, EffectSourceUtil.OVERCHARM);
EffectSourceUtil.clear(target, EffectSourceUtil.FRACTURE); EffectSourceUtil.clear(target, EffectSourceUtil.FRACTURE);
ControlEffectHelper.clearRuntimeState(target);
HemorrhageTracker.clear(target); HemorrhageTracker.clear(target);
} }
@@ -446,13 +472,17 @@ public final class AilmentApi {
private static boolean addSourcedEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, private static boolean addSourcedEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean ambient, boolean visible, int duration, int amplifier, boolean ambient, boolean visible,
boolean icon) { boolean icon) {
MobEffectInstance previous = target.getEffect(effect);
if (previous != null && (previous.isInfiniteDuration() || previous.getAmplifier() > amplifier)) return false;
if (previous == null || previous.getAmplifier() == amplifier)
return target.addEffect(instance(effect, duration, amplifier, ambient, visible, icon), source);
// Source metadata has one authoritative owner, so never allow vanilla to retain the previous source as a
// hidden fallback. A snapshot provides a real rollback even after the live duration has been set to zero.
MobEffectInstance snapshot = new MobEffectInstance(previous);
previous.mapDuration(ignored -> 0);
boolean applied = target.addEffect(instance(effect, duration, amplifier, ambient, visible, icon), source); boolean applied = target.addEffect(instance(effect, duration, amplifier, ambient, visible, icon), source);
if (applied && AscendCompat.isAscendLoaded()) { if (!applied && target.getEffect(effect) == previous) previous.update(snapshot);
// 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.
MobEffectInstance active = target.getEffect(effect);
if (active != null && active.getAmplifier() == amplifier) active.mapDuration(ignored -> duration);
}
return applied; return applied;
} }
private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
@@ -462,18 +492,7 @@ public final class AilmentApi {
private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect, private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean ambient, boolean visible, int duration, int amplifier, boolean ambient, boolean visible,
boolean icon) { boolean icon) {
MobEffectInstance previous = target.getEffect(effect); return addSourcedEffect(target, source, effect, duration, amplifier, ambient, visible, icon);
if (previous == null || previous.getAmplifier() >= amplifier)
return addSourcedEffect(target, source, effect, duration, amplifier, ambient, visible, icon);
// 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, ambient, visible, icon);
if (!applied && target.getEffect(effect) == previous)
previous.mapDuration(ignored -> previousDuration);
return applied;
} }
private static boolean externalApplicationValid(LivingEntity target, @Nullable LivingEntity source, private static boolean externalApplicationValid(LivingEntity target, @Nullable LivingEntity source,
MobEffect effect, int requestedTicks, int amplifier) { MobEffect effect, int requestedTicks, int amplifier) {
@@ -499,9 +518,18 @@ public final class AilmentApi {
return target != null && !target.level().isClientSide && target.isAlive() return target != null && !target.level().isClientSide && target.isAlive()
&& (!(target instanceof Player player) || !player.isSpectator()); && (!(target instanceof Player player) || !player.isSpectator());
} }
private static boolean applicationValid(LivingEntity target, @Nullable LivingEntity source) {
if (!serverValid(target)) return false;
return source == null || source.isAlive() && !source.isSpectator()
&& !source.level().isClientSide && source.level() == target.level();
}
private static boolean harmfulApplicationValid(LivingEntity target, @Nullable LivingEntity source) {
if (!applicationValid(target, source)) return false;
return !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer)
|| source == target || sourcePlayer.canHarmPlayer(targetPlayer);
}
private static boolean hostileControlValid(LivingEntity target, @Nullable LivingEntity source) { private static boolean hostileControlValid(LivingEntity target, @Nullable LivingEntity source) {
if (!serverValid(target) || source == null || !source.isAlive() if (source == null || !applicationValid(target, source)) return false;
|| source.level().isClientSide || source.isSpectator()) return false;
return !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer) return !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer)
|| sourcePlayer.canHarmPlayer(targetPlayer); || sourcePlayer.canHarmPlayer(targetPlayer);
} }
@@ -513,22 +541,21 @@ public final class AilmentApi {
return (int) Math.min(Integer.MAX_VALUE, (long) a + b); return (int) Math.min(Integer.MAX_VALUE, (long) a + b);
} }
private static boolean pvpProcReady(LivingEntity target, @Nullable LivingEntity source, AilmentType type) { private static boolean pvpProcReady(LivingEntity target, @Nullable LivingEntity source, AilmentType type) {
if (source != null && source.level() != target.level()) return false; if (type == AilmentType.OVERCHARM ? !applicationValid(target, source)
: !harmfulApplicationValid(target, source)) return false;
if (!(source instanceof Player sourcePlayer) || !(target instanceof Player targetPlayer) || source == target) if (!(source instanceof Player sourcePlayer) || !(target instanceof Player targetPlayer) || source == target)
return true; return true;
if (!sourcePlayer.canHarmPlayer(targetPlayer)) return false;
String key = pvpCooldownKey(type); String key = pvpCooldownKey(type);
long now = source.level().getGameTime(); long now = source.level().getGameTime();
return !source.getPersistentData().contains(key) return !source.getPersistentData().contains(key)
|| now - source.getPersistentData().getLong(key) || now - source.getPersistentData().getLong(key)
>= AilmentsConfig.value(AilmentsConfig.PVP_PROC_COOLDOWN_TICKS); >= AilmentsConfig.value(AilmentsConfig.PVP_PROC_COOLDOWN_TICKS);
} }
private static boolean finishApplication(boolean applied, LivingEntity target, private static void recordPvpProc(boolean applied, LivingEntity target,
@Nullable LivingEntity source, AilmentType type) { @Nullable LivingEntity source, AilmentType type) {
if (applied && source instanceof Player && target instanceof Player && source != target) { if (applied && source instanceof Player && target instanceof Player && source != target) {
source.getPersistentData().putLong(pvpCooldownKey(type), source.level().getGameTime()); source.getPersistentData().putLong(pvpCooldownKey(type), source.level().getGameTime());
} }
return applied;
} }
private static String pvpCooldownKey(AilmentType type) { private static String pvpCooldownKey(AilmentType type) {
return "ascend_ailments.pvp_proc." + type.name().toLowerCase(java.util.Locale.ROOT); return "ascend_ailments.pvp_proc." + type.name().toLowerCase(java.util.Locale.ROOT);
@@ -1,5 +1,6 @@
package net.fretux.ailments.compat; package net.fretux.ailments.compat;
import com.mojang.logging.LogUtils;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.Capability;
@@ -9,11 +10,15 @@ import net.minecraftforge.fml.ModList;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Optional; import java.util.Optional;
import org.slf4j.Logger;
/** Reflection boundary: no Ascend type appears in a loadable signature or constant-pool class reference. */ /** Reflection boundary: no Ascend type appears in a loadable signature or constant-pool class reference. */
public final class AscendCompat { public final class AscendCompat {
private static final Logger LOGGER = LogUtils.getLogger();
private static final String PROVIDER = "net.fretux.ascend.player.PlayerStatsProvider"; private static final String PROVIDER = "net.fretux.ascend.player.PlayerStatsProvider";
private static boolean resolved; private static volatile boolean resolutionAttempted;
private static boolean available;
private static boolean invocationFailureLogged;
private static Capability<?> statsCapability; private static Capability<?> statsCapability;
private static Method getAttributeLevel; private static Method getAttributeLevel;
@@ -22,44 +27,52 @@ public final class AscendCompat {
/** Resolves the optional public API without requiring an entity; useful for lifecycle diagnostics. */ /** Resolves the optional public API without requiring an entity; useful for lifecycle diagnostics. */
public static boolean validateIntegration() { public static boolean validateIntegration() {
if (!isAscendLoaded()) return false; if (!isAscendLoaded()) return false;
try { return resolve();
resolve();
return statsCapability != null && getAttributeLevel != null;
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
return false;
}
} }
public static int getArcane(LivingEntity source) { public static int getArcane(LivingEntity source) {
if (!(source instanceof Player player) || !isAscendLoaded()) return 0; if (!(source instanceof Player player) || !isAscendLoaded()) return 0;
try { try {
resolve(); if (!resolve()) return 0;
if (statsCapability == null || getAttributeLevel == null) return 0;
LazyOptional<?> lazy = player.getCapability(statsCapability); LazyOptional<?> lazy = player.getCapability(statsCapability);
Optional<?> stats = lazy.resolve(); Optional<?> stats = lazy.resolve();
if (stats.isEmpty()) return 0; if (stats.isEmpty()) return 0;
return Math.max(0, ((Number) getAttributeLevel.invoke(stats.get(), "arcane")).intValue()); return Math.max(0, ((Number) getAttributeLevel.invoke(stats.get(), "arcane")).intValue());
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) { } catch (ReflectiveOperationException | LinkageError | RuntimeException error) {
disableAfterInvocationFailure(error);
return 0; return 0;
} }
} }
private static synchronized void resolve() throws ReflectiveOperationException { private static synchronized boolean resolve() {
if (resolved) return; if (resolutionAttempted) return available;
Class<?> provider = Class.forName(PROVIDER, false, AscendCompat.class.getClassLoader()); resolutionAttempted = true;
Field capabilityField = provider.getField("PLAYER_STATS"); try {
Capability<?> resolvedCapability = (Capability<?>) capabilityField.get(null); Class<?> provider = Class.forName(PROVIDER, false, AscendCompat.class.getClassLoader());
Class<?> statsClass = Class.forName("net.fretux.ascend.player.PlayerStats", false, Field capabilityField = provider.getField("PLAYER_STATS");
AscendCompat.class.getClassLoader()); Capability<?> resolvedCapability = (Capability<?>) capabilityField.get(null);
Method resolvedGetter = statsClass.getMethod("getAttributeLevel", String.class); Class<?> statsClass = Class.forName("net.fretux.ascend.player.PlayerStats", false,
Class<?> returnType = resolvedGetter.getReturnType(); AscendCompat.class.getClassLoader());
boolean numericPrimitive = returnType == byte.class || returnType == short.class || returnType == int.class Method resolvedGetter = statsClass.getMethod("getAttributeLevel", String.class);
|| returnType == long.class || returnType == float.class || returnType == double.class; Class<?> returnType = resolvedGetter.getReturnType();
if (!Number.class.isAssignableFrom(returnType) && !numericPrimitive) boolean numericPrimitive = returnType == byte.class || returnType == short.class || returnType == int.class
throw new NoSuchMethodException("getAttributeLevel(String) does not return a number"); || returnType == long.class || returnType == float.class || returnType == double.class;
statsCapability = resolvedCapability; if (!Number.class.isAssignableFrom(returnType) && !numericPrimitive)
getAttributeLevel = resolvedGetter; throw new NoSuchMethodException("getAttributeLevel(String) does not return a number");
resolved = true; statsCapability = resolvedCapability;
getAttributeLevel = resolvedGetter;
available = true;
} catch (ReflectiveOperationException | LinkageError | RuntimeException error) {
LOGGER.warn("Ascend is loaded, but its player-stats integration is incompatible; Arcane scaling is disabled", error);
}
return available;
}
private static synchronized void disableAfterInvocationFailure(Throwable error) {
available = false;
if (invocationFailureLogged) return;
invocationFailureLogged = true;
LOGGER.warn("Ascend Arcane lookup failed; affected applications will use neutral scaling", error);
} }
private AscendCompat() {} private AscendCompat() {}
} }
@@ -2,6 +2,8 @@ package net.fretux.ailments.event;
import net.fretux.ailments.AscendAilments; import net.fretux.ailments.AscendAilments;
import net.fretux.ailments.api.AilmentApi; import net.fretux.ailments.api.AilmentApi;
import net.fretux.ailments.api.AilmentApplication;
import net.fretux.ailments.api.AilmentType;
import net.fretux.ailments.config.AilmentsConfig; import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.damage.ModDamageSources; import net.fretux.ailments.damage.ModDamageSources;
import net.fretux.ailments.registry.ModEffects; import net.fretux.ailments.registry.ModEffects;
@@ -69,13 +71,24 @@ public final class AilmentEvents {
Entity attacker = event.getSource().getEntity(); Entity attacker = event.getSource().getEntity();
if (attacker instanceof LivingEntity living && !(living instanceof Player) if (attacker instanceof LivingEntity living && !(living instanceof Player)
&& living.hasEffect(ModEffects.CHARM.get()) && living.hasEffect(ModEffects.CHARM.get())
&& ControlEffectHelper.isOwnCharmSource(living, event.getEntity())) event.setCanceled(true); && ControlEffectHelper.isOwnCharmSource(living, event.getEntity())) {
LivingEntity taunter = living.hasEffect(ModEffects.TAUNT.get())
&& !MentalControlUtil.isMentalControlResistant(living)
? ControlEffectHelper.validTauntSource(living) : null;
if (taunter == null || !taunter.getUUID().equals(event.getEntity().getUUID())) event.setCanceled(true);
}
} }
@SubscribeEvent @SubscribeEvent
public static void onChangeTarget(LivingChangeTargetEvent event) { public static void onChangeTarget(LivingChangeTargetEvent event) {
if (event.getEntity().level().isClientSide) return; if (event.getEntity().level().isClientSide) return;
if (!(event.getEntity() instanceof Mob mob)) return; if (!(event.getEntity() instanceof Mob mob)) return;
LivingEntity fearSource = EffectSourceUtil.getSourceInLevel(mob, EffectSourceUtil.FEAR);
if (mob.hasEffect(ModEffects.FEAR.get()) && !MentalControlUtil.isMentalControlResistant(mob)
&& fearSource != null && fearSource.isAlive()) {
event.setNewTarget(null);
return;
}
LivingEntity taunter = mob.hasEffect(ModEffects.TAUNT.get()) LivingEntity taunter = mob.hasEffect(ModEffects.TAUNT.get())
&& !MentalControlUtil.isMentalControlResistant(mob) && !MentalControlUtil.isMentalControlResistant(mob)
? ControlEffectHelper.validTauntSource(mob) : null; ? ControlEffectHelper.validTauntSource(mob) : null;
@@ -106,9 +119,11 @@ public final class AilmentEvents {
&& ControlEffectHelper.isOwnCharmSource(victim, attacker)) && ControlEffectHelper.isOwnCharmSource(victim, attacker))
amount *= (float) AilmentsConfig.value(AilmentsConfig.CHARM_VICTIM_BONUS); amount *= (float) AilmentsConfig.value(AilmentsConfig.CHARM_VICTIM_BONUS);
if (attacker != null && attacker.hasEffect(ModEffects.CHARM.get())) { if (attacker != null && attacker.hasEffect(ModEffects.CHARM.get())) {
amount *= (float) (ControlEffectHelper.isOwnCharmSource(attacker, victim) LivingEntity charmSource = AilmentApi.getCharmSource(attacker);
? AilmentsConfig.value(AilmentsConfig.CHARM_SOURCE_PENALTY) if (charmSource != null)
: AilmentsConfig.value(AilmentsConfig.CHARM_OTHER_BONUS)); amount *= (float) (charmSource.getUUID().equals(victim.getUUID())
? AilmentsConfig.value(AilmentsConfig.CHARM_SOURCE_PENALTY)
: AilmentsConfig.value(AilmentsConfig.CHARM_OTHER_BONUS));
} }
// 3. Taunt rules; each logically distinct rule is applied once. // 3. Taunt rules; each logically distinct rule is applied once.
@@ -142,8 +157,8 @@ public final class AilmentEvents {
if (!(causing instanceof LivingEntity attacker) || attacker == victim) return; if (!(causing instanceof LivingEntity attacker) || attacker == victim) return;
if (attacker.hasEffect(ModEffects.OVERCHARM.get())) if (attacker.hasEffect(ModEffects.OVERCHARM.get()))
AilmentApi.applyCharm(victim, attacker, AilmentApi.applyProcEffect(victim, attacker, AilmentType.CHARM,
AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION)); AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION), 0);
if (damageSource.getDirectEntity() == attacker) if (damageSource.getDirectEntity() == attacker)
applyTaggedWeaponAilments(attacker.getMainHandItem(), victim, attacker); applyTaggedWeaponAilments(attacker.getMainHandItem(), victim, attacker);
} }
@@ -152,16 +167,23 @@ public final class AilmentEvents {
if (stack.isEmpty()) return; if (stack.isEmpty()) return;
if (isAutomaticBleedWeapon(stack) && attacker.getRandom().nextDouble() if (isAutomaticBleedWeapon(stack) && attacker.getRandom().nextDouble()
< AilmentsConfig.value(AilmentsConfig.AUTOMATIC_WEAPON_BLEED_CHANCE)) < AilmentsConfig.value(AilmentsConfig.AUTOMATIC_WEAPON_BLEED_CHANCE))
AilmentApi.applyBleed(victim, attacker); AilmentApi.applyProcEffect(victim, attacker, AilmentApplication.stack(AilmentType.BLEED));
if (stack.is(ModItemTags.SOUL_ROT_ON_HIT)) AilmentApi.applySoulRot(victim, attacker); if (stack.is(ModItemTags.SOUL_ROT_ON_HIT))
if (stack.is(ModItemTags.FRACTURE_ON_HIT)) AilmentApi.applyFracture(victim, attacker); AilmentApi.applyProcEffect(victim, attacker, AilmentApplication.stack(AilmentType.SOUL_ROT));
if (stack.is(ModItemTags.FRACTURE_ON_HIT))
AilmentApi.applyProcEffect(victim, attacker, AilmentApplication.stack(AilmentType.FRACTURE));
int duration = AilmentsConfig.value(AilmentsConfig.TAGGED_WEAPON_EFFECT_DURATION); int duration = AilmentsConfig.value(AilmentsConfig.TAGGED_WEAPON_EFFECT_DURATION);
if (stack.is(ModItemTags.FEAR_ON_HIT)) AilmentApi.applyFear(victim, attacker, duration); if (stack.is(ModItemTags.FEAR_ON_HIT))
if (stack.is(ModItemTags.CHARM_ON_HIT)) AilmentApi.applyCharm(victim, attacker, duration); AilmentApi.applyProcEffect(victim, attacker, AilmentType.FEAR, duration, 0);
if (stack.is(ModItemTags.TAUNT_ON_HIT)) AilmentApi.applyTaunt(victim, attacker, duration, 0); if (stack.is(ModItemTags.CHARM_ON_HIT))
if (stack.is(ModItemTags.OVERCHARM_ON_HIT)) AilmentApi.applyOvercharm(victim, attacker, duration); AilmentApi.applyProcEffect(victim, attacker, AilmentType.CHARM, duration, 0);
if (stack.is(ModItemTags.WINDED_ON_HIT)) AilmentApi.applyWinded(victim, attacker, duration); if (stack.is(ModItemTags.TAUNT_ON_HIT))
AilmentApi.applyProcEffect(victim, attacker, AilmentType.TAUNT, duration, 0);
if (stack.is(ModItemTags.OVERCHARM_ON_HIT))
AilmentApi.applyProcEffect(victim, attacker, AilmentType.OVERCHARM, duration, 0);
if (stack.is(ModItemTags.WINDED_ON_HIT))
AilmentApi.applyProcEffect(victim, attacker, AilmentType.WINDED, duration, 0);
} }
private static boolean isAutomaticBleedWeapon(ItemStack stack) { private static boolean isAutomaticBleedWeapon(ItemStack stack) {
@@ -26,16 +26,41 @@ public final class ControlEffectHelper {
public static void handleLivingTick(LivingEntity entity) { public static void handleLivingTick(LivingEntity entity) {
if (entity.level().isClientSide) return; if (entity.level().isClientSide) return;
cleanDotData(entity); CompoundTag data = entity.getPersistentData();
handleSoulRotFood(entity); boolean soulRotRelevant = entity.hasEffect(ModEffects.SOUL_ROT.get())
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.SOUL_ROT)
|| data.contains(SOUL_FOOD) || data.contains(SOUL_SATURATION);
boolean bleedRelevant = entity.hasEffect(ModEffects.BLEED.get())
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.BLEED) || HemorrhageTracker.hasProgress(entity);
boolean fractureRelevant = entity.hasEffect(ModEffects.FRACTURE.get())
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.FRACTURE);
boolean controlRelevant = entity.hasEffect(ModEffects.FEAR.get()) || entity.hasEffect(ModEffects.TAUNT.get())
|| entity.hasEffect(ModEffects.CHARM.get()) || entity.hasEffect(ModEffects.OVERCHARM.get())
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.FEAR)
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.TAUNT)
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.CHARM)
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.OVERCHARM);
if (!soulRotRelevant && !bleedRelevant && !fractureRelevant && !controlRelevant) return;
cleanDotData(entity, soulRotRelevant, bleedRelevant, fractureRelevant);
if (soulRotRelevant) handleSoulRotFood(entity);
if (MentalControlUtil.isMentalControlImmune(entity)) {
removeControlEffect(entity, ModEffects.FEAR.get(), EffectSourceUtil.FEAR);
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM);
}
if (entity.hasEffect(ModEffects.FEAR.get())) tickFear(entity); if (entity.hasEffect(ModEffects.FEAR.get())) tickFear(entity);
else { else {
EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR); EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR);
entity.getPersistentData().remove(FEAR_DISTANCE); entity.getPersistentData().remove(FEAR_DISTANCE);
clearFearPlayerPenalties(entity); clearFearPlayerPenalties(entity);
} }
if (entity.hasEffect(ModEffects.TAUNT.get())) tickTaunt(entity); boolean fearDominant = entity.hasEffect(ModEffects.FEAR.get())
else EffectSourceUtil.clear(entity, EffectSourceUtil.TAUNT); && !MentalControlUtil.isMentalControlResistant(entity)
&& validControlSource(entity, EffectSourceUtil.FEAR) != null;
if (entity.hasEffect(ModEffects.TAUNT.get())) {
if (!fearDominant) tickTaunt(entity);
} else EffectSourceUtil.clear(entity, EffectSourceUtil.TAUNT);
if (entity.hasEffect(ModEffects.CHARM.get())) validateCharm(entity); if (entity.hasEffect(ModEffects.CHARM.get())) validateCharm(entity);
else EffectSourceUtil.clear(entity, EffectSourceUtil.CHARM); else EffectSourceUtil.clear(entity, EffectSourceUtil.CHARM);
if (!entity.hasEffect(ModEffects.OVERCHARM.get())) if (!entity.hasEffect(ModEffects.OVERCHARM.get()))
@@ -48,9 +73,14 @@ public final class ControlEffectHelper {
entity.getPersistentData().remove(FEAR_DISTANCE); entity.getPersistentData().remove(FEAR_DISTANCE);
return; return;
} }
LivingEntity source = validControlSource(entity, EffectSourceUtil.FEAR); LivingEntity loadedSource = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.FEAR);
if (loadedSource != null && !loadedSource.isAlive()) {
removeControlEffect(entity, ModEffects.FEAR.get(), EffectSourceUtil.FEAR);
entity.getPersistentData().remove(FEAR_DISTANCE);
return;
}
LivingEntity source = loadedSource;
if (source == null) { if (source == null) {
if (entity instanceof Mob mob) mob.getNavigation().stop();
entity.getPersistentData().remove(FEAR_DISTANCE); entity.getPersistentData().remove(FEAR_DISTANCE);
clearFearPlayerPenalties(entity); clearFearPlayerPenalties(entity);
return; return;
@@ -74,8 +104,9 @@ public final class ControlEffectHelper {
if (horizontal.lengthSqr() < 1.0E-8) horizontal = new Vec3(1, 0, 0); if (horizontal.lengthSqr() < 1.0E-8) horizontal = new Vec3(1, 0, 0);
Vec3 destination = mob.position().add(horizontal.normalize() Vec3 destination = mob.position().add(horizontal.normalize()
.scale(AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_DISTANCE))); .scale(AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_DISTANCE)));
mob.getNavigation().moveTo(destination.x, destination.y, destination.z, if (mob.tickCount % 10 == 0)
AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_SPEED)); mob.getNavigation().moveTo(destination.x, destination.y, destination.z,
AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_SPEED));
} }
private static void restrictFearedPlayer(Player player, LivingEntity source) { private static void restrictFearedPlayer(Player player, LivingEntity source) {
@@ -107,11 +138,13 @@ public final class ControlEffectHelper {
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT); removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
return; return;
} }
LivingEntity source = validControlSource(entity, EffectSourceUtil.TAUNT); LivingEntity loadedSource = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.TAUNT);
if (source == null) { if (loadedSource != null && !loadedSource.isAlive()) {
if (entity instanceof Mob mob && mob.getTarget() != null) mob.setTarget(null); removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
return; return;
} }
LivingEntity source = loadedSource;
if (source == null) return;
if (MentalControlUtil.isMentalControlResistant(entity)) return; if (MentalControlUtil.isMentalControlResistant(entity)) return;
if (entity instanceof Mob mob && mob.getTarget() != source) mob.setTarget(source); if (entity instanceof Mob mob && mob.getTarget() != source) mob.setTarget(source);
else if (entity instanceof Player player) else if (entity instanceof Player player)
@@ -121,17 +154,31 @@ public final class ControlEffectHelper {
private static void validateCharm(LivingEntity entity) { private static void validateCharm(LivingEntity entity) {
if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.CHARM) == null) if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.CHARM) == null)
removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM); removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM);
else {
LivingEntity source = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.CHARM);
if (source != null && !source.isAlive())
removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM);
}
} }
public static boolean isOwnCharmSource(LivingEntity charmed, LivingEntity possibleSource) { public static boolean isOwnCharmSource(LivingEntity charmed, LivingEntity possibleSource) {
LivingEntity source = EffectSourceUtil.getSource(charmed, EffectSourceUtil.CHARM); UUID sourceId = EffectSourceUtil.getSourceUuid(charmed, EffectSourceUtil.CHARM);
return source != null && source.getUUID().equals(possibleSource.getUUID()); return sourceId != null && sourceId.equals(possibleSource.getUUID());
} }
public static LivingEntity validTauntSource(LivingEntity entity) { public static LivingEntity validTauntSource(LivingEntity entity) {
return validControlSource(entity, EffectSourceUtil.TAUNT); return validControlSource(entity, EffectSourceUtil.TAUNT);
} }
/** Clears transient state that is not owned by the vanilla MobEffect instance itself. */
public static void clearRuntimeState(LivingEntity entity) {
CompoundTag data = entity.getPersistentData();
data.remove(FEAR_DISTANCE);
data.remove(SOUL_FOOD);
data.remove(SOUL_SATURATION);
clearFearPlayerPenalties(entity);
}
private static LivingEntity validControlSource(LivingEntity target, String key) { private static LivingEntity validControlSource(LivingEntity target, String key) {
LivingEntity source = EffectSourceUtil.getSourceInLevel(target, key); LivingEntity source = EffectSourceUtil.getSourceInLevel(target, key);
return source != null && source.isAlive() ? source : null; return source != null && source.isAlive() ? source : null;
@@ -180,24 +227,25 @@ public final class ControlEffectHelper {
data.putFloat(SOUL_SATURATION, food.getSaturationLevel()); data.putFloat(SOUL_SATURATION, food.getSaturationLevel());
} }
private static void cleanDotData(LivingEntity entity) { private static void cleanDotData(LivingEntity entity, boolean soulRotRelevant, boolean bleedRelevant,
if (!AilmentApi.canSoulRot(entity)) { boolean fractureRelevant) {
if (soulRotRelevant && !AilmentApi.canSoulRot(entity)) {
if (entity.hasEffect(ModEffects.SOUL_ROT.get())) entity.removeEffect(ModEffects.SOUL_ROT.get()); if (entity.hasEffect(ModEffects.SOUL_ROT.get())) entity.removeEffect(ModEffects.SOUL_ROT.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT); EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT);
} else if (!entity.hasEffect(ModEffects.SOUL_ROT.get())) { } else if (soulRotRelevant && !entity.hasEffect(ModEffects.SOUL_ROT.get())) {
EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT); EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT);
} }
if (!AilmentApi.canBleed(entity)) { if (bleedRelevant && !AilmentApi.canBleed(entity)) {
if (entity.hasEffect(ModEffects.BLEED.get())) entity.removeEffect(ModEffects.BLEED.get()); if (entity.hasEffect(ModEffects.BLEED.get())) entity.removeEffect(ModEffects.BLEED.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED); EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
HemorrhageTracker.clearIfPresent(entity); HemorrhageTracker.clearIfPresent(entity);
} else if (!entity.hasEffect(ModEffects.BLEED.get())) { } else if (bleedRelevant && !entity.hasEffect(ModEffects.BLEED.get())) {
EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED); EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
} }
if (!AilmentApi.canFracture(entity)) { if (fractureRelevant && !AilmentApi.canFracture(entity)) {
if (entity.hasEffect(ModEffects.FRACTURE.get())) entity.removeEffect(ModEffects.FRACTURE.get()); if (entity.hasEffect(ModEffects.FRACTURE.get())) entity.removeEffect(ModEffects.FRACTURE.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE); EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE);
} else if (!entity.hasEffect(ModEffects.FRACTURE.get())) { } else if (fractureRelevant && !entity.hasEffect(ModEffects.FRACTURE.get())) {
EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE); EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE);
} }
} }
@@ -64,6 +64,12 @@ public final class EffectSourceUtil {
return data.contains(key) ? Math.max(0, data.getFloat(key)) : 1.0F; return data.contains(key) ? Math.max(0, data.getFloat(key)) : 1.0F;
} }
/** Returns whether source or potency metadata exists for an ailment without resolving an entity. */
public static boolean hasData(LivingEntity target, String ailment) {
CompoundTag data = target.getPersistentData();
return data.contains(sourceKey(ailment)) || data.contains(potencyKey(ailment));
}
public static void clear(LivingEntity target, String ailment) { public static void clear(LivingEntity target, String ailment) {
target.getPersistentData().remove(sourceKey(ailment)); target.getPersistentData().remove(sourceKey(ailment));
target.getPersistentData().remove(potencyKey(ailment)); target.getPersistentData().remove(potencyKey(ailment));
@@ -54,6 +54,10 @@ public final class HemorrhageTracker {
return data.contains(PROGRESS) ? Math.max(0.0, data.getDouble(PROGRESS)) : 0.0; return data.contains(PROGRESS) ? Math.max(0.0, data.getDouble(PROGRESS)) : 0.0;
} }
public static boolean hasProgress(LivingEntity target) {
return target.getPersistentData().contains(PROGRESS);
}
public static double getFraction(LivingEntity target) { public static double getFraction(LivingEntity target) {
double threshold = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_THRESHOLD); double threshold = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_THRESHOLD);
return threshold <= 0 ? 0.0 : Math.min(1.0, getProgress(target) / threshold); return threshold <= 0 ? 0.0 : Math.min(1.0, getProgress(target) / threshold);
+1 -1
View File
@@ -75,7 +75,7 @@ description='''${mod_description}'''
[[dependencies.${mod_id}]] [[dependencies.${mod_id}]]
modId="ascend" modId="ascend"
mandatory=false mandatory=false
versionRange="[0,)" versionRange="[3.0.7,4)"
ordering="AFTER" ordering="AFTER"
side="BOTH" side="BOTH"