Initial Ascend Ailments implementation

This commit is contained in:
2026-08-25 20:59:25 +02:00
commit 03deff254f
65 changed files with 4313 additions and 0 deletions
@@ -0,0 +1,43 @@
package net.fretux.ailments;
import com.mojang.logging.LogUtils;
import net.fretux.ailments.command.AilmentCommands;
import net.fretux.ailments.compat.AscendCompat;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.registry.ModEffects;
import net.fretux.ailments.network.AilmentNetwork;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger;
@Mod(AscendAilments.MOD_ID)
public final class AscendAilments {
public static final String MOD_ID = "ascend_ailments";
private static final Logger LOGGER = LogUtils.getLogger();
public AscendAilments(FMLJavaModLoadingContext context) {
IEventBus modBus = context.getModEventBus();
ModEffects.register(modBus);
AilmentNetwork.register();
modBus.addListener(this::commonSetup);
context.registerConfig(ModConfig.Type.COMMON, AilmentsConfig.SPEC,
"ascend-ailments-common.toml");
MinecraftForge.EVENT_BUS.addListener(this::registerCommands);
}
private void commonSetup(FMLCommonSetupEvent event) {
event.enqueueWork(() -> {
if (AscendCompat.isAscendLoaded())
LOGGER.info("Ascend Arcane integration API available: {}", AscendCompat.validateIntegration());
});
}
private void registerCommands(RegisterCommandsEvent event) {
AilmentCommands.register(event.getDispatcher());
}
}
@@ -0,0 +1,368 @@
package net.fretux.ailments.api;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.registry.ModEffects;
import net.fretux.ailments.util.EffectSourceUtil;
import net.fretux.ailments.util.MentalControlUtil;
import net.fretux.ailments.util.ModEntityTypeTags;
import net.fretux.ailments.util.HemorrhageTracker;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.MobType;
import net.minecraft.world.entity.monster.EnderMan;
import net.minecraft.world.entity.player.Player;
import javax.annotation.Nullable;
import java.util.Objects;
/** Stable, source-aware entry point for applying and querying Ascend: Ailments effects. */
public final class AilmentApi {
/**
* Applies one explicit ailment through the same validation, immunity, scaling, and source tracking used by
* the effect-specific methods. This is the primary entry point for skill engines and item integrations.
*/
public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, AilmentType type,
int requestedTicks, int amplifier) {
Objects.requireNonNull(type, "type");
if (requestedTicks < 1 || amplifier < 0) return false;
return switch (type) {
case SOUL_ROT -> applySoulRot(target, source, requestedTicks, amplifier);
case BLEED -> applyBleed(target, source, requestedTicks, amplifier);
case FRACTURE -> applyFracture(target, source, requestedTicks, amplifier);
case FEAR -> applyFear(target, source, requestedTicks);
case CHARM -> applyCharm(target, source, requestedTicks);
case TAUNT -> applyTaunt(target, source, requestedTicks, amplifier);
case OVERCHARM -> applyOvercharm(target, source, requestedTicks);
case WINDED -> applyWinded(target, source, requestedTicks);
};
}
/** Applies one explicit amplifier-zero ailment. */
public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, AilmentType type,
int requestedTicks) {
return applyEffect(target, source, type, requestedTicks, 0);
}
/** Applies one reusable application descriptor. */
public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source,
AilmentApplication application) {
Objects.requireNonNull(application, "application");
if (application.isStacking()) {
return switch (application.type()) {
case BLEED -> applyBleed(target, source);
case SOUL_ROT -> applySoulRot(target, source);
case FRACTURE -> applyFracture(target, source);
default -> false;
};
}
return applyEffect(target, source, application.type(), application.durationTicks(), application.amplifier());
}
/**
* Applies one or more descriptors in order and returns the number accepted by their ailment rules.
* A rejected effect does not prevent later effects from being attempted.
*/
public static int applyEffects(LivingEntity target, @Nullable LivingEntity source,
AilmentApplication... applications) {
Objects.requireNonNull(applications, "applications");
int applied = 0;
for (AilmentApplication application : applications) {
if (applyEffect(target, source, application)) applied++;
}
return applied;
}
/** Iterable overload for data-driven integrations. */
public static int applyEffects(LivingEntity target, @Nullable LivingEntity source,
Iterable<AilmentApplication> applications) {
Objects.requireNonNull(applications, "applications");
int applied = 0;
for (AilmentApplication application : applications) {
if (applyEffect(target, source, application)) applied++;
}
return applied;
}
/** Returns the registered MobEffect represented by a stable public ailment identifier. */
public static MobEffect getEffect(AilmentType type) {
Objects.requireNonNull(type, "type");
return switch (type) {
case SOUL_ROT -> ModEffects.SOUL_ROT.get();
case BLEED -> ModEffects.BLEED.get();
case FRACTURE -> ModEffects.FRACTURE.get();
case FEAR -> ModEffects.FEAR.get();
case CHARM -> ModEffects.CHARM.get();
case TAUNT -> ModEffects.TAUNT.get();
case OVERCHARM -> ModEffects.OVERCHARM.get();
case WINDED -> ModEffects.WINDED.get();
};
}
/** Applies one Soul Rot stack. Extra stacks use 180/200/220/240 tick base durations. */
public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source) {
return applySoulRot(target, source, true);
}
/** 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) || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
MobEffect effect = ModEffects.SOUL_ROT.get();
MobEffectInstance current = target.getEffect(effect);
int currentStacks = current == null ? 0 : current.getAmplifier() + 1;
int maxStacks = AilmentsConfig.value(AilmentsConfig.SOUL_ROT_MAX_STACKS);
int stacks = Math.min(currentStacks + 1, maxStacks);
int amplifier = stacks - 1;
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));
if (applied) {
EffectSourceUtil.setSource(target, EffectSourceUtil.SOUL_ROT, source);
EffectSourceUtil.setPotency(target, EffectSourceUtil.SOUL_ROT,
AilmentScaling.getPotencyMultiplier(source, AilmentType.SOUL_ROT));
}
return finishApplication(applied, target, source, AilmentType.SOUL_ROT);
}
/** 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) || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) 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);
}
/** 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;
MobEffect effect = ModEffects.BLEED.get();
MobEffectInstance current = target.getEffect(effect);
int amplifier = current == null ? 0 : Math.min(current.getAmplifier() + 1,
AilmentsConfig.value(AilmentsConfig.BLEED_MAX_AMPLIFIER));
int base = amplifier <= 0 ? AilmentsConfig.value(AilmentsConfig.BLEED_DURATION_0)
: 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));
if (applied) {
EffectSourceUtil.setSource(target, EffectSourceUtil.BLEED, source);
EffectSourceUtil.setPotency(target, EffectSourceUtil.BLEED,
AilmentScaling.getPotencyMultiplier(source, AilmentType.BLEED));
}
return finishApplication(applied, target, source, AilmentType.BLEED);
}
/** 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;
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);
}
/** Applies one Fracture stack and refreshes its configured, source-scaled duration. */
public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source) {
return applyFracture(target, source, AilmentsConfig.value(AilmentsConfig.FRACTURE_DURATION), 0);
}
/** Applies one Fracture stack and refreshes the source-scaled requested duration. */
public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
return applyFracture(target, source, requestedTicks, 0);
}
/**
* Applies or increases Fracture, capped at three stacks. The requested amplifier acts as a minimum, while an
* existing Fracture always gains one stack. Arcane scales only duration; attribute penalties remain fixed.
*/
public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source,
int requestedTicks, int amplifier) {
if (!serverValid(target) || requestedTicks < 1 || amplifier < 0
|| !pvpProcReady(target, source, AilmentType.FRACTURE)) return false;
MobEffect effect = ModEffects.FRACTURE.get();
MobEffectInstance current = target.getEffect(effect);
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));
if (applied) EffectSourceUtil.setSource(target, EffectSourceUtil.FRACTURE, source);
return finishApplication(applied, target, source, AilmentType.FRACTURE);
}
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;
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);
}
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;
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);
}
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;
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);
}
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));
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);
}
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)),
target, source, AilmentType.WINDED);
}
public static boolean canBleed(LivingEntity target) {
if (target instanceof Player) return true;
return target.getMobType() != MobType.UNDEAD && !target.getType().is(ModEntityTypeTags.BLEED_IMMUNE);
}
public static boolean isMentalControlResistant(LivingEntity target) {
return MentalControlUtil.isMentalControlResistant(target);
}
public static boolean isMentalControlImmune(LivingEntity target) {
return MentalControlUtil.isMentalControlImmune(target);
}
public static int getBleedStacks(LivingEntity target) { return stacks(target, ModEffects.BLEED.get()); }
public static int getSoulRotStacks(LivingEntity target) { return stacks(target, ModEffects.SOUL_ROT.get()); }
public static int getFractureStacks(LivingEntity target) { return stacks(target, ModEffects.FRACTURE.get()); }
/** Returns raw Hemorrhage progress in the configured threshold's units. */
public static double getHemorrhageProgress(LivingEntity target) {
return HemorrhageTracker.getProgress(target);
}
/** Returns normalized Hemorrhage bar progress from 0.0 through 1.0. */
public static double getHemorrhageFraction(LivingEntity target) {
return HemorrhageTracker.getFraction(target);
}
public static void clearHemorrhage(LivingEntity target) { HemorrhageTracker.clear(target); }
@Nullable public static LivingEntity getCharmSource(LivingEntity target) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.CHARM);
}
@Nullable public static LivingEntity getFearSource(LivingEntity target) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.FEAR);
}
@Nullable public static LivingEntity getTauntSource(LivingEntity target) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.TAUNT);
}
@Nullable public static LivingEntity getBleedSource(LivingEntity target) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.BLEED);
}
@Nullable public static LivingEntity getSoulRotSource(LivingEntity target) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.SOUL_ROT);
}
@Nullable public static LivingEntity getFractureSource(LivingEntity target) {
return EffectSourceUtil.getSource(target, EffectSourceUtil.FRACTURE);
}
/** Removes all registered ailments and all associated source/potency metadata. */
public static void clearAll(LivingEntity target) {
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());
target.removeEffect(ModEffects.WINDED.get());
target.removeEffect(ModEffects.FRACTURE.get());
EffectSourceUtil.clear(target, EffectSourceUtil.SOUL_ROT);
EffectSourceUtil.clear(target, EffectSourceUtil.BLEED);
EffectSourceUtil.clear(target, EffectSourceUtil.FEAR);
EffectSourceUtil.clear(target, EffectSourceUtil.CHARM);
EffectSourceUtil.clear(target, EffectSourceUtil.TAUNT);
EffectSourceUtil.clear(target, EffectSourceUtil.OVERCHARM);
EffectSourceUtil.clear(target, EffectSourceUtil.FRACTURE);
HemorrhageTracker.clear(target);
}
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));
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));
if (applied) {
EffectSourceUtil.setSource(target, key, source);
EffectSourceUtil.setPotency(target, key, AilmentScaling.getPotencyMultiplier(source, type));
}
return applied;
}
private static MobEffectInstance instance(MobEffect effect, int duration, int amplifier, boolean icon) {
return new MobEffectInstance(effect, duration, amplifier, false, false, icon);
}
private static boolean serverValid(LivingEntity target) {
return target != null && !target.level().isClientSide && target.isAlive()
&& (!(target instanceof Player player) || !player.isSpectator());
}
private static boolean hostileControlValid(LivingEntity target, @Nullable LivingEntity source) {
if (!serverValid(target) || source == null || !source.isAlive()
|| source.level().isClientSide || source.isSpectator()) return false;
return !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer)
|| sourcePlayer.canHarmPlayer(targetPlayer);
}
private static int stacks(LivingEntity target, MobEffect effect) {
MobEffectInstance active = target.getEffect(effect);
return active == null ? 0 : active.getAmplifier() + 1;
}
private static int safeAdd(int a, int b) {
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;
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) {
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);
}
private AilmentApi() {}
}
@@ -0,0 +1,70 @@
package net.fretux.ailments.api;
import java.util.Objects;
/**
* Immutable description of one ailment application.
*
* <p>Use {@link #timed(AilmentType, int, int)} when the caller controls the duration and
* amplifier. Use {@link #stack(AilmentType)} for the library's configured Bleed, Soul Rot, or Fracture
* stacking behavior.</p>
*/
public final class AilmentApplication {
private final AilmentType type;
private final int durationTicks;
private final int amplifier;
private final boolean stacking;
private AilmentApplication(AilmentType type, int durationTicks, int amplifier, boolean stacking) {
this.type = Objects.requireNonNull(type, "type");
this.durationTicks = durationTicks;
this.amplifier = amplifier;
this.stacking = stacking;
}
/** Creates an explicit, source-scaled application. Duration is measured in ticks. */
public static AilmentApplication timed(AilmentType type, int durationTicks, int amplifier) {
if (durationTicks < 1) throw new IllegalArgumentException("durationTicks must be positive");
if (amplifier < 0) throw new IllegalArgumentException("amplifier must not be negative");
return new AilmentApplication(type, durationTicks, amplifier, false);
}
/** Creates an explicit amplifier-zero, source-scaled application. */
public static AilmentApplication timed(AilmentType type, int durationTicks) {
return timed(type, durationTicks, 0);
}
/**
* Adds one stack using the configured duration and cap.
*
* @throws IllegalArgumentException if {@code type} does not support stacking
*/
public static AilmentApplication stack(AilmentType type) {
Objects.requireNonNull(type, "type");
if (type != AilmentType.BLEED && type != AilmentType.SOUL_ROT && type != AilmentType.FRACTURE) {
throw new IllegalArgumentException("Only BLEED, SOUL_ROT, and FRACTURE support stack applications");
}
return new AilmentApplication(type, 0, 0, true);
}
public AilmentType type() { return type; }
public int durationTicks() { return durationTicks; }
public int amplifier() { return amplifier; }
public boolean isStacking() { return stacking; }
@Override public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof AilmentApplication that)) return false;
return durationTicks == that.durationTicks && amplifier == that.amplifier
&& stacking == that.stacking && type == that.type;
}
@Override public int hashCode() {
return Objects.hash(type, durationTicks, amplifier, stacking);
}
@Override public String toString() {
return stacking ? "AilmentApplication[" + type + ", stack]"
: "AilmentApplication[" + type + ", " + durationTicks + " ticks, amplifier=" + amplifier + "]";
}
}
@@ -0,0 +1,42 @@
package net.fretux.ailments.api;
import net.fretux.ailments.compat.AscendCompat;
import net.fretux.ailments.config.AilmentsConfig;
import net.minecraft.world.entity.LivingEntity;
import javax.annotation.Nullable;
/** Central, application-time Arcane scaling. The normalized curve is concave and reaches 1 at 100 Arcane. */
public final class AilmentScaling {
public static double getDurationMultiplier(@Nullable LivingEntity source) {
return getDurationMultiplierForArcane(AscendCompat.getArcane(source));
}
public static double getPotencyMultiplier(@Nullable LivingEntity source, AilmentType type) {
return getPotencyMultiplierForArcane(AscendCompat.getArcane(source), type);
}
static double getPotencyMultiplierForArcane(int arcane, AilmentType type) {
double maximumBonus = switch (type) {
case SOUL_ROT -> AilmentsConfig.value(AilmentsConfig.SOUL_ROT_ARCANE_POTENCY);
case BLEED -> AilmentsConfig.value(AilmentsConfig.BLEED_ARCANE_POTENCY);
default -> 0.0;
};
return 1.0 + normalizedArcane(arcane) * maximumBonus;
}
public static int scaleDuration(int requestedTicks, @Nullable LivingEntity source) {
if (requestedTicks <= 0) return 1;
return Math.max(1, (int) Math.min(Integer.MAX_VALUE,
Math.round(requestedTicks * getDurationMultiplier(source))));
}
public static double getDurationMultiplierForArcane(int arcane) {
return 1.0 + normalizedArcane(arcane) * AilmentsConfig.value(AilmentsConfig.ARCANE_DURATION_BONUS);
}
static double normalizedArcane(int arcane) {
return arcane <= 0 ? 0.0 : Math.pow(Math.min(100, arcane) / 100.0, 0.8);
}
private AilmentScaling() {}
}
@@ -0,0 +1,6 @@
package net.fretux.ailments.api;
/** Public identifiers for ailment-specific scaling and integrations. */
public enum AilmentType {
SOUL_ROT, BLEED, FRACTURE, FEAR, CHARM, TAUNT, OVERCHARM, WINDED
}
@@ -0,0 +1,16 @@
package net.fretux.ailments.client;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
@OnlyIn(Dist.CLIENT)
public final class ClientHemorrhageState {
private static float progress;
public static float getProgress() { return progress; }
public static void setProgress(float value) {
progress = Math.max(0.0F, Math.min(1.0F, value));
}
private ClientHemorrhageState() {}
}
@@ -0,0 +1,48 @@
package net.fretux.ailments.client;
import net.fretux.ailments.AscendAilments;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.network.chat.Component;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RegisterGuiOverlaysEvent;
import net.minecraftforge.client.gui.overlay.IGuiOverlay;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = AscendAilments.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public final class HemorrhageOverlay {
private static final int BAR_WIDTH = 96;
private static final int BAR_HEIGHT = 5;
private static final int BORDER = 1;
private static final IGuiOverlay OVERLAY = (gui, graphics, partialTick, screenWidth, screenHeight) -> {
float progress = ClientHemorrhageState.getProgress();
Minecraft minecraft = Minecraft.getInstance();
if (progress <= 0.0F || minecraft.options.hideGui || minecraft.player == null
|| !minecraft.player.isAlive()) return;
// Souls-like status buildup placement: centered below the crosshair, clear of boss bars and the hotbar.
int x = (screenWidth - BAR_WIDTH) / 2;
int y = Math.min(screenHeight - 52, screenHeight / 2 + 54);
int filled = Math.round((BAR_WIDTH - BORDER * 2) * progress);
graphics.fill(x, y, x + BAR_WIDTH, y + BAR_HEIGHT, 0xB8000000);
graphics.fill(x + BORDER, y + BORDER, x + BAR_WIDTH - BORDER,
y + BAR_HEIGHT - BORDER, 0xA02A0A0A);
if (filled > 0) graphics.fill(x + BORDER, y + BORDER, x + BORDER + filled,
y + BAR_HEIGHT - BORDER, 0xFFE04444);
Font font = minecraft.font;
Component label = Component.translatable("gui.ascend_ailments.hemorrhage");
int labelX = (screenWidth - font.width(label)) / 2;
graphics.drawString(font, label, labelX, y - 10, 0xFFD8A0A0, true);
};
@SubscribeEvent
public static void registerOverlay(RegisterGuiOverlaysEvent event) {
event.registerAboveAll("hemorrhage", OVERLAY);
}
private HemorrhageOverlay() {}
}
@@ -0,0 +1,99 @@
package net.fretux.ailments.command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.context.CommandContext;
import net.fretux.ailments.api.AilmentApi;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
public final class AilmentCommands {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(Commands.literal("ascendailments").requires(source -> source.hasPermission(2))
.then(Commands.literal("clear").then(Commands.argument("target", EntityArgument.entity())
.executes(ctx -> withTarget(ctx, target -> { AilmentApi.clearAll(target); return true; }))))
.then(Commands.literal("apply").then(Commands.argument("target", EntityArgument.entity())
.then(Commands.literal("bleed")
.executes(ctx -> withTarget(ctx, target -> AilmentApi.applyBleed(target, null)))
.then(explicitDot(false)))
.then(Commands.literal("soul_rot")
.executes(ctx -> withTarget(ctx, target -> AilmentApi.applySoulRot(target, null)))
.then(explicitDot(true)))
.then(control("fracture", AilmentApi::applyFracture, true))
.then(control("fear", (target, source, duration, amplifier) ->
AilmentApi.applyFear(target, source, duration), false))
.then(control("charm", (target, source, duration, amplifier) ->
AilmentApi.applyCharm(target, source, duration), false))
.then(control("taunt", AilmentApi::applyTaunt, true))
.then(Commands.literal("overcharm")
.then(Commands.argument("duration", IntegerArgumentType.integer(1))
.executes(ctx -> withTarget(ctx, target -> AilmentApi.applyOvercharm(target,
null, IntegerArgumentType.getInteger(ctx, "duration"))))))
.then(Commands.literal("winded")
.then(Commands.argument("duration", IntegerArgumentType.integer(1))
.executes(ctx -> withTarget(ctx, target -> AilmentApi.applyWinded(target,
null, IntegerArgumentType.getInteger(ctx, "duration")))))))));
}
private static com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> explicitDot(boolean soulRot) {
return Commands.argument("duration", IntegerArgumentType.integer(1))
.executes(ctx -> withTarget(ctx, target -> soulRot
? AilmentApi.applySoulRot(target, null, IntegerArgumentType.getInteger(ctx, "duration"), 0)
: AilmentApi.applyBleed(target, null, IntegerArgumentType.getInteger(ctx, "duration"), 0)))
.then(Commands.argument("amplifier", IntegerArgumentType.integer(0, soulRot ? 4 : 2))
.executes(ctx -> withTarget(ctx, target -> soulRot
? AilmentApi.applySoulRot(target, null,
IntegerArgumentType.getInteger(ctx, "duration"),
IntegerArgumentType.getInteger(ctx, "amplifier"))
: AilmentApi.applyBleed(target, null,
IntegerArgumentType.getInteger(ctx, "duration"),
IntegerArgumentType.getInteger(ctx, "amplifier")))));
}
private static com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> control(
String name, ControlApplication application, boolean amplifier) {
var duration = Commands.argument("duration", IntegerArgumentType.integer(1))
.executes(ctx -> applyControl(ctx, application, 0));
if (amplifier) duration.then(Commands.argument("amplifier", IntegerArgumentType.integer(0, 255))
.executes(ctx -> applyControl(ctx, application,
IntegerArgumentType.getInteger(ctx, "amplifier"))));
return Commands.literal(name).then(Commands.argument("source", EntityArgument.entity()).then(duration));
}
private static int applyControl(CommandContext<CommandSourceStack> ctx, ControlApplication application,
int amplifier) throws com.mojang.brigadier.exceptions.CommandSyntaxException {
Entity targetEntity = EntityArgument.getEntity(ctx, "target");
Entity sourceEntity = EntityArgument.getEntity(ctx, "source");
if (!(targetEntity instanceof LivingEntity target) || !(sourceEntity instanceof LivingEntity source)) {
ctx.getSource().sendFailure(Component.literal("Target and source must be living entities"));
return 0;
}
boolean applied = application.apply(target, source, IntegerArgumentType.getInteger(ctx, "duration"), amplifier);
return result(ctx, applied);
}
private static int withTarget(CommandContext<CommandSourceStack> ctx, TargetApplication application)
throws com.mojang.brigadier.exceptions.CommandSyntaxException {
Entity entity = EntityArgument.getEntity(ctx, "target");
if (!(entity instanceof LivingEntity target)) {
ctx.getSource().sendFailure(Component.literal("Target must be a living entity"));
return 0;
}
return result(ctx, application.apply(target));
}
private static int result(CommandContext<CommandSourceStack> ctx, boolean applied) {
if (!applied) { ctx.getSource().sendFailure(Component.literal("Ailment was rejected")); return 0; }
ctx.getSource().sendSuccess(() -> Component.literal("Ailment operation succeeded"), true);
return 1;
}
@FunctionalInterface private interface TargetApplication { boolean apply(LivingEntity target); }
@FunctionalInterface private interface ControlApplication {
boolean apply(LivingEntity target, LivingEntity source, int duration, int amplifier);
}
private AilmentCommands() {}
}
@@ -0,0 +1,58 @@
package net.fretux.ailments.compat;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.fml.ModList;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Optional;
/** Reflection boundary: no Ascend type appears in a loadable signature or constant-pool class reference. */
public final class AscendCompat {
private static final String PROVIDER = "net.fretux.ascend.player.PlayerStatsProvider";
private static boolean resolved;
private static Capability<?> statsCapability;
private static Method getAttributeLevel;
public static boolean isAscendLoaded() { return ModList.get().isLoaded("ascend"); }
/** 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 ignored) {
return false;
}
}
public static int getArcane(LivingEntity source) {
if (!(source instanceof Player player) || !isAscendLoaded()) return 0;
try {
resolve();
if (statsCapability == null || getAttributeLevel == null) 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 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);
Class<?> statsClass = Class.forName("net.fretux.ascend.player.PlayerStats", false,
AscendCompat.class.getClassLoader());
getAttributeLevel = statsClass.getMethod("getAttributeLevel", String.class);
}
private AscendCompat() {}
}
@@ -0,0 +1,141 @@
package net.fretux.ailments.config;
import net.minecraftforge.common.ForgeConfigSpec;
import java.util.List;
public final class AilmentsConfig {
private static final ForgeConfigSpec.Builder B = new ForgeConfigSpec.Builder();
public static final ForgeConfigSpec.DoubleValue ARCANE_DURATION_BONUS;
public static final ForgeConfigSpec.DoubleValue SOUL_ROT_ARCANE_POTENCY;
public static final ForgeConfigSpec.DoubleValue BLEED_ARCANE_POTENCY;
public static final ForgeConfigSpec.IntValue SOUL_ROT_DURATION, SOUL_ROT_MAX_STACKS;
public static final ForgeConfigSpec.DoubleValue SOUL_ROT_EXHAUSTION_BASE, SOUL_ROT_EXHAUSTION_PER_AMP,
SOUL_ROT_DAMAGE_TAKEN, SOUL_ROT_HEALING;
public static final ForgeConfigSpec.IntValue BLEED_DURATION_0, BLEED_DURATION_1, BLEED_DURATION_2,
BLEED_INTERVAL_0, BLEED_INTERVAL_1, BLEED_INTERVAL_2, BLEED_MAX_AMPLIFIER;
public static final ForgeConfigSpec.DoubleValue BLEED_DAMAGE_0, BLEED_DAMAGE_1, BLEED_DAMAGE_2;
public static final ForgeConfigSpec.IntValue HEMORRHAGE_MINIMUM_STACKS;
public static final ForgeConfigSpec.DoubleValue HEMORRHAGE_THRESHOLD, HEMORRHAGE_FILL_PER_TICK,
HEMORRHAGE_MAX_HEALTH_DAMAGE;
public static final ForgeConfigSpec.BooleanValue HEMORRHAGE_SHOW_PLAYER_BAR;
public static final ForgeConfigSpec.DoubleValue FEAR_IMMUNITY_HEALTH, FEAR_REDUCED_HEALTH,
FEAR_FLEE_SPEED, FEAR_FLEE_DISTANCE, FEAR_PLAYER_PUSH;
public static final ForgeConfigSpec.IntValue FRACTURE_DURATION;
public static final ForgeConfigSpec.DoubleValue CHARM_VICTIM_BONUS, CHARM_SOURCE_PENALTY, CHARM_OTHER_BONUS;
public static final ForgeConfigSpec.DoubleValue TAUNT_VICTIM_MULTIPLIER, TAUNT_SOURCE_MULTIPLIER;
public static final ForgeConfigSpec.IntValue OVERCHARM_CHARM_DURATION;
public static final ForgeConfigSpec.DoubleValue OVERCHARM_DAMAGE_BONUS, WINDED_MOVEMENT_REDUCTION,
WINDED_ATTACK_REDUCTION;
public static final ForgeConfigSpec.IntValue PVP_PROC_COOLDOWN_TICKS;
public static final ForgeConfigSpec.BooleanValue AUTOMATIC_WEAPON_BLEED;
public static final ForgeConfigSpec.DoubleValue AUTOMATIC_WEAPON_BLEED_CHANCE;
public static final ForgeConfigSpec.IntValue TAGGED_WEAPON_EFFECT_DURATION;
public static final ForgeConfigSpec.ConfigValue<List<? extends String>> AUTOMATIC_WEAPON_BLEED_KEYWORDS;
static {
B.push("arcane");
ARCANE_DURATION_BONUS = d("durationBonusAt100", 0.30, 0, 5);
SOUL_ROT_ARCANE_POTENCY = d("soulRotExhaustionBonusAt100", 0.20, 0, 5);
BLEED_ARCANE_POTENCY = d("bleedDamageBonusAt100", 0.20, 0, 5);
B.pop().push("soulRot");
SOUL_ROT_DURATION = i("baseDurationTicks", 160, 1, Integer.MAX_VALUE);
SOUL_ROT_MAX_STACKS = i("maximumStacks", 5, 1, 5);
SOUL_ROT_EXHAUSTION_BASE = d("exhaustionBase", 1.2, 0, 1000);
SOUL_ROT_EXHAUSTION_PER_AMP = d("exhaustionPerAmplifier", 0.4, 0, 1000);
SOUL_ROT_DAMAGE_TAKEN = d("damageTakenMultiplier", 1.10, 0, 100);
SOUL_ROT_HEALING = d("healingMultiplier", 0.25, 0, 100);
B.pop().push("bleed");
BLEED_DURATION_0 = i("durationStack1", 100, 1, Integer.MAX_VALUE);
BLEED_DURATION_1 = i("durationStack2", 120, 1, Integer.MAX_VALUE);
BLEED_DURATION_2 = i("durationStack3", 140, 1, Integer.MAX_VALUE);
BLEED_INTERVAL_0 = i("intervalStack1", 30, 1, 1200);
BLEED_INTERVAL_1 = i("intervalStack2", 25, 1, 1200);
BLEED_INTERVAL_2 = i("intervalStack3", 20, 1, 1200);
BLEED_DAMAGE_0 = d("damageStack1", 1, 0, 1000);
BLEED_DAMAGE_1 = d("damageStack2", 1, 0, 1000);
BLEED_DAMAGE_2 = d("damageStack3", 2, 0, 1000);
BLEED_MAX_AMPLIFIER = i("maximumAmplifier", 2, 0, 2);
HEMORRHAGE_MINIMUM_STACKS = B.comment(
"Bleed stacks required before damaging Bleed ticks build Hemorrhage.")
.defineInRange("hemorrhageMinimumStacks", 2, 2, 3);
HEMORRHAGE_THRESHOLD = B.comment("Progress required to trigger Hemorrhage.")
.defineInRange("hemorrhageThreshold", 100.0, 1.0, 1000000.0);
HEMORRHAGE_FILL_PER_TICK = B.comment(
"Base progress added by each successful Bleed damage tick; snapshotted Arcane potency multiplies it.")
.defineInRange("hemorrhageFillPerBleedTick", 10.0, 0.0, 1000000.0);
HEMORRHAGE_MAX_HEALTH_DAMAGE = B.comment(
"Fraction of the victim's maximum health dealt as Bleed damage when Hemorrhage fills.")
.defineInRange("hemorrhageMaxHealthDamage", 0.20, 0.0, 100.0);
HEMORRHAGE_SHOW_PLAYER_BAR = B.comment(
"Show player victims their current Hemorrhage progress as a compact HUD meter.")
.define("showHemorrhagePlayerBar", true);
B.pop().push("fracture");
FRACTURE_DURATION = B.comment("Base duration refreshed whenever Fracture is reapplied.")
.defineInRange("baseDurationTicks", 160, 1, Integer.MAX_VALUE);
B.pop().push("fear");
FEAR_IMMUNITY_HEALTH = d("immunityHealthThreshold", 150, 1, 1000000);
FEAR_REDUCED_HEALTH = d("reducedDurationHealthThreshold", 50, 0, 1000000);
FEAR_FLEE_SPEED = d("fleeSpeed", 1.25, 0, 100);
FEAR_FLEE_DISTANCE = d("fleeDistance", 6, 0, 1000);
FEAR_PLAYER_PUSH = d("playerPushStrength", 0.18, 0, 10);
B.pop().push("charm");
CHARM_VICTIM_BONUS = d("charmedVictimMultiplier", 1.10, 0, 100);
CHARM_SOURCE_PENALTY = d("attackerVsSourceMultiplier", 0.85, 0, 100);
CHARM_OTHER_BONUS = d("attackerVsOthersMultiplier", 1.15, 0, 100);
B.pop().push("taunt");
TAUNT_VICTIM_MULTIPLIER = d("victimDamageMultiplier", 1.20, 0, 100);
TAUNT_SOURCE_MULTIPLIER = d("attackerVsSourceMultiplier", 1.20, 0, 100);
B.pop().push("overcharm");
OVERCHARM_CHARM_DURATION = i("charmDurationTicks", 100, 1, Integer.MAX_VALUE);
OVERCHARM_DAMAGE_BONUS = d("preCharmedMeleeMultiplier", 1.10, 0, 100);
B.pop().push("winded");
WINDED_MOVEMENT_REDUCTION = d("movementReduction", 0.25, 0, 1);
WINDED_ATTACK_REDUCTION = d("attackSpeedReduction", 0.25, 0, 1);
B.pop().push("integration");
PVP_PROC_COOLDOWN_TICKS = B.comment(
"Minimum ticks between successful procs of the same ailment by one player against other players.",
"This cannot be configured below 60 ticks (3 seconds).")
.defineInRange("pvpProcCooldownTicks", 60, 60, Integer.MAX_VALUE);
AUTOMATIC_WEAPON_BLEED = B.comment(
"Automatically add one Bleed stack on direct melee hits with matching modded weapons.")
.define("automaticWeaponBleed", true);
AUTOMATIC_WEAPON_BLEED_CHANCE = B.comment(
"Chance per eligible melee hit to apply automatic weapon Bleed.")
.defineInRange("automaticWeaponBleedChance", 1.0, 0.0, 1.0);
TAGGED_WEAPON_EFFECT_DURATION = B.comment(
"Base duration used by fear, charm, taunt, overcharm, and winded on-hit item tags.",
"Soul Rot, Bleed, and Fracture continue to use their ailment-specific configured durations.")
.defineInRange("taggedWeaponEffectDurationTicks", 100, 1, Integer.MAX_VALUE);
AUTOMATIC_WEAPON_BLEED_KEYWORDS = B.comment(
"Case-insensitive registry-path fragments used to identify modded katana-like weapons.",
"Items in #ascend_ailments:bleed_on_hit are eligible independently of this list.")
.defineListAllowEmpty("automaticWeaponBleedKeywords",
List.of("katana", "wakizashi", "nodachi", "odachi", "uchigatana", "tachi"),
value -> value instanceof String keyword && !keyword.isBlank());
B.pop();
}
public static final ForgeConfigSpec SPEC = B.build();
private static ForgeConfigSpec.DoubleValue d(String key, double value, double min, double max) {
return B.defineInRange(key, value, min, max);
}
private static ForgeConfigSpec.IntValue i(String key, int value, int min, int max) {
return B.defineInRange(key, value, min, max);
}
public static double value(ForgeConfigSpec.DoubleValue value) {
return SPEC.isLoaded() ? value.get() : value.getDefault();
}
public static int value(ForgeConfigSpec.IntValue value) {
return SPEC.isLoaded() ? value.get() : value.getDefault();
}
public static boolean value(ForgeConfigSpec.BooleanValue value) {
return SPEC.isLoaded() ? value.get() : value.getDefault();
}
public static List<? extends String> weaponBleedKeywords() {
return SPEC.isLoaded() ? AUTOMATIC_WEAPON_BLEED_KEYWORDS.get()
: AUTOMATIC_WEAPON_BLEED_KEYWORDS.getDefault();
}
private AilmentsConfig() {}
}
@@ -0,0 +1,20 @@
package net.fretux.ailments.damage;
import net.fretux.ailments.AscendAilments;
import net.minecraft.core.registries.Registries;
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.level.Level;
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));
}
private ModDamageSources() {}
}
@@ -0,0 +1,30 @@
package net.fretux.ailments.effect;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.damage.ModDamageSources;
import net.fretux.ailments.util.EffectSourceUtil;
import net.fretux.ailments.util.HemorrhageTracker;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectCategory;
import net.minecraft.world.entity.LivingEntity;
public final class BleedEffect extends MobEffect {
public BleedEffect() { super(MobEffectCategory.HARMFUL, 0xAA0000); }
@Override public boolean isDurationEffectTick(int duration, int amplifier) {
int interval = amplifier <= 0 ? AilmentsConfig.value(AilmentsConfig.BLEED_INTERVAL_0)
: amplifier == 1 ? AilmentsConfig.value(AilmentsConfig.BLEED_INTERVAL_1)
: AilmentsConfig.value(AilmentsConfig.BLEED_INTERVAL_2);
return duration % interval == 0;
}
@Override public void applyEffectTick(LivingEntity entity, int amplifier) {
if (entity.level().isClientSide) return;
double base = amplifier <= 0 ? AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_0)
: 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));
if (damaged) HemorrhageTracker.recordBleedDamage(entity, amplifier, potency);
}
}
@@ -0,0 +1,27 @@
package net.fretux.ailments.effect;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectCategory;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
/** Three-stack armor-breaking ailment. Arcane scaling is handled only when its duration is applied. */
public final class FractureEffect extends MobEffect {
public static final String ARMOR_UUID = "9d353786-61e3-43d8-b980-1471e9b84e58";
public static final String TOUGHNESS_UUID = "6610760d-38d6-42de-b3ab-621d84896d42";
public FractureEffect() {
super(MobEffectCategory.HARMFUL, 0x8B8178);
addAttributeModifier(Attributes.ARMOR, ARMOR_UUID, -2.0, AttributeModifier.Operation.ADDITION);
addAttributeModifier(Attributes.ARMOR_TOUGHNESS, TOUGHNESS_UUID, -1.0,
AttributeModifier.Operation.ADDITION);
}
@Override
public double getAttributeModifierValue(int amplifier, AttributeModifier modifier) {
int stacks = Math.min(3, Math.max(0, amplifier) + 1);
if (modifier.getId().toString().equals(ARMOR_UUID)) return -2.0 * stacks;
if (modifier.getId().toString().equals(TOUGHNESS_UUID)) return -1.0 * stacks;
return super.getAttributeModifierValue(amplifier, modifier);
}
}
@@ -0,0 +1,21 @@
package net.fretux.ailments.effect;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.util.EffectSourceUtil;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectCategory;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
public final class SoulRotEffect extends MobEffect {
public SoulRotEffect() { super(MobEffectCategory.HARMFUL, 0x8E0E0E); }
@Override public boolean isDurationEffectTick(int duration, int amplifier) { return duration % 20 == 0; }
@Override public void applyEffectTick(LivingEntity entity, int amplifier) {
if (entity.level().isClientSide || !(entity instanceof Player player)) return;
float base = (float) (AilmentsConfig.value(AilmentsConfig.SOUL_ROT_EXHAUSTION_BASE)
+ AilmentsConfig.value(AilmentsConfig.SOUL_ROT_EXHAUSTION_PER_AMP) * amplifier);
player.causeFoodExhaustion(base * EffectSourceUtil.getPotency(entity, EffectSourceUtil.SOUL_ROT));
}
}
@@ -0,0 +1,29 @@
package net.fretux.ailments.effect;
import net.fretux.ailments.config.AilmentsConfig;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectCategory;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
public final class WindedEffect extends MobEffect {
public static final String MOVEMENT_UUID = "f0eeefea-3f57-4ebb-b8d6-d4ae352efb50";
public static final String ATTACK_SPEED_UUID = "3b6a4969-0cd5-4e9c-9b64-a8cd0a0f8d0f";
public WindedEffect() {
super(MobEffectCategory.HARMFUL, 0x567051);
addAttributeModifier(Attributes.MOVEMENT_SPEED, MOVEMENT_UUID, -0.25,
AttributeModifier.Operation.MULTIPLY_TOTAL);
addAttributeModifier(Attributes.ATTACK_SPEED, ATTACK_SPEED_UUID, -0.25,
AttributeModifier.Operation.MULTIPLY_TOTAL);
}
@Override
public double getAttributeModifierValue(int amplifier, AttributeModifier modifier) {
if (modifier.getId().toString().equals(MOVEMENT_UUID))
return -AilmentsConfig.value(AilmentsConfig.WINDED_MOVEMENT_REDUCTION);
if (modifier.getId().toString().equals(ATTACK_SPEED_UUID))
return -AilmentsConfig.value(AilmentsConfig.WINDED_ATTACK_REDUCTION);
return super.getAttributeModifierValue(amplifier, modifier);
}
}
@@ -0,0 +1,159 @@
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.registry.ModEffects;
import net.fretux.ailments.util.ControlEffectHelper;
import net.fretux.ailments.util.EffectSourceUtil;
import net.fretux.ailments.util.MentalControlUtil;
import net.fretux.ailments.util.ModItemTags;
import net.fretux.ailments.util.HemorrhageTracker;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
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.LivingHealEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.registries.ForgeRegistries;
import java.util.Locale;
/** One deterministic damage pipeline: Soul Rot, Charm, Taunt, then Overcharm. */
@Mod.EventBusSubscriber(modid = AscendAilments.MOD_ID)
public final class AilmentEvents {
@SubscribeEvent
public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
HemorrhageTracker.syncDisplay(event.getEntity());
}
@SubscribeEvent
public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) {
HemorrhageTracker.hideDisplay(event.getEntity());
}
@SubscribeEvent
public static void onDeath(LivingDeathEvent event) {
HemorrhageTracker.clear(event.getEntity());
}
@SubscribeEvent
public static void onLivingTick(LivingEvent.LivingTickEvent event) {
ControlEffectHelper.handleLivingTick(event.getEntity());
}
@SubscribeEvent
public static void onHeal(LivingHealEvent event) {
if (!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) {
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);
}
@SubscribeEvent
public static void onChangeTarget(LivingChangeTargetEvent event) {
if (!(event.getEntity() instanceof Mob mob)) return;
LivingEntity taunter = mob.hasEffect(ModEffects.TAUNT.get())
&& !MentalControlUtil.isMentalControlResistant(mob)
? ControlEffectHelper.validTauntSource(mob) : null;
if (taunter != null) {
if (event.getNewTarget() != taunter) event.setNewTarget(taunter);
return;
}
if (mob.hasEffect(ModEffects.CHARM.get()) && event.getNewTarget() != null
&& ControlEffectHelper.isOwnCharmSource(mob, event.getNewTarget())) event.setNewTarget(null);
}
@SubscribeEvent
public static void onHurt(LivingHurtEvent event) {
if (event.isCanceled()) return;
LivingEntity victim = event.getEntity();
DamageSource damageSource = event.getSource();
Entity causing = damageSource.getEntity();
LivingEntity attacker = causing instanceof LivingEntity living ? living : null;
float amount = event.getAmount();
// 1. Victim vulnerability.
if (victim.hasEffect(ModEffects.SOUL_ROT.get()))
amount *= (float) AilmentsConfig.value(AilmentsConfig.SOUL_ROT_DAMAGE_TAKEN);
// 2. Charm victim and attacker rules.
if (attacker != null && !(victim instanceof Player) && victim.hasEffect(ModEffects.CHARM.get())
&& 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));
}
// 3. Taunt rules; each logically distinct rule is applied once.
if (victim.hasEffect(ModEffects.TAUNT.get()))
amount *= (float) AilmentsConfig.value(AilmentsConfig.TAUNT_VICTIM_MULTIPLIER);
if (attacker != null && attacker.hasEffect(ModEffects.TAUNT.get())) {
LivingEntity taunter = EffectSourceUtil.getSource(attacker, EffectSourceUtil.TAUNT);
if (taunter != null && taunter.getUUID().equals(victim.getUUID()))
amount *= (float) AilmentsConfig.value(AilmentsConfig.TAUNT_SOURCE_MULTIPLIER);
}
// 4. Snapshot pre-hit Charm before Overcharm refreshes it.
if (attacker != null && attacker != victim && attacker.hasEffect(ModEffects.OVERCHARM.get())) {
boolean alreadyCharmedByAttacker = victim.hasEffect(ModEffects.CHARM.get())
&& ControlEffectHelper.isOwnCharmSource(victim, attacker);
boolean directPlayerMelee = attacker instanceof Player && damageSource.getDirectEntity() == attacker;
if (alreadyCharmedByAttacker && directPlayerMelee)
amount *= (float) AilmentsConfig.value(AilmentsConfig.OVERCHARM_DAMAGE_BONUS);
AilmentApi.applyCharm(victim, attacker,
AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION));
}
if (attacker != null && attacker != victim && amount > 0
&& damageSource.getDirectEntity() == attacker)
applyTaggedWeaponAilments(attacker.getMainHandItem(), victim, attacker);
event.setAmount(amount);
}
private static void applyTaggedWeaponAilments(ItemStack stack, LivingEntity victim, LivingEntity attacker) {
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);
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);
}
private static boolean isAutomaticBleedWeapon(ItemStack stack) {
if (!AilmentsConfig.value(AilmentsConfig.AUTOMATIC_WEAPON_BLEED) || stack.isEmpty()) return false;
if (stack.is(ModItemTags.BLEED_ON_HIT)) return true;
ResourceLocation id = ForgeRegistries.ITEMS.getKey(stack.getItem());
if (id == null || id.getNamespace().equals("minecraft")) return false;
String path = id.getPath().toLowerCase(Locale.ROOT);
return AilmentsConfig.weaponBleedKeywords().stream()
.map(keyword -> keyword.toLowerCase(Locale.ROOT))
.anyMatch(path::contains);
}
private AilmentEvents() {}
}
@@ -0,0 +1,30 @@
package net.fretux.ailments.network;
import net.fretux.ailments.AscendAilments;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkDirection;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.PacketDistributor;
import net.minecraftforge.network.simple.SimpleChannel;
public final class AilmentNetwork {
private static final String PROTOCOL = "1";
private static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel(
ResourceLocation.fromNamespaceAndPath(AscendAilments.MOD_ID, "main"),
() -> PROTOCOL, PROTOCOL::equals, PROTOCOL::equals);
public static void register() {
CHANNEL.messageBuilder(ClientboundHemorrhagePacket.class, 0, NetworkDirection.PLAY_TO_CLIENT)
.encoder(ClientboundHemorrhagePacket::encode)
.decoder(ClientboundHemorrhagePacket::decode)
.consumerMainThread(ClientboundHemorrhagePacket::handle)
.add();
}
public static void sendHemorrhageProgress(ServerPlayer player, float fraction) {
CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), new ClientboundHemorrhagePacket(fraction));
}
private AilmentNetwork() {}
}
@@ -0,0 +1,31 @@
package net.fretux.ailments.network;
import net.fretux.ailments.client.ClientHemorrhageState;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.DistExecutor;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public record ClientboundHemorrhagePacket(float fraction) {
public ClientboundHemorrhagePacket {
fraction = Math.max(0.0F, Math.min(1.0F, fraction));
}
public static void encode(ClientboundHemorrhagePacket packet, FriendlyByteBuf buffer) {
buffer.writeFloat(packet.fraction);
}
public static ClientboundHemorrhagePacket decode(FriendlyByteBuf buffer) {
return new ClientboundHemorrhagePacket(buffer.readFloat());
}
public static void handle(ClientboundHemorrhagePacket packet,
Supplier<NetworkEvent.Context> contextSupplier) {
NetworkEvent.Context context = contextSupplier.get();
context.enqueueWork(() -> DistExecutor.unsafeRunWhenOn(Dist.CLIENT,
() -> () -> ClientHemorrhageState.setProgress(packet.fraction)));
context.setPacketHandled(true);
}
}
@@ -0,0 +1,38 @@
package net.fretux.ailments.registry;
import net.fretux.ailments.AscendAilments;
import net.fretux.ailments.effect.BleedEffect;
import net.fretux.ailments.effect.FractureEffect;
import net.fretux.ailments.effect.SoulRotEffect;
import net.fretux.ailments.effect.WindedEffect;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectCategory;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
public final class ModEffects {
public static final DeferredRegister<MobEffect> EFFECTS =
DeferredRegister.create(ForgeRegistries.MOB_EFFECTS, AscendAilments.MOD_ID);
public static final RegistryObject<MobEffect> SOUL_ROT = EFFECTS.register("soul_rot", SoulRotEffect::new);
public static final RegistryObject<MobEffect> BLEED = EFFECTS.register("bleed", BleedEffect::new);
public static final RegistryObject<MobEffect> FRACTURE = EFFECTS.register("fracture", FractureEffect::new);
public static final RegistryObject<MobEffect> FEAR = EFFECTS.register("fear",
() -> new SimpleAilmentEffect(MobEffectCategory.HARMFUL, 0xAA0000));
public static final RegistryObject<MobEffect> CHARM = EFFECTS.register("charm",
() -> new SimpleAilmentEffect(MobEffectCategory.HARMFUL, 0xCC7AB6));
public static final RegistryObject<MobEffect> TAUNT = EFFECTS.register("taunt",
() -> new SimpleAilmentEffect(MobEffectCategory.HARMFUL, 0xCC7AB6));
public static final RegistryObject<MobEffect> OVERCHARM = EFFECTS.register("overcharm",
() -> new SimpleAilmentEffect(MobEffectCategory.BENEFICIAL, 0xFFD966));
public static final RegistryObject<MobEffect> WINDED = EFFECTS.register("winded", WindedEffect::new);
public static void register(IEventBus bus) { EFFECTS.register(bus); }
private ModEffects() {}
private static final class SimpleAilmentEffect extends MobEffect {
private SimpleAilmentEffect(MobEffectCategory category, int color) { super(category, color); }
}
}
@@ -0,0 +1,149 @@
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.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.food.FoodData;
import net.minecraft.world.phys.Vec3;
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";
public static void handleLivingTick(LivingEntity entity) {
if (entity.level().isClientSide) return;
handleSoulRotFood(entity);
cleanDotData(entity);
if (entity.hasEffect(ModEffects.FEAR.get())) tickFear(entity);
else {
EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR);
entity.getPersistentData().remove(FEAR_DISTANCE);
}
if (entity.hasEffect(ModEffects.TAUNT.get())) 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) {
LivingEntity source = validControlSource(entity, EffectSourceUtil.FEAR);
if (source == null) {
entity.removeEffect(ModEffects.FEAR.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR);
entity.getPersistentData().remove(FEAR_DISTANCE);
return;
}
if (MentalControlUtil.isMentalControlResistant(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)));
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);
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));
}
}
private static void tickTaunt(LivingEntity entity) {
LivingEntity source = validControlSource(entity, EffectSourceUtil.TAUNT);
if (source == null) {
entity.removeEffect(ModEffects.TAUNT.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.TAUNT);
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 (validControlSource(entity, EffectSourceUtil.CHARM) == null) {
entity.removeEffect(ModEffects.CHARM.get());
EffectSourceUtil.clear(entity, 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());
}
public static LivingEntity validTauntSource(LivingEntity entity) {
return validControlSource(entity, EffectSourceUtil.TAUNT);
}
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;
}
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) {
if (!entity.hasEffect(ModEffects.SOUL_ROT.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT);
if (!entity.hasEffect(ModEffects.BLEED.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
if (!entity.hasEffect(ModEffects.FRACTURE.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE);
}
private ControlEffectHelper() {}
}
@@ -0,0 +1,62 @@
package net.fretux.ailments.util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import javax.annotation.Nullable;
import java.util.UUID;
public final class EffectSourceUtil {
public static final String SOUL_ROT = "ascend_ailments.soul_rot";
public static final String BLEED = "ascend_ailments.bleed";
public static final String FRACTURE = "ascend_ailments.fracture";
public static final String FEAR = "ascend_ailments.fear";
public static final String CHARM = "ascend_ailments.charm";
public static final String TAUNT = "ascend_ailments.taunt";
public static final String OVERCHARM = "ascend_ailments.overcharm";
public static void setSource(LivingEntity target, String ailment, @Nullable LivingEntity source) {
CompoundTag data = target.getPersistentData();
if (source == null) data.remove(sourceKey(ailment));
else data.putUUID(sourceKey(ailment), source.getUUID());
}
@Nullable public static UUID getSourceUuid(LivingEntity target, String ailment) {
CompoundTag data = target.getPersistentData();
String key = sourceKey(ailment);
return data.hasUUID(key) ? data.getUUID(key) : null;
}
@Nullable public static LivingEntity getSource(LivingEntity target, String ailment) {
UUID id = getSourceUuid(target, ailment);
if (id == null || !(target.level() instanceof ServerLevel level)) return null;
MinecraftServer server = level.getServer();
for (ServerLevel candidate : server.getAllLevels()) {
Entity entity = candidate.getEntity(id);
if (entity instanceof LivingEntity living) return living;
}
return null;
}
public static void setPotency(LivingEntity target, String ailment, double potency) {
target.getPersistentData().putFloat(potencyKey(ailment), (float) Math.max(0, potency));
}
public static float getPotency(LivingEntity target, String ailment) {
CompoundTag data = target.getPersistentData();
String key = potencyKey(ailment);
return data.contains(key) ? Math.max(0, data.getFloat(key)) : 1.0F;
}
public static void clear(LivingEntity target, String ailment) {
target.getPersistentData().remove(sourceKey(ailment));
target.getPersistentData().remove(potencyKey(ailment));
}
private static String sourceKey(String ailment) { return ailment + ".source"; }
private static String potencyKey(String ailment) { return ailment + ".potency"; }
private EffectSourceUtil() {}
}
@@ -0,0 +1,71 @@
package net.fretux.ailments.util;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.damage.ModDamageSources;
import net.fretux.ailments.network.AilmentNetwork;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
/** Server-authoritative Hemorrhage progress produced by stacked Bleed damage. */
public final class HemorrhageTracker {
private static final String PROGRESS = "ascend_ailments.hemorrhage.progress";
/**
* Records one successful Bleed damage tick and triggers the max-health burst when full.
* Arcane potency is snapshotted when Bleed is applied, so an unloaded source cannot change an active meter.
*/
public static boolean recordBleedDamage(LivingEntity target, int amplifier, double arcanePotency) {
if (target.level().isClientSide
|| amplifier + 1 < AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_MINIMUM_STACKS)) return false;
double threshold = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_THRESHOLD);
double added = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_FILL_PER_TICK)
* Math.max(0.0, arcanePotency);
if (added <= 0) return false;
double progress = Math.min(threshold, getProgress(target) + added);
if (progress < threshold) {
target.getPersistentData().putDouble(PROGRESS, progress);
syncDisplay(target);
return false;
}
clear(target);
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);
}
return true;
}
public static double getProgress(LivingEntity target) {
CompoundTag data = target.getPersistentData();
return data.contains(PROGRESS) ? Math.max(0.0, data.getDouble(PROGRESS)) : 0.0;
}
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);
}
public static void clear(LivingEntity target) {
target.getPersistentData().remove(PROGRESS);
hideDisplay(target);
}
/** Synchronizes the compact client HUD after login or a meter update. */
public static void syncDisplay(LivingEntity target) {
if (!(target instanceof ServerPlayer player)) return;
double fraction = getFraction(player);
AilmentNetwork.sendHemorrhageProgress(player,
AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_SHOW_PLAYER_BAR) ? (float) fraction : 0.0F);
}
/** Hides the client HUD while retaining persistent meter progress. */
public static void hideDisplay(LivingEntity target) {
if (target instanceof ServerPlayer player) AilmentNetwork.sendHemorrhageProgress(player, 0.0F);
}
private HemorrhageTracker() {}
}
@@ -0,0 +1,16 @@
package net.fretux.ailments.util;
import net.minecraft.world.entity.LivingEntity;
public final class MentalControlUtil {
public static boolean isMentalControlResistant(LivingEntity target) {
return target.getType().is(ModEntityTypeTags.MENTAL_CONTROL_RESISTANT);
}
public static boolean isMentalControlImmune(LivingEntity target) {
return target.getType().is(ModEntityTypeTags.MENTAL_CONTROL_IMMUNE);
}
public static int resistantDuration(int duration, int divisor) {
return Math.max(20, duration / divisor);
}
private MentalControlUtil() {}
}
@@ -0,0 +1,18 @@
package net.fretux.ailments.util;
import net.fretux.ailments.AscendAilments;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.EntityType;
public final class ModEntityTypeTags {
public static final TagKey<EntityType<?>> BLEED_IMMUNE = tag("bleed_immune");
public static final TagKey<EntityType<?>> MENTAL_CONTROL_RESISTANT = tag("mental_control_resistant");
public static final TagKey<EntityType<?>> MENTAL_CONTROL_IMMUNE = tag("mental_control_immune");
private static TagKey<EntityType<?>> tag(String path) {
return TagKey.create(Registries.ENTITY_TYPE,
ResourceLocation.fromNamespaceAndPath(AscendAilments.MOD_ID, path));
}
private ModEntityTypeTags() {}
}
@@ -0,0 +1,28 @@
package net.fretux.ailments.util;
import net.fretux.ailments.AscendAilments;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
/** Public item tags consumed by Ascend: Ailments integrations. */
public final class ModItemTags {
/** Items in this tag apply one Bleed stack on direct melee hits. */
public static final TagKey<Item> BLEED_ON_HIT = tag("bleed_on_hit");
/** Items in these tags apply the corresponding ailment on direct melee hits. */
public static final TagKey<Item> SOUL_ROT_ON_HIT = tag("soul_rot_on_hit");
public static final TagKey<Item> FRACTURE_ON_HIT = tag("fracture_on_hit");
public static final TagKey<Item> FEAR_ON_HIT = tag("fear_on_hit");
public static final TagKey<Item> CHARM_ON_HIT = tag("charm_on_hit");
public static final TagKey<Item> TAUNT_ON_HIT = tag("taunt_on_hit");
public static final TagKey<Item> OVERCHARM_ON_HIT = tag("overcharm_on_hit");
public static final TagKey<Item> WINDED_ON_HIT = tag("winded_on_hit");
private static TagKey<Item> tag(String path) {
return TagKey.create(Registries.ITEM,
ResourceLocation.fromNamespaceAndPath(AscendAilments.MOD_ID, path));
}
private ModItemTags() {}
}