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
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);
// 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);
```
`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
`applyScaledEffect` provides the non-stacking Arcane-scaled variant. Source-aware applications reject infinite or
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;
`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
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
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
`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
snapshotted on application so source unloading does not alter an active effect.
missing capabilities, non-player sources, and absent/unloaded source data all fall back to 1.0x. An incompatible API
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
@@ -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;
}
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,
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,30 +27,27 @@ 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;
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);
@@ -59,7 +61,18 @@ public final class AscendCompat {
throw new NoSuchMethodException("getAttributeLevel(String) does not return a number");
statsCapability = resolvedCapability;
getAttributeLevel = resolvedGetter;
resolved = true;
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,7 +119,9 @@ 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)
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));
}
@@ -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,6 +104,7 @@ 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)));
if (mob.tickCount % 10 == 0)
mob.getNavigation().moveTo(destination.x, destination.y, destination.z,
AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_SPEED));
}
@@ -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);
+1 -1
View File
@@ -75,7 +75,7 @@ description='''${mod_description}'''
[[dependencies.${mod_id}]]
modId="ascend"
mandatory=false
versionRange="[0,)"
versionRange="[3.0.7,4)"
ordering="AFTER"
side="BOTH"