diff --git a/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityLeechData.java b/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityLeechData.java index 50a4e71bd..87d7c0bc7 100644 --- a/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityLeechData.java +++ b/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityLeechData.java @@ -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 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 en : store.entrySet()) { ResourceType rt = en.getKey(); float capPercentPerSec = data.getUnit() @@ -49,7 +33,6 @@ public void onSecondUseLeeches(EntityData data) { en.setValue(clamped); } - // 2) Apply per-resource leech once for (Map.Entry entry : store.entrySet()) { ResourceType rt = entry.getKey(); float reservoir = entry.getValue(); @@ -62,7 +45,6 @@ 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; @@ -70,32 +52,25 @@ public void onSecondUseLeeches(EntityData data) { 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); } } diff --git a/src/main/java/com/robertx22/mine_and_slash/capability/entity/ResourceTracker.java b/src/main/java/com/robertx22/mine_and_slash/capability/entity/ResourceTracker.java index e208d0b41..507b45a00 100644 --- a/src/main/java/com/robertx22/mine_and_slash/capability/entity/ResourceTracker.java +++ b/src/main/java/com/robertx22/mine_and_slash/capability/entity/ResourceTracker.java @@ -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. @@ -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 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); @@ -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. - *

Note: Pass an {@link java.util.EnumSet} to guarantee stable drain order.

- */ public int consumeThresholdsAcross(java.util.Set 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; @@ -63,33 +54,30 @@ public int consumeThresholdsAcross(java.util.Set 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 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> keyProgress = new java.util.EnumMap<>(ResourceType.class); private java.util.Map 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); @@ -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; @@ -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(); } diff --git a/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffect.java b/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffect.java index efcde2d0a..40c55eac0 100644 --- a/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffect.java +++ b/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffect.java @@ -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; @@ -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; @@ -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) { diff --git a/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffectInstanceData.java b/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffectInstanceData.java index 35e1ec449..041b1fb19 100644 --- a/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffectInstanceData.java +++ b/src/main/java/com/robertx22/mine_and_slash/database/data/exile_effects/ExileEffectInstanceData.java @@ -9,6 +9,8 @@ import java.text.DecimalFormat; import java.util.UUID; +import java.util.Map; +import java.util.HashMap; public class ExileEffectInstanceData { @@ -22,6 +24,8 @@ public class ExileEffectInstanceData { public float str_multi = 1; public int ticks_left = 0; + public Map 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 diff --git a/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/ComponentPart.java b/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/ComponentPart.java index a882c79be..7dbc8b7e0 100644 --- a/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/ComponentPart.java +++ b/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/ComponentPart.java @@ -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; @@ -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) {} + } } } diff --git a/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/actions/ExileEffectAction.java b/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/actions/ExileEffectAction.java index ceed92427..fef387aa7 100644 --- a/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/actions/ExileEffectAction.java +++ b/src/main/java/com/robertx22/mine_and_slash/database/data/spells/components/actions/ExileEffectAction.java @@ -59,6 +59,12 @@ public void tryActivate(Collection targets, SpellCtx ctx, MapHolde targets.forEach(t -> { if (RandomUtils.roll(chance)) { + // If ON_EXPIRE, skip only GIVE_STACKS (handled upstream to avoid same-tick races), + // but DO allow REMOVE_STACKS so consumptions like Overheat work on entity-expire. + if (ctx.activation == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE + && action == GiveOrTake.GIVE_STACKS) { + return; + } ExilePotionEvent potionEvent = EventBuilder.ofEffect(ctx.calculatedSpellData, ctx.caster, t, Load.Unit(ctx.caster) .getLevel(), potion, action.getOther(), duration, infinite) .setSpell(ctx.calculatedSpellData.getSpell()) @@ -68,7 +74,11 @@ public void tryActivate(Collection targets, SpellCtx ctx, MapHolde potionEvent.spellid = ctx.calculatedSpellData.getSpell() .GUID(); - potionEvent.Activate(); + try { + potionEvent.Activate(); + } catch (Exception ex) { + throw ex; + } } }); diff --git a/src/main/java/com/robertx22/mine_and_slash/database/data/spells/spell_classes/SpellCtx.java b/src/main/java/com/robertx22/mine_and_slash/database/data/spells/spell_classes/SpellCtx.java index 12389ec60..677cab702 100644 --- a/src/main/java/com/robertx22/mine_and_slash/database/data/spells/spell_classes/SpellCtx.java +++ b/src/main/java/com/robertx22/mine_and_slash/database/data/spells/spell_classes/SpellCtx.java @@ -29,6 +29,11 @@ public class SpellCtx { public CalculatedSpellData calculatedSpellData; + public String expiringEffectId = null; + public java.util.Map onExpireEffectDurationTicks = java.util.Collections.emptyMap(); + public java.util.Set onExpireApplied = new java.util.HashSet<>(); + + public SpellCtx setSourceEntity(Entity en) { this.sourceEntity = en; return this; @@ -103,7 +108,12 @@ public static SpellCtx onExpire(LivingEntity caster, Entity sourceEntity, Calcul Objects.requireNonNull(sourceEntity); Objects.requireNonNull(data); LivingEntity target = sourceEntity instanceof LivingEntity ? (LivingEntity) sourceEntity : null; - return new SpellCtx(EntityActivation.ON_EXPIRE, sourceEntity, caster, target, data); + SpellCtx ctx = new SpellCtx(EntityActivation.ON_EXPIRE, sourceEntity, caster, target, data); + if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE + && caster instanceof net.minecraft.server.level.ServerPlayer sp) { + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[EFFECT][EXPIRE] Ctx built; caster=" + caster.getName().getString() + ", target=" + (target == null ? "null" : target.getName().getString()))); + } + return ctx; } public static SpellCtx onTick(LivingEntity caster, Entity sourceEntity, CalculatedSpellData data) { diff --git a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java index 6c4f43aee..9bdaa0750 100644 --- a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java @@ -6,6 +6,7 @@ import com.robertx22.mine_and_slash.database.registry.ExileDB; import com.robertx22.mine_and_slash.uncommon.datasaving.Load; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; /** * Utility for applying short-TTL "state" effects (e.g., leeching_state) to players. @@ -25,42 +26,43 @@ 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); + return applyEffect((LivingEntity) sp, effect, durationTicks, stacks); } public static ExileEffectInstanceData applyEffect(ServerPlayer sp, ExileEffect effect, int durationTicks, int stacks) { - if (effect == null) return null; + return applyEffect((LivingEntity) sp, effect, durationTicks, stacks, true); + } + + public static ExileEffectInstanceData applyEffect(LivingEntity entity, ExileEffect effect, int durationTicks, int stacks) { + return applyEffect(entity, effect, durationTicks, stacks, true); + } + + public static ExileEffectInstanceData applyEffect(LivingEntity entity, ExileEffect effect, int durationTicks, int stacks, boolean markDirty) { + if (effect == null || entity == null) return null; + if (durationTicks <= 0 || stacks <= 0) return null; - var unit = Load.Unit(sp); + var unit = Load.Unit(entity); var store = unit.getStatusEffectsData(); - var inst = store.getOrCreate(effect); // persist if missing + var inst = store.getOrCreate(effect); - // 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 + try { effect.onApply(entity); } catch (Exception ignored) {} + if (markDirty) { + unit.equipmentCache.STATUS.setDirty(); + if (entity instanceof ServerPlayer) { + unit.sync.setDirty(); + } + } 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); diff --git a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceRestore.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceRestore.java index e4e6b9fa8..d782bf693 100644 --- a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceRestore.java +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceRestore.java @@ -15,48 +15,31 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -/** - * Fires whenever a resource actually restored ("applied > 0"). - * Semantics are *literal*: we refresh short-TTL "state" flags only when ticks land. - * - First hit does NOT benefit. - * - Subsequent hits benefit while ticks are flowing (TTL bridges tick gaps). - * - * Extension points: - * 1) Add a new RestoreType branch in {@link #onRestore(ServerPlayer, ResourceType, float, RestoreType)}. - * 2) Keep any per-type registry sanity checks in a tiny ensure*Present(...) method. - * 3) Apply/refresh states via EffectUtils.applyState(...) with a conservative TTL. - */ + public class OnResourceRestore { - // ===== Gameplay tuning ===== - /** State lifetime in ticks; should exceed your leech cadence + jitter. */ private static final int STATE_TICKS = 20; - // ===== Debug controls ===== - /** Global toggle for chat debug. Safe to leave false in prod. */ public static boolean DEBUG_ENABLED = false; - /** Ignore tiny restores in debug spam. */ + public static float MIN_DEBUG_AMOUNT = 1.0f; - /** Which restore kinds print debug (default: leech only). */ + private static final EnumSet DEBUG_TYPES = EnumSet.of(RestoreType.leech); - /** Per (player, resource, type) cooldown for chat spam. */ + private static final Map nextAllowedTick = new ConcurrentHashMap<>(); private static final int PRINT_COOLDOWN_TICKS = 5; // 0.25s @20tps - /** Public entrypoint from restore sites. Pass the entity that RECEIVED the restore (attacker for leech). */ + public static void trigger(LivingEntity entity, ResourceType type, float amount, RestoreType restoreType) { - // Must only be called when net-applied > 0 if (amount <= 0) return; - // 1) Apply/refresh state flags on ServerPlayer only (current design scope). if (entity instanceof ServerPlayer sp) { onRestore(sp, type, amount, restoreType); } - // 2) Optional debug print (player-only) if (entity instanceof ServerPlayer sp) { maybeDebugRestore(sp, type, amount, restoreType); } @@ -72,38 +55,29 @@ private static void onRestore(ServerPlayer sp, case leech -> applyLeechStates(sp, type); // === Add NEW RestoreType cases here === // case -> applyNewKindStates(sp, type, amount); - default -> { /* ignore other kinds by default */ } + default -> {} } } - /** Leech: refresh generic + per-resource flags on the SOURCE player. */ private static void applyLeechStates(ServerPlayer sp, ResourceType type) { - // Optional runtime sanity (helps catch missing datapack JSON in dev) if (!ensureLeechEffectsPresent(sp, type)) { - return; // don’t pretend we applied anything + return; } - // Generic "while leeching" EffectUtils.applyState(sp, ModEffects.LEECHING_STATE, STATE_TICKS, 1); - // Per-resource "while leeching [resource]" var fx = ModEffects.LEECHING_STATE_BY_RES.get(type); if (fx != null) { EffectUtils.applyState(sp, fx, STATE_TICKS, 1); } } - /** - * Datapack/registry guard. Returns true if both generic and per-resource - * leech flags exist in the ExileDB registry. - */ private static boolean ensureLeechEffectsPresent(ServerPlayer sp, ResourceType type) { var anyFx = ExileDB.ExileEffects().get(ModEffects.LEECHING_STATE.GUID()); var byResFx = ExileDB.ExileEffects().get(ModEffects.LEECHING_STATE_BY_RES.get(type).GUID()); if (anyFx != null && byResFx != null) return true; - // Only nag in dev when debug is on; silence in prod. if (DEBUG_ENABLED) { sp.sendSystemMessage(Component.literal( "[RESTORE][WARN] Missing leech effects: any=" + (anyFx != null) + @@ -115,7 +89,6 @@ private static boolean ensureLeechEffectsPresent(ServerPlayer sp, ResourceType t return false; } - /** Centralized debug; respects type filters & cooldown. */ private static void maybeDebugRestore(ServerPlayer sp, ResourceType type, float amount, diff --git a/src/main/java/com/robertx22/mine_and_slash/event_hooks/ontick/OnServerTick.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/ontick/OnServerTick.java index e49628ff1..3684b622c 100644 --- a/src/main/java/com/robertx22/mine_and_slash/event_hooks/ontick/OnServerTick.java +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/ontick/OnServerTick.java @@ -18,6 +18,7 @@ import com.robertx22.mine_and_slash.uncommon.utilityclasses.LevelUtils; import com.robertx22.mine_and_slash.uncommon.utilityclasses.WorldUtils; import com.robertx22.mine_and_slash.vanilla_mc.packets.MapCompletePacket; +import com.robertx22.mine_and_slash.vanilla_mc.packets.ThresholdUiPacket; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -128,29 +129,30 @@ public static void onEndTick(ServerPlayer player) { playerData.spellCastingData.charges.onTicks(player, 5); } - // Every second, apply passive decay to inactive threshold progress if (age % 20 == 0) { long now = player.level().getGameTime(); var unit = Load.Unit(player); if (unit != null) { - // Iterate only active keys per resource for (var rt : ResourceType.values()) { for (var key : unit.getSpendRuntime().getActiveKeys(rt)) { var spec = unit.getSpendRuntime().getSpec(key); + if (!spec.showUi()) continue; long lastAct = unit.getSpendRuntime().getLastActivity(key); if (lastAct <= 0) continue; long since = now - lastAct; if (since < 300) continue; // < 15s long lastDecay = unit.getSpendRuntime().getLastDecay(key); - if (lastDecay == now) continue; // already decayed this second + if (lastDecay == now) continue; unit.getSpendRuntime().markDecay(key, now); - // Decay rate: 15% of this threshold's breakpoint per second float thr = spec.thresholdFor(unit); if (thr <= 0f) continue; - float decayPerSecond = thr * 0.15f; + float decayPerSecond = thr * 0.15f; // Decay rate: 15% of Threshold per second float newVal = unit.getResourceTracker().decayKeyProgress(key, rt, decayPerSecond); + if (unit.getSpendRuntime().progressScaledChanged(key, newVal, 10)) { + Packets.sendToClient(player, new ThresholdUiPacket(key, rt.id, newVal > 0f, newVal)); + } if (newVal <= 0f) { unit.getSpendRuntime().removeActive(rt, key); } diff --git a/src/main/java/com/robertx22/mine_and_slash/gui/overlays/EffectsOverlay.java b/src/main/java/com/robertx22/mine_and_slash/gui/overlays/EffectsOverlay.java index 9a75266d3..5ee1ec4f6 100644 --- a/src/main/java/com/robertx22/mine_and_slash/gui/overlays/EffectsOverlay.java +++ b/src/main/java/com/robertx22/mine_and_slash/gui/overlays/EffectsOverlay.java @@ -34,7 +34,7 @@ public static void render(GuiGraphics gui, boolean horizontal) { // Minecraft mc = Minecraft.getInstance(); for (Map.Entry en : Load.Unit(p).getStatusEffectsData().exileMap.entrySet()) { - if (!en.getValue().shouldRemove()) { + if (!en.getValue().shouldRemove() && !shouldHideLeeching(en.getKey())) { var eff = ExileDB.ExileEffects().get(en.getKey()); gui.blit(SlashRef.guiId("effect/effect_bg"), x, y, bgX, bgY, 0, 0, bgX, bgY, bgX, bgY); @@ -58,5 +58,37 @@ public static void render(GuiGraphics gui, boolean horizontal) { } } + var thresholdMap = com.robertx22.mine_and_slash.mechanics.thresholds.ui.ThresholdUiClient.visibleEntries(); + if (!thresholdMap.isEmpty()) { + for (var e : thresholdMap.entrySet()) { + String key = e.getKey(); + String resId = e.getValue(); + var rt = com.robertx22.mine_and_slash.saveclasses.unit.ResourceType.ofId(resId); + if (rt == null) continue; + + gui.blit(SlashRef.guiId("effect/effect_bg"), x, y, bgX, bgY, 0, 0, bgX, bgY, bgX, bgY); + gui.blit(SlashRef.guiId("effect/effect_overlay"), x, y, bgX, bgY, 0, 0, bgX, bgY, bgX, bgY); + + float prog = com.robertx22.mine_and_slash.mechanics.thresholds.ui.ThresholdUiClient.getProgress(key); + GuiUtils.renderScaledText(gui, (int) x + 10, (int) y + 10, 0.7F, String.valueOf((int) prog), ChatFormatting.YELLOW); + + if (horizontal) { + x += bgX; + } else { + y += bgY; + } + } + } + + } + + private static boolean shouldHideLeeching(String effectId) { + if (effectId == null) { + return false; + } + if (effectId.equals("leeching_state")) { + return true; + } + return effectId.startsWith("leeching_") && effectId.endsWith("_state"); } } diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java index 0269aeca7..df2de7f0d 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java @@ -31,36 +31,19 @@ public DataDrivenSpendThresholdSpec( boolean showUi ) { super(resource, 0f, key, - lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, dropProgressOnProc); + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, dropProgressOnProc, showUi); this.mode = mode; this.value = value; this.multiplyByLevel = multiplyByLevel; this.percentMaxOf = percentMaxOf; } - // Backward-compatible ctor (defaults showUi=false) - public DataDrivenSpendThresholdSpec( - String key, - ResourceType resource, - ThresholdMode mode, - float value, - boolean multiplyByLevel, - @Nullable ResourceType percentMaxOf, - Set lockWhileEffectIds, - int cooldownTicks, - boolean lockWhileCooldown, - boolean dropProgressWhileLocked, - boolean dropProgressOnProc - ) { - this(key, resource, mode, value, multiplyByLevel, percentMaxOf, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, dropProgressOnProc, false); - } @Override public float thresholdFor(EntityData unit) { float base; switch (mode) { 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; @@ -74,7 +57,5 @@ public float thresholdFor(EntityData unit) { @Override public void onProc(ServerPlayer sp, int procs) { - // No default action here; datapack loader wires actions. } - } diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java index 5dc8a298f..1eaf4b3e3 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java @@ -2,15 +2,16 @@ import com.robertx22.mine_and_slash.capability.entity.EntityData; import com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceLost; +import com.robertx22.mine_and_slash.vanilla_mc.packets.ThresholdUiPacket; +import com.robertx22.library_of_exile.main.Packets; import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.network.chat.Component; -/** Registers global spend thresholds at startup. */ public final class SpendThresholdManager { private SpendThresholdManager() {} public static void registerDefaults() { - // No-op: thresholds are defined via datapack JSON. } // ===== DEBUGGING ===== @@ -22,12 +23,12 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (loss <= 0f) return; var tracker = unit.getResourceTracker(); - tracker.addLoss(type, loss); // general counter (optional) + tracker.addLoss(type, loss); var specs = SpendThresholdRegistry.resolveFor(unit, type); if (specs.isEmpty()) { if (debug) { - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal( + sp.sendSystemMessage(Component.literal( "[SPEND] +" + String.format(java.util.Locale.US, "%.1f", loss) + " " + type.id + " (no specs)" )); @@ -38,27 +39,28 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t for (SpendThresholdSpec spec : specs) { final String key = spec.keyFor(unit); - // Cooldown-as-lock if (spec.lockWhileCooldown() && unit.getSpendRuntime().isCoolingDown(key, now)) { if (spec.dropProgressWhileLocked()) { tracker.clearKey(type, key); } if (debug) { long rem = unit.getSpendRuntime().cooldownRemainingTicks(key, now); - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal( - "[SPEND:" + spec.key() + "] locked by cooldown (" + rem + "t ~ " + fmtSec((float) rem) + "s)" + sp.sendSystemMessage(Component.literal( + "[SPEND:" + spec.key() + "] locked by cooldown (" + rem + "t ~ " + fmtSec((float) rem) + "s) ui=" + (spec.showUi() ? "on" : "off") )); } continue; } - // Locks (effects + optional perk requirement) if (spec.isLockedFor(unit)) { if (spec.dropProgressWhileLocked()) { tracker.clearKey(type, key); } if (debug) { - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked")); + sp.sendSystemMessage(Component.literal("[SPEND:" + spec.key() + "] locked ui=" + (spec.showUi() ? "on" : "off"))); + } + if (spec.showUi()) { + Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); } continue; } @@ -71,6 +73,11 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (loss > 0f && procs == 0) { unit.getSpendRuntime().markActivity(key, now); unit.getSpendRuntime().markActive(type, key, spec); + if (spec.showUi()) { + float curInit = tracker.getKeyProgress(key, type); + boolean show = curInit > 0f; + Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, curInit)); + } } if (procs > 0) { spec.onProc(sp, procs); @@ -78,12 +85,22 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (spec.dropProgressOnProc()) { tracker.clearKey(type, key); } - if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); + if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ") ui=" + (spec.showUi() ? "on" : "off")); + if (spec.showUi()) { + Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); + if (debug) dbg(sp, "[UI:" + key + "] close " + type.id); + } unit.getSpendRuntime().removeActive(type, key); } else { float cur = tracker.getKeyProgress(key, type); if (debug) { - dbg(sp, "[SPEND:" + spec.key() + "] +" + fmt(loss) + " " + type.id + " (cur=" + fmt(cur) + " / " + fmt(threshold) + ")"); + dbg(sp, "[SPEND:" + spec.key() + "] +" + fmt(loss) + " " + type.id + " (cur=" + fmt(cur) + " / " + fmt(threshold) + ") ui=" + (spec.showUi() ? "on" : "off")); + } + if (spec.showUi()) { + if (unit.getSpendRuntime().progressScaledChanged(key, cur, 10)) { + boolean show = cur > 0f; + Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, cur)); + } } if (cur <= 0f) { unit.getSpendRuntime().removeActive(type, key); @@ -95,7 +112,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t // --- helpers --- private static void dbg(ServerPlayer sp, String msg) { if (!OnResourceLost.DEBUG_ENABLED) return; - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal(msg)); + sp.sendSystemMessage(Component.literal(msg)); } private static String fmt(float v) { return String.format(java.util.Locale.US, "%.1f", v); } private static String fmtSec(float s) { return String.format(java.util.Locale.US, "%.1f", s); } diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRuntime.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRuntime.java index 2ae6b3bc0..aa78d39ce 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRuntime.java +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRuntime.java @@ -81,6 +81,14 @@ public boolean progressIntChanged(String key, int intProgress) { return false; } + public boolean progressScaledChanged(String key, float progress, int perUnit) { + if (perUnit <= 1) { + return progressIntChanged(key, (int) progress); + } + int scaled = Math.round(progress * perUnit); + return progressIntChanged(key, scaled); + } + // === Active key index === public void markActive(ResourceType rt, String key, SpendThresholdSpec spec) { if (rt == null || key == null || key.isEmpty() || spec == null) return; @@ -88,7 +96,6 @@ public void markActive(ResourceType rt, String key, SpendThresholdSpec spec) { if (set == null) { set = new HashSet<>(); activeByResource.put(rt, set); - // create and cache a read-only view for this resource set to avoid future allocations activeByResourceReadOnly.put(rt, java.util.Collections.unmodifiableSet(set)); } set.add(key); @@ -108,7 +115,6 @@ public void removeActive(ResourceType rt, String key) { } KeyState ks = states.get(key); if (ks != null) { - // clear volatile state but keep cooldown to preserve gating behavior ks.spec = null; ks.lastActivityTick = 0L; ks.lastDecayTick = 0L; @@ -118,13 +124,7 @@ public void removeActive(ResourceType rt, String key) { public Set getActiveKeys(ResourceType rt) { var s = activeByResource.get(rt); - if (s == null || s.isEmpty()) return java.util.Set.of(); - var view = activeByResourceReadOnly.get(rt); - if (view == null) { - view = java.util.Collections.unmodifiableSet(s); - activeByResourceReadOnly.put(rt, view); - } - return view; + return (s == null || s.isEmpty()) ? java.util.Set.of() : java.util.Set.copyOf(s); } public SpendThresholdSpec getSpec(String key) { diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java index 3d2e2afc6..f4d1b26a0 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java @@ -12,20 +12,18 @@ public abstract class SpendThresholdSpec { private final ResourceType resource; - private final float perLevelFactor; // used by default thresholdFor() + private final float perLevelFactor; private final String key; - // gating/cooldown controls private final Set lockWhileEffectIds; private final int cooldownTicks; - private final boolean lockWhileCooldown; // treat cooldown as a lock + private final boolean lockWhileCooldown; // Used to lock the threshold while the cooldown is active. **RECOMMENDED FOR DEBUGGING ONLY** private final boolean dropProgressWhileLocked; private final boolean dropProgressOnProc; + private final boolean showUi; - // registry ordering (lower runs first) private int priority = 0; - // Full ctor used by data-driven impl public SpendThresholdSpec(ResourceType resource, float perLevelFactor, String key, @@ -33,8 +31,8 @@ public SpendThresholdSpec(ResourceType resource, int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean dropProgressOnProc - ) { + boolean dropProgressOnProc, + boolean showUi) { this.resource = resource; this.perLevelFactor = perLevelFactor; this.key = key; @@ -43,9 +41,9 @@ public SpendThresholdSpec(ResourceType resource, this.lockWhileCooldown = lockWhileCooldown; this.dropProgressWhileLocked = dropProgressWhileLocked; this.dropProgressOnProc = dropProgressOnProc; + this.showUi = showUi; } - // ===== accessors ===== public ResourceType resource() { return resource; } public String key() { return key; } @@ -55,6 +53,7 @@ public SpendThresholdSpec(ResourceType resource, public boolean dropProgressOnProc() { return dropProgressOnProc; } public int cooldownTicks() { return cooldownTicks; } public int priority() { return priority; } + public boolean showUi() { return showUi; } public SpendThresholdSpec withPriority(int p) { @@ -62,12 +61,18 @@ public SpendThresholdSpec withPriority(int p) { return this; } - /** Default threshold = perLevelFactor × LVL. Subclasses may override. */ + public SpendThresholdSpec withShowUi(boolean on) { + return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.dropProgressOnProc, 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); + } + public float thresholdFor(EntityData unit) { return Math.max(0f, perLevelFactor * Math.max(1, unit.getLevel())); } - /** True if any gating effect is active. */ public boolean isEffectLocked(EntityData unit) { if (lockWhileEffectIds.isEmpty()) return false; var store = unit.getStatusEffectsData(); @@ -82,14 +87,12 @@ 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) { unit.getSpendRuntime().startCooldown(keyFor(unit), now, cooldownTicks); } } - /** Called when one or more thresholds are consumed. */ public abstract void onProc(ServerPlayer sp, int procs); } diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java index 7e1fdb5b2..aabccd1df 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java @@ -23,7 +23,7 @@ public static class Threshold { public String mode = "FLAT"; public float value = 0f; @SerializedName("multiply_by_level") public boolean multiplyByLevel = false; - @SerializedName("percent_of") public String percentOf; // optional + @SerializedName("percent_of") public String percentOf; } public Threshold threshold = new Threshold(); @@ -54,13 +54,14 @@ public static class ProcAction { public SpendThresholdSpec toSpec() { ResourceType res = parseResource(resource, ResourceType.energy); - // Modes supported: FLAT (optionally with multiply_by_level) or PERCENT_OF_MAX String rawMode = (threshold.mode == null ? "FLAT" : threshold.mode.trim()).toUpperCase(Locale.ROOT); boolean mult = threshold.multiplyByLevel; - DataDrivenSpendThresholdSpec.ThresholdMode mode = - "PERCENT_OF_MAX".equals(rawMode) - ? DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX - : DataDrivenSpendThresholdSpec.ThresholdMode.FLAT; // default + treats legacy values as FLAT + DataDrivenSpendThresholdSpec.ThresholdMode mode; + if ("PERCENT_OF_MAX".equals(rawMode)) { + mode = DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX; + } else { + mode = DataDrivenSpendThresholdSpec.ThresholdMode.FLAT; // default + treats legacy values as FLAT + } ResourceType percentOf = null; if (mode == DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX @@ -100,6 +101,18 @@ public void onProc(ServerPlayer sp, int procs) { int durTicks = Math.max(1, a.durationTicks); int stacks = Math.max(1, a.stacks); var inst = EffectUtils.applyEffect(sp, effect, durTicks, stacks); + + 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 = Math.max(0, e.getValue()); + if (ticks > 0) { + inst.onExpireEffectDurationTicks.put(e.getKey(), ticks); + } + } + } } } @@ -123,7 +136,6 @@ private static ResourceType parseResource(String s, ResourceType fallback) { for (ResourceType rt : ResourceType.values()) { if (rt.name().equalsIgnoreCase(s)) return rt; try { - // if your enum exposes an id/string, handle it here: var idField = rt.getClass().getField("id"); Object idVal = idField.get(rt); if (idVal instanceof String && ((String) idVal).equalsIgnoreCase(s)) return rt; diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ui/ThresholdUiClient.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ui/ThresholdUiClient.java new file mode 100644 index 000000000..c8226610f --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ui/ThresholdUiClient.java @@ -0,0 +1,41 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds.ui; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class ThresholdUiClient { + private ThresholdUiClient() {} + + private static final Map keyToResource = new HashMap<>(); + private static final Map keyToProgress = new HashMap<>(); + + public static void applyUpdate(String key, String resourceId, boolean show) { + if (key == null || key.isEmpty()) return; + if (show) { + keyToResource.put(key, resourceId == null ? "" : resourceId); + } else { + keyToResource.remove(key); + keyToProgress.remove(key); + } + } + + public static void setProgress(String key, float progress) { + if (key == null || key.isEmpty()) return; + keyToProgress.put(key, Math.max(0f, progress)); + } + + public static float getProgress(String key) { + return keyToProgress.getOrDefault(key, 0f); + } + + public static boolean isVisible(String key) { + return keyToResource.containsKey(key); + } + + public static Map visibleEntries() { + return Collections.unmodifiableMap(keyToResource); + } +} + + diff --git a/src/main/java/com/robertx22/mine_and_slash/mmorpg/DebugHud.java b/src/main/java/com/robertx22/mine_and_slash/mmorpg/DebugHud.java new file mode 100644 index 000000000..191a552d2 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mmorpg/DebugHud.java @@ -0,0 +1,33 @@ +package com.robertx22.mine_and_slash.mmorpg; + +/** + * Central debug HUD/flags. Toggle via commands or config as needed. + * Keep flags conservative (default false) unless explicitly enabled. + */ +public final class DebugHud { + + private DebugHud() {} + + public static volatile boolean ON_EXPIRE = false; + + private static final java.util.concurrent.ConcurrentHashMap LAST_MSG_MS = new java.util.concurrent.ConcurrentHashMap<>(); + + public static void send(net.minecraft.server.level.ServerPlayer sp, String key, String msg) { + send(sp, key, msg, 500); // default 0.5s throttle per key + } + + public static void send(net.minecraft.server.level.ServerPlayer sp, String key, String msg, int minIntervalMs) { + if (sp == null) return; + long now = System.currentTimeMillis(); + String k = sp.getUUID().toString() + ":" + key; + Long last = LAST_MSG_MS.get(k); + if (last != null && (now - last) < Math.max(0, minIntervalMs)) { + return; + } + LAST_MSG_MS.put(k, now); + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal(msg)); + } + +} + + diff --git a/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/client/S2CPacketRegister.java b/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/client/S2CPacketRegister.java index 4da55e2ff..c31b28e26 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/client/S2CPacketRegister.java +++ b/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/client/S2CPacketRegister.java @@ -33,6 +33,7 @@ public static void register() { Packets.registerServerToClient(MMORPG.NETWORK, new MapCompletePacket(), i++); Packets.registerServerToClient(MMORPG.NETWORK, new OpenEntityStatsReplyPacket(), i++); + Packets.registerServerToClient(MMORPG.NETWORK, new ThresholdUiPacket(), i++); } } diff --git a/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/server/CommandRegister.java b/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/server/CommandRegister.java index 7107e06d6..695cfc572 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/server/CommandRegister.java +++ b/src/main/java/com/robertx22/mine_and_slash/mmorpg/registers/server/CommandRegister.java @@ -25,6 +25,7 @@ import com.robertx22.mine_and_slash.vanilla_mc.commands.stats.GiveStat; import com.robertx22.mine_and_slash.vanilla_mc.commands.stats.ListStats; import com.robertx22.mine_and_slash.vanilla_mc.commands.stats.RemoveStat; +import com.robertx22.mine_and_slash.vanilla_mc.new_commands.DebugCommands; import com.robertx22.mine_and_slash.vanilla_mc.new_commands.BuilderToolCommands; import com.robertx22.mine_and_slash.vanilla_mc.new_commands.DevCommands; import com.robertx22.mine_and_slash.vanilla_mc.new_commands.EntityCommands; @@ -40,6 +41,7 @@ public static void Register(CommandDispatcher dispatcher) { EntityCommands.init(dispatcher); PlayerCommands.init(dispatcher); DevCommands.init(dispatcher); + DebugCommands.init(dispatcher); BuilderToolCommands.reg(dispatcher); GiveExactUnique.register(dispatcher); diff --git a/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/ExilePotionEvent.java b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/ExilePotionEvent.java index 768a78341..8e80885aa 100644 --- a/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/ExilePotionEvent.java +++ b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/ExilePotionEvent.java @@ -92,6 +92,10 @@ protected void activate() { } Load.Unit(target).equipmentCache.STATUS.setDirty(); + + if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && target instanceof net.minecraft.server.level.ServerPlayer sp) { + com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp, "expire_reapplied_" + effect.GUID(), "[EFFECT][EXPIRE] Reapplied " + effect.GUID() + " ticks_left=" + extraData.ticks_left + " stacks=" + extraData.stacks + " [id=" + System.identityHashCode(extraData) + "]", 400); + } } diff --git a/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/new_commands/DebugCommands.java b/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/new_commands/DebugCommands.java new file mode 100644 index 000000000..198e4ca73 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/new_commands/DebugCommands.java @@ -0,0 +1,83 @@ +package com.robertx22.mine_and_slash.vanilla_mc.new_commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.robertx22.library_of_exile.command_wrapper.CommandBuilder; +import com.robertx22.library_of_exile.command_wrapper.PermWrapper; +import com.robertx22.library_of_exile.command_wrapper.StringWrapper; +import com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceLost; +import com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceRestore; +import com.robertx22.mine_and_slash.vanilla_mc.commands.CommandRefs; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; + +import java.util.List; + +public class DebugCommands { + + public static void init(CommandDispatcher dispatcher) { + + // /mine_and_slash debug + CommandBuilder.of(CommandRefs.ID, dispatcher, x -> { + StringWrapper SUBJECT = new StringWrapper("subject", () -> List.of("spend_threshold", "resource_restore", "onexpire")); + StringWrapper STATE = new StringWrapper("state", () -> List.of("true", "false", "toggle", "on", "off")); + + x.addLiteral("debug", PermWrapper.OP); + x.addArg(SUBJECT); + x.addArg(STATE); + + x.action(e -> { + String subject = SUBJECT.get(e); + String state = STATE.get(e); + + String subjLower = subject == null ? "" : subject.toLowerCase(java.util.Locale.ROOT); + String stateLower = state == null ? "" : state.toLowerCase(java.util.Locale.ROOT); + boolean subjOk = subjLower.equals("spend_threshold") || subjLower.equals("resource_restore") || subjLower.equals("onexpire"); + boolean stateOk = stateLower.equals("true") || stateLower.equals("false") || stateLower.equals("toggle") || stateLower.equals("on") || stateLower.equals("off"); + + if (!subjOk || !stateOk) { + e.getSource().sendFailure(Component.literal("Usage: /" + CommandRefs.ID + " debug ")); + return; + } + + boolean current; + if ("resource_restore".equalsIgnoreCase(subject)) { + current = OnResourceRestore.DEBUG_ENABLED; + } else if ("spend_threshold".equalsIgnoreCase(subject)) { + current = OnResourceLost.DEBUG_ENABLED; // spend_threshold + } else { + current = com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE; + } + + boolean newValue = current; + if ("true".equalsIgnoreCase(state) || "on".equalsIgnoreCase(state)) { + newValue = true; + } else if ("false".equalsIgnoreCase(state) || "off".equalsIgnoreCase(state)) { + newValue = false; + } else if ("toggle".equalsIgnoreCase(state)) { + newValue = !current; + } + + if ("resource_restore".equalsIgnoreCase(subject)) { + OnResourceRestore.DEBUG_ENABLED = newValue; + } else if ("spend_threshold".equalsIgnoreCase(subject)) { + OnResourceLost.DEBUG_ENABLED = newValue; + } else { + com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE = newValue; + } + + String label = "resource_restore".equalsIgnoreCase(subject) ? "ResourceRestore" : "spend_threshold".equalsIgnoreCase(subject) ? "SpendThresholds" : "OnExpire"; + Component msg = Component.literal("[" + label + "] Debug: " + (newValue ? "ON" : "OFF")); + CommandSourceStack src = e.getSource(); + if (src.getEntity() instanceof Player p) { + p.sendSystemMessage(msg); + } else { + src.sendSuccess(() -> msg, true); + } + }); + + }, "Toggle debug: /" + CommandRefs.ID + " debug "); + } +} + + diff --git a/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/packets/ThresholdUiPacket.java b/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/packets/ThresholdUiPacket.java new file mode 100644 index 000000000..a721865e8 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/packets/ThresholdUiPacket.java @@ -0,0 +1,61 @@ +package com.robertx22.mine_and_slash.vanilla_mc.packets; + +import com.robertx22.library_of_exile.main.MyPacket; +import com.robertx22.library_of_exile.packets.ExilePacketContext; +import com.robertx22.mine_and_slash.mechanics.thresholds.ui.ThresholdUiClient; +import com.robertx22.mine_and_slash.mmorpg.SlashRef; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public class ThresholdUiPacket extends MyPacket { + + public String key = ""; + public String resourceId = ""; + public boolean show = false; + public float progress = 0f; + + public ThresholdUiPacket() { + } + + public ThresholdUiPacket(String key, String resourceId, boolean show, float progress) { + this.key = key == null ? "" : key; + this.resourceId = resourceId == null ? "" : resourceId; + this.show = show; + this.progress = progress; + } + + @Override + public ResourceLocation getIdentifier() { + return new ResourceLocation(SlashRef.MODID, "threshold_ui"); + } + + @Override + public void loadFromData(FriendlyByteBuf buf) { + this.key = buf.readUtf(256); + this.resourceId = buf.readUtf(64); + this.show = buf.readBoolean(); + this.progress = buf.readFloat(); + } + + @Override + public void saveToData(FriendlyByteBuf buf) { + buf.writeUtf(key); + buf.writeUtf(resourceId); + buf.writeBoolean(show); + buf.writeFloat(progress); + } + + @Override + public void onReceived(ExilePacketContext ctx) { + ThresholdUiClient.applyUpdate(key, resourceId, show); + if (show) { + ThresholdUiClient.setProgress(key, progress); + } + } + + @Override + public MyPacket newInstance() { + return new ThresholdUiPacket(); + } +} + diff --git a/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/potion_effects/EntityStatusEffectsData.java b/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/potion_effects/EntityStatusEffectsData.java index 19b9007a6..794a18298 100644 --- a/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/potion_effects/EntityStatusEffectsData.java +++ b/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/potion_effects/EntityStatusEffectsData.java @@ -6,6 +6,7 @@ import com.robertx22.mine_and_slash.saveclasses.ExactStatData; import com.robertx22.mine_and_slash.saveclasses.unit.stat_ctx.SimpleStatCtx; import com.robertx22.mine_and_slash.saveclasses.unit.stat_ctx.StatContext; +import com.robertx22.mine_and_slash.mmorpg.DebugHud; import net.minecraft.world.entity.LivingEntity; import java.util.ArrayList; @@ -35,43 +36,74 @@ public void tick(LivingEntity en) { exileMap.entrySet().removeIf(x -> !ExileDB.ExileEffects().isRegistered(x.getKey())); for (Map.Entry e : exileMap.entrySet()) { - e.getValue().ticks_left--; + ExileEffectInstanceData inst = e.getValue(); + // Clamp to prevent negative countdowns + if (inst.ticks_left > 0) { + inst.ticks_left = inst.ticks_left - 1; + } else if (inst.ticks_left < 0) { + inst.ticks_left = 0; + } ExileEffect eff = ExileDB.ExileEffects().get(e.getKey()); if (eff != null) { - eff.onTick(en, e.getValue()); + eff.onTick(en, inst); } } // todo this is probably bit laggy per tick no? - List removed = new ArrayList<>(); + List toCallOnRemove = new ArrayList<>(); + List toDeleteKeys = new ArrayList<>(); if (en.tickCount % 80 == 0) { - // Prevent keeping e.g. auras and stances after respeccing - // Has to string compare spell UUIDs to look up the new spell level, so it's done infrequently - exileMap.entrySet().removeIf(x -> { - if (x.getValue().shouldRemove() || x.getValue().isSpellNoLongerAllocated(en)) { - removed.add(ExileDB.ExileEffects().get(x.getKey())); - return true; + for (Map.Entry entry : exileMap.entrySet()) { + boolean shouldDrop = entry.getValue().shouldRemove() || entry.getValue().isSpellNoLongerAllocated(en); + if (shouldDrop) { + ExileEffect eff = ExileDB.ExileEffects().get(entry.getKey()); + if (eff != null) { + eff.onRemove(en); + ExileEffectInstanceData inst = exileMap.get(entry.getKey()); + if (inst == null || inst.shouldRemove()) { + toDeleteKeys.add(entry.getKey()); + } else if (DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { + DebugHud.send(sp, "expire_kept_" + entry.getKey(), "[EFFECT][EXPIRE] Kept " + entry.getKey() + " after onRemove (ticks_left=" + inst.ticks_left + ")", 400); + } + } } - return false; - }); + } } else { - exileMap.entrySet().removeIf(x -> { - if (x.getValue().shouldRemove()) { - removed.add(ExileDB.ExileEffects().get(x.getKey())); - return true; + for (Map.Entry entry : exileMap.entrySet()) { + if (entry.getValue().shouldRemove()) { + ExileEffect eff = ExileDB.ExileEffects().get(entry.getKey()); + if (eff != null) { + eff.onRemove(en); + ExileEffectInstanceData inst = exileMap.get(entry.getKey()); + if (inst == null || inst.shouldRemove()) { + toDeleteKeys.add(entry.getKey()); + } else if (DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { + DebugHud.send(sp, "expire_kept_" + entry.getKey(), "[EFFECT][EXPIRE] Kept " + entry.getKey() + " after onRemove (ticks_left=" + inst.ticks_left + ")", 400); + } + } } - return false; - }); + } } - for (ExileEffect eff : removed) { - eff.onRemove(en); + + + for (String key : toDeleteKeys) { + ExileEffectInstanceData current = exileMap.remove(key); + if (DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { + if (current != null) { + DebugHud.send(sp, "expire_removed_" + key, "[EFFECT][EXPIRE] Removed " + key + " (ticks_left=" + current.ticks_left + ", stacks=" + current.stacks + ") [id=" + System.identityHashCode(current) + "]", 200); + } else { + DebugHud.send(sp, "expire_removed_" + key, "[EFFECT][EXPIRE] Removed " + key + " (no current instance)", 200); + } + } } } + // Removed: processDeferredApplies; no deferral now + public boolean has(ExileEffect eff) { return this.exileMap.containsKey(eff.GUID()) && !exileMap.get(eff.GUID()).shouldRemove(); } diff --git a/src/main/resources/data/mmorpg/spend_thresholds/energy_xlvl_wotj.json b/src/main/resources/data/mmorpg/spend_thresholds/energy_xlvl_wotj.json index 5c8d0c3e2..5741c2585 100644 --- a/src/main/resources/data/mmorpg/spend_thresholds/energy_xlvl_wotj.json +++ b/src/main/resources/data/mmorpg/spend_thresholds/energy_xlvl_wotj.json @@ -1,6 +1,7 @@ { "key": "ENERGY_XLVL_WOTJ", "resource": "energy", + "show_ui": true, "enabled": true, "priority": 0, "threshold": { @@ -8,11 +9,11 @@ "value": 10, "multiply_by_level": true }, - "cooldown_ticks": 400, + "cooldown_ticks": 0, "require_stat": "unlock_wotj", "locks": { "effects": ["wrath_of_the_juggernaut", "burnout"], - "lock_while_cooldown": true, + "lock_while_cooldown": false, "drop_progress_while_locked": true, "drop_progress_on_proc": true }, @@ -21,7 +22,10 @@ "action": "exile_effect", "exile_potion_id": "wrath_of_the_juggernaut", "duration_ticks": 200, - "stacks": 1 + "stacks": 1, + "on_expire": { + "burnout": 200 + } } ] } \ No newline at end of file