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
@@ -1,9 +1,9 @@
package net.fretux.ailments.api;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.compat.AscendCompat;
import net.fretux.ailments.registry.ModEffects;
import net.fretux.ailments.util.EffectSourceUtil;
import net.fretux.ailments.util.ControlEffectHelper;
import net.fretux.ailments.util.MentalControlUtil;
import net.fretux.ailments.util.ModEntityTypeTags;
import net.fretux.ailments.util.HemorrhageTracker;
@@ -88,9 +88,33 @@ public final class AilmentApi {
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.
* 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,
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. */
public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source,
boolean extendWithStacks) {
if (!serverValid(target) || !canSoulRot(target)
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
if (!harmfulApplicationValid(target, source) || !canSoulRot(target)) return false;
MobEffect effect = ModEffects.SOUL_ROT.get();
MobEffectInstance current = target.getEffect(effect);
int currentStacks = current == null ? 0 : current.getAmplifier() + 1;
@@ -219,23 +242,23 @@ public final class AilmentApi {
EffectSourceUtil.setPotency(target, EffectSourceUtil.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. */
public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source,
int requestedTicks, int amplifier) {
if (!serverValid(target) || !canSoulRot(target)
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
if (!harmfulApplicationValid(target, source) || !canSoulRot(target) || requestedTicks < 1 || amplifier < 0)
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,
requestedTicks, capped, AilmentType.SOUL_ROT), target, source, AilmentType.SOUL_ROT);
return applyDot(target, source, ModEffects.SOUL_ROT.get(), EffectSourceUtil.SOUL_ROT,
requestedTicks, capped, AilmentType.SOUL_ROT);
}
/** Applies or adds one Bleed stack, returning false for immune targets. */
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();
MobEffectInstance current = target.getEffect(effect);
int amplifier = current == null ? 0 : Math.min(current.getAmplifier() + 1,
@@ -250,17 +273,18 @@ public final class AilmentApi {
EffectSourceUtil.setPotency(target, EffectSourceUtil.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. */
public static boolean applyBleed(LivingEntity target, @Nullable LivingEntity source,
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),
AilmentsConfig.value(AilmentsConfig.BLEED_MAX_AMPLIFIER));
return finishApplication(applyDot(target, source, ModEffects.BLEED.get(), EffectSourceUtil.BLEED,
requestedTicks, capped, AilmentType.BLEED), target, source, AilmentType.BLEED);
return applyDot(target, source, ModEffects.BLEED.get(), EffectSourceUtil.BLEED,
requestedTicks, capped, AilmentType.BLEED);
}
/** 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,
int requestedTicks, int amplifier) {
if (!serverValid(target) || !canFracture(target) || requestedTicks < 1 || amplifier < 0
|| !pvpProcReady(target, source, AilmentType.FRACTURE)) return false;
if (!harmfulApplicationValid(target, source) || !canFracture(target) || requestedTicks < 1 || amplifier < 0)
return false;
MobEffect effect = ModEffects.FRACTURE.get();
MobEffectInstance current = target.getEffect(effect);
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);
boolean applied = addStackingEffect(target, source, effect, duration, appliedAmplifier, true);
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) {
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target)
|| target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_IMMUNITY_HEALTH)
|| !pvpProcReady(target, source, AilmentType.FEAR)) return false;
if (!hostileControlValid(target, source) || requestedTicks < 1
|| MentalControlUtil.isMentalControlImmune(target)
|| target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_IMMUNITY_HEALTH)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
if (target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_REDUCED_HEALTH)) duration /= 2;
if (MentalControlUtil.isMentalControlResistant(target))
duration = MentalControlUtil.resistantDuration(duration, 3);
if (target instanceof EnderMan) duration = Math.max(1, duration / 2);
return finishApplication(applySourced(target, source, ModEffects.FEAR.get(), EffectSourceUtil.FEAR,
Math.max(1, duration), 0), target, source, AilmentType.FEAR);
return applySourced(target, source, ModEffects.FEAR.get(), EffectSourceUtil.FEAR,
Math.max(1, duration), 0);
}
public static boolean applyCharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target)
|| !pvpProcReady(target, source, AilmentType.CHARM)) return false;
if (!hostileControlValid(target, source) || requestedTicks < 1
|| MentalControlUtil.isMentalControlImmune(target)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
if (MentalControlUtil.isMentalControlResistant(target))
duration = MentalControlUtil.resistantDuration(duration, 2);
return finishApplication(applySourced(target, source, ModEffects.CHARM.get(), EffectSourceUtil.CHARM,
duration, 0), target, source, AilmentType.CHARM);
return applySourced(target, source, ModEffects.CHARM.get(), EffectSourceUtil.CHARM, duration, 0);
}
public static boolean applyTaunt(LivingEntity target, @Nullable LivingEntity source, int requestedTicks, int amplifier) {
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target)
|| !pvpProcReady(target, source, AilmentType.TAUNT)) return false;
if (!hostileControlValid(target, source) || requestedTicks < 1 || amplifier < 0
|| MentalControlUtil.isMentalControlImmune(target)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
if (MentalControlUtil.isMentalControlResistant(target))
duration = MentalControlUtil.resistantDuration(duration, 2);
return finishApplication(applySourced(target, source, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT,
duration, Math.max(0, amplifier)), target, source, AilmentType.TAUNT);
return applySourced(target, source, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT,
duration, amplifier);
}
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);
boolean result = addSourcedEffect(target, source, ModEffects.OVERCHARM.get(), duration, 0, true);
if (result) {
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) {
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.WINDED)) return false;
if (!harmfulApplicationValid(target, source) || requestedTicks < 1) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
return finishApplication(addSourcedEffect(target, source, ModEffects.WINDED.get(), duration, 0, true),
target, source, AilmentType.WINDED);
return addSourcedEffect(target, source, ModEffects.WINDED.get(), duration, 0, true);
}
public static boolean canBleed(LivingEntity target) {
@@ -370,7 +392,9 @@ public final class AilmentApi {
public static double getHemorrhageFraction(LivingEntity 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) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.CHARM);
}
@@ -392,6 +416,7 @@ public final class AilmentApi {
/** Removes all registered ailments and all associated source/potency metadata. */
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.FEAR.get()); target.removeEffect(ModEffects.CHARM.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.OVERCHARM);
EffectSourceUtil.clear(target, EffectSourceUtil.FRACTURE);
ControlEffectHelper.clearRuntimeState(target);
HemorrhageTracker.clear(target);
}
@@ -446,13 +472,17 @@ public final class AilmentApi {
private static boolean addSourcedEffect(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.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);
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.
MobEffectInstance active = target.getEffect(effect);
if (active != null && active.getAmplifier() == amplifier) active.mapDuration(ignored -> duration);
}
if (!applied && target.getEffect(effect) == previous) previous.update(snapshot);
return applied;
}
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,
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, 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;
return addSourcedEffect(target, source, effect, duration, amplifier, ambient, visible, icon);
}
private static boolean externalApplicationValid(LivingEntity target, @Nullable LivingEntity source,
MobEffect effect, int requestedTicks, int amplifier) {
@@ -499,9 +518,18 @@ public final class AilmentApi {
return target != null && !target.level().isClientSide && target.isAlive()
&& (!(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) {
if (!serverValid(target) || source == null || !source.isAlive()
|| source.level().isClientSide || source.isSpectator()) return false;
if (source == null || !applicationValid(target, source)) return false;
return !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer)
|| sourcePlayer.canHarmPlayer(targetPlayer);
}
@@ -513,22 +541,21 @@ public final class AilmentApi {
return (int) Math.min(Integer.MAX_VALUE, (long) a + b);
}
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)
return true;
if (!sourcePlayer.canHarmPlayer(targetPlayer)) return false;
String key = pvpCooldownKey(type);
long now = source.level().getGameTime();
return !source.getPersistentData().contains(key)
|| now - source.getPersistentData().getLong(key)
>= AilmentsConfig.value(AilmentsConfig.PVP_PROC_COOLDOWN_TICKS);
}
private static boolean finishApplication(boolean applied, LivingEntity target,
@Nullable LivingEntity source, AilmentType type) {
private static void recordPvpProc(boolean applied, LivingEntity target,
@Nullable LivingEntity source, AilmentType type) {
if (applied && source instanceof Player && target instanceof Player && source != target) {
source.getPersistentData().putLong(pvpCooldownKey(type), source.level().getGameTime());
}
return applied;
}
private static String pvpCooldownKey(AilmentType type) {
return "ascend_ailments.pvp_proc." + type.name().toLowerCase(java.util.Locale.ROOT);
@@ -1,5 +1,6 @@
package net.fretux.ailments.compat;
import com.mojang.logging.LogUtils;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.common.capabilities.Capability;
@@ -9,11 +10,15 @@ import net.minecraftforge.fml.ModList;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Optional;
import org.slf4j.Logger;
/** Reflection boundary: no Ascend type appears in a loadable signature or constant-pool class reference. */
public final class AscendCompat {
private static final Logger LOGGER = LogUtils.getLogger();
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 Method getAttributeLevel;
@@ -22,44 +27,52 @@ public final class AscendCompat {
/** Resolves the optional public API without requiring an entity; useful for lifecycle diagnostics. */
public static boolean validateIntegration() {
if (!isAscendLoaded()) return false;
try {
resolve();
return statsCapability != null && getAttributeLevel != null;
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
return false;
}
return resolve();
}
public static int getArcane(LivingEntity source) {
if (!(source instanceof Player player) || !isAscendLoaded()) return 0;
try {
resolve();
if (statsCapability == null || getAttributeLevel == null) return 0;
if (!resolve()) return 0;
LazyOptional<?> lazy = player.getCapability(statsCapability);
Optional<?> stats = lazy.resolve();
if (stats.isEmpty()) return 0;
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;
}
}
private static synchronized void resolve() throws ReflectiveOperationException {
if (resolved) return;
Class<?> provider = Class.forName(PROVIDER, false, AscendCompat.class.getClassLoader());
Field capabilityField = provider.getField("PLAYER_STATS");
Capability<?> resolvedCapability = (Capability<?>) capabilityField.get(null);
Class<?> statsClass = Class.forName("net.fretux.ascend.player.PlayerStats", false,
AscendCompat.class.getClassLoader());
Method resolvedGetter = statsClass.getMethod("getAttributeLevel", String.class);
Class<?> returnType = resolvedGetter.getReturnType();
boolean numericPrimitive = returnType == byte.class || returnType == short.class || returnType == int.class
|| returnType == long.class || returnType == float.class || returnType == double.class;
if (!Number.class.isAssignableFrom(returnType) && !numericPrimitive)
throw new NoSuchMethodException("getAttributeLevel(String) does not return a number");
statsCapability = resolvedCapability;
getAttributeLevel = resolvedGetter;
resolved = true;
private static synchronized boolean resolve() {
if (resolutionAttempted) return available;
resolutionAttempted = true;
try {
Class<?> provider = Class.forName(PROVIDER, false, AscendCompat.class.getClassLoader());
Field capabilityField = provider.getField("PLAYER_STATS");
Capability<?> resolvedCapability = (Capability<?>) capabilityField.get(null);
Class<?> statsClass = Class.forName("net.fretux.ascend.player.PlayerStats", false,
AscendCompat.class.getClassLoader());
Method resolvedGetter = statsClass.getMethod("getAttributeLevel", String.class);
Class<?> returnType = resolvedGetter.getReturnType();
boolean numericPrimitive = returnType == byte.class || returnType == short.class || returnType == int.class
|| returnType == long.class || returnType == float.class || returnType == double.class;
if (!Number.class.isAssignableFrom(returnType) && !numericPrimitive)
throw new NoSuchMethodException("getAttributeLevel(String) does not return a number");
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() {}
}
@@ -2,6 +2,8 @@ package net.fretux.ailments.event;
import net.fretux.ailments.AscendAilments;
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.damage.ModDamageSources;
import net.fretux.ailments.registry.ModEffects;
@@ -69,13 +71,24 @@ public final class AilmentEvents {
Entity attacker = event.getSource().getEntity();
if (attacker instanceof LivingEntity living && !(living instanceof Player)
&& 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
public static void onChangeTarget(LivingChangeTargetEvent event) {
if (event.getEntity().level().isClientSide) 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())
&& !MentalControlUtil.isMentalControlResistant(mob)
? ControlEffectHelper.validTauntSource(mob) : null;
@@ -106,9 +119,11 @@ public final class AilmentEvents {
&& ControlEffectHelper.isOwnCharmSource(victim, attacker))
amount *= (float) AilmentsConfig.value(AilmentsConfig.CHARM_VICTIM_BONUS);
if (attacker != null && attacker.hasEffect(ModEffects.CHARM.get())) {
amount *= (float) (ControlEffectHelper.isOwnCharmSource(attacker, victim)
? AilmentsConfig.value(AilmentsConfig.CHARM_SOURCE_PENALTY)
: AilmentsConfig.value(AilmentsConfig.CHARM_OTHER_BONUS));
LivingEntity charmSource = AilmentApi.getCharmSource(attacker);
if (charmSource != null)
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.
@@ -142,8 +157,8 @@ public final class AilmentEvents {
if (!(causing instanceof LivingEntity attacker) || attacker == victim) return;
if (attacker.hasEffect(ModEffects.OVERCHARM.get()))
AilmentApi.applyCharm(victim, attacker,
AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION));
AilmentApi.applyProcEffect(victim, attacker, AilmentType.CHARM,
AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION), 0);
if (damageSource.getDirectEntity() == attacker)
applyTaggedWeaponAilments(attacker.getMainHandItem(), victim, attacker);
}
@@ -152,16 +167,23 @@ public final class AilmentEvents {
if (stack.isEmpty()) return;
if (isAutomaticBleedWeapon(stack) && attacker.getRandom().nextDouble()
< AilmentsConfig.value(AilmentsConfig.AUTOMATIC_WEAPON_BLEED_CHANCE))
AilmentApi.applyBleed(victim, attacker);
if (stack.is(ModItemTags.SOUL_ROT_ON_HIT)) AilmentApi.applySoulRot(victim, attacker);
if (stack.is(ModItemTags.FRACTURE_ON_HIT)) AilmentApi.applyFracture(victim, attacker);
AilmentApi.applyProcEffect(victim, attacker, AilmentApplication.stack(AilmentType.BLEED));
if (stack.is(ModItemTags.SOUL_ROT_ON_HIT))
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);
if (stack.is(ModItemTags.FEAR_ON_HIT)) AilmentApi.applyFear(victim, attacker, duration);
if (stack.is(ModItemTags.CHARM_ON_HIT)) AilmentApi.applyCharm(victim, attacker, duration);
if (stack.is(ModItemTags.TAUNT_ON_HIT)) AilmentApi.applyTaunt(victim, attacker, duration, 0);
if (stack.is(ModItemTags.OVERCHARM_ON_HIT)) AilmentApi.applyOvercharm(victim, attacker, duration);
if (stack.is(ModItemTags.WINDED_ON_HIT)) AilmentApi.applyWinded(victim, attacker, duration);
if (stack.is(ModItemTags.FEAR_ON_HIT))
AilmentApi.applyProcEffect(victim, attacker, AilmentType.FEAR, duration, 0);
if (stack.is(ModItemTags.CHARM_ON_HIT))
AilmentApi.applyProcEffect(victim, attacker, AilmentType.CHARM, duration, 0);
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) {
@@ -26,16 +26,41 @@ public final class ControlEffectHelper {
public static void handleLivingTick(LivingEntity entity) {
if (entity.level().isClientSide) return;
cleanDotData(entity);
handleSoulRotFood(entity);
CompoundTag data = entity.getPersistentData();
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);
else {
EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR);
entity.getPersistentData().remove(FEAR_DISTANCE);
clearFearPlayerPenalties(entity);
}
if (entity.hasEffect(ModEffects.TAUNT.get())) tickTaunt(entity);
else EffectSourceUtil.clear(entity, EffectSourceUtil.TAUNT);
boolean fearDominant = entity.hasEffect(ModEffects.FEAR.get())
&& !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);
else EffectSourceUtil.clear(entity, EffectSourceUtil.CHARM);
if (!entity.hasEffect(ModEffects.OVERCHARM.get()))
@@ -48,9 +73,14 @@ public final class ControlEffectHelper {
entity.getPersistentData().remove(FEAR_DISTANCE);
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 (entity instanceof Mob mob) mob.getNavigation().stop();
entity.getPersistentData().remove(FEAR_DISTANCE);
clearFearPlayerPenalties(entity);
return;
@@ -74,8 +104,9 @@ public final class ControlEffectHelper {
if (horizontal.lengthSqr() < 1.0E-8) horizontal = new Vec3(1, 0, 0);
Vec3 destination = mob.position().add(horizontal.normalize()
.scale(AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_DISTANCE)));
mob.getNavigation().moveTo(destination.x, destination.y, destination.z,
AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_SPEED));
if (mob.tickCount % 10 == 0)
mob.getNavigation().moveTo(destination.x, destination.y, destination.z,
AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_SPEED));
}
private static void restrictFearedPlayer(Player player, LivingEntity source) {
@@ -107,11 +138,13 @@ public final class ControlEffectHelper {
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
return;
}
LivingEntity source = validControlSource(entity, EffectSourceUtil.TAUNT);
if (source == null) {
if (entity instanceof Mob mob && mob.getTarget() != null) mob.setTarget(null);
LivingEntity loadedSource = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.TAUNT);
if (loadedSource != null && !loadedSource.isAlive()) {
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
return;
}
LivingEntity source = loadedSource;
if (source == null) return;
if (MentalControlUtil.isMentalControlResistant(entity)) return;
if (entity instanceof Mob mob && mob.getTarget() != source) mob.setTarget(source);
else if (entity instanceof Player player)
@@ -121,17 +154,31 @@ public final class ControlEffectHelper {
private static void validateCharm(LivingEntity entity) {
if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.CHARM) == null)
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) {
LivingEntity source = EffectSourceUtil.getSource(charmed, EffectSourceUtil.CHARM);
return source != null && source.getUUID().equals(possibleSource.getUUID());
UUID sourceId = EffectSourceUtil.getSourceUuid(charmed, EffectSourceUtil.CHARM);
return sourceId != null && sourceId.equals(possibleSource.getUUID());
}
public static LivingEntity validTauntSource(LivingEntity entity) {
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) {
LivingEntity source = EffectSourceUtil.getSourceInLevel(target, key);
return source != null && source.isAlive() ? source : null;
@@ -180,24 +227,25 @@ public final class ControlEffectHelper {
data.putFloat(SOUL_SATURATION, food.getSaturationLevel());
}
private static void cleanDotData(LivingEntity entity) {
if (!AilmentApi.canSoulRot(entity)) {
private static void cleanDotData(LivingEntity entity, boolean soulRotRelevant, boolean bleedRelevant,
boolean fractureRelevant) {
if (soulRotRelevant && !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())) {
} else if (soulRotRelevant && !entity.hasEffect(ModEffects.SOUL_ROT.get())) {
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());
EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
HemorrhageTracker.clearIfPresent(entity);
} else if (!entity.hasEffect(ModEffects.BLEED.get())) {
} else if (bleedRelevant && !entity.hasEffect(ModEffects.BLEED.get())) {
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());
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);
}
}
@@ -64,6 +64,12 @@ public final class EffectSourceUtil {
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) {
target.getPersistentData().remove(sourceKey(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;
}
public static boolean hasProgress(LivingEntity target) {
return target.getPersistentData().contains(PROGRESS);
}
public static double getFraction(LivingEntity target) {
double threshold = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_THRESHOLD);
return threshold <= 0 ? 0.0 : Math.min(1.0, getProgress(target) / threshold);