Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ff6fb59
Conditional Leeching + Conditional Predicate
Dubledice Aug 24, 2025
bf52165
New STATS
Dubledice Aug 24, 2025
7336969
NEW API + EVENT HOOKS
Dubledice Aug 24, 2025
62bcd68
Merge branch '1.20-Forge' into 1.20-Forge
Dubledice Aug 26, 2025
2559965
Merge branch 'mahjerion:1.20-Forge' into 1.20-Forge
Dubledice Aug 29, 2025
211c0ff
Merge branch 'mahjerion:1.20-Forge' into 1.20-Forge
Dubledice Sep 3, 2025
6ec6525
Suggested Fixes: Datapack Threshold
Dubledice Sep 3, 2025
c0a0476
Merge pull request #1 from Dubledice/pr/suggested-fixes
Dubledice Sep 3, 2025
5aece79
Suggested Fixes: Extension
Dubledice Sep 3, 2025
1fdcb7f
Merge pull request #2 from Dubledice/pr/suggested-fixes
Dubledice Sep 3, 2025
f06bf30
Suggested Fixes: Cleanup
Dubledice Sep 3, 2025
e83314d
Merge pull request #3 from Dubledice/pr/suggested-fixes
Dubledice Sep 3, 2025
0ee8eb6
Suggested Fixes: SpendThresholdManager.java
Dubledice Sep 3, 2025
c364377
Merge pull request #4 from Dubledice/pr/suggested-fixes
Dubledice Sep 3, 2025
26310f1
Suggested Fixes: SpendThresholdSpec.java
Dubledice Sep 3, 2025
5976286
Suggested Fixes: SpendThresholdSpec.java
Dubledice Sep 3, 2025
308fd77
Merge pull request #5 from Dubledice/pr/suggested-fixes
Dubledice Sep 3, 2025
46bc42a
Threshold Decay: implement decay in manager/runtime and tick processing
Dubledice Sep 3, 2025
88db75f
Threshold-Decay Fixed + Cleanup
Dubledice Sep 4, 2025
4618f8d
Quick getter Fix for 25percent_health.json
Dubledice Sep 4, 2025
9660577
Suggested Fixes
Dubledice Sep 4, 2025
a36d2c9
Suggested Fixes: Performance Optimization
Dubledice Sep 5, 2025
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
@@ -1,5 +1,6 @@
package com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders;

import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType;
import com.robertx22.library_of_exile.registry.ExileRegistryInit;
import com.robertx22.mine_and_slash.aoe_data.database.ailments.Ailments;
import com.robertx22.mine_and_slash.aoe_data.database.exile_effects.ExileEffectBuilder;
Expand Down Expand Up @@ -36,8 +37,10 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.UUID;
import java.util.Map;

import static net.minecraft.world.entity.ai.attributes.Attributes.*;

Expand Down Expand Up @@ -68,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 @@ -100,6 +105,50 @@ public static List<EffectCtx> getCurses() {

public static int ESSENCE_OF_FROST_MAX_STACKS = 5;

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

// Pretty names for resources (UI text)
private static final Map<ResourceType, String> RES_NAME = Map.of(
ResourceType.health, "Health",
ResourceType.mana, "Mana",
ResourceType.energy, "Energy",
ResourceType.magic_shield, "Magic Shield",
ResourceType.blood, "Blood"
);

// Suggested elements per resource (only used for coloring/category)

// ---------- Generic flags ----------
public static final EffectCtx LEECHING_STATE = state(
"leeching_state", "Leeching (State)", Elements.Physical
);
public static final EffectCtx REGEN_STATE = state(
"regen_state", "Regenerating (State)", Elements.Physical
);

// ---------- Per-resource flags (generated) ----------
public static final EnumMap<ResourceType, EffectCtx> LEECHING_STATE_BY_RES = new EnumMap<>(ResourceType.class);
public static final EnumMap<ResourceType, EffectCtx> REGEN_STATE_BY_RES = new EnumMap<>(ResourceType.class);

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

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

public static void init() {

}
Expand Down Expand Up @@ -359,6 +408,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 All @@ -385,6 +450,24 @@ public void registerAll() {
.tick(20D))
.buildForEffect())
.build();

// (NEW) Leeching & Healing
ExileEffectBuilder.of(LEECHING_STATE)
.maxStacks(1)
.build();

ExileEffectBuilder.of(REGEN_STATE)
.maxStacks(1)
.build();

// Register per-resource flags
for (EffectCtx ctx : LEECHING_STATE_BY_RES.values()) {
ExileEffectBuilder.of(ctx).maxStacks(1).build();
}

for (EffectCtx ctx : REGEN_STATE_BY_RES.values()) {
ExileEffectBuilder.of(ctx).maxStacks(1).build();
}

ExileEffectBuilder.of(BLIZZARD_REDUCE_HEAL_STRENGTH)
.maxStacks(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import com.robertx22.mine_and_slash.aoe_data.database.spells.schools.WaterSpells;
import com.robertx22.mine_and_slash.aoe_data.database.stats.base.EffectCtx;
import com.robertx22.mine_and_slash.database.data.spells.components.actions.PositionSource;
import com.robertx22.mine_and_slash.uncommon.effectdatas.rework.action.MissingResourceScalingEffect;
import com.robertx22.mine_and_slash.database.data.stats.layers.StatLayers;
import com.robertx22.mine_and_slash.database.data.stats.types.resources.mana.Mana;
import com.robertx22.mine_and_slash.mmorpg.MMORPG;
Expand All @@ -19,6 +18,9 @@
import com.robertx22.mine_and_slash.uncommon.effectdatas.rework.number_provider.NumberProvider;
import com.robertx22.mine_and_slash.uncommon.interfaces.EffectSides;
import com.robertx22.mine_and_slash.uncommon.utilityclasses.AllyOrEnemy;
// (NEW) Import for new Leeching and Healing Helpers
import com.robertx22.mine_and_slash.uncommon.effectdatas.rework.condition.HasExileEffectCondition;


import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -202,6 +204,26 @@ public void registerAll() {
}
}

/** While leeching (any resource) on Source side. */
public static HasExileEffectCondition whileLeeching() {
return new HasExileEffectCondition(ModEffects.LEECHING_STATE);
}

/** While regenerating (any resource) on Source side. */
public static HasExileEffectCondition whileRegen() {
return new HasExileEffectCondition(ModEffects.REGEN_STATE);
}

/** While leeching a specific resource on Source side. */
public static HasExileEffectCondition whileLeeching(ResourceType rt) {
return new HasExileEffectCondition(ModEffects.LEECHING_STATE_BY_RES.get(rt));
}

/** While regenerating a specific resource on Source side. */
public static HasExileEffectCondition whileRegen(ResourceType rt) {
return new HasExileEffectCondition(ModEffects.REGEN_STATE_BY_RES.get(rt));
}


// Resource scaling config for missing resource percentage
// This is used to define how much stat to apply based on the percentage of missing resource
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.robertx22.mine_and_slash.aoe_data.database.stats;

import com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects;
import com.robertx22.mine_and_slash.aoe_data.database.stat_conditions.StatConditions;
import com.robertx22.mine_and_slash.aoe_data.database.stat_effects.StatEffects;
import com.robertx22.mine_and_slash.aoe_data.database.stats.base.DatapackStatBuilder;
import com.robertx22.mine_and_slash.aoe_data.database.stats.base.EmptyAccessor;
import com.robertx22.mine_and_slash.database.data.stats.Stat;
import com.robertx22.mine_and_slash.database.data.stats.Stat.StatGroup;
import com.robertx22.mine_and_slash.database.data.stats.StatGuiGroup;
import com.robertx22.mine_and_slash.database.data.stats.StatScaling;
import com.robertx22.mine_and_slash.database.data.stats.datapacks.test.DataPackStatAccessor;
Expand All @@ -16,6 +18,7 @@
import com.robertx22.mine_and_slash.uncommon.effectdatas.DamageEvent;
import com.robertx22.mine_and_slash.uncommon.effectdatas.SpendResourceEvent;
import com.robertx22.mine_and_slash.uncommon.effectdatas.rework.EventData;
import com.robertx22.mine_and_slash.uncommon.effectdatas.rework.condition.HasExileEffectCondition;
import com.robertx22.mine_and_slash.uncommon.enumclasses.AttackType;
import com.robertx22.mine_and_slash.uncommon.enumclasses.Elements;
import com.robertx22.mine_and_slash.uncommon.enumclasses.PlayStyle;
Expand Down Expand Up @@ -580,6 +583,25 @@ public class OffenseStats {
.build();


// While Leeching (any) → contributes to existing Crit Damage bucket
public static final DataPackStatAccessor<EmptyAccessor> WHILE_LEECHING_MS_MORE_DAMAGE = DatapackStatBuilder
.ofSingle("while_leeching_ms_more_damage", Elements.Physical)
.worksWithEvent(DamageEvent.ID)
.setPriority(StatPriority.Damage.DAMAGE_LAYERS)
.setSide(EffectSides.Source)
.addCondition(new HasExileEffectCondition(ModEffects.LEECHING_STATE_BY_RES.get(ResourceType.magic_shield)))
.addCondition(StatConditions.IS_NOT_DOT)
.addEffect(StatEffects.Layers.ADDITIVE_DAMAGE_PERCENT)
.setLocName(x -> "More Damage while Leeching Magic Shield")
.setLocDesc(x -> Stat.VAL1 + "% More Damage While Leeching Magic Shield.")
.modifyAfterDone(x -> {
x.is_perc = true; // percent bonus
})
.build();




public static void init() {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,21 @@ public class ResourceStats {
})
.build();

// Allows life leech to persist when at full Health (reservoir is NOT discarded).
public static final DataPackStatAccessor<EmptyAccessor> LEECH_AT_FULL_HEALTH = DatapackStatBuilder
.ofSingle("leech_at_full_health", Elements.Physical)
.setLocName(x -> "Leech at Full Health")
.setLocDesc(x -> "Allows Leech to Persist When at Full Health")
.modifyAfterDone(x -> {
x.is_perc = false; // treat as boolean (0 = off, >0 = on)
x.base = 0;
x.min = 0;
x.max = 1;
x.format = ChatFormatting.RED.getName();
x.group = Stat.StatGroup.MAIN;
})
.build();

public static void init() {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.robertx22.mine_and_slash.event_hooks.ontick.UnequipGear;
import com.robertx22.mine_and_slash.event_hooks.player.OnLogin;
import com.robertx22.mine_and_slash.loot.LootModifiersList;
import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdRuntime;
import com.robertx22.mine_and_slash.mmorpg.MMORPG;
import com.robertx22.mine_and_slash.mmorpg.SlashRef;
import com.robertx22.mine_and_slash.saveclasses.CustomExactStatsData;
Expand Down Expand Up @@ -959,6 +960,14 @@ public void onSpellHitTarget(Entity spellEntity, LivingEntity target) {

}

// Tracks LOSS of resources (spend, drains, damage, etc.)
private final ResourceTracker resourceTracker = new ResourceTracker();
public ResourceTracker getResourceTracker() { return resourceTracker; }

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



public boolean alreadyHit(Entity spellEntity, LivingEntity target) {
// this makes sure piercing projectiles hit target only once and then pass through
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,99 @@
import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType;
import com.robertx22.mine_and_slash.uncommon.MathHelper;

import java.util.HashMap;
import java.util.EnumMap;
import java.util.Map;

/**
* Holds pending leech “reservoirs” per resource and applies them once per second.
*
* Design notes:
* - Clamp each reservoir to “≤ 5 seconds worth of per-second cap”.
* - Drain by the intended ‘take’ (min(reservoir, perSecondCap)), not by what was actually applied,
* so duration semantics remain consistent even if the target is capped/full.
* - Prune tiny leftovers to keep the map small.
*/
public class EntityLeechData {

private static final float EPS = 0.1f; // tiny cutoff to treat as zero
private final EnumMap<ResourceType, Float> store = new EnumMap<>(ResourceType.class);

private HashMap<ResourceType, Float> map = new HashMap<>();

public void addLeech(ResourceType type, float num) {
if (!map.containsKey(type)) {
map.put(type, 0f);
/** Adds (or subtracts) pending leech for a resource. */
public void addLeech(ResourceType type, float amount) {
store.merge(type, amount, Float::sum);
// prune tiny / negative leftovers
if (store.getOrDefault(type, 0f) <= EPS) {
store.remove(type);
}
float fi = num + map.get(type);

map.put(type, fi);
}

// todo implement expiration after 5s
/**
* Called once per second. Applies up to the per-second cap for each resource,
* then drains the reservoir by the amount we *intended* to take.
*/
public void onSecondUseLeeches(EntityData data) {


// don't allow to accumulate more than x depending on total resource
// currently lets try with capping it to 5 seconds of regen.
for (Map.Entry<ResourceType, Float> en : map.entrySet()) {
float leechMaxPerSec = 5F * data.getUnit().getCalculatedStat(ResourceStats.LEECH_CAP.get(en.getKey())).getValue() / 100F;
float max = data.getMaximumResource(en.getKey()) * leechMaxPerSec;
float fi = MathHelper.clamp(en.getValue(), 0, max);
map.put(en.getKey(), fi);
// 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 capPercentPerSec = data.getUnit()
.getCalculatedStat(ResourceStats.LEECH_CAP.get(rt))
.getValue() / 100F;

float maxRes = data.getResources().getMax(data.entity, rt);
float fiveSecs = 5F * capPercentPerSec * maxRes; // “5 seconds worth” reservoir cap
float clamped = MathHelper.clamp(en.getValue(), 0, fiveSecs);
en.setValue(clamped);
}

for (Map.Entry<ResourceType, Float> entry : map.entrySet()) {
float leechMaxPerSec = data.getUnit().getCalculatedStat(ResourceStats.LEECH_CAP.get(entry.getKey())).getValue() / 100F;

float num = entry.getValue();

if (num > 1) {
float maxres = data.getResources().getMax(data.entity, entry.getKey());

float max = leechMaxPerSec * maxres;

if (num > max) {
num = max;
// 2) Apply per-resource leech once
for (Map.Entry<ResourceType, Float> entry : store.entrySet()) {
ResourceType rt = entry.getKey();
float reservoir = entry.getValue();
if (reservoir <= EPS) continue;

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

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

// Intended drain this second (bounded by per-second cap and reservoir)
float take = Math.min(reservoir, perSecondCap);
if (take <= EPS) continue;

// Hook: a future stat could allow full-health leeching
final boolean allowFullLeech = data.getUnit()
.getCalculatedStat(ResourceStats.LEECH_AT_FULL_HEALTH.get()).getValue() > 0;

// Apply and get what actually landed
float applied = data.getResources().restoreAndReturnApplied(
data.entity, rt, take,
com.robertx22.mine_and_slash.uncommon.effectdatas.rework.RestoreType.leech
);

// Full-resource policy:
// - Non-health: never persist at full → discard.
// - Health: persist only if 'leech_at_full_health' is enabled.
// If nothing landed (resource is full), enforce full-resource policy.
if (applied <= EPS) { // use EPS to avoid float noise
boolean keepReservoir =
(rt == ResourceType.health) && allowFullLeech; // only health with talent

if (!keepReservoir) {
entry.setValue(0f); // discard reservoir
}

addLeech(entry.getKey(), -num);
data.getResources().restore(data.entity, entry.getKey(), num);
continue; // skip draining by 'take'
}
}


// Normal path: drain by intended 'take' to preserve ≤5s duration
entry.setValue(reservoir - take);
}

// 3) Prune empty entries to keep the map small
store.entrySet().removeIf(e -> e.getValue() <= EPS);
}
}

Loading