fix various issues throughout the entire project

This commit is contained in:
2026-08-26 19:38:15 +02:00
parent 03deff254f
commit 5650e9979f
10 changed files with 164 additions and 53 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ Item tags:
Direct melee hits with items in these tags apply their corresponding ailment. Soul Rot, Bleed, and Fracture use
their configured stack/duration behavior; the other tags use `taggedWeaponEffectDurationTicks`. Bleed also applies to non-vanilla item
registry paths containing a configured katana-family keyword. Automatic application, proc chance, matching keywords,
and the PvP proc cooldown are configurable under `integration` in `ascend-ailments-common.toml`. PvP cooldowns are
and the PvP proc cooldown are configurable under `integration` in `ascend-ailments-server.toml`. PvP cooldowns are
tracked per attacking player and ailment; their configured minimum is 60 ticks (3 seconds).
## Admin commands
@@ -25,8 +25,9 @@ public final class AscendAilments {
ModEffects.register(modBus);
AilmentNetwork.register();
modBus.addListener(this::commonSetup);
context.registerConfig(ModConfig.Type.COMMON, AilmentsConfig.SPEC,
"ascend-ailments-common.toml");
// Gameplay settings belong to the server and are synchronized to multiplayer clients by Forge.
context.registerConfig(ModConfig.Type.SERVER, AilmentsConfig.SPEC,
"ascend-ailments-server.toml");
MinecraftForge.EVENT_BUS.addListener(this::registerCommands);
}
@@ -1,6 +1,7 @@
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.MentalControlUtil;
@@ -117,8 +118,7 @@ public final class AilmentApi {
int base = AilmentsConfig.value(AilmentsConfig.SOUL_ROT_DURATION);
if (extendWithStacks) base = safeAdd(base, amplifier * 20);
int duration = AilmentScaling.scaleDuration(base, source);
target.removeEffect(effect);
boolean applied = target.addEffect(instance(effect, duration, amplifier, true));
boolean applied = addStackingEffect(target, source, effect, duration, amplifier, true);
if (applied) {
EffectSourceUtil.setSource(target, EffectSourceUtil.SOUL_ROT, source);
EffectSourceUtil.setPotency(target, EffectSourceUtil.SOUL_ROT,
@@ -148,8 +148,7 @@ public final class AilmentApi {
: amplifier == 1 ? AilmentsConfig.value(AilmentsConfig.BLEED_DURATION_1)
: AilmentsConfig.value(AilmentsConfig.BLEED_DURATION_2);
int duration = AilmentScaling.scaleDuration(base, source);
target.removeEffect(effect);
boolean applied = target.addEffect(instance(effect, duration, amplifier, true));
boolean applied = addStackingEffect(target, source, effect, duration, amplifier, true);
if (applied) {
EffectSourceUtil.setSource(target, EffectSourceUtil.BLEED, source);
EffectSourceUtil.setPotency(target, EffectSourceUtil.BLEED,
@@ -191,8 +190,7 @@ public final class AilmentApi {
int stackedAmplifier = current == null ? 0 : Math.min(2, current.getAmplifier() + 1);
int appliedAmplifier = Math.min(2, Math.max(stackedAmplifier, amplifier));
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
target.removeEffect(effect);
boolean applied = target.addEffect(instance(effect, duration, appliedAmplifier, true));
boolean applied = addStackingEffect(target, source, effect, duration, appliedAmplifier, true);
if (applied) EffectSourceUtil.setSource(target, EffectSourceUtil.FRACTURE, source);
return finishApplication(applied, target, source, AilmentType.FRACTURE);
}
@@ -233,12 +231,8 @@ public final class AilmentApi {
public static boolean applyOvercharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.OVERCHARM)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
boolean result = target.addEffect(instance(ModEffects.OVERCHARM.get(), duration, 0, true));
boolean result = addSourcedEffect(target, source, ModEffects.OVERCHARM.get(), duration, 0, true);
if (result) {
// Ascend itself scales beneficial effects received by players. Restore the source-scaled duration so
// this API remains the single authority and cannot be scaled twice when both mods are installed.
MobEffectInstance active = target.getEffect(ModEffects.OVERCHARM.get());
if (active != null) active.mapDuration(ignored -> duration);
EffectSourceUtil.setSource(target, EffectSourceUtil.OVERCHARM, source);
}
return finishApplication(result, target, source, AilmentType.OVERCHARM);
@@ -247,7 +241,7 @@ public final class AilmentApi {
public static boolean applyWinded(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.WINDED)) return false;
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
return finishApplication(target.addEffect(instance(ModEffects.WINDED.get(), duration, 0, true)),
return finishApplication(addSourcedEffect(target, source, ModEffects.WINDED.get(), duration, 0, true),
target, source, AilmentType.WINDED);
}
@@ -311,15 +305,14 @@ public final class AilmentApi {
private static boolean applySourced(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
String key, int duration, int amplifier) {
boolean applied = target.addEffect(instance(effect, Math.max(1, duration), amplifier, true));
boolean applied = addSourcedEffect(target, source, effect, Math.max(1, duration), amplifier, true);
if (applied) EffectSourceUtil.setSource(target, key, source);
return applied;
}
private static boolean applyDot(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
String key, int requestedTicks, int amplifier, AilmentType type) {
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
target.removeEffect(effect);
boolean applied = target.addEffect(instance(effect, duration, amplifier, true));
boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, true);
if (applied) {
EffectSourceUtil.setSource(target, key, source);
EffectSourceUtil.setPotency(target, key, AilmentScaling.getPotencyMultiplier(source, type));
@@ -329,6 +322,32 @@ public final class AilmentApi {
private static MobEffectInstance instance(MobEffect effect, int duration, int amplifier, boolean icon) {
return new MobEffectInstance(effect, duration, amplifier, false, false, icon);
}
private static boolean addSourcedEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean icon) {
boolean applied = target.addEffect(instance(effect, duration, amplifier, icon), source);
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);
}
return applied;
}
private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean icon) {
MobEffectInstance previous = target.getEffect(effect);
if (previous == null || previous.getAmplifier() >= amplifier)
return addSourcedEffect(target, source, effect, duration, amplifier, icon);
// Vanilla preserves a lower-amplifier effect as a hidden fallback when upgrading. These ailments refresh
// to one authoritative stack state, so suppress that fallback without emitting a remove/add lifecycle.
int previousDuration = previous.getDuration();
previous.mapDuration(ignored -> 0);
boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, icon);
if (!applied && target.getEffect(effect) == previous)
previous.mapDuration(ignored -> previousDuration);
return applied;
}
private static boolean serverValid(LivingEntity target) {
return target != null && !target.level().isClientSide && target.isAlive()
&& (!(target instanceof Player player) || !player.isSpectator());
@@ -347,7 +366,10 @@ 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 instanceof Player) || !(target instanceof Player) || source == target) return true;
if (source != null && source.level() != target.level()) 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)
@@ -25,7 +25,7 @@ public final class AscendCompat {
try {
resolve();
return statsCapability != null && getAttributeLevel != null;
} catch (ReflectiveOperationException | LinkageError ignored) {
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
return false;
}
}
@@ -39,20 +39,27 @@ public final class AscendCompat {
Optional<?> stats = lazy.resolve();
if (stats.isEmpty()) return 0;
return Math.max(0, ((Number) getAttributeLevel.invoke(stats.get(), "arcane")).intValue());
} catch (ReflectiveOperationException | LinkageError ignored) {
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
return 0;
}
}
private static synchronized void resolve() throws ReflectiveOperationException {
if (resolved) return;
resolved = true;
Class<?> provider = Class.forName(PROVIDER, false, AscendCompat.class.getClassLoader());
Field capabilityField = provider.getField("PLAYER_STATS");
statsCapability = (Capability<?>) capabilityField.get(null);
Capability<?> resolvedCapability = (Capability<?>) capabilityField.get(null);
Class<?> statsClass = Class.forName("net.fretux.ascend.player.PlayerStats", false,
AscendCompat.class.getClassLoader());
getAttributeLevel = statsClass.getMethod("getAttributeLevel", String.class);
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 AscendCompat() {}
}
@@ -6,15 +6,23 @@ import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.damagesource.DamageType;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
public final class ModDamageSources {
public static final ResourceKey<DamageType> BLEED = ResourceKey.create(Registries.DAMAGE_TYPE,
ResourceLocation.fromNamespaceAndPath(AscendAilments.MOD_ID, "bleed"));
public static DamageSource bleed(Level level) {
return new DamageSource(level.registryAccess().registryOrThrow(Registries.DAMAGE_TYPE)
.getHolderOrThrow(BLEED));
return bleed(level, null);
}
public static DamageSource bleed(Level level, @Nullable Entity source) {
var type = level.registryAccess().registryOrThrow(Registries.DAMAGE_TYPE).getHolderOrThrow(BLEED);
// Periodic Bleed has an owning attacker for attribution, but no direct entity (it is not another melee hit).
return source == null ? new DamageSource(type) : new DamageSource(type, null, source);
}
private ModDamageSources() {}
}
@@ -24,7 +24,8 @@ public final class BleedEffect extends MobEffect {
: amplifier == 1 ? AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_1)
: AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_2);
float potency = EffectSourceUtil.getPotency(entity, EffectSourceUtil.BLEED);
boolean damaged = entity.hurt(ModDamageSources.bleed(entity.level()), (float) (base * potency));
LivingEntity source = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.BLEED);
boolean damaged = entity.hurt(ModDamageSources.bleed(entity.level(), source), (float) (base * potency));
if (damaged) HemorrhageTracker.recordBleedDamage(entity, amplifier, potency);
}
}
@@ -3,6 +3,7 @@ package net.fretux.ailments.event;
import net.fretux.ailments.AscendAilments;
import net.fretux.ailments.api.AilmentApi;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.damage.ModDamageSources;
import net.fretux.ailments.registry.ModEffects;
import net.fretux.ailments.util.ControlEffectHelper;
import net.fretux.ailments.util.EffectSourceUtil;
@@ -20,10 +21,12 @@ import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.event.entity.living.LivingChangeTargetEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.living.LivingDamageEvent;
import net.minecraftforge.event.entity.living.LivingHealEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.registries.ForgeRegistries;
@@ -54,12 +57,14 @@ public final class AilmentEvents {
@SubscribeEvent
public static void onHeal(LivingHealEvent event) {
if (!event.isCanceled() && event.getEntity().hasEffect(ModEffects.SOUL_ROT.get()))
if (!event.getEntity().level().isClientSide && !event.isCanceled()
&& event.getEntity().hasEffect(ModEffects.SOUL_ROT.get()))
event.setAmount((float) (event.getAmount() * AilmentsConfig.value(AilmentsConfig.SOUL_ROT_HEALING)));
}
@SubscribeEvent
public static void onAttack(LivingAttackEvent event) {
if (event.getEntity().level().isClientSide) return;
Entity attacker = event.getSource().getEntity();
if (attacker instanceof LivingEntity living && !(living instanceof Player)
&& living.hasEffect(ModEffects.CHARM.get())
@@ -68,6 +73,7 @@ public final class AilmentEvents {
@SubscribeEvent
public static void onChangeTarget(LivingChangeTargetEvent event) {
if (event.getEntity().level().isClientSide) return;
if (!(event.getEntity() instanceof Mob mob)) return;
LivingEntity taunter = mob.hasEffect(ModEffects.TAUNT.get())
&& !MentalControlUtil.isMentalControlResistant(mob)
@@ -82,11 +88,12 @@ public final class AilmentEvents {
@SubscribeEvent
public static void onHurt(LivingHurtEvent event) {
if (event.isCanceled()) return;
if (event.getEntity().level().isClientSide || event.isCanceled()) return;
LivingEntity victim = event.getEntity();
DamageSource damageSource = event.getSource();
Entity causing = damageSource.getEntity();
LivingEntity attacker = causing instanceof LivingEntity living ? living : null;
LivingEntity attacker = !damageSource.is(ModDamageSources.BLEED) && causing instanceof LivingEntity living
? living : null;
float amount = event.getAmount();
// 1. Victim vulnerability.
@@ -119,14 +126,25 @@ public final class AilmentEvents {
boolean directPlayerMelee = attacker instanceof Player && damageSource.getDirectEntity() == attacker;
if (alreadyCharmedByAttacker && directPlayerMelee)
amount *= (float) AilmentsConfig.value(AilmentsConfig.OVERCHARM_DAMAGE_BONUS);
}
event.setAmount(amount);
}
/** Applies hit-triggered ailments only after armor, absorption, and earlier cancellation hooks are resolved. */
@SubscribeEvent(priority = EventPriority.LOWEST)
public static void onDamage(LivingDamageEvent event) {
if (event.getEntity().level().isClientSide || event.isCanceled() || event.getAmount() <= 0) return;
LivingEntity victim = event.getEntity();
DamageSource damageSource = event.getSource();
if (damageSource.is(ModDamageSources.BLEED)) return;
Entity causing = damageSource.getEntity();
if (!(causing instanceof LivingEntity attacker) || attacker == victim) return;
if (attacker.hasEffect(ModEffects.OVERCHARM.get()))
AilmentApi.applyCharm(victim, attacker,
AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION));
}
if (attacker != null && attacker != victim && amount > 0
&& damageSource.getDirectEntity() == attacker)
if (damageSource.getDirectEntity() == attacker)
applyTaggedWeaponAilments(attacker.getMainHandItem(), victim, attacker);
event.setAmount(amount);
}
private static void applyTaggedWeaponAilments(ItemStack stack, LivingEntity victim, LivingEntity attacker) {
@@ -6,17 +6,23 @@ import net.fretux.ailments.registry.ModEffects;
import net.minecraft.commands.arguments.EntityAnchorArgument;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.food.FoodData;
import net.minecraft.world.phys.Vec3;
import java.util.UUID;
public final class ControlEffectHelper {
private static final String FEAR_DISTANCE = "ascend_ailments.fear.last_distance_sqr";
private static final String SOUL_FOOD = "ascend_ailments.soul_rot.last_food";
private static final String SOUL_SATURATION = "ascend_ailments.soul_rot.last_saturation";
private static final UUID FEAR_MOVEMENT_UUID = UUID.fromString("8158dc2a-8de9-49a2-b086-bd99ade47661");
private static final UUID FEAR_ATTACK_UUID = UUID.fromString("4468e408-78e8-49f6-a7bc-9ae9028a0f48");
public static void handleLivingTick(LivingEntity entity) {
if (entity.level().isClientSide) return;
@@ -26,6 +32,7 @@ public final class ControlEffectHelper {
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);
@@ -36,14 +43,20 @@ public final class ControlEffectHelper {
}
private static void tickFear(LivingEntity entity) {
LivingEntity source = validControlSource(entity, EffectSourceUtil.FEAR);
if (source == null) {
entity.removeEffect(ModEffects.FEAR.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR);
if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.FEAR) == null) {
removeControlEffect(entity, ModEffects.FEAR.get(), EffectSourceUtil.FEAR);
entity.getPersistentData().remove(FEAR_DISTANCE);
return;
}
LivingEntity source = validControlSource(entity, EffectSourceUtil.FEAR);
if (source == null) {
if (entity instanceof Mob mob) mob.getNavigation().stop();
entity.getPersistentData().remove(FEAR_DISTANCE);
clearFearPlayerPenalties(entity);
return;
}
if (MentalControlUtil.isMentalControlResistant(entity)) {
clearFearPlayerPenalties(entity);
MobEffectInstance winded = entity.getEffect(ModEffects.WINDED.get());
if (winded == null || winded.getDuration() < 10)
entity.addEffect(new MobEffectInstance(ModEffects.WINDED.get(), 25, 0, false, false, false));
@@ -86,17 +99,17 @@ public final class ControlEffectHelper {
}
}
data.putDouble(FEAR_DISTANCE, distance);
if (distance <= 36.0) {
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 20, 3, false, false, false));
player.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 20, 0, false, false, false));
}
setFearPlayerPenalties(player, distance <= 36.0);
}
private static void tickTaunt(LivingEntity entity) {
if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.TAUNT) == null) {
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
return;
}
LivingEntity source = validControlSource(entity, EffectSourceUtil.TAUNT);
if (source == null) {
entity.removeEffect(ModEffects.TAUNT.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.TAUNT);
if (entity instanceof Mob mob && mob.getTarget() != null) mob.setTarget(null);
return;
}
if (MentalControlUtil.isMentalControlResistant(entity)) return;
@@ -106,10 +119,8 @@ public final class ControlEffectHelper {
}
private static void validateCharm(LivingEntity entity) {
if (validControlSource(entity, EffectSourceUtil.CHARM) == null) {
entity.removeEffect(ModEffects.CHARM.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.CHARM);
}
if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.CHARM) == null)
removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM);
}
public static boolean isOwnCharmSource(LivingEntity charmed, LivingEntity possibleSource) {
@@ -122,8 +133,37 @@ public final class ControlEffectHelper {
}
private static LivingEntity validControlSource(LivingEntity target, String key) {
LivingEntity source = EffectSourceUtil.getSource(target, key);
return source != null && source.isAlive() && source.level() == target.level() ? source : null;
LivingEntity source = EffectSourceUtil.getSourceInLevel(target, key);
return source != null && source.isAlive() ? source : null;
}
private static void setFearPlayerPenalties(Player player, boolean active) {
setTransientModifier(player.getAttribute(Attributes.MOVEMENT_SPEED), FEAR_MOVEMENT_UUID,
"Ascend Ailments fear movement", -0.60, AttributeModifier.Operation.MULTIPLY_TOTAL, active);
setTransientModifier(player.getAttribute(Attributes.ATTACK_DAMAGE), FEAR_ATTACK_UUID,
"Ascend Ailments fear weakness", -4.0, AttributeModifier.Operation.ADDITION, active);
}
private static void setTransientModifier(AttributeInstance attribute, UUID id, String name, double amount,
AttributeModifier.Operation operation, boolean active) {
if (attribute == null) return;
AttributeModifier existing = attribute.getModifier(id);
if (!active) {
if (existing != null) attribute.removeModifier(id);
} else if (existing == null) {
attribute.addTransientModifier(new AttributeModifier(id, name, amount, operation));
}
}
private static void clearFearPlayerPenalties(LivingEntity entity) {
if (entity instanceof Player player) setFearPlayerPenalties(player, false);
}
private static void removeControlEffect(LivingEntity entity, net.minecraft.world.effect.MobEffect effect,
String key) {
entity.removeEffect(effect);
EffectSourceUtil.clear(entity, key);
if (key.equals(EffectSourceUtil.FEAR)) clearFearPlayerPenalties(entity);
}
private static void handleSoulRotFood(LivingEntity entity) {
@@ -34,13 +34,26 @@ public final class EffectSourceUtil {
UUID id = getSourceUuid(target, ailment);
if (id == null || !(target.level() instanceof ServerLevel level)) return null;
MinecraftServer server = level.getServer();
Entity player = server.getPlayerList().getPlayer(id);
if (player instanceof LivingEntity living) return living;
Entity local = level.getEntity(id);
if (local instanceof LivingEntity living) return living;
for (ServerLevel candidate : server.getAllLevels()) {
if (candidate == level) continue;
Entity entity = candidate.getEntity(id);
if (entity instanceof LivingEntity living) return living;
}
return null;
}
/** Resolves a loaded source only in the target's current level without scanning every server dimension. */
@Nullable public static LivingEntity getSourceInLevel(LivingEntity target, String ailment) {
UUID id = getSourceUuid(target, ailment);
if (id == null || !(target.level() instanceof ServerLevel level)) return null;
Entity entity = level.getEntity(id);
return entity instanceof LivingEntity living ? living : null;
}
public static void setPotency(LivingEntity target, String ailment, double potency) {
target.getPersistentData().putFloat(potencyKey(ailment), (float) Math.max(0, potency));
}
@@ -34,7 +34,8 @@ public final class HemorrhageTracker {
if (target.isAlive()) {
float damage = (float) (target.getMaxHealth()
* AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_MAX_HEALTH_DAMAGE));
if (damage > 0) target.hurt(ModDamageSources.bleed(target.level()), damage);
LivingEntity source = EffectSourceUtil.getSourceInLevel(target, EffectSourceUtil.BLEED);
if (damage > 0) target.hurt(ModDamageSources.bleed(target.level(), source), damage);
}
return true;
}