Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public class ModEffects implements ExileRegistryInit {
public static EffectCtx BLIND = new EffectCtx("blind", "Blind", Elements.Shadow, EffectType.negative);
public static EffectCtx STUN = new EffectCtx("stun", "Stun", Elements.Physical, EffectType.negative);
public static EffectCtx GALE_FORCE = new EffectCtx("gale_force", "Gale Force", Elements.Physical, EffectType.beneficial);
public static EffectCtx WRATH_OF_THE_JUGGERNAUT = new EffectCtx("wrath_of_the_juggernaut", "Wrath of the Juggernaut", Elements.Physical, EffectType.beneficial);
public static EffectCtx BURNOUT = new EffectCtx("burnout", "Burnout", Elements.Physical, EffectType.negative);

// these could be used for map affixes
public static EffectCtx SLOW = new EffectCtx("slow", "Lethargy", Elements.Physical, EffectType.negative);
Expand Down Expand Up @@ -105,8 +107,7 @@ public static List<EffectCtx> getCurses() {

// ---------- Helper ----------
private static EffectCtx state(String id, String name, Elements elem) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be done as method overload

Suggested change
private static EffectCtx state(String id, String name, Elements elem) {
private static EffectCtx state(String id, String name, Elements elem) {
return new EffectCtx(id, name, elem, EffectType.beneficial);
}
private static EffectCtx statePhysical(String id, String name) {
return state(id, name, Elements.Physical);
}

// EffectCtx constructor already adds itself to ModEffects.ALL in this file's pattern
return new EffectCtx(id, name, elem, EffectType.beneficial);
return new EffectCtx(id, name, (elem != null ? elem : Elements.Physical), EffectType.beneficial);
}

// Pretty names for resources (UI text)
Expand All @@ -119,13 +120,6 @@ private static EffectCtx state(String id, String name, Elements elem) {
);

// Suggested elements per resource (only used for coloring/category)
private static final Map<ResourceType, Elements> RES_ELEM = Map.of(
ResourceType.health, Elements.Physical,
ResourceType.mana, Elements.Cold,
ResourceType.energy, Elements.Nature,
ResourceType.magic_shield, Elements.Shadow,
ResourceType.blood, Elements.Fire
);

// ---------- Generic flags ----------
public static final EffectCtx LEECHING_STATE = state(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In next PR after you move thresholds generation into database it should be possible to replace raw text with translatable placeholders similar to locNameForLangFile function

Expand All @@ -141,14 +135,13 @@ private static EffectCtx state(String id, String name, Elements elem) {

static {
for (var rt : RES_NAME.keySet()) {
var elem = RES_ELEM.get(rt);
var nice = RES_NAME.get(rt);

LEECHING_STATE_BY_RES.put(
rt, state("leeching_" + rt.id + "_state", "Leeching " + nice, elem)
rt, state("leeching_" + rt.id + "_state", "Leeching " + nice, null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always a good behavior to get rid of arguments that can be null

Suggested change
rt, state("leeching_" + rt.id + "_state", "Leeching " + nice, null)
rt, statePhysical("leeching_" + rt.id + "_state", "Leeching " + nice)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, same thing here. I can't remove null. It causes errors because of how EffectCtx is setup.

);
REGEN_STATE_BY_RES.put(
rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, elem)
rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, null)
rt, statePhysical("regen_" + rt.id + "_state", "Regenerating " + nice)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why statePhysical?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also changing it to:

            REGEN_STATE_BY_RES.put(
                rt, statePhysical("regen_" + rt.id + "_state", "Regenerating " + nice)
            );

causes errors. Only rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, null) is accepted.

Copy link

@SaloEater SaloEater Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that null you had before meant Elements.Physical in your state method little higher here

Copy link
Author

@Dubledice Dubledice Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that null you had before meant Elements.Physical in your state method little higher here

Sure, but I can not change it to anything else other than using physical or another element. If you want me to change it to rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, Elements.Physical) I can do that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but I can not change it to anything else other than using physical. If you want me to change it to rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, Elements.Physical) I can do that.

Yea, either this or what I suggested before

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but I can not change it to anything else other than using physical. If you want me to change it to rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, Elements.Physical) I can do that.

Yea, either this or what I suggested before

I can't do what you suggested before because of how EffectCtx is defined. I will have to use a specific element from now on. Which I assume I can just default everything to phys if I have to.

);
}
}
Expand Down Expand Up @@ -412,6 +405,22 @@ public void registerAll() {
.addTags(EffectTags.song, EffectTags.offensive)
.build();

ExileEffectBuilder.of(WRATH_OF_THE_JUGGERNAUT)
.vanillaStat(VanillaStatData.create(ATTACK_SPEED, 0.30F, ModType.MORE, UUID.fromString("0c7a6e2c-5e5c-4f2f-9e3b-2a8e3c1a1f30")))
.vanillaStat(VanillaStatData.create(KNOCKBACK_RESISTANCE, 1.0F, ModType.FLAT, UUID.fromString("a9d9c9f2-9f0f-4521-9c3e-9f7a1c2b5e11")))
.stat(10, 10, DefenseStats.DAMAGE_REDUCTION.get(), ModType.FLAT)
.stat(100, 100, SpellChangeStats.COOLDOWN_REDUCTION_PER_SPELL_TAG.get(SpellTags.weapon_skill), ModType.FLAT)
.spell(SpellBuilder.forEffect()
.buildForEffect())
.addTags(EffectTags.offensive)
.maxStacks(1)
.build();

ExileEffectBuilder.of(BURNOUT)
.maxStacks(1)
.addTags(EffectTags.negative)
.build();


ExileEffectBuilder.of(REJUVENATE)
.maxStacks(5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,7 @@ public void onSpellHitTarget(Entity spellEntity, LivingEntity target) {
private final ResourceTracker resourceTracker = new ResourceTracker();
public ResourceTracker getResourceTracker() { return resourceTracker; }

public final SpendThresholdRuntime spendRuntime = new SpendThresholdRuntime();
private final SpendThresholdRuntime spendRuntime = new SpendThresholdRuntime();
public SpendThresholdRuntime getSpendRuntime() { return spendRuntime; }


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ public void onSecondUseLeeches(EntityData data) {
// 1) Clamp stored leech per resource to ≤ 5s of cap (prevents unbounded queues)
for (Map.Entry<ResourceType, Float> en : store.entrySet()) {
ResourceType rt = en.getKey();
float capPctPerSec = data.getUnit()
float capPercentPerSec = data.getUnit()
.getCalculatedStat(ResourceStats.LEECH_CAP.get(rt))
.getValue() / 100F;

float maxRes = data.getResources().getMax(data.entity, rt);
float fiveSecs = 5F * capPctPerSec * maxRes; // “5 seconds worth” reservoir cap
float fiveSecs = 5F * capPercentPerSec * maxRes; // “5 seconds worth” reservoir cap
float clamped = MathHelper.clamp(en.getValue(), 0, fiveSecs);
en.setValue(clamped);
}
Expand All @@ -55,12 +55,12 @@ public void onSecondUseLeeches(EntityData data) {
float reservoir = entry.getValue();
if (reservoir <= EPS) continue;

float capPctPerSec = data.getUnit()
float capPercentPerSec = data.getUnit()
.getCalculatedStat(ResourceStats.LEECH_CAP.get(rt))
.getValue() / 100F;

float maxRes = data.getResources().getMax(data.entity, rt);
float perSecondCap = capPctPerSec * maxRes;
float perSecondCap = capPercentPerSec * maxRes;

// Intended drain this second (bounded by per-second cap and reservoir)
float take = Math.min(reservoir, perSecondCap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,6 @@ public static void trigger(LivingEntity entity, ResourceType type, float loss, L
long now = sp.level().getGameTime(); // ticks
SpendThresholdManager.processSpend(sp, unit, type, loss, now);
}

/** Wire health damage into the unified loss path. */
@net.minecraftforge.eventbus.api.SubscribeEvent
public static void onLivingDamage(net.minecraftforge.event.entity.living.LivingDamageEvent evt) {
if (!(evt.getEntity() instanceof ServerPlayer sp)) return;
float applied = evt.getAmount();
if (applied > 0f) {
trigger(sp, ResourceType.health, applied, LossSource.Damage);
}
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class OnResourceRestore {

// ===== Gameplay tuning =====
/** State lifetime in ticks; should exceed your leech cadence + jitter. */
private static final int STATE_TICKS = 60; // ~3.0s @20tps
private static final int STATE_TICKS = 20;

// ===== Debug controls =====
/** Global toggle for chat debug. Safe to leave false in prod. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,58 +9,77 @@

public class DataDrivenSpendThresholdSpec extends SpendThresholdSpec {

public enum ThresholdMode { X_PER_LEVEL, FLAT, PCT_OF_MAX }
public enum ThresholdMode { FLAT, PERCENT_OF_MAX }

private final ThresholdMode mode;
private final float value;
private final boolean multiplyByLevel;
@Nullable private final ResourceType pctMaxOf;
@Nullable private final ResourceType percentMaxOf;
private final boolean showUi;

public DataDrivenSpendThresholdSpec(
String key,
ResourceType resource,
ThresholdMode mode,
float value,
boolean multiplyByLevel,
@Nullable ResourceType pctMaxOf,
@Nullable ResourceType percentMaxOf,
Set<String> lockWhileEffectIds,
int cooldownTicks,
boolean lockWhileCooldown,
boolean dropProgressWhileLocked,
boolean resetProgressOnProc
boolean resetProgressOnProc,
boolean showUi
) {
super(resource, /*perLevelFactor (unused)*/ 0f, key,
lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc);
super(resource, 0f, key,
lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, showUi);
this.mode = mode;
this.value = value;
this.multiplyByLevel = multiplyByLevel;
this.pctMaxOf = pctMaxOf;
this.percentMaxOf = percentMaxOf;
this.showUi = showUi;
}

// Backward-compatible ctor (defaults showUi=false)
public DataDrivenSpendThresholdSpec(
String key,
ResourceType resource,
ThresholdMode mode,
float value,
boolean multiplyByLevel,
@Nullable ResourceType percentMaxOf,
Set<String> lockWhileEffectIds,
int cooldownTicks,
boolean lockWhileCooldown,
boolean dropProgressWhileLocked,
boolean resetProgressOnProc
) {
this(key, resource, mode, value, multiplyByLevel, percentMaxOf, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false);
}

@Override
public float thresholdFor(EntityData unit) {
float base;
switch (mode) {
case X_PER_LEVEL:
base = value * Math.max(1, unit.getLevel());
break;
case FLAT:
base = value * (multiplyByLevel ? Math.max(1, unit.getLevel()) : 1f);
break;
case PCT_OF_MAX:
ResourceType rt = (pctMaxOf != null) ? pctMaxOf : resource();
float max = unit.getResources().getMax(unit.getEntity(), rt);
case PERCENT_OF_MAX -> {
// default to this spec's resource if percentOf is null
ResourceType tgt = (percentMaxOf != null) ? percentMaxOf : resource();
float max = unit.getResources().getMax(unit.getEntity(), tgt);
base = (value / 100f) * max;
if (multiplyByLevel) base *= Math.max(1, unit.getLevel());
break;
default:
base = 0f;
}
case FLAT -> base = value;
default -> base = value;
}
if (multiplyByLevel) base *= Math.max(1, unit.getLevel());
return Math.max(0f, base);
}

@Override
public void onProc(ServerPlayer sp, int procs) {
// No default action here; datapack loader wires actions.
}

public boolean showUi() { return showUi; }

// Perk lock is handled by callers (anonymous subclass) when needed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment needed here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this will be implemented later. It is placeholder for now.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, just make sure next time no placeholders are added.
They just add extra lines and some complexity to the PR while doing literally nothing, I think everyone would appreciate such approach

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType;
import net.minecraft.server.level.ServerPlayer;

import javax.annotation.Nullable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
Expand All @@ -22,15 +21,11 @@ public abstract class SpendThresholdSpec {
private final boolean lockWhileCooldown; // treat cooldown as a lock
private final boolean dropProgressWhileLocked;
private final boolean resetProgressOnProc;
private final boolean showUi; // whether to render progress HUD for this spec

// registry ordering (lower runs first)
private int priority = 0;

// Legacy/simple ctor
public SpendThresholdSpec(ResourceType resource, float perLevelFactor, String key) {
this(resource, perLevelFactor, key, Collections.emptySet(), 0, false, true, true);
}

// Full ctor used by data-driven impl
public SpendThresholdSpec(ResourceType resource,
float perLevelFactor,
Expand All @@ -39,7 +34,8 @@ public SpendThresholdSpec(ResourceType resource,
int cooldownTicks,
boolean lockWhileCooldown,
boolean dropProgressWhileLocked,
boolean resetProgressOnProc) {
boolean resetProgressOnProc,
boolean showUi) {
this.resource = resource;
this.perLevelFactor = perLevelFactor;
this.key = key;
Expand All @@ -48,6 +44,19 @@ public SpendThresholdSpec(ResourceType resource,
this.lockWhileCooldown = lockWhileCooldown;
this.dropProgressWhileLocked = dropProgressWhileLocked;
this.resetProgressOnProc = resetProgressOnProc;
this.showUi = showUi;
}

// Backward-compatible ctor (defaults showUi=false)
public SpendThresholdSpec(ResourceType resource,
float perLevelFactor,
String key,
Set<String> lockWhileEffectIds,
int cooldownTicks,
boolean lockWhileCooldown,
boolean dropProgressWhileLocked,
boolean resetProgressOnProc) {
this(resource, perLevelFactor, key, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false);
}

// ===== accessors =====
Expand All @@ -59,6 +68,7 @@ public SpendThresholdSpec(ResourceType resource,
public boolean resetOnProc() { return resetProgressOnProc; }
public int cooldownTicks() { return cooldownTicks; }
public int priority() { return priority; }
public boolean showUi() { return showUi; }

// fluent config (for code-defined specs)
Copy link

@SaloEater SaloEater Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Methods from 73 to 111 still needed to be removed excluding withPriority method

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dubledice this is still pending

public SpendThresholdSpec withCooldownSeconds(int seconds) {
Expand Down Expand Up @@ -88,9 +98,18 @@ public SpendThresholdSpec withPriority(int p) {

private SpendThresholdSpec newWrapper(Set<String> lockIds, int cooldown, boolean lockCD, boolean dropLocked, boolean resetOnProc) {
// create a shallow “copy” retaining dynamic behavior (onProc/thresholdFor come from subclass)
return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, lockIds, cooldown, lockCD, dropLocked, resetOnProc) {
return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, lockIds, cooldown, lockCD, dropLocked, resetOnProc, this.showUi) {
@Override public float thresholdFor(EntityData unit) { return SpendThresholdSpec.this.thresholdFor(unit); }
@Override public void onProc(ServerPlayer sp, int procs) { SpendThresholdSpec.this.onProc(sp, procs); }
@Override public boolean isLockedFor(EntityData unit) { return SpendThresholdSpec.this.isLockedFor(unit); }
}.withPriority(this.priority);
}

public SpendThresholdSpec withShowUi(boolean on) {
return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc, on) {
@Override public float thresholdFor(EntityData unit) { return SpendThresholdSpec.this.thresholdFor(unit); }
@Override public void onProc(ServerPlayer sp, int procs) { SpendThresholdSpec.this.onProc(sp, procs); }
@Override public boolean isLockedFor(EntityData unit) { return SpendThresholdSpec.this.isLockedFor(unit); }
}.withPriority(this.priority);
}

Expand All @@ -104,12 +123,16 @@ public boolean isEffectLocked(EntityData unit) {
if (lockWhileEffectIds.isEmpty()) return false;
var store = unit.getStatusEffectsData();
for (String id : lockWhileEffectIds) {
ExileEffect fx = ExileDB.ExileEffects().get(id);
if (fx != null && store.has(fx)) return true;
ExileEffect effect = ExileDB.ExileEffects().get(id);
if (effect != null && store.has(effect)) return true;
}
return false;
}

public boolean isLockedFor(EntityData unit) {
return isEffectLocked(unit);
}

/** Start cooldown (no-op if cooldownTicks == 0). */
public void startCooldown(EntityData unit, long now) {
if (cooldownTicks > 0) {
Expand All @@ -122,7 +145,7 @@ public void startCooldown(EntityData unit, long now) {

// time helpers
public static int secondsToTicks(int seconds) {
return (seconds <= 0) ? 0 : Math.max(1, seconds * 20);
return (seconds <= 0) ? 0 : seconds * 20;
}
public static float ticksToSeconds(int ticks) { return ticks / 20f; }
}
Loading