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
Expand Up @@ -7,36 +7,20 @@
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 static final float EPS = 0.1f;
private final EnumMap<ResourceType, Float> store = new EnumMap<>(ResourceType.class);

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

/**
* 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) {

// 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()
Expand All @@ -49,7 +33,6 @@ public void onSecondUseLeeches(EntityData data) {
en.setValue(clamped);
}

// 2) Apply per-resource leech once
for (Map.Entry<ResourceType, Float> entry : store.entrySet()) {
ResourceType rt = entry.getKey();
float reservoir = entry.getValue();
Expand All @@ -62,40 +45,32 @@ public void onSecondUseLeeches(EntityData data) {
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
if (applied <= EPS) {
boolean keepReservoir =
(rt == ResourceType.health) && allowFullLeech; // only health with talent

if (!keepReservoir) {
entry.setValue(0f); // discard reservoir
}
continue; // skip draining by 'take'
continue;
}


// 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType;

/**
/*
* Accumulates resource LOSS per type (spend, drains, damage, etc).
* Call addLoss(...) whenever a resource actually decreases.
* Use consumeThresholds(...) / addAndConsumeForKey(...) to fire effects and keep remainder.
Expand All @@ -11,21 +11,17 @@ public class ResourceTracker {
private static final float EPS = 1e-4f;
private static final float DEFAULT_KEY_PROGRESS = 0f;

// Global per-resource accumulators (used for simple thresholds or debug)
private final java.util.EnumMap<ResourceType, Float> lost = new java.util.EnumMap<>(ResourceType.class);

/** Record an actual decrease in a resource. */
public void addLoss(ResourceType rt, float amount) {
if (amount <= 0f) return;
lost.merge(rt, amount, Float::sum);
}

/** Current accumulated loss for a resource. */
public float getLoss(ResourceType rt) {
return lost.getOrDefault(rt, 0f);
}

/** Consume thresholds for a single resource; keep remainder. */
public int consumeThresholds(ResourceType type, float threshold) {
if (threshold <= 0f) return 0;
float have = lost.getOrDefault(type, 0f);
Expand All @@ -39,16 +35,11 @@ public int consumeThresholds(ResourceType type, float threshold) {
return procs;
}

/**
* Consume as many full thresholds as available across a set of resources (combined bucket).
* Drain is deterministic: the iteration order of the set decides which resource is consumed first.
* <p><b>Note:</b> Pass an {@link java.util.EnumSet} to guarantee stable drain order.</p>
*/
public int consumeThresholdsAcross(java.util.Set<ResourceType> types, float threshold) {
if (threshold <= 0f || types == null || types.isEmpty()) return 0;

int procs = 0;
// Loop while the combined total can pay for at least one threshold

while (total(types) + EPS >= threshold) {
float need = threshold;

Expand All @@ -63,33 +54,30 @@ public int consumeThresholdsAcross(java.util.Set<ResourceType> types, float thre
else lost.put(rt, remaining);
need -= take;
}
if (need <= EPS) break; // satisfied this proc
if (need <= EPS) break;
}
procs++;
}

// Prune tiny leftovers just in case
for (ResourceType rt : types) {
if (lost.getOrDefault(rt, 0f) <= EPS) lost.remove(rt);
}
return procs;
}

/** Sum of accumulated losses for the given set. */
private float total(java.util.Set<ResourceType> types) {
float sum = 0f;
for (ResourceType rt : types) sum += lost.getOrDefault(rt, 0f);
return sum;
}

// Per-key cursors so multiple specs on the same resource don't interfere
private final java.util.EnumMap<ResourceType, java.util.Map<String, Float>> keyProgress =
new java.util.EnumMap<>(ResourceType.class);

private java.util.Map<String, Float> getKeyProgressOrCreate(ResourceType rt) {
return keyProgress.computeIfAbsent(rt, __ -> new java.util.HashMap<>());
}

public void clearKey(ResourceType rt, String key) {
if (key == null || key.isEmpty()) return;
var byKey = keyProgress.get(rt);
Expand All @@ -100,7 +88,6 @@ public void clearKey(ResourceType rt, String key) {
}
}

/** Add loss to a specific key’s cursor for this resource and consume thresholds. */
public int addAndConsumeForKey(String key, ResourceType rt, float add, float threshold) {
if (key == null || key.isEmpty() || add <= 0f || threshold <= 0f) return 0;

Expand All @@ -118,33 +105,35 @@ public int addAndConsumeForKey(String key, ResourceType rt, float add, float thr
return procs;
}

/** Read current cursor for debug/UI. */
public float getKeyProgress(String key, ResourceType rt) {
var byKey = keyProgress.get(rt);
return byKey == null ? DEFAULT_KEY_PROGRESS : byKey.getOrDefault(key, DEFAULT_KEY_PROGRESS);
}


/**
* Decrease the cursor by a fixed amount, clamped at zero. Returns the new value.
*/
public void setKeyProgress(String key, ResourceType rt, float value) {
if (key == null || key.isEmpty()) return;
var byKey = keyProgress.computeIfAbsent(rt, __ -> new java.util.HashMap<>());
float val = Math.max(0f, value);
if (val <= EPS) byKey.remove(key); else byKey.put(key, val);
if (byKey.isEmpty()) keyProgress.remove(rt);
}

public float decayKeyProgress(String key, ResourceType rt, float amount) {
if (key == null || key.isEmpty() || amount <= 0f) return getKeyProgress(key, rt);
var byKey = keyProgress.get(rt);
if (byKey == null) return DEFAULT_KEY_PROGRESS;
float cur = byKey.getOrDefault(key, DEFAULT_KEY_PROGRESS);
if (byKey == null) return 0f;
float cur = byKey.getOrDefault(key, 0f);
float next = Math.max(0f, cur - amount);
if (next <= EPS) byKey.remove(key); else byKey.put(key, next);
if (byKey.isEmpty()) keyProgress.remove(rt);
return next;
}

/** Optional utility if you want to wipe a resource’s accumulator. */
public void clear(ResourceType rt) {
lost.remove(rt);
}

/** Optional: wipe all. */
public void clearAll() {
lost.clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import com.robertx22.mine_and_slash.database.data.value_calc.LeveledValue;
import com.robertx22.mine_and_slash.database.registry.ExileDB;
import com.robertx22.mine_and_slash.database.registry.ExileRegistryTypes;
import com.robertx22.mine_and_slash.event_hooks.my_events.EffectUtils;
import com.robertx22.mine_and_slash.mmorpg.DebugHud;
import com.robertx22.mine_and_slash.mmorpg.SlashRef;
import com.robertx22.mine_and_slash.saveclasses.ExactStatData;
import com.robertx22.mine_and_slash.saveclasses.gearitem.gear_bases.StatRangeInfo;
Expand All @@ -27,6 +29,7 @@
import com.robertx22.mine_and_slash.uncommon.localization.Words;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.Item;
Expand Down Expand Up @@ -252,16 +255,61 @@ public void onRemove(LivingEntity target) {

if (data != null) {
LivingEntity caster = data.getCaster(target.level());
if (caster != null && spell != null) {

if (caster == null) {
caster = target;
}
// --- Debug: show expire intent
if (DebugHud.ON_EXPIRE) {
if (target instanceof ServerPlayer sp) {
DebugHud.send(sp, "expire_onremove_" + GUID(), "[EFFECT][EXPIRE] onRemove(" + GUID() + ") stacks=" + data.stacks + ", ticks_left=" + data.ticks_left + ", infinite=" + data.is_infinite, 200);
}
if (caster instanceof ServerPlayer spc && caster != target) {
DebugHud.send(spc, "expire_onremove_" + GUID(), "[EFFECT][EXPIRE] Trigger from " + GUID() + " on target " + target.getName().getString(), 200);
}
}
if (spell != null && caster != null) {
SpellCtx ctx = SpellCtx.onExpire(caster, target, data.calcSpell);
ctx.expiringEffectId = this.GUID();
ctx.onExpireEffectDurationTicks = (data.onExpireEffectDurationTicks == null)
? java.util.Collections.emptyMap()
: java.util.Collections.unmodifiableMap(data.onExpireEffectDurationTicks);
spell.tryActivate(Spell.DEFAULT_EN_NAME, ctx); // source is default name at all times
if (DebugHud.ON_EXPIRE && target instanceof ServerPlayer sp2) {
DebugHud.send(sp2, "expire_dispatched_" + GUID(), "[EFFECT][EXPIRE] Dispatched attached spell for " + GUID(), 400);
}

if (!target.level().isClientSide && data.onExpireEffectDurationTicks != null && !data.onExpireEffectDurationTicks.isEmpty()) {
boolean anyApplied = false;
for (var entry : data.onExpireEffectDurationTicks.entrySet()) {
String effId = entry.getKey();
int durTicks = Math.max(1, entry.getValue());
if (ctx.onExpireApplied != null && ctx.onExpireApplied.contains(effId)) {
continue;
}
var extraEff = ExileDB.ExileEffects().get(effId);
if (extraEff == null) {
continue;
}
var instT = EffectUtils.applyEffect(target, extraEff, durTicks, 1, false);
if (instT != null) {
instT.is_infinite = false;
instT.caster_uuid = caster.getStringUUID();
if (DebugHud.ON_EXPIRE && target instanceof ServerPlayer spx) {
DebugHud.send(spx, "expire_extra_" + effId, "[EFFECT][EXPIRE] Extra-applied " + effId + " tl=" + instT.ticks_left, 400);
}
anyApplied = true;
}
}
if (anyApplied) {
var unitT = Load.Unit(target);
unitT.sync.setDirty();
}
}
}
}

EntityData unitdata = Load.Unit(target);
unitdata.getStatusEffectsData()
.get(this).stacks = 0;
unitdata.equipmentCache.STATUS.setDirty();
Load.Unit(target).equipmentCache.STATUS.setDirty();


} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import java.text.DecimalFormat;
import java.util.UUID;
import java.util.Map;
import java.util.HashMap;

public class ExileEffectInstanceData {

Expand All @@ -22,6 +24,8 @@ public class ExileEffectInstanceData {
public float str_multi = 1;
public int ticks_left = 0;

public Map<String, Integer> onExpireEffectDurationTicks = new HashMap<>();

public boolean isSpellNoLongerAllocated(LivingEntity en) {
if (self_cast) {
//calcSpell.equals(CalculatedSpellData.NO_SPELL_RELATED) indicate this effect is not related to a spell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import com.robertx22.mine_and_slash.database.data.spells.components.conditions.EffectCondition;
import com.robertx22.mine_and_slash.database.data.spells.components.selectors.BaseTargetSelector;
import com.robertx22.mine_and_slash.database.data.spells.components.selectors.TargetSelector;
import com.robertx22.mine_and_slash.database.registry.ExileDB;
import com.robertx22.mine_and_slash.database.data.spells.components.actions.ExileEffectAction;
import com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField;
import com.robertx22.mine_and_slash.database.data.spells.spell_classes.SpellCtx;
import com.robertx22.mine_and_slash.mmorpg.DebugHud;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.server.level.ServerPlayer;

import java.util.*;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -131,6 +135,52 @@ public void tryActivate(SpellCtx ctx) {
System.out.print(part.type + " action is null");
} else {
action.tryActivate(list, ctx, part);
if (DebugHud.ON_EXPIRE
&& ctx.activation == EntityActivation.ON_EXPIRE) {
if (ctx.caster instanceof ServerPlayer spc) {
DebugHud.send(spc, "expire_action_" + part.type, "[EFFECT][EXPIRE] Action=" + part.type + " targets=" + list.size(), 400);
}
}

if (!ctx.world.isClientSide && ctx.activation == EntityActivation.ON_EXPIRE
&& part.type.equals(SpellAction.EXILE_EFFECT.GUID())) {
try {
String actionType = part.get(MapField.POTION_ACTION);
if (actionType == null || !actionType.equals(ExileEffectAction.GiveOrTake.GIVE_STACKS.name())) {
continue;
}
String effId = part.get(MapField.EXILE_POTION_ID);
Double durD = part.getOrDefault(MapField.POTION_DURATION, 0D);
Double cntD = part.getOrDefault(MapField.COUNT, 1D);
int duration = Math.max(1, durD.intValue());
if (ctx.onExpireEffectDurationTicks != null && ctx.onExpireEffectDurationTicks.containsKey(effId)) {
int override = ctx.onExpireEffectDurationTicks.get(effId);
if (override > 0) {
duration = override;
}
}
int stacks = Math.max(1, cntD.intValue());
var effect = ExileDB.ExileEffects().get(effId);
if (effect != null) {
for (LivingEntity tgt : list) {
var unit = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(tgt);
var store = unit.getStatusEffectsData();
var inst = store.getOrCreate(effect);
inst.stacks = Math.max(inst.stacks, stacks);
inst.ticks_left = Math.max(inst.ticks_left, duration);
inst.is_infinite = false;
inst.caster_uuid = ctx.caster.getStringUUID();
try { effect.onApply(tgt); } catch (Exception ignored) {}
unit.equipmentCache.STATUS.setDirty();
unit.sync.setDirty();
if (ctx.caster instanceof ServerPlayer spc) {
DebugHud.send(spc, "expire_fallback_direct_" + effId, "[EFFECT][EXPIRE] Direct-applied " + effId + " tl=" + inst.ticks_left + " x" + inst.stacks, 400);
}
}
ctx.onExpireApplied.add(effId);
}
} catch (Exception ignored) {}
}
}
}

Expand Down
Loading