Initial Ascend Ailments implementation
This commit is contained in:
@@ -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() {}
|
||||
}
|
||||
Reference in New Issue
Block a user