Skip to content

Conversation

@Dubledice
Copy link

PR to Parent branch.

Commits summarized:

  • Addition of new Conditional Effects via HasExileEffectCondition.java
  • Introduce ResourceTracker to accumulate loss per resource and per “spec key”
  • Add SpendThresholdRegistry (global spec registration + resolution) and SpendThresholdManager
  • Clean up at ResourcesData.modify(...):
  • One additional Stats in ResourceStats.java
  • Two additional Stats in OffenseStats.java
  • EntityLeechData.java refactor for necessary logic improvements
  • New API + event hooks in ResourcesData.java @ line 118 -> 198
  • New event hook in DamageEvent.java for health tracking

General:

  • All new systems should be datapack-friendly and side-correct; debug is gated by flags and sent to the player only

IMPORTANT: The commits on Aug 19, 2025 were mandatory changes for me to access submodules, please review those changes carefully as I don't want them to cause issues for the rest of the team.

Contains (NEW) API + Event Hooks.

Contains Refactored/Factored Code.

Desc: Made for new "WHILE_LEECHING" system. Also houses new HasExileEffectCondition, e.g. allowing WHILE_(...) tags.
Added new More Damage While Leeching Magic Shield Offense Stat.

Added new Leech At Full Health Conditional Resource Stat,
Implemented brand new Resource Threshold fetcher, tracking ALL resources for conditional stats/effects.

Desc: Completely Datapack driven.
@Dubledice Dubledice requested a review from SaloEater August 26, 2025 19:19
}

// Pretty names for resources (UI text)
private static final Map<ResourceType, String> RES_NAME = Map.of(

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 use ResourceType to handle translations instead of raw text

Copy link
Author

Choose a reason for hiding this comment

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

I had figured that leeching was hardcoded and wouldn't need datapack functionality, unless you want me to use datadriven resource types.

);

// ---------- 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

@mahjerion
Copy link
Owner

mahjerion commented Aug 28, 2025

I see a lot of conversations resolved, but where any changes actually made? I'd like some of the conventions to be standardized. If it helps I can comment which conversations you've resolved I want actioned to make it more clear.

@Dubledice
Copy link
Author

Dubledice commented Aug 28, 2025

I see a lot of conversations resolved, but where any changes actually made? I'd like some of the conventions to be standardized. If it helps I can comment which conversations you've resolved I want actioned to make it more clear.

Yeah, please do. Although for many if not all resolved commits, I simply implemented the suggested changes.

@SaloEater
Copy link

but where any changes actually made?

It's still an ongoing process on making it datapack-driven, I hope too that Dice will push everything after it's done

@mahjerion
Copy link
Owner

but where any changes actually made?

It's still an ongoing process on making it datapack-driven, I hope too that Dice will push everything after it's done

Oh yeah, thanks for working with duble on this. Yeah spoke with duble earlier, I will wait patiently

@Dubledice Dubledice requested a review from SaloEater September 3, 2025 14:11
@Dubledice
Copy link
Author

Dubledice commented Sep 3, 2025

Added Suggested Fixes: Datapack Threshold to the PR. Contains all requested changes. Contains potentially some placeholder values like "on_expire" which requires the onExpire refactor to work, but it now is all datapack driven.

EDIT: Some of the PR is missing, I will add it named "Suggested Fixes: Extension". Apologies for the mistake.

EDIT 2: Suggested Fixes: Extension has been added.

public static int ESSENCE_OF_FROST_MAX_STACKS = 5;

// ---------- 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);
}


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.


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

@SerializedName("effect_id") public String effectId;
@SerializedName("duration_ticks") public int durationTicks = 200;
@SerializedName("exile_potion_id") public String effectId;
@SerializedName("duration_seconds") public int durationSeconds = 0;

Choose a reason for hiding this comment

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

Can you return back to ticks instead of seconds everywhere?
It seems like your code needs ticks and MnS also usually uses ticks

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.

I can revert this, but, the code still uses ticks. I just did some math to convert seconds to ticks because I am a neanderthal and cant count ticks. If it's an issue I can just deal with it and eventually I will remember the conversion.

Choose a reason for hiding this comment

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

Yes, please, let's use ticks instead of seconds

for (ProcAction a : onProc) {
if (!"apply_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue;
if (!"exile_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue;
var effect = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(a.effectId);

Choose a reason for hiding this comment

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

add an import and

Suggested change
var effect = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(a.effectId);
var effect = ExileDB.ExileEffects().get(a.effectId);


// Attach on-expire duration overrides (convert seconds -> ticks)
if (a.onExpire != null && !a.onExpire.isEmpty()) {
if (inst.onExpireEffectDurationTicks == null) {

Choose a reason for hiding this comment

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

This PR doesn't have ExileEffectInstanceData.onExpireEffectDurationTicks, looks like this belongs to an another PR

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 is apart of another PR. If it's causing errors just comment it out as it will be used later.

Choose a reason for hiding this comment

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

If you try to run your branch now, it won't even compile due to this error, so yea, just comment it out until next PR

Copy link
Author

Choose a reason for hiding this comment

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

Is it specifically lines 108-118 that is causing issues?

Choose a reason for hiding this comment

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

I mean, just try to compile the mod

Mine-And-Slash-Rework-new/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java:109: error: cannot find symbol
                        if (inst.onExpireEffectDurationTicks == null) {
                                ^
  symbol:   variable onExpireEffectDurationTicks
  location: variable inst of type ExileEffectInstanceData

Copy link
Author

Choose a reason for hiding this comment

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

Okay, got it. Thank you!

var unit = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(sp);
var store = unit.getStatusEffectsData();

for (ProcAction a : onProc) {

Choose a reason for hiding this comment

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

Replace this loop with

for (ProcAction a : onProc) {
                    if (!"exile_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue;
                    var effect = ExileDB.ExileEffects().get(a.effectId);
                    if (effect == null) continue;

                    int durTicks = SpendThresholdSpec.secondsToTicks(a.durationSeconds);
                    int stacks = Math.max(1, a.stacks);
                    var inst = EffectUtils.applyEffect(sp, effect, durTicks, stacks);

                    // Attach on-expire duration overrides (convert seconds -> ticks)
                    if (a.onExpire != null && !a.onExpire.isEmpty()) {
                        if (inst.onExpireEffectDurationTicks == null) {
                            inst.onExpireEffectDurationTicks = new java.util.HashMap<>();
                        }
                        for (var e : a.onExpire.entrySet()) {
                            int ticks = SpendThresholdSpec.secondsToTicks(Math.max(0, e.getValue()));
                            if (ticks > 0) {
                                inst.onExpireEffectDurationTicks.put(e.getKey(), ticks);
                            }
                        }
                    }

                    effect.onApply(sp);
                    unit.sync.setDirty();
                }

To make this work, replace EffectUtils with this:

public final class EffectUtils {
    private EffectUtils() {}

    /**
     * Apply/refresh a state effect on the player.
     *
     * @param sp            target player
     * @param ctx           effect context (ids defined in ModEffects)
     * @param durationTicks desired remaining lifetime (ticks); merged via MAX
     * @param stacks        desired stacks; clamped to effect.max_stacks and merged via MAX
     * @return ExileEffectInstanceData for the applied effect, or null if resolve failed.
     */
    public static ExileEffectInstanceData applyState(ServerPlayer sp, EffectCtx ctx, int durationTicks, int stacks) {
        final ExileEffect effect = resolveEffect(ctx);
        if (effect == null) return null;

        return applyEffect(sp, effect, durationTicks, stacks);
    }

    public static ExileEffectInstanceData applyEffect(ServerPlayer sp, ExileEffect effect, int durationTicks, int stacks) {
        if (effect == null) return null;

        var unit  = Load.Unit(sp);
        var store = unit.getStatusEffectsData();
        var inst  = store.getOrCreate(effect); // persist if missing

        // Merge stacks/ticks: refresh semantics (never decrease on re-apply)
        final int wanted = Math.max(1, stacks);
        final int capped = (effect.max_stacks > 0) ? Math.min(wanted, effect.max_stacks) : wanted;
        inst.stacks     = Math.max(inst.stacks, capped);
        inst.ticks_left = Math.max(inst.ticks_left, durationTicks);

        // Keep vanilla stats / one-of-a-kind cleanup in sync
        effect.onApply(sp);
        unit.sync.setDirty(); // network/state sync
        return inst;
    }

    /** Try both resourcePath (preferred) and id; some data uses either. */
    private static ExileEffect resolveEffect(EffectCtx ctx) {
        ExileEffect eff = ExileDB.ExileEffects().get(ctx.resourcePath);
        if (eff == null) eff = ExileDB.ExileEffects().get(ctx.id);
        return eff;
    }
}

Copy link
Author

Choose a reason for hiding this comment

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

What is wrong with the current version?

Choose a reason for hiding this comment

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

It's a good behavior to have no duplicated code and your for loop was almost a clean copy of EffectUtils.applyEffect so let's keep code clean

Copy link
Author

Choose a reason for hiding this comment

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

Replace this loop with

for (ProcAction a : onProc) {
                    if (!"exile_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue;
                    var effect = ExileDB.ExileEffects().get(a.effectId);
                    if (effect == null) continue;

                    int durTicks = SpendThresholdSpec.secondsToTicks(a.durationSeconds);
                    int stacks = Math.max(1, a.stacks);
                    var inst = EffectUtils.applyEffect(sp, effect, durTicks, stacks);

                    // Attach on-expire duration overrides (convert seconds -> ticks)
                    if (a.onExpire != null && !a.onExpire.isEmpty()) {
                        if (inst.onExpireEffectDurationTicks == null) {
                            inst.onExpireEffectDurationTicks = new java.util.HashMap<>();
                        }
                        for (var e : a.onExpire.entrySet()) {
                            int ticks = SpendThresholdSpec.secondsToTicks(Math.max(0, e.getValue()));
                            if (ticks > 0) {
                                inst.onExpireEffectDurationTicks.put(e.getKey(), ticks);
                            }
                        }
                    }

                    effect.onApply(sp);
                    unit.sync.setDirty();
                }

To make this work, replace EffectUtils with this:

public final class EffectUtils {
    private EffectUtils() {}

    /**
     * Apply/refresh a state effect on the player.
     *
     * @param sp            target player
     * @param ctx           effect context (ids defined in ModEffects)
     * @param durationTicks desired remaining lifetime (ticks); merged via MAX
     * @param stacks        desired stacks; clamped to effect.max_stacks and merged via MAX
     * @return ExileEffectInstanceData for the applied effect, or null if resolve failed.
     */
    public static ExileEffectInstanceData applyState(ServerPlayer sp, EffectCtx ctx, int durationTicks, int stacks) {
        final ExileEffect effect = resolveEffect(ctx);
        if (effect == null) return null;

        return applyEffect(sp, effect, durationTicks, stacks);
    }

    public static ExileEffectInstanceData applyEffect(ServerPlayer sp, ExileEffect effect, int durationTicks, int stacks) {
        if (effect == null) return null;

        var unit  = Load.Unit(sp);
        var store = unit.getStatusEffectsData();
        var inst  = store.getOrCreate(effect); // persist if missing

        // Merge stacks/ticks: refresh semantics (never decrease on re-apply)
        final int wanted = Math.max(1, stacks);
        final int capped = (effect.max_stacks > 0) ? Math.min(wanted, effect.max_stacks) : wanted;
        inst.stacks     = Math.max(inst.stacks, capped);
        inst.ticks_left = Math.max(inst.ticks_left, durationTicks);

        // Keep vanilla stats / one-of-a-kind cleanup in sync
        effect.onApply(sp);
        unit.sync.setDirty(); // network/state sync
        return inst;
    }

    /** Try both resourcePath (preferred) and id; some data uses either. */
    private static ExileEffect resolveEffect(EffectCtx ctx) {
        ExileEffect eff = ExileDB.ExileEffects().get(ctx.resourcePath);
        if (eff == null) eff = ExileDB.ExileEffects().get(ctx.id);
        return eff;
    }
}

This ended up breaking my code in EffectUtils.java, but I will fix it since I see the vision.

@Dubledice Dubledice requested a review from SaloEater September 3, 2025 18:40
@Dubledice
Copy link
Author

Added changes to new PR: Suggested Fixes: Cleanup. Awaiting review.

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 final class SpendThresholdManager {
private SpendThresholdManager() {}

public static void registerDefaults() {

Choose a reason for hiding this comment

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

This still needs to be removed

Copy link
Author

Choose a reason for hiding this comment

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

I can't delete SpendThresholdManager.java as it is used for onExpire logic later. It also handles debugging.

@Dubledice
Copy link
Author

@Dubledice
Copy link
Author

Added Suggested Fixes: SpendThresholdSpec.java to the PR.

@Dubledice Dubledice requested a review from SaloEater September 3, 2025 21:18
Copy link

@SaloEater SaloEater left a comment

Choose a reason for hiding this comment

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

Fingers crossed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants