Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
@@ -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) {

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

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(

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.

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(

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

"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