package net.fretux.ailments.util; import net.fretux.ailments.api.AilmentApi; import net.fretux.ailments.config.AilmentsConfig; 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.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; 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); } 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())) EffectSourceUtil.clear(entity, EffectSourceUtil.OVERCHARM); } private static void tickFear(LivingEntity entity) { if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.FEAR) == null) { removeControlEffect(entity, ModEffects.FEAR.get(), EffectSourceUtil.FEAR); entity.getPersistentData().remove(FEAR_DISTANCE); return; } 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) { 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)); return; } if (entity instanceof Mob mob) flee(mob, source); else if (entity instanceof Player player) restrictFearedPlayer(player, source); } private static void flee(Mob mob, LivingEntity source) { mob.setTarget(null); if (mob.distanceToSqr(source) > 400.0) return; Vec3 away = mob.position().subtract(source.position()); Vec3 horizontal = new Vec3(away.x, 0, away.z); 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)); } private static void restrictFearedPlayer(Player player, LivingEntity source) { double distance = player.distanceToSqr(source); CompoundTag data = player.getPersistentData(); if (distance <= 144.0) { Vec3 toward = source.position().subtract(player.position()); Vec3 horizontal = new Vec3(toward.x, 0, toward.z); if (horizontal.lengthSqr() >= 1.0E-8) { Vec3 direction = horizontal.normalize(); Vec3 velocity = player.getDeltaMovement(); double towardSpeed = velocity.x * direction.x + velocity.z * direction.z; if (towardSpeed > 0) player.setDeltaMovement(velocity.x - direction.x * towardSpeed, velocity.y, velocity.z - direction.z * towardSpeed); if (data.contains(FEAR_DISTANCE) && distance < data.getDouble(FEAR_DISTANCE)) { double push = AilmentsConfig.value(AilmentsConfig.FEAR_PLAYER_PUSH); player.push(-direction.x * push, 0, -direction.z * push); player.hurtMarked = true; } } } data.putDouble(FEAR_DISTANCE, distance); 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 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) player.lookAt(EntityAnchorArgument.Anchor.EYES, source.getEyePosition()); } 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) { 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; } 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) { if (!(entity instanceof Player player)) return; CompoundTag data = player.getPersistentData(); if (!player.hasEffect(ModEffects.SOUL_ROT.get())) { data.remove(SOUL_FOOD); data.remove(SOUL_SATURATION); return; } FoodData food = player.getFoodData(); if (data.contains(SOUL_FOOD)) food.setFoodLevel(Math.min(food.getFoodLevel(), data.getInt(SOUL_FOOD))); if (data.contains(SOUL_SATURATION)) food.setSaturation(Math.min(food.getSaturationLevel(), data.getFloat(SOUL_SATURATION))); data.putInt(SOUL_FOOD, food.getFoodLevel()); data.putFloat(SOUL_SATURATION, food.getSaturationLevel()); } 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 (soulRotRelevant && !entity.hasEffect(ModEffects.SOUL_ROT.get())) { EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT); } 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 (bleedRelevant && !entity.hasEffect(ModEffects.BLEED.get())) { EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED); } if (fractureRelevant && !AilmentApi.canFracture(entity)) { if (entity.hasEffect(ModEffects.FRACTURE.get())) entity.removeEffect(ModEffects.FRACTURE.get()); EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE); } else if (fractureRelevant && !entity.hasEffect(ModEffects.FRACTURE.get())) { EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE); } } private ControlEffectHelper() {} }