From ff6fb59be9fcd936bf0550d34f0ea207ad03e5e6 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Sun, 24 Aug 2025 03:12:58 -0400 Subject: [PATCH 01/20] Conditional Leeching + Conditional Predicate Contains (NEW) API + Event Hooks. Contains Refactored/Factored Code. Desc: Made for new "WHILE_LEECHING" system. Also houses new HasExileEffectCondition, e.g. allowing WHILE_(...) tags. --- .../exile_effects/adders/ModEffects.java | 71 ++++++++ .../database/stat_effects/StatEffects.java | 24 ++- .../capability/entity/EntityLeechData.java | 112 +++++++++---- .../event_hooks/my_events/EffectUtils.java | 60 +++++++ .../my_events/OnResourceRestore.java | 153 ++++++++++++++++++ .../saveclasses/unit/ResourcesData.java | 121 ++++++++++++-- .../effectdatas/RestoreResourceEvent.java | 7 +- .../condition/HasExileEffectCondition.java | 133 +++++++++++++++ 8 files changed, 635 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceRestore.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/rework/condition/HasExileEffectCondition.java diff --git a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java index c9bed4d9a..16ce3a7ac 100644 --- a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java +++ b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java @@ -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; @@ -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.*; @@ -98,6 +101,56 @@ public static List getCurses() { public static int ESSENCE_OF_FROST_MAX_STACKS = 5; + // ---------- Helper ---------- + private static EffectCtx state(String id, String name, Elements elem) { + // EffectCtx constructor already adds itself to ModEffects.ALL in this file's pattern + return new EffectCtx(id, name, elem, EffectType.beneficial); + } + + // Pretty names for resources (UI text) + private static final Map RES_NAME = Map.of( + ResourceType.health, "Health", + ResourceType.mana, "Mana", + ResourceType.energy, "Energy", + ResourceType.magic_shield, "Magic Shield", + ResourceType.blood, "Blood" + ); + + // Suggested elements per resource (only used for coloring/category) + private static final Map RES_ELEM = Map.of( + ResourceType.health, Elements.Physical, + ResourceType.mana, Elements.Cold, + ResourceType.energy, Elements.Nature, + ResourceType.magic_shield, Elements.Shadow, + ResourceType.blood, Elements.Fire + ); + + // ---------- Generic flags ---------- + public static final EffectCtx LEECHING_STATE = state( + "leeching_state", "Leeching (State)", Elements.Physical + ); + public static final EffectCtx REGEN_STATE = state( + "regen_state", "Regenerating (State)", Elements.Physical + ); + + // ---------- Per-resource flags (generated) ---------- + public static final EnumMap LEECHING_STATE_BY_RES = new EnumMap<>(ResourceType.class); + public static final EnumMap REGEN_STATE_BY_RES = new EnumMap<>(ResourceType.class); + + static { + for (var rt : RES_NAME.keySet()) { + var elem = RES_ELEM.get(rt); + var nice = RES_NAME.get(rt); + + LEECHING_STATE_BY_RES.put( + rt, state("leeching_" + rt.id + "_state", "Leeching " + nice, elem) + ); + REGEN_STATE_BY_RES.put( + rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, elem) + ); + } + } + public static void init() { } @@ -384,6 +437,24 @@ public void registerAll() { .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(); + } + } } diff --git a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stat_effects/StatEffects.java b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stat_effects/StatEffects.java index c1ca620f8..be6f9fd00 100644 --- a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stat_effects/StatEffects.java +++ b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stat_effects/StatEffects.java @@ -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; @@ -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; @@ -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 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 db7f134d4..fc78a9c10 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 @@ -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 store = new EnumMap<>(ResourceType.class); - private HashMap 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 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 en : store.entrySet()) { + ResourceType rt = en.getKey(); + float capPctPerSec = data.getUnit() + .getCalculatedStat(ResourceStats.LEECH_CAP.get(rt)) + .getValue() / 100F; + + float maxRes = data.getResources().getMax(data.entity, rt); + float fiveSecs = 5F * capPctPerSec * maxRes; // “5 seconds worth” reservoir cap + float clamped = MathHelper.clamp(en.getValue(), 0, fiveSecs); + en.setValue(clamped); } - for (Map.Entry 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 entry : store.entrySet()) { + ResourceType rt = entry.getKey(); + float reservoir = entry.getValue(); + if (reservoir <= EPS) continue; + + float capPctPerSec = data.getUnit() + .getCalculatedStat(ResourceStats.LEECH_CAP.get(rt)) + .getValue() / 100F; + + float maxRes = data.getResources().getMax(data.entity, rt); + float perSecondCap = capPctPerSec * 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); } } + 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 new file mode 100644 index 000000000..e8f2d668f --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java @@ -0,0 +1,60 @@ +package com.robertx22.mine_and_slash.event_hooks.my_events; + +import com.robertx22.mine_and_slash.aoe_data.database.stats.base.EffectCtx; +import com.robertx22.mine_and_slash.database.data.exile_effects.ExileEffect; +import com.robertx22.mine_and_slash.database.registry.ExileDB; +import com.robertx22.mine_and_slash.uncommon.datasaving.Load; +import net.minecraft.server.level.ServerPlayer; + +/** + * Utility for applying short-TTL "state" effects (e.g., leeching_state) to players. + * + * Semantics: + * - Resolve the effect from {@link EffectCtx}. + * - Ensure a stored runtime instance exists (store.getOrCreate). + * - Refresh by taking MAX of existing vs. new stacks/ticks (never reduces). + * - Call effect.onApply(...) and mark the unit dirty for sync. + * + * Notes: + * - This variant is player-only by design. If you need NPCs/mobs later, + * add an overload for LivingEntity and only sync when the target is a ServerPlayer. + * - If resolve fails (null effect), we silently no-op; OnResourceRestore already + * provides a dev-only registry warning at the call site. + */ +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 + */ + public static void applyState(ServerPlayer sp, EffectCtx ctx, int durationTicks, int stacks) { + final ExileEffect effect = resolveEffect(ctx); + if (effect == null) return; + + var unit = Load.Unit(sp); + var store = unit.getStatusEffectsData(); + var inst = store.getOrCreate(effect); // persist if missing + + // Merge stacks/ticks: refresh semantics (never decrease on re-apply) + final int wanted = Math.max(1, stacks); + final int capped = (effect.max_stacks > 0) ? Math.min(wanted, effect.max_stacks) : wanted; + inst.stacks = Math.max(inst.stacks, capped); + inst.ticks_left = Math.max(inst.ticks_left, durationTicks); + + // Keep vanilla stats / one-of-a-kind cleanup in sync + effect.onApply(sp); + unit.sync.setDirty(); // network/state sync + } + + /** Try both resourcePath (preferred) and id; some data uses either. */ + private static ExileEffect resolveEffect(EffectCtx ctx) { + ExileEffect eff = ExileDB.ExileEffects().get(ctx.resourcePath); + if (eff == null) eff = ExileDB.ExileEffects().get(ctx.id); + return eff; + } +} 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 new file mode 100644 index 000000000..63e2aa50f --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceRestore.java @@ -0,0 +1,153 @@ +package com.robertx22.mine_and_slash.event_hooks.my_events; + +import com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects; +import com.robertx22.mine_and_slash.database.registry.ExileDB; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; +import com.robertx22.mine_and_slash.uncommon.effectdatas.rework.RestoreType; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; + +import java.util.EnumSet; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +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 = 60; // ~3.0s @20tps + + // ===== 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); + } + } + + // ====== RestoreType routing (single-responsibility helpers below) ====== + + private static void onRestore(ServerPlayer sp, + ResourceType type, + float amount, + RestoreType restoreType) { + switch (restoreType) { + case leech -> applyLeechStates(sp, type); + // === Add NEW RestoreType cases here === + // case -> applyNewKindStates(sp, type, amount); + default -> { /* ignore other kinds by 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 + } + + // 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) + + ", byRes=" + (byResFx != null) + + " id(any)=" + ModEffects.LEECHING_STATE.GUID() + + " id(byRes)=" + ModEffects.LEECHING_STATE_BY_RES.get(type).GUID() + )); + } + return false; + } + + /** Centralized debug; respects type filters & cooldown. */ + private static void maybeDebugRestore(ServerPlayer sp, + ResourceType type, + float amount, + RestoreType restoreType) { + if (!DEBUG_ENABLED) return; + if (!DEBUG_TYPES.contains(restoreType)) return; + if (amount < MIN_DEBUG_AMOUNT) return; + + long now = sp.level().getGameTime(); + Key key = new Key(sp.getUUID(), type, restoreType); + long allowedAt = nextAllowedTick.getOrDefault(key, 0L); + if (now < allowedAt) return; + + String msg = String.format(Locale.US, + "[RESTORE] +%.1f %s via %s", amount, type.name(), restoreType.name()); + sp.sendSystemMessage(Component.literal(msg)); + nextAllowedTick.put(key, now + PRINT_COOLDOWN_TICKS); + } + + // ====== internals ====== + + private record Key(UUID player, ResourceType type, RestoreType restoreType) { + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Key k)) return false; + return Objects.equals(player, k.player) && type == k.type && restoreType == k.restoreType; + } + @Override public int hashCode() { return Objects.hash(player, type, restoreType); } + } +} + + + + + \ No newline at end of file diff --git a/src/main/java/com/robertx22/mine_and_slash/saveclasses/unit/ResourcesData.java b/src/main/java/com/robertx22/mine_and_slash/saveclasses/unit/ResourcesData.java index dd4f5275e..291ad1e20 100644 --- a/src/main/java/com/robertx22/mine_and_slash/saveclasses/unit/ResourcesData.java +++ b/src/main/java/com/robertx22/mine_and_slash/saveclasses/unit/ResourcesData.java @@ -3,6 +3,7 @@ import com.robertx22.mine_and_slash.capability.entity.EntityData; import com.robertx22.mine_and_slash.config.forge.ServerContainer; import com.robertx22.mine_and_slash.database.data.stats.types.resources.energy.Energy; +import com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceLost; import com.robertx22.mine_and_slash.uncommon.datasaving.Load; import com.robertx22.mine_and_slash.uncommon.effectdatas.SpendResourceEvent; import com.robertx22.mine_and_slash.uncommon.enumclasses.ModType; @@ -114,25 +115,125 @@ public void spend(LivingEntity en, ResourceType type, float amount) { modify(en, Use.SPEND, type, amount); } + // (START NEW) Overload with RestoreType — callers who know the type should use this + public void restore(LivingEntity en, ResourceType type, float amount, + com.robertx22.mine_and_slash.uncommon.effectdatas.rework.RestoreType rtype) { + if (amount <= 0) return; + + float applied = applyRestoreAndReturnApplied(en, type, amount); // NEW + if (applied <= 0) return; // nothing actually restored (e.g., full health) + + // Fire the unified event with the true applied amount + com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceRestore.trigger(en, type, applied, rtype); + } + + // Overload without RestoreType public void restore(LivingEntity en, ResourceType type, float amount) { - modify(en, Use.RESTORE, type, amount); + // Default to regen when no context is provided + restore(en, type, amount, + com.robertx22.mine_and_slash.uncommon.effectdatas.rework.RestoreType.regen); + } + + // Returns the LEECH actual amount that was applied (0 if capped) + public float restoreAndReturnApplied(LivingEntity en, ResourceType type, float amount, + com.robertx22.mine_and_slash.uncommon.effectdatas.rework.RestoreType rtype) { + if (amount <= 0) return 0f; + float applied = applyRestoreAndReturnApplied(en, type, amount); + if (applied > 0f) { + com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceRestore.trigger(en, type, applied, rtype); + } + return applied; + } + + // Returns the REGEN actual amount that was applied (0 if capped) + private float applyRestoreAndReturnApplied(LivingEntity en, ResourceType type, float amount) { + float applied = applyAndReturnAppliedClamped(en, type, amount); // unified path + cap(en, type); // harmless no-op if already clamped + sync(en); + return applied; + } + + // Returns the current value for non-Health resources (Health uses HealthUtils) + private float getCurrent(ResourceType type, LivingEntity en) { + return switch (type) { + case mana -> mana; + case blood -> blood; + case energy -> energy; + case magic_shield -> magic_shield; + case health -> HealthUtils.getCurrentHealth(en); + }; + } + + private void setCurrent(ResourceType type, float value) { + switch (type) { + case mana -> mana = value; + case blood -> blood = value; + case energy -> energy = value; + case magic_shield -> magic_shield = value; + case health -> { /* health is applied via heal() below */ } + } } + /** + * Compute applied AFTER clamping to max, then write the new value. + * Health heals via HealthUtils; others assign the clamped value. + */ + private float applyAndReturnAppliedClamped(LivingEntity en, ResourceType type, float amount) { + if (amount <= 0f) return 0f; + + float before = getCurrent(type, en); + float max = getMax(en, type); // you already have this method + float after = Math.max(0f, Math.min(before + amount, max)); + float applied = Math.max(0f, after - before); + + if (applied <= 0f) return 0f; + + if (type == ResourceType.health) { + HealthUtils.heal(en, applied); // apply only what fits + } else { + setCurrent(type, after); // write clamped value + } + return applied; + } + // ===(END NEW)=== + + public void modify(LivingEntity en, Use use, ResourceType type, float amount) { - if (amount == 0) { - return; + if (amount == 0f) return; + + // Health restore goes through vanilla heal (keeps heart UI etc.) + if (type == ResourceType.health) { + if (use == Use.RESTORE) { + HealthUtils.heal(en, amount); + cap(en, type); + sync(en); + } else { + // Optional: warn if someone tries to "spend" health via this path + // en.sendSystemMessage(Component.literal("[WARN] modify() called with SPEND health")); + } + return; // done with health either way } + + // --- non-health resources (mana, energy, blood, magic_shield) --- + float before = get(en, type); + float newVal = getModifiedValue(en, type, use, amount); + if (type == ResourceType.mana) { - mana = getModifiedValue(en, type, use, amount); + mana = newVal; } else if (type == ResourceType.blood) { - blood = getModifiedValue(en, type, use, amount); + blood = newVal; } else if (type == ResourceType.energy) { - energy = getModifiedValue(en, type, use, amount); + energy = newVal; } else if (type == ResourceType.magic_shield) { - magic_shield = getModifiedValue(en, type, use, amount); - } else if (type == ResourceType.health) { - if (use == Use.RESTORE) { - HealthUtils.heal(en, amount); + magic_shield = newVal; + } // health spend/drain is handled via damage events + + // Notify spend tracker only on SPEND for non-health + if (use == Use.SPEND) { + float spent = Math.max(0f, before - Math.max(newVal, 0f)); + if (spent > 0f) { + OnResourceLost.trigger(en, type, spent, OnResourceLost.LossSource.SpendOrDrain + ); } } cap(en, type); diff --git a/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/RestoreResourceEvent.java b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/RestoreResourceEvent.java index 8d294ccbc..c6737d022 100644 --- a/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/RestoreResourceEvent.java +++ b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/RestoreResourceEvent.java @@ -54,7 +54,12 @@ protected void activate() { } } - this.targetData.getResources().restore(target, data.getResourceType(), num); + // Guard against no-op / negative input after scaling + if (num <= 0) { + return; + } + + this.targetData.getResources().restore(target, data.getResourceType(), num,data.getRestoreType()); if (this.data.getResourceType() == ResourceType.health) { if (data.getRestoreType() == RestoreType.heal) { diff --git a/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/rework/condition/HasExileEffectCondition.java b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/rework/condition/HasExileEffectCondition.java new file mode 100644 index 000000000..527be18f3 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/rework/condition/HasExileEffectCondition.java @@ -0,0 +1,133 @@ +package com.robertx22.mine_and_slash.uncommon.effectdatas.rework.condition; + +import com.robertx22.mine_and_slash.aoe_data.database.stats.base.EffectCtx; +import com.robertx22.mine_and_slash.database.data.exile_effects.ExileEffect; +import com.robertx22.mine_and_slash.database.registry.ExileDB; +import com.robertx22.mine_and_slash.database.data.stats.Stat; +import com.robertx22.mine_and_slash.saveclasses.unit.StatData; +import com.robertx22.mine_and_slash.uncommon.datasaving.Load; +import com.robertx22.mine_and_slash.uncommon.effectdatas.EffectEvent; +import com.robertx22.mine_and_slash.uncommon.interfaces.EffectSides; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; + +/** + * Datapack-serializable condition that returns true iff the chosen side + * (Source/Target) currently has a given {@link ExileEffect} active. + * + * Usage in code: + * .setSide(EffectSides.Source) + * .addCondition(new HasExileEffectCondition(ModEffects.LEECHING_STATE)) // e.g. "while leeching" + * + * Usage in datapack JSON (shape depends on your builder/serializer wiring): + * { + * "type": "has_exile_effect", + * "effectId": "leeching_state" + * } + * + * Notes: + * - We rely on the store's real state via `has(fx)` (correct now that apply uses getOrCreate()). + * - Keep this condition lightweight: just resolve the effect and query the status store. + * - Debug can be toggled globally via OnResourceRestore.DEBUG_ENABLED (no spam by default). + */ +public class HasExileEffectCondition extends StatCondition { + + /** Serialized id of the effect to check (e.g., "leeching_state"). */ + public String effectId = ""; + + /** Stable serializer id (must match your registry/serializer entry). */ + private static final String SER_ID = "has_exile_effect"; + + /** Convenience ctor for code paths (accepts an EffectCtx like ModEffects.LEECHING_STATE). */ + public HasExileEffectCondition(EffectCtx ctx) { + super(SER_ID + "_" + ctx.resourcePath, SER_ID); + this.effectId = ctx.resourcePath; + } + + /** No-arg ctor for (de)serialization. */ + public HasExileEffectCondition() { + super("", SER_ID); + } + + @Override + public boolean can(EffectEvent event, EffectSides statSource, StatData data, Stat stat) { + if (effectId == null || effectId.isEmpty()) return false; + + // Resolve the effect definition + final ExileEffect fx = ExileDB.ExileEffects().get(effectId); + if (fx == null) return false; + + // Pick the entity to check based on the stat's side (match the stat!) + final LivingEntity who = (statSource == EffectSides.Source) ? event.source : event.target; + if (who == null) return false; + + // Read live status (no allocations): true iff stored instance exists AND is not removed + final var effData = Load.Unit(who).getStatusEffectsData(); + final boolean has = (effData != null) && effData.has(fx); + + // Optional lightweight debug (off by default) + if (com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceRestore.DEBUG_ENABLED + && who instanceof ServerPlayer sp) { + final String side = (statSource == EffectSides.Source ? "SRC" : "TGT"); + final String flags = compactLeechFlags(effData); // e.g. "AMH" or "-" + sp.sendSystemMessage(Component.literal( + "[HasExileEffect] " + side + + " id=" + effectId + + " has=" + (has ? "Y" : "X") + + " flags=" + (flags.isEmpty() ? "-" : flags) + )); + } + + return has; + } + + // ----- helpers (class scope) ----- + + private static String compactLeechFlags( + com.robertx22.mine_and_slash.vanilla_mc.potion_effects.EntityStatusEffectsData effData) { + if (effData == null) return ""; + + StringBuilder b = new StringBuilder(); + + if (hasFx(effData, com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects.LEECHING_STATE.GUID())) + b.append('A'); // Any leech + + if (hasFx(effData, com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects + .LEECHING_STATE_BY_RES.get(ResourceType.health).GUID())) + b.append('H'); + + if (hasFx(effData, com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects + .LEECHING_STATE_BY_RES.get(ResourceType.mana).GUID())) + b.append('M'); + + if (hasFx(effData, com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects + .LEECHING_STATE_BY_RES.get(ResourceType.energy).GUID())) + b.append('E'); + + if (hasFx(effData, com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects + .LEECHING_STATE_BY_RES.get(ResourceType.magic_shield).GUID())) + b.append('S'); + + if (hasFx(effData, com.robertx22.mine_and_slash.aoe_data.database.exile_effects.adders.ModEffects + .LEECHING_STATE_BY_RES.get(ResourceType.blood).GUID())) + b.append('B'); + + return b.toString(); + } + + private static boolean hasFx( + com.robertx22.mine_and_slash.vanilla_mc.potion_effects.EntityStatusEffectsData effData, + String effectId) { + var fx = ExileDB.ExileEffects().get(effectId); + return fx != null && effData.has(fx); + } + + @Override + public Class getSerClass() { + return HasExileEffectCondition.class; + } +} + + From bf52165f6b891a2ecc7218c01d3941e5ad263e67 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Sun, 24 Aug 2025 03:19:04 -0400 Subject: [PATCH 02/20] New STATS Added new More Damage While Leeching Magic Shield Offense Stat. Added new Leech At Full Health Conditional Resource Stat, --- .../aoe_data/database/stats/OffenseStats.java | 22 +++++++++++++++++++ .../database/stats/ResourceStats.java | 15 +++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/OffenseStats.java b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/OffenseStats.java index ef2605dcf..212da9278 100644 --- a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/OffenseStats.java +++ b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/OffenseStats.java @@ -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; @@ -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; @@ -580,6 +583,25 @@ public class OffenseStats { .build(); + // While Leeching (any) → contributes to existing Crit Damage bucket + public static final DataPackStatAccessor 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() { } diff --git a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/ResourceStats.java b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/ResourceStats.java index 0257e4fa4..875bdf031 100644 --- a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/ResourceStats.java +++ b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/stats/ResourceStats.java @@ -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 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() { } From 7336969fe552b6ff7496b63744aa047b4f89a8c5 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Sun, 24 Aug 2025 03:20:36 -0400 Subject: [PATCH 03/20] NEW API + EVENT HOOKS Implemented brand new Resource Threshold fetcher, tracking ALL resources for conditional stats/effects. Desc: Completely Datapack driven. --- .../capability/entity/EntityData.java | 9 + .../capability/entity/ResourceTracker.java | 131 +++++++++++++++ .../event_hooks/my_events/OnResourceLost.java | 46 +++++ .../DataDrivenSpendThresholdSpec.java | 66 ++++++++ .../mechanics/thresholds/SpendKeys.java | 13 ++ .../thresholds/SpendThresholdContributor.java | 9 + .../thresholds/SpendThresholdManager.java | 159 ++++++++++++++++++ .../thresholds/SpendThresholdRegistry.java | 41 +++++ .../thresholds/SpendThresholdRuntime.java | 34 ++++ .../thresholds/SpendThresholdSpec.java | 128 ++++++++++++++ .../mechanics/thresholds/ThresholdsInit.java | 17 ++ .../datapack/SpendThresholdDef.java | 95 +++++++++++ .../SpendThresholdsReloadListener.java | 53 ++++++ .../uncommon/effectdatas/DamageEvent.java | 11 ++ 14 files changed, 812 insertions(+) create mode 100644 src/main/java/com/robertx22/mine_and_slash/capability/entity/ResourceTracker.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendKeys.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRegistry.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRuntime.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ThresholdsInit.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java diff --git a/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java b/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java index 9bfbb9c0c..f4da89b0a 100644 --- a/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java +++ b/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java @@ -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; @@ -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; } + + public 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 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 new file mode 100644 index 000000000..5dc7d2a1b --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/capability/entity/ResourceTracker.java @@ -0,0 +1,131 @@ +package com.robertx22.mine_and_slash.capability.entity; + +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. + */ +public class ResourceTracker { + private static final float EPS = 1e-4f; + + // 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); + if (have + EPS < threshold) return 0; + + int procs = (int) Math.floor((have + EPS) / threshold); + float remainder = have - procs * threshold; + + if (remainder <= EPS) lost.remove(type); + else lost.put(type, remainder); + 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; + + for (ResourceType rt : types) { + float have = lost.getOrDefault(rt, 0f); + if (have <= 0f) continue; + + float take = Math.min(have, need); + if (take > 0f) { + float remaining = have - take; + if (remaining <= EPS) lost.remove(rt); + else lost.put(rt, remaining); + need -= take; + } + if (need <= EPS) break; // satisfied this proc + } + 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); + + public void clearKey(ResourceType rt, String key) { + if (key == null || key.isEmpty()) return; + var byKey = keyProgress.get(rt); + if (byKey == null) return; + byKey.remove(key); + if (byKey.isEmpty()) { + keyProgress.remove(rt); + } + } + + /** 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; + + var byKey = keyProgress.computeIfAbsent(rt, __ -> new java.util.HashMap<>()); + float cur = byKey.getOrDefault(key, 0f) + add; + + int procs = 0; + while (cur + EPS >= threshold) { + cur -= threshold; + procs++; + } + if (cur <= EPS) byKey.remove(key); + else byKey.put(key, cur); + + return procs; + } + + /** Read current cursor for debug/UI. */ + public float getKeyProgress(String key, ResourceType rt) { + var byKey = keyProgress.get(rt); + return byKey == null ? 0f : byKey.getOrDefault(key, 0f); + } + + /** 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/event_hooks/my_events/OnResourceLost.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java new file mode 100644 index 000000000..04e2ec2dc --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java @@ -0,0 +1,46 @@ +package com.robertx22.mine_and_slash.event_hooks.my_events; + +import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdManager; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; +import com.robertx22.mine_and_slash.uncommon.datasaving.Load; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; + +/** + * Unified entrypoint for resource LOSS (spend, drains, damage). + * Health damage integration calls this via the LivingDamageEvent handler below. + * + * Debug printing is handled inside SpendThresholdManager and is toggled by + * OnResourceLost.DEBUG_ENABLED. + */ +public final class OnResourceLost { + private OnResourceLost() {} + + public enum LossSource { SpendOrDrain, Damage, Other } + + /** Toggle SpendThresholdManager debug logs per player. */ + public static boolean DEBUG_ENABLED = false; + + /** Call this whenever a resource actually goes down. */ + public static void trigger(LivingEntity entity, ResourceType type, float loss, LossSource source) { + if (loss <= 0f) return; + if (!(entity instanceof ServerPlayer sp)) return; + + var unit = Load.Unit(sp); + long now = sp.level().getGameTime(); // ticks + SpendThresholdManager.processSpend(sp, unit, type, loss, now); + } + + /** Wire health damage into the unified loss path. */ + @net.minecraftforge.eventbus.api.SubscribeEvent + public static void onLivingDamage(net.minecraftforge.event.entity.living.LivingDamageEvent evt) { + if (!(evt.getEntity() instanceof ServerPlayer sp)) return; + float applied = evt.getAmount(); + if (applied > 0f) { + trigger(sp, ResourceType.health, applied, LossSource.Damage); + } + } +} + + + 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 new file mode 100644 index 000000000..d35e248a9 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java @@ -0,0 +1,66 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +import com.robertx22.mine_and_slash.capability.entity.EntityData; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; +import java.util.Set; + +public class DataDrivenSpendThresholdSpec extends SpendThresholdSpec { + + public enum ThresholdMode { X_PER_LEVEL, FLAT, PCT_OF_MAX } + + private final ThresholdMode mode; + private final float value; + private final boolean multiplyByLevel; + @Nullable private final ResourceType pctMaxOf; + + public DataDrivenSpendThresholdSpec( + String key, + ResourceType resource, + ThresholdMode mode, + float value, + boolean multiplyByLevel, + @Nullable ResourceType pctMaxOf, + Set lockWhileEffectIds, + int cooldownTicks, + boolean lockWhileCooldown, + boolean dropProgressWhileLocked, + boolean resetProgressOnProc + ) { + super(resource, /*perLevelFactor (unused)*/ 0f, key, + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc); + this.mode = mode; + this.value = value; + this.multiplyByLevel = multiplyByLevel; + this.pctMaxOf = pctMaxOf; + } + + @Override + public float thresholdFor(EntityData unit) { + float base; + switch (mode) { + case X_PER_LEVEL: + base = value * Math.max(1, unit.getLevel()); + break; + case FLAT: + base = value * (multiplyByLevel ? Math.max(1, unit.getLevel()) : 1f); + break; + case PCT_OF_MAX: + ResourceType rt = (pctMaxOf != null) ? pctMaxOf : resource(); + float max = unit.getResources().getMax(unit.getEntity(), rt); + base = (value / 100f) * max; + if (multiplyByLevel) base *= Math.max(1, unit.getLevel()); + break; + default: + base = 0f; + } + return Math.max(0f, base); + } + + @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/SpendKeys.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendKeys.java new file mode 100644 index 000000000..ba7bbe834 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendKeys.java @@ -0,0 +1,13 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; +import com.robertx22.mine_and_slash.uncommon.datasaving.Load; +import net.minecraft.server.level.ServerPlayer; + +public final class SpendKeys { + private SpendKeys() {} + public static String key(String nodeId, ResourceType rt) { return "spend." + rt.id + "." + nodeId; } + public static float threshold(ServerPlayer sp, float perLevelFactor) { + return perLevelFactor * Load.Unit(sp).getLevel(); + } +} diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java new file mode 100644 index 000000000..9180a6299 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java @@ -0,0 +1,9 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +import com.robertx22.mine_and_slash.capability.entity.EntityData; +import java.util.List; + +public interface SpendThresholdContributor { + /** Return zero or more specs active for this unit (e.g., from allocated talents). */ + List getSpendThresholds(EntityData unit); +} 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 new file mode 100644 index 000000000..fde101c48 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java @@ -0,0 +1,159 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +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.saveclasses.unit.ResourceType; +import net.minecraft.server.level.ServerPlayer; + +/** Registers global spend thresholds at startup. */ +public final class SpendThresholdManager { + private SpendThresholdManager() {} + + public static void registerDefaults() { + + // 30 × LVL Energy, 10s cooldown, locked while effects are active + SpendThresholdRegistry.registerGlobal( + new DataDrivenSpendThresholdSpec( + /* key */ "ENERGY_XLVL_WRATH", + /* resource */ ResourceType.energy, + /* mode */ DataDrivenSpendThresholdSpec.ThresholdMode.X_PER_LEVEL, + /* value */ 30f, // "X" in X per level + /* multiplyByLvl */ true, + /* pctMaxOf */ null, // not used for X_PER_LEVEL + /* lock effects */ java.util.Set.of("wrath_of_the_juggernaut", "Burnout"), + /* cooldown ticks */ SpendThresholdSpec.secondsToTicks(10), + /* lockIfCooldown */ true, + /* dropWhileLock */ true, + /* resetOnProc */ true + ) { + @Override public void onProc(ServerPlayer sp, int procs) { + // TODO: apply Wrath here + } + } + .withPriority(0) + ); + + // 30 × LVL Mana (example) + SpendThresholdRegistry.registerGlobal( + new DataDrivenSpendThresholdSpec( + "MANA_XLVL", + ResourceType.mana, + DataDrivenSpendThresholdSpec.ThresholdMode.X_PER_LEVEL, + 30f, + true, // multiply by level + null, + java.util.Set.of(), // no lock effects + SpendThresholdSpec.secondsToTicks(10), + true, // lock while cooldown + true, // drop progress while locked + true // reset on proc + ) { + @Override public void onProc(ServerPlayer sp, int procs) { + // TODO: apply your mana proc effect here + } + } + .withPriority(0) + ); + + // Example: Took 20% of Max Health (percent-of-max mode) + SpendThresholdRegistry.registerGlobal( + new DataDrivenSpendThresholdSpec( + "TOOK_20PCT_HEALTH", + ResourceType.health, + DataDrivenSpendThresholdSpec.ThresholdMode.PCT_OF_MAX, + 20f, // 20% + false, // multiplyByLevel (usually false for %) + ResourceType.health, // pct-of which pool + java.util.Set.of(), // no lock effects by default + 0, // no cooldown by default (tune as you like) + false, // don't treat cooldown as lock (since 0) + true, // drop progress if ever locked + true // reset on proc + ) { + @Override public void onProc(ServerPlayer sp, int procs) { + // TODO: apply your “took X% max life” effect here + } + } + .withPriority(0) + ); + } + + // ===== DEBUGGING ===== + public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType type, float loss, long now) { + processSpend(sp, unit, type, loss, now, OnResourceLost.DEBUG_ENABLED); + } + + public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType type, float loss, long now, boolean debug) { + if (loss <= 0f) return; + + var tracker = unit.getResourceTracker(); + tracker.addLoss(type, loss); // general counter (optional) + + var specs = SpendThresholdRegistry.resolveFor(unit, type); + if (specs.isEmpty()) { + if (debug) { + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal( + "[SPEND] +" + String.format(java.util.Locale.US, "%.1f", loss) + + " " + type.id + " (no specs)" + )); + } + return; + } + + 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); // ensure this exists + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal( + "[SPEND:" + spec.key() + "] locked by cooldown (" + rem + "t ~ " + fmtSec(rem) + "s)" + )); + } + continue; + } + + // Effect lock + if (spec.isEffectLocked(unit)) { + if (spec.dropProgressWhileLocked()) { + tracker.clearKey(type, key); + } + if (debug) { + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal( + "[SPEND:" + spec.key() + "] locked by active effect(s)" + )); + } + continue; + } + + + float threshold = spec.thresholdFor(unit); + if (threshold <= 0f) continue; + + int procs = tracker.addAndConsumeForKey(key, type, loss, threshold); + if (procs > 0) { + spec.onProc(sp, procs); + spec.startCooldown(unit, now); + if (spec.resetOnProc()) { + tracker.clearKey(type, key); + } + if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); + } else if (debug) { + float cur = tracker.getKeyProgress(key, type); + dbg(sp, "[SPEND:" + spec.key() + "] +" + fmt(loss) + " " + type.id + " (cur=" + fmt(cur) + " / " + fmt(threshold) + ")"); + } + } + } + + // --- helpers --- + private static void dbg(ServerPlayer sp, String msg) { + if (!OnResourceLost.DEBUG_ENABLED) return; + sp.sendSystemMessage(net.minecraft.network.chat.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/SpendThresholdRegistry.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRegistry.java new file mode 100644 index 000000000..2305f408c --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRegistry.java @@ -0,0 +1,41 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +import com.robertx22.mine_and_slash.capability.entity.EntityData; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; + +import java.util.*; + +public final class SpendThresholdRegistry { + private SpendThresholdRegistry() {} + + private static final Map> BY_RES = new EnumMap<>(ResourceType.class); + private static boolean FROZEN = false; + private static int COUNT = 0; + + public static void clearAll() { + BY_RES.clear(); + FROZEN = false; + COUNT = 0; + } + + public static void registerGlobal(SpendThresholdSpec spec) { registerGlobal(spec, 0); } + + public static void registerGlobal(SpendThresholdSpec spec, int priority) { + if (spec == null || FROZEN) return; + spec.withPriority(priority); + BY_RES.computeIfAbsent(spec.resource(), __ -> new ArrayList<>()).add(spec); + COUNT++; + } + + /** Returns a copy sorted by priority (low first). */ + public static List resolveFor(EntityData unit, ResourceType rt) { + var list = BY_RES.get(rt); + if (list == null || list.isEmpty()) return List.of(); + var copy = new ArrayList<>(list); + copy.sort(Comparator.comparingInt(SpendThresholdSpec::priority)); + return copy; + } + + public static void freeze() { FROZEN = true; } + public static int size() { return COUNT; } +} 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 new file mode 100644 index 000000000..4273a4a1e --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdRuntime.java @@ -0,0 +1,34 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +import java.util.HashMap; +import java.util.Map; + +public class SpendThresholdRuntime { + // gameTime (ticks) when each key’s cooldown ends + private final Map cooldownUntil = new HashMap<>(); + + /** Start/refresh cooldown for a key. */ + public void startCooldown(String key, long now, int cooldownTicks) { + if (cooldownTicks <= 0) return; + cooldownUntil.put(key, now + cooldownTicks); + } + + /** True if now is still before the stored end time. */ + public boolean isCoolingDown(String key, long now) { + Long until = cooldownUntil.get(key); + return until != null && now < until; + } + + /** Remaining ticks until ready (0 if no cooldown / already ready). */ + public int cooldownRemainingTicks(String key, long now) { + Long until = cooldownUntil.get(key); + if (until == null) return 0; + long rem = until - now; + return (int) Math.max(0, rem); + } + + /** Clear a key’s cooldown (optional utility). */ + public void clearCooldown(String key) { + cooldownUntil.remove(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 new file mode 100644 index 000000000..c368db7fd --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java @@ -0,0 +1,128 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +import com.robertx22.mine_and_slash.capability.entity.EntityData; +import com.robertx22.mine_and_slash.database.data.exile_effects.ExileEffect; +import com.robertx22.mine_and_slash.database.registry.ExileDB; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public abstract class SpendThresholdSpec { + private final ResourceType resource; + private final float perLevelFactor; // used by default thresholdFor() + 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 dropProgressWhileLocked; + private final boolean resetProgressOnProc; + + // registry ordering (lower runs first) + private int priority = 0; + + // Legacy/simple ctor + public SpendThresholdSpec(ResourceType resource, float perLevelFactor, String key) { + this(resource, perLevelFactor, key, Collections.emptySet(), 0, false, true, true); + } + + // Full ctor used by data-driven impl + public SpendThresholdSpec(ResourceType resource, + float perLevelFactor, + String key, + Set lockWhileEffectIds, + int cooldownTicks, + boolean lockWhileCooldown, + boolean dropProgressWhileLocked, + boolean resetProgressOnProc) { + this.resource = resource; + this.perLevelFactor = perLevelFactor; + this.key = key; + this.lockWhileEffectIds = (lockWhileEffectIds == null) ? Collections.emptySet() : Set.copyOf(lockWhileEffectIds); + this.cooldownTicks = Math.max(0, cooldownTicks); + this.lockWhileCooldown = lockWhileCooldown; + this.dropProgressWhileLocked = dropProgressWhileLocked; + this.resetProgressOnProc = resetProgressOnProc; + } + + // ===== accessors ===== + public ResourceType resource() { return resource; } + public String key() { return key; } + public String keyFor(EntityData unit) { return key; } + public boolean lockWhileCooldown() { return lockWhileCooldown; } + public boolean dropProgressWhileLocked() { return dropProgressWhileLocked; } + public boolean resetOnProc() { return resetProgressOnProc; } + public int cooldownTicks() { return cooldownTicks; } + public int priority() { return priority; } + + // fluent config (for code-defined specs) + public SpendThresholdSpec withCooldownSeconds(int seconds) { + return withCooldownTicks(secondsToTicks(seconds)); + } + public SpendThresholdSpec withCooldownTicks(int ticks) { + return newWrapper(this.lockWhileEffectIds, Math.max(0, ticks), this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc); + } + public SpendThresholdSpec lockWhile(String... effectIds) { + Set s = new HashSet<>(this.lockWhileEffectIds); + if (effectIds != null) Collections.addAll(s, effectIds); + return newWrapper(s, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc); + } + public SpendThresholdSpec lockWhileCooldown(boolean on) { + return newWrapper(this.lockWhileEffectIds, this.cooldownTicks, on, this.dropProgressWhileLocked, this.resetProgressOnProc); + } + public SpendThresholdSpec dropProgressLocked(boolean on) { + return newWrapper(this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, on, this.resetProgressOnProc); + } + public SpendThresholdSpec resetOnProc(boolean on) { + return newWrapper(this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, on); + } + public SpendThresholdSpec withPriority(int p) { + this.priority = p; + return this; + } + + private SpendThresholdSpec newWrapper(Set lockIds, int cooldown, boolean lockCD, boolean dropLocked, boolean resetOnProc) { + // create a shallow “copy” retaining dynamic behavior (onProc/thresholdFor come from subclass) + return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, lockIds, cooldown, lockCD, dropLocked, resetOnProc) { + @Override public float thresholdFor(EntityData unit) { return SpendThresholdSpec.this.thresholdFor(unit); } + @Override public void onProc(ServerPlayer sp, int procs) { SpendThresholdSpec.this.onProc(sp, procs); } + }.withPriority(this.priority); + } + + /** Default threshold = perLevelFactor × LVL. Subclasses may override. */ + 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(); + for (String id : lockWhileEffectIds) { + ExileEffect fx = ExileDB.ExileEffects().get(id); + if (fx != null && store.has(fx)) return true; + } + return false; + } + + /** 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); + + // time helpers + public static int secondsToTicks(int seconds) { + return (seconds <= 0) ? 0 : Math.max(1, seconds * 20); + } + public static float ticksToSeconds(int ticks) { return ticks / 20f; } +} diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ThresholdsInit.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ThresholdsInit.java new file mode 100644 index 000000000..77aa79389 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ThresholdsInit.java @@ -0,0 +1,17 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds; + +import com.robertx22.mine_and_slash.mmorpg.SlashRef; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; + +@Mod.EventBusSubscriber(modid = SlashRef.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) +public final class ThresholdsInit { + private ThresholdsInit() {} + + @SubscribeEvent + public static void onCommonSetup(final FMLCommonSetupEvent e) { + e.enqueueWork(SpendThresholdManager::registerDefaults); + System.out.println("[SpendThresholds] defaults registered"); + } +} 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 new file mode 100644 index 000000000..69ee1202a --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java @@ -0,0 +1,95 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds.datapack; + +import com.google.gson.annotations.SerializedName; +import com.robertx22.mine_and_slash.mechanics.thresholds.DataDrivenSpendThresholdSpec; +import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdSpec; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; +import net.minecraft.server.level.ServerPlayer; + +import java.util.*; + +public class SpendThresholdDef { + public String key; + public String resource = "energy"; + public boolean enabled = true; + public int priority = 0; + + public static class Threshold { + public String mode = "X_PER_LEVEL"; + public float value = 0f; + @SerializedName("multiply_by_level") public boolean multiplyByLevel = false; + @SerializedName("pct_of") public String pctOf; // optional + } + public Threshold threshold = new Threshold(); + + public static class Locks { + public List effects = new ArrayList<>(); + @SerializedName("lock_while_cooldown") public boolean lockWhileCooldown = true; + @SerializedName("drop_progress_while_locked") public boolean dropProgressWhileLocked = true; + @SerializedName("reset_progress_on_proc") public boolean resetProgressOnProc = true; + } + public Locks locks = new Locks(); + + @SerializedName("cooldown_seconds") + public int cooldownSeconds = 0; + + public static class ProcAction { + public String action; // "apply_effect" + @SerializedName("effect_id") public String effectId; + @SerializedName("duration_ticks") public int durationTicks = 200; + public int stacks = 1; + } + @SerializedName("on_proc") + public List onProc = new ArrayList<>(); + + public SpendThresholdSpec toSpec() { + ResourceType res = ResourceType.valueOf(resource.toLowerCase(java.util.Locale.ROOT)); + DataDrivenSpendThresholdSpec.ThresholdMode mode = + DataDrivenSpendThresholdSpec.ThresholdMode.valueOf(threshold.mode.toUpperCase(Locale.ROOT)); + + ResourceType pctOf = null; + if ("PCT_OF_MAX".equalsIgnoreCase(threshold.mode) && threshold.pctOf != null && !threshold.pctOf.isEmpty()) { + pctOf = ResourceType.valueOf(threshold.pctOf.toLowerCase(Locale.ROOT)); + } + + Set lockEff = new HashSet<>(locks.effects); + + return new DataDrivenSpendThresholdSpec( + key, + res, + mode, + threshold.value, + threshold.multiplyByLevel, + pctOf, + lockEff, + SpendThresholdSpec.secondsToTicks(cooldownSeconds), + locks.lockWhileCooldown, + locks.dropProgressWhileLocked, + locks.resetProgressOnProc + ) { + @Override + public void onProc(ServerPlayer sp, int procs) { + if (onProc == null || onProc.isEmpty()) return; + + var unit = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(sp); + var store = unit.getStatusEffectsData(); + + for (ProcAction a : onProc) { + if (!"apply_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue; + + var fx = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(a.effectId); + if (fx == null) continue; + + var inst = store.getOrCreate(fx); + int stacks = Math.max(1, a.stacks); + if (fx.max_stacks > 0) stacks = Math.min(stacks, fx.max_stacks); + inst.stacks = Math.max(inst.stacks, stacks); + inst.ticks_left = Math.max(inst.ticks_left, Math.max(1, a.durationTicks)); + + fx.onApply(sp); + unit.sync.setDirty(); + } + } + }.withPriority(priority); + } +} diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java new file mode 100644 index 000000000..87669dd33 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java @@ -0,0 +1,53 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds.datapack; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdRegistry; +import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraftforge.event.AddReloadListenerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; + +import java.util.Map; + +@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE) +public class SpendThresholdsReloadListener extends SimpleJsonResourceReloadListener { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public SpendThresholdsReloadListener() { + super(GSON, "spend_thresholds"); + } + + @Override + protected void apply(Map map, + ResourceManager rm, ProfilerFiller profiler) { + SpendThresholdRegistry.clearAll(); + SpendThresholdManager.registerDefaults(); + + int loaded = 0; + for (var e : map.entrySet()) { + try { + SpendThresholdDef def = GSON.fromJson(e.getValue(), SpendThresholdDef.class); + if (def != null && def.enabled && def.key != null && !def.key.isEmpty()) { + SpendThresholdRegistry.registerGlobal(def.toSpec(), def.priority); + loaded++; + } + } catch (Exception ex) { + System.err.println("[SpendThresholds] Failed " + e.getKey() + ": " + ex.getMessage()); + } + } + + SpendThresholdRegistry.freeze(); + System.out.println("[SpendThresholds] Loaded " + loaded + " datapack specs; total=" + SpendThresholdRegistry.size()); + } + + @SubscribeEvent + public static void onAddReload(AddReloadListenerEvent evt) { + evt.addListener(new SpendThresholdsReloadListener()); + } +} diff --git a/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/DamageEvent.java b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/DamageEvent.java index 827dc6e08..435c562ac 100644 --- a/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/DamageEvent.java +++ b/src/main/java/com/robertx22/mine_and_slash/uncommon/effectdatas/DamageEvent.java @@ -627,6 +627,17 @@ protected void activate() { dmg = DamageAbsorbedByMana.modifyEntityDamage(this, dmg); dmg = MagicShield.modifyEntityDamage(this, dmg); } + + // (START NEW) trigger resource lost event for health + if (target instanceof ServerPlayer sp) { + com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceLost.trigger( + sp, + com.robertx22.mine_and_slash.saveclasses.unit.ResourceType.health, + dmg, + com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceLost.LossSource.Damage + ); + } + // (END NEW) float vanillaDamage = HealthUtils.realToVanilla(target, dmg); From 6ec65255bca2e0724022e07805a637dae877322b Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:02:34 -0400 Subject: [PATCH 04/20] Suggested Fixes: Datapack Threshold --- .../DataDrivenSpendThresholdSpec.java | 59 ++++++--- .../thresholds/SpendThresholdSpec.java | 45 +++++-- .../datapack/SpendThresholdDef.java | 116 +++++++++++++----- .../SpendThresholdsReloadListener.java | 2 - .../spend_thresholds/25percent_health.json | 23 ++++ .../spend_thresholds/energy_xlvl_wotj.json | 31 +++++ 6 files changed, 214 insertions(+), 62 deletions(-) create mode 100644 src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json create mode 100644 src/main/resources/data/mmorpg/spend_thresholds/energy_xlvl_wotj.json 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 d35e248a9..ca498f77e 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 @@ -9,12 +9,13 @@ public class DataDrivenSpendThresholdSpec extends SpendThresholdSpec { - public enum ThresholdMode { X_PER_LEVEL, FLAT, PCT_OF_MAX } + public enum ThresholdMode { FLAT, PERCENT_OF_MAX } private final ThresholdMode mode; private final float value; private final boolean multiplyByLevel; - @Nullable private final ResourceType pctMaxOf; + @Nullable private final ResourceType percentMaxOf; + private final boolean showUi; public DataDrivenSpendThresholdSpec( String key, @@ -22,40 +23,54 @@ public DataDrivenSpendThresholdSpec( ThresholdMode mode, float value, boolean multiplyByLevel, - @Nullable ResourceType pctMaxOf, + @Nullable ResourceType percentMaxOf, Set lockWhileEffectIds, int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc + boolean resetProgressOnProc, + boolean showUi ) { - super(resource, /*perLevelFactor (unused)*/ 0f, key, - lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc); + super(resource, 0f, key, + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, showUi); this.mode = mode; this.value = value; this.multiplyByLevel = multiplyByLevel; - this.pctMaxOf = pctMaxOf; + this.percentMaxOf = percentMaxOf; + this.showUi = showUi; + } + + // 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 resetProgressOnProc + ) { + this(key, resource, mode, value, multiplyByLevel, percentMaxOf, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false); } @Override public float thresholdFor(EntityData unit) { float base; switch (mode) { - case X_PER_LEVEL: - base = value * Math.max(1, unit.getLevel()); - break; - case FLAT: - base = value * (multiplyByLevel ? Math.max(1, unit.getLevel()) : 1f); - break; - case PCT_OF_MAX: - ResourceType rt = (pctMaxOf != null) ? pctMaxOf : resource(); - float max = unit.getResources().getMax(unit.getEntity(), rt); + 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; - if (multiplyByLevel) base *= Math.max(1, unit.getLevel()); - break; - default: - base = 0f; + } + case FLAT -> base = value; + default -> base = value; } + if (multiplyByLevel) base *= Math.max(1, unit.getLevel()); return Math.max(0f, base); } @@ -63,4 +78,8 @@ public float thresholdFor(EntityData unit) { public void onProc(ServerPlayer sp, int procs) { // No default action here; datapack loader wires actions. } + + public boolean showUi() { return showUi; } + + // Perk lock is handled by callers (anonymous subclass) when needed. } 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 c368db7fd..2931d8d5a 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 @@ -6,7 +6,6 @@ import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; import net.minecraft.server.level.ServerPlayer; -import javax.annotation.Nullable; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -22,15 +21,11 @@ public abstract class SpendThresholdSpec { private final boolean lockWhileCooldown; // treat cooldown as a lock private final boolean dropProgressWhileLocked; private final boolean resetProgressOnProc; + private final boolean showUi; // whether to render progress HUD for this spec // registry ordering (lower runs first) private int priority = 0; - // Legacy/simple ctor - public SpendThresholdSpec(ResourceType resource, float perLevelFactor, String key) { - this(resource, perLevelFactor, key, Collections.emptySet(), 0, false, true, true); - } - // Full ctor used by data-driven impl public SpendThresholdSpec(ResourceType resource, float perLevelFactor, @@ -39,7 +34,8 @@ public SpendThresholdSpec(ResourceType resource, int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc) { + boolean resetProgressOnProc, + boolean showUi) { this.resource = resource; this.perLevelFactor = perLevelFactor; this.key = key; @@ -48,6 +44,19 @@ public SpendThresholdSpec(ResourceType resource, this.lockWhileCooldown = lockWhileCooldown; this.dropProgressWhileLocked = dropProgressWhileLocked; this.resetProgressOnProc = resetProgressOnProc; + this.showUi = showUi; + } + + // Backward-compatible ctor (defaults showUi=false) + public SpendThresholdSpec(ResourceType resource, + float perLevelFactor, + String key, + Set lockWhileEffectIds, + int cooldownTicks, + boolean lockWhileCooldown, + boolean dropProgressWhileLocked, + boolean resetProgressOnProc) { + this(resource, perLevelFactor, key, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false); } // ===== accessors ===== @@ -59,6 +68,7 @@ public SpendThresholdSpec(ResourceType resource, public boolean resetOnProc() { return resetProgressOnProc; } public int cooldownTicks() { return cooldownTicks; } public int priority() { return priority; } + public boolean showUi() { return showUi; } // fluent config (for code-defined specs) public SpendThresholdSpec withCooldownSeconds(int seconds) { @@ -88,9 +98,18 @@ public SpendThresholdSpec withPriority(int p) { private SpendThresholdSpec newWrapper(Set lockIds, int cooldown, boolean lockCD, boolean dropLocked, boolean resetOnProc) { // create a shallow “copy” retaining dynamic behavior (onProc/thresholdFor come from subclass) - return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, lockIds, cooldown, lockCD, dropLocked, resetOnProc) { + return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, lockIds, cooldown, lockCD, dropLocked, resetOnProc, this.showUi) { + @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 SpendThresholdSpec withShowUi(boolean on) { + return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc, 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); } @@ -104,12 +123,16 @@ public boolean isEffectLocked(EntityData unit) { if (lockWhileEffectIds.isEmpty()) return false; var store = unit.getStatusEffectsData(); for (String id : lockWhileEffectIds) { - ExileEffect fx = ExileDB.ExileEffects().get(id); - if (fx != null && store.has(fx)) return true; + ExileEffect effect = ExileDB.ExileEffects().get(id); + if (effect != null && store.has(effect)) return true; } return false; } + 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) { @@ -122,7 +145,7 @@ public void startCooldown(EntityData unit, long now) { // time helpers public static int secondsToTicks(int seconds) { - return (seconds <= 0) ? 0 : Math.max(1, seconds * 20); + return (seconds <= 0) ? 0 : seconds * 20; } public static float ticksToSeconds(int ticks) { return ticks / 20f; } } 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 69ee1202a..79746ff32 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 @@ -10,21 +10,23 @@ public class SpendThresholdDef { public String key; - public String resource = "energy"; + public String resource = ""; public boolean enabled = true; public int priority = 0; + @SerializedName("show_ui") + public boolean showUi = false; public static class Threshold { - public String mode = "X_PER_LEVEL"; + public String mode = "FLAT"; public float value = 0f; @SerializedName("multiply_by_level") public boolean multiplyByLevel = false; - @SerializedName("pct_of") public String pctOf; // optional + @SerializedName("percent_of") public String percentOf; // optional } public Threshold threshold = new Threshold(); public static class Locks { public List effects = new ArrayList<>(); - @SerializedName("lock_while_cooldown") public boolean lockWhileCooldown = true; + @SerializedName("lock_while_cooldown") public boolean lockWhileCooldown = false; @SerializedName("drop_progress_while_locked") public boolean dropProgressWhileLocked = true; @SerializedName("reset_progress_on_proc") public boolean resetProgressOnProc = true; } @@ -33,63 +35,119 @@ public static class Locks { @SerializedName("cooldown_seconds") public int cooldownSeconds = 0; + @SerializedName("require_stat") + public String requireStatId = ""; + public static class ProcAction { public String action; // "apply_effect" - @SerializedName("effect_id") public String effectId; - @SerializedName("duration_ticks") public int durationTicks = 200; + @SerializedName("exile_potion_id") public String effectId; + @SerializedName("duration_seconds") public int durationSeconds = 0; public int stacks = 1; + @SerializedName("on_expire") public java.util.Map onExpire = java.util.Collections.emptyMap(); } @SerializedName("on_proc") public List onProc = new ArrayList<>(); public SpendThresholdSpec toSpec() { - ResourceType res = ResourceType.valueOf(resource.toLowerCase(java.util.Locale.ROOT)); - DataDrivenSpendThresholdSpec.ThresholdMode mode = - DataDrivenSpendThresholdSpec.ThresholdMode.valueOf(threshold.mode.toUpperCase(Locale.ROOT)); + ResourceType res = parseResource(resource, ResourceType.energy); - ResourceType pctOf = null; - if ("PCT_OF_MAX".equalsIgnoreCase(threshold.mode) && threshold.pctOf != null && !threshold.pctOf.isEmpty()) { - pctOf = ResourceType.valueOf(threshold.pctOf.toLowerCase(Locale.ROOT)); + // 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 + + ResourceType percentOf = null; + if (mode == DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX + && threshold.percentOf != null && !threshold.percentOf.isEmpty()) { + percentOf = parseResource(threshold.percentOf, res); // default to this spec’s resource if bad input } - Set lockEff = new HashSet<>(locks.effects); + Set lockEff = (locks != null && locks.effects != null) + ? new HashSet<>(locks.effects) : Collections.emptySet(); return new DataDrivenSpendThresholdSpec( key, res, mode, threshold.value, - threshold.multiplyByLevel, - pctOf, + mult, + percentOf, lockEff, SpendThresholdSpec.secondsToTicks(cooldownSeconds), - locks.lockWhileCooldown, - locks.dropProgressWhileLocked, - locks.resetProgressOnProc + locks != null && locks.lockWhileCooldown, + locks != null && locks.dropProgressWhileLocked, + locks != null && locks.resetProgressOnProc, + showUi ) { @Override public void onProc(ServerPlayer sp, int procs) { if (onProc == null || onProc.isEmpty()) return; - var unit = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(sp); + var unit = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(sp); var store = unit.getStatusEffectsData(); for (ProcAction a : onProc) { - if (!"apply_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue; + if (!"exile_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue; + var effect = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(a.effectId); + if (effect == null) continue; - var fx = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(a.effectId); - if (fx == null) continue; - - var inst = store.getOrCreate(fx); + var inst = store.getOrCreate(effect); int stacks = Math.max(1, a.stacks); - if (fx.max_stacks > 0) stacks = Math.min(stacks, fx.max_stacks); - inst.stacks = Math.max(inst.stacks, stacks); - inst.ticks_left = Math.max(inst.ticks_left, Math.max(1, a.durationTicks)); - - fx.onApply(sp); + if (effect.max_stacks > 0) stacks = Math.min(stacks, effect.max_stacks); + inst.stacks = Math.max(inst.stacks, stacks); + int durTicks = SpendThresholdSpec.secondsToTicks(a.durationSeconds); + inst.ticks_left = Math.max(inst.ticks_left, Math.max(1, durTicks)); + + inst.self_cast = true; + inst.caster_uuid = sp.getUUID().toString(); + + // Attach on-expire duration overrides (convert seconds -> ticks) + if (a.onExpire != null && !a.onExpire.isEmpty()) { + if (inst.onExpireEffectDurationTicks == null) { + inst.onExpireEffectDurationTicks = new java.util.HashMap<>(); + } + for (var e : a.onExpire.entrySet()) { + int ticks = SpendThresholdSpec.secondsToTicks(Math.max(0, e.getValue())); + if (ticks > 0) { + inst.onExpireEffectDurationTicks.put(e.getKey(), ticks); + } + } + } + + effect.onApply(sp); unit.sync.setDirty(); } } + + @Override + public boolean isLockedFor(com.robertx22.mine_and_slash.capability.entity.EntityData unit) { + if (super.isEffectLocked(unit)) return true; + if (requireStatId != null && !requireStatId.isEmpty()) { + var st = com.robertx22.mine_and_slash.database.registry.ExileDB.Stats().get(requireStatId); + if (st != null) { + return unit.getUnit().getCalculatedStat(st).getValue() <= 0; + } + } + return false; + } }.withPriority(priority); } + + // --- helpers --- + private static ResourceType parseResource(String s, ResourceType fallback) { + if (s == null) return 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; + } catch (NoSuchFieldException | IllegalAccessException ignored) {} + } + return fallback; + } } diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java index 87669dd33..1b2ccbac0 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java @@ -4,7 +4,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdRegistry; -import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdManager; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.profiling.ProfilerFiller; @@ -27,7 +26,6 @@ public SpendThresholdsReloadListener() { protected void apply(Map map, ResourceManager rm, ProfilerFiller profiler) { SpendThresholdRegistry.clearAll(); - SpendThresholdManager.registerDefaults(); int loaded = 0; for (var e : map.entrySet()) { diff --git a/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json new file mode 100644 index 000000000..84ff2d0fc --- /dev/null +++ b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json @@ -0,0 +1,23 @@ +{ + "key": "25PERCENT_HEALTH", + "resource": "health", + "enabled": true, + "priority": 0, + "threshold": { + "mode": "PERCENT_OF_MAX", + "value": 25, + "multiply_by_level": false, + "percent_of": "health" + }, + "cooldown_seconds": 0, + "locks": { + "effects": [], + "drop_progress_while_locked": true, + "reset_progress_on_proc": true + }, + "on_proc": [ + { + + } + ] + } \ No newline at end of file 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 new file mode 100644 index 000000000..b58cea578 --- /dev/null +++ b/src/main/resources/data/mmorpg/spend_thresholds/energy_xlvl_wotj.json @@ -0,0 +1,31 @@ +{ + "key": "ENERGY_XLVL_WOTJ", + "resource": "energy", + "enabled": true, + "show_ui": true, + "priority": 0, + "threshold": { + "mode": "FLAT", + "value": 10, + "multiply_by_level": true + }, + "cooldown_seconds": 20, + "require_stat": "unlock_wotj", + "locks": { + "effects": ["wrath_of_the_juggernaut", "burnout"], + "lock_while_cooldown": true, + "drop_progress_while_locked": true, + "reset_progress_on_proc": true + }, + "on_proc": [ + { + "action": "exile_effect", + "exile_potion_id": "wrath_of_the_juggernaut", + "duration_seconds": 10, + "stacks": 1, + "on_expire": { + "burnout": 10 + } + } + ] + } \ No newline at end of file From 5aece7934c35fa3d31e8c5258f845f3eb94eea2b Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:53:06 -0400 Subject: [PATCH 05/20] Suggested Fixes: Extension --- .../exile_effects/adders/ModEffects.java | 33 ++++++++++++------- .../capability/entity/EntityData.java | 2 +- .../capability/entity/EntityLeechData.java | 8 ++--- .../event_hooks/my_events/OnResourceLost.java | 10 ------ .../my_events/OnResourceRestore.java | 2 +- .../mmorpg/event_registers/CommonEvents.java | 9 +++++ 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java index 723819ab2..80a4124be 100644 --- a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java +++ b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java @@ -71,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); @@ -105,8 +107,7 @@ public static List getCurses() { // ---------- Helper ---------- private static EffectCtx state(String id, String name, Elements elem) { - // EffectCtx constructor already adds itself to ModEffects.ALL in this file's pattern - return new EffectCtx(id, name, elem, EffectType.beneficial); + return new EffectCtx(id, name, (elem != null ? elem : Elements.Physical), EffectType.beneficial); } // Pretty names for resources (UI text) @@ -119,13 +120,6 @@ private static EffectCtx state(String id, String name, Elements elem) { ); // Suggested elements per resource (only used for coloring/category) - private static final Map RES_ELEM = Map.of( - ResourceType.health, Elements.Physical, - ResourceType.mana, Elements.Cold, - ResourceType.energy, Elements.Nature, - ResourceType.magic_shield, Elements.Shadow, - ResourceType.blood, Elements.Fire - ); // ---------- Generic flags ---------- public static final EffectCtx LEECHING_STATE = state( @@ -141,14 +135,13 @@ private static EffectCtx state(String id, String name, Elements elem) { static { for (var rt : RES_NAME.keySet()) { - var elem = RES_ELEM.get(rt); var nice = RES_NAME.get(rt); LEECHING_STATE_BY_RES.put( - rt, state("leeching_" + rt.id + "_state", "Leeching " + nice, elem) + rt, state("leeching_" + rt.id + "_state", "Leeching " + nice, null) ); REGEN_STATE_BY_RES.put( - rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, elem) + rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, null) ); } } @@ -412,6 +405,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) diff --git a/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java b/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java index f4da89b0a..b11c18b28 100644 --- a/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java +++ b/src/main/java/com/robertx22/mine_and_slash/capability/entity/EntityData.java @@ -964,7 +964,7 @@ public void onSpellHitTarget(Entity spellEntity, LivingEntity target) { private final ResourceTracker resourceTracker = new ResourceTracker(); public ResourceTracker getResourceTracker() { return resourceTracker; } - public final SpendThresholdRuntime spendRuntime = new SpendThresholdRuntime(); + private final SpendThresholdRuntime spendRuntime = new SpendThresholdRuntime(); public SpendThresholdRuntime getSpendRuntime() { return spendRuntime; } 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 fc78a9c10..50a4e71bd 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 @@ -39,12 +39,12 @@ 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 capPctPerSec = data.getUnit() + float capPercentPerSec = data.getUnit() .getCalculatedStat(ResourceStats.LEECH_CAP.get(rt)) .getValue() / 100F; float maxRes = data.getResources().getMax(data.entity, rt); - float fiveSecs = 5F * capPctPerSec * maxRes; // “5 seconds worth” reservoir cap + float fiveSecs = 5F * capPercentPerSec * maxRes; // “5 seconds worth” reservoir cap float clamped = MathHelper.clamp(en.getValue(), 0, fiveSecs); en.setValue(clamped); } @@ -55,12 +55,12 @@ public void onSecondUseLeeches(EntityData data) { float reservoir = entry.getValue(); if (reservoir <= EPS) continue; - float capPctPerSec = data.getUnit() + float capPercentPerSec = data.getUnit() .getCalculatedStat(ResourceStats.LEECH_CAP.get(rt)) .getValue() / 100F; float maxRes = data.getResources().getMax(data.entity, rt); - float perSecondCap = capPctPerSec * maxRes; + float perSecondCap = capPercentPerSec * maxRes; // Intended drain this second (bounded by per-second cap and reservoir) float take = Math.min(reservoir, perSecondCap); diff --git a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java index 04e2ec2dc..3e29a8206 100644 --- a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java @@ -30,16 +30,6 @@ public static void trigger(LivingEntity entity, ResourceType type, float loss, L long now = sp.level().getGameTime(); // ticks SpendThresholdManager.processSpend(sp, unit, type, loss, now); } - - /** Wire health damage into the unified loss path. */ - @net.minecraftforge.eventbus.api.SubscribeEvent - public static void onLivingDamage(net.minecraftforge.event.entity.living.LivingDamageEvent evt) { - if (!(evt.getEntity() instanceof ServerPlayer sp)) return; - float applied = evt.getAmount(); - if (applied > 0f) { - trigger(sp, ResourceType.health, applied, LossSource.Damage); - } - } } 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 63e2aa50f..e4e6b9fa8 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 @@ -30,7 +30,7 @@ public class OnResourceRestore { // ===== Gameplay tuning ===== /** State lifetime in ticks; should exceed your leech cadence + jitter. */ - private static final int STATE_TICKS = 60; // ~3.0s @20tps + private static final int STATE_TICKS = 20; // ===== Debug controls ===== /** Global toggle for chat debug. Safe to leave false in prod. */ diff --git a/src/main/java/com/robertx22/mine_and_slash/mmorpg/event_registers/CommonEvents.java b/src/main/java/com/robertx22/mine_and_slash/mmorpg/event_registers/CommonEvents.java index a30d2cfd6..bb6e7d15c 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mmorpg/event_registers/CommonEvents.java +++ b/src/main/java/com/robertx22/mine_and_slash/mmorpg/event_registers/CommonEvents.java @@ -4,6 +4,7 @@ import com.robertx22.library_of_exile.events.base.ExileEvents; import com.robertx22.mine_and_slash.database.DatabaseCaches; import com.robertx22.mine_and_slash.database.data.spells.summons.entity.SummonEntity; +import com.robertx22.mine_and_slash.event_hooks.my_events.OnResourceLost; import com.robertx22.mine_and_slash.event_hooks.damage_hooks.LivingHurtUtils; import com.robertx22.mine_and_slash.event_hooks.damage_hooks.reworked.NewDamageMain; import com.robertx22.mine_and_slash.event_hooks.entity.OnMobSpawn; @@ -62,6 +63,14 @@ public static void register() { }); + ForgeEvents.registerForgeEvent(LivingDamageEvent.class, evt -> { + if (!(evt.getEntity() instanceof ServerPlayer sp)) return; + float applied = evt.getAmount(); + if (applied > 0f) { + OnResourceLost.trigger(sp, ResourceType.health, applied, OnResourceLost.LossSource.Damage); + } + }); + OnItemInteract.register(); From f06bf301415ee27aa08b7b6d26255e5b313765ff Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:38:07 -0400 Subject: [PATCH 06/20] Suggested Fixes: Cleanup --- .../exile_effects/adders/ModEffects.java | 9 +++-- .../event_hooks/my_events/EffectUtils.java | 13 ++++++- .../thresholds/SpendThresholdSpec.java | 8 ---- .../datapack/SpendThresholdDef.java | 37 ++++++++----------- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java index 80a4124be..f46d0d29d 100644 --- a/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java +++ b/src/main/java/com/robertx22/mine_and_slash/aoe_data/database/exile_effects/adders/ModEffects.java @@ -107,7 +107,10 @@ public static List getCurses() { // ---------- Helper ---------- private static EffectCtx state(String id, String name, Elements elem) { - return new EffectCtx(id, name, (elem != null ? elem : Elements.Physical), EffectType.beneficial); + 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) @@ -138,10 +141,10 @@ private static EffectCtx state(String id, String name, Elements elem) { var nice = RES_NAME.get(rt); LEECHING_STATE_BY_RES.put( - rt, state("leeching_" + rt.id + "_state", "Leeching " + nice, null) + rt, statePhysical("leeching_" + rt.id + "_state", "Leeching " + nice) ); REGEN_STATE_BY_RES.put( - rt, state("regen_" + rt.id + "_state", "Regenerating " + nice, null) + rt, statePhysical("regen_" + rt.id + "_state", "Regenerating " + nice) ); } } 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 e8f2d668f..6c4f43aee 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 @@ -2,6 +2,7 @@ import com.robertx22.mine_and_slash.aoe_data.database.stats.base.EffectCtx; import com.robertx22.mine_and_slash.database.data.exile_effects.ExileEffect; +import com.robertx22.mine_and_slash.database.data.exile_effects.ExileEffectInstanceData; import com.robertx22.mine_and_slash.database.registry.ExileDB; import com.robertx22.mine_and_slash.uncommon.datasaving.Load; import net.minecraft.server.level.ServerPlayer; @@ -31,10 +32,17 @@ private EffectUtils() {} * @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 void applyState(ServerPlayer sp, EffectCtx ctx, int durationTicks, int stacks) { + public static ExileEffectInstanceData applyState(ServerPlayer sp, EffectCtx ctx, int durationTicks, int stacks) { final ExileEffect effect = resolveEffect(ctx); - if (effect == null) return; + if (effect == null) return null; + + return applyEffect(sp, effect, durationTicks, stacks); + } + + public static ExileEffectInstanceData applyEffect(ServerPlayer sp, ExileEffect effect, int durationTicks, int stacks) { + if (effect == null) return null; var unit = Load.Unit(sp); var store = unit.getStatusEffectsData(); @@ -49,6 +57,7 @@ public static void applyState(ServerPlayer sp, EffectCtx ctx, int durationTicks, // Keep vanilla stats / one-of-a-kind cleanup in sync effect.onApply(sp); unit.sync.setDirty(); // network/state sync + return inst; } /** Try both resourcePath (preferred) and id; some data uses either. */ 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 2931d8d5a..d541a295f 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 @@ -71,9 +71,6 @@ public SpendThresholdSpec(ResourceType resource, public boolean showUi() { return showUi; } // fluent config (for code-defined specs) - public SpendThresholdSpec withCooldownSeconds(int seconds) { - return withCooldownTicks(secondsToTicks(seconds)); - } public SpendThresholdSpec withCooldownTicks(int ticks) { return newWrapper(this.lockWhileEffectIds, Math.max(0, ticks), this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc); } @@ -143,9 +140,4 @@ public void startCooldown(EntityData unit, long now) { /** Called when one or more thresholds are consumed. */ public abstract void onProc(ServerPlayer sp, int procs); - // time helpers - public static int secondsToTicks(int seconds) { - return (seconds <= 0) ? 0 : seconds * 20; - } - public static float ticksToSeconds(int ticks) { return ticks / 20f; } } 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 79746ff32..7af2dc981 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 @@ -5,6 +5,9 @@ import com.robertx22.mine_and_slash.mechanics.thresholds.SpendThresholdSpec; import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; import net.minecraft.server.level.ServerPlayer; +import com.robertx22.mine_and_slash.capability.entity.EntityData; +import com.robertx22.mine_and_slash.database.registry.ExileDB; +import com.robertx22.mine_and_slash.event_hooks.my_events.EffectUtils; import java.util.*; @@ -32,8 +35,8 @@ public static class Locks { } public Locks locks = new Locks(); - @SerializedName("cooldown_seconds") - public int cooldownSeconds = 0; + @SerializedName("cooldown_ticks") + public int cooldownTicks = 0; @SerializedName("require_stat") public String requireStatId = ""; @@ -41,7 +44,7 @@ public static class Locks { public static class ProcAction { public String action; // "apply_effect" @SerializedName("exile_potion_id") public String effectId; - @SerializedName("duration_seconds") public int durationSeconds = 0; + @SerializedName("duration_ticks") public int durationTicks = 0; public int stacks = 1; @SerializedName("on_expire") public java.util.Map onExpire = java.util.Collections.emptyMap(); } @@ -76,7 +79,7 @@ public SpendThresholdSpec toSpec() { mult, percentOf, lockEff, - SpendThresholdSpec.secondsToTicks(cooldownSeconds), + cooldownTicks, locks != null && locks.lockWhileCooldown, locks != null && locks.dropProgressWhileLocked, locks != null && locks.resetProgressOnProc, @@ -91,42 +94,34 @@ public void onProc(ServerPlayer sp, int procs) { for (ProcAction a : onProc) { if (!"exile_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue; - var effect = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(a.effectId); + var effect = ExileDB.ExileEffects().get(a.effectId); if (effect == null) continue; - var inst = store.getOrCreate(effect); + int durTicks = Math.max(1, a.durationTicks); int stacks = Math.max(1, a.stacks); - if (effect.max_stacks > 0) stacks = Math.min(stacks, effect.max_stacks); - inst.stacks = Math.max(inst.stacks, stacks); - int durTicks = SpendThresholdSpec.secondsToTicks(a.durationSeconds); - inst.ticks_left = Math.max(inst.ticks_left, Math.max(1, durTicks)); + var inst = EffectUtils.applyEffect(sp, effect, durTicks, stacks); - inst.self_cast = true; - inst.caster_uuid = sp.getUUID().toString(); - - // Attach on-expire duration overrides (convert seconds -> ticks) - if (a.onExpire != null && !a.onExpire.isEmpty()) { + // Attach on-expire duration overrides (ticks directly) + /*if (a.onExpire != null && !a.onExpire.isEmpty()) { if (inst.onExpireEffectDurationTicks == null) { inst.onExpireEffectDurationTicks = new java.util.HashMap<>(); } for (var e : a.onExpire.entrySet()) { - int ticks = SpendThresholdSpec.secondsToTicks(Math.max(0, e.getValue())); + int ticks = Math.max(0, e.getValue()); if (ticks > 0) { inst.onExpireEffectDurationTicks.put(e.getKey(), ticks); } } - } + }*/ // TODO: Add back in when onExpire is implemented - effect.onApply(sp); - unit.sync.setDirty(); } } @Override - public boolean isLockedFor(com.robertx22.mine_and_slash.capability.entity.EntityData unit) { + public boolean isLockedFor(EntityData unit) { if (super.isEffectLocked(unit)) return true; if (requireStatId != null && !requireStatId.isEmpty()) { - var st = com.robertx22.mine_and_slash.database.registry.ExileDB.Stats().get(requireStatId); + var st = ExileDB.Stats().get(requireStatId); if (st != null) { return unit.getUnit().getCalculatedStat(st).getValue() <= 0; } From 0ee8eb6912c9828dedc234a6508e23c2e4abee3c Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:21:28 -0400 Subject: [PATCH 07/20] Suggested Fixes: SpendThresholdManager.java --- .../thresholds/SpendThresholdManager.java | 91 +++---------------- 1 file changed, 13 insertions(+), 78 deletions(-) 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 fde101c48..39cc5beb2 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 @@ -10,72 +10,7 @@ public final class SpendThresholdManager { private SpendThresholdManager() {} public static void registerDefaults() { - - // 30 × LVL Energy, 10s cooldown, locked while effects are active - SpendThresholdRegistry.registerGlobal( - new DataDrivenSpendThresholdSpec( - /* key */ "ENERGY_XLVL_WRATH", - /* resource */ ResourceType.energy, - /* mode */ DataDrivenSpendThresholdSpec.ThresholdMode.X_PER_LEVEL, - /* value */ 30f, // "X" in X per level - /* multiplyByLvl */ true, - /* pctMaxOf */ null, // not used for X_PER_LEVEL - /* lock effects */ java.util.Set.of("wrath_of_the_juggernaut", "Burnout"), - /* cooldown ticks */ SpendThresholdSpec.secondsToTicks(10), - /* lockIfCooldown */ true, - /* dropWhileLock */ true, - /* resetOnProc */ true - ) { - @Override public void onProc(ServerPlayer sp, int procs) { - // TODO: apply Wrath here - } - } - .withPriority(0) - ); - - // 30 × LVL Mana (example) - SpendThresholdRegistry.registerGlobal( - new DataDrivenSpendThresholdSpec( - "MANA_XLVL", - ResourceType.mana, - DataDrivenSpendThresholdSpec.ThresholdMode.X_PER_LEVEL, - 30f, - true, // multiply by level - null, - java.util.Set.of(), // no lock effects - SpendThresholdSpec.secondsToTicks(10), - true, // lock while cooldown - true, // drop progress while locked - true // reset on proc - ) { - @Override public void onProc(ServerPlayer sp, int procs) { - // TODO: apply your mana proc effect here - } - } - .withPriority(0) - ); - - // Example: Took 20% of Max Health (percent-of-max mode) - SpendThresholdRegistry.registerGlobal( - new DataDrivenSpendThresholdSpec( - "TOOK_20PCT_HEALTH", - ResourceType.health, - DataDrivenSpendThresholdSpec.ThresholdMode.PCT_OF_MAX, - 20f, // 20% - false, // multiplyByLevel (usually false for %) - ResourceType.health, // pct-of which pool - java.util.Set.of(), // no lock effects by default - 0, // no cooldown by default (tune as you like) - false, // don't treat cooldown as lock (since 0) - true, // drop progress if ever locked - true // reset on proc - ) { - @Override public void onProc(ServerPlayer sp, int procs) { - // TODO: apply your “took X% max life” effect here - } - } - .withPriority(0) - ); + // No-op: thresholds are defined via datapack JSON. } // ===== DEBUGGING ===== @@ -109,24 +44,20 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) { - long rem = unit.getSpendRuntime().cooldownRemainingTicks(key, now); // ensure this exists - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal( - "[SPEND:" + spec.key() + "] locked by cooldown (" + rem + "t ~ " + fmtSec(rem) + "s)" - )); + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked by cooldown")); } continue; } - // Effect lock - if (spec.isEffectLocked(unit)) { + // 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 by active effect(s)" - )); + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked")); } + // UI updates omitted (packet not included in this commit) continue; } @@ -135,6 +66,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (threshold <= 0f) continue; int procs = tracker.addAndConsumeForKey(key, type, loss, threshold); + // activity tracking omitted for compatibility if (procs > 0) { spec.onProc(sp, procs); spec.startCooldown(unit, now); @@ -142,9 +74,13 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); - } else if (debug) { + // UI/active tracking omitted + } else { float cur = tracker.getKeyProgress(key, type); - dbg(sp, "[SPEND:" + spec.key() + "] +" + fmt(loss) + " " + type.id + " (cur=" + fmt(cur) + " / " + fmt(threshold) + ")"); + if (debug) { + dbg(sp, "[SPEND:" + spec.key() + "] +" + fmt(loss) + " " + type.id + " (cur=" + fmt(cur) + " / " + fmt(threshold) + ")"); + } + // UI/active tracking omitted } } } @@ -155,5 +91,4 @@ private static void dbg(ServerPlayer sp, String msg) { sp.sendSystemMessage(net.minecraft.network.chat.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); } } From 26310f1247664a34f926e74b4f4c310ce20b59d7 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:02:45 -0400 Subject: [PATCH 08/20] Suggested Fixes: SpendThresholdSpec.java --- .../thresholds/SpendThresholdSpec.java | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) 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 d541a295f..9da88c64e 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 @@ -7,7 +7,6 @@ import net.minecraft.server.level.ServerPlayer; import java.util.Collections; -import java.util.HashSet; import java.util.Set; public abstract class SpendThresholdSpec { @@ -70,37 +69,12 @@ public SpendThresholdSpec(ResourceType resource, public int priority() { return priority; } public boolean showUi() { return showUi; } - // fluent config (for code-defined specs) - public SpendThresholdSpec withCooldownTicks(int ticks) { - return newWrapper(this.lockWhileEffectIds, Math.max(0, ticks), this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc); - } - public SpendThresholdSpec lockWhile(String... effectIds) { - Set s = new HashSet<>(this.lockWhileEffectIds); - if (effectIds != null) Collections.addAll(s, effectIds); - return newWrapper(s, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc); - } - public SpendThresholdSpec lockWhileCooldown(boolean on) { - return newWrapper(this.lockWhileEffectIds, this.cooldownTicks, on, this.dropProgressWhileLocked, this.resetProgressOnProc); - } - public SpendThresholdSpec dropProgressLocked(boolean on) { - return newWrapper(this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, on, this.resetProgressOnProc); - } - public SpendThresholdSpec resetOnProc(boolean on) { - return newWrapper(this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, on); - } + public SpendThresholdSpec withPriority(int p) { this.priority = p; return this; } - private SpendThresholdSpec newWrapper(Set lockIds, int cooldown, boolean lockCD, boolean dropLocked, boolean resetOnProc) { - // create a shallow “copy” retaining dynamic behavior (onProc/thresholdFor come from subclass) - return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, lockIds, cooldown, lockCD, dropLocked, resetOnProc, this.showUi) { - @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 SpendThresholdSpec withShowUi(boolean on) { return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc, on) { From 5976286b9e9a70d427adb51382058f5c99d1163a Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:10:14 -0400 Subject: [PATCH 09/20] Suggested Fixes: SpendThresholdSpec.java --- .../mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java | 1 + 1 file changed, 1 insertion(+) 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 9da88c64e..6653eadcc 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 @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.Set; + public abstract class SpendThresholdSpec { private final ResourceType resource; private final float perLevelFactor; // used by default thresholdFor() From 46bc42a0710d7bc1d31eb18698504385e0201fac Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:41:25 -0400 Subject: [PATCH 10/20] Threshold Decay: implement decay in manager/runtime and tick processing --- .../capability/entity/ResourceTracker.java | 28 ++++++- .../event_hooks/ontick/OnServerTick.java | 38 +++++++++ .../thresholds/SpendThresholdManager.java | 34 ++++++-- .../thresholds/SpendThresholdRuntime.java | 79 +++++++++++++++++-- 4 files changed, 168 insertions(+), 11 deletions(-) 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 5dc7d2a1b..b80800811 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 @@ -85,7 +85,7 @@ private float total(java.util.Set types) { private final java.util.EnumMap> keyProgress = new java.util.EnumMap<>(ResourceType.class); - public void clearKey(ResourceType rt, String key) { + public void clearKey(ResourceType rt, String key) { if (key == null || key.isEmpty()) return; var byKey = keyProgress.get(rt); if (byKey == null) return; @@ -119,6 +119,32 @@ public float getKeyProgress(String key, ResourceType rt) { return byKey == null ? 0f : byKey.getOrDefault(key, 0f); } + /** + * Set the exact cursor value for a specific key/resource. + * If value <= EPS the key entry is removed. + */ + 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); + } + + /** + * Decrease the cursor by a fixed amount, clamped at zero. Returns the new value. + */ + 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 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); 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 e65b65747..a0de8f541 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,6 +129,43 @@ 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 : com.robertx22.mine_and_slash.saveclasses.unit.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 + 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 newVal = unit.getResourceTracker().decayKeyProgress(key, rt, decayPerSecond); + int cint = (int) newVal; + if (unit.getSpendRuntime().progressIntChanged(key, cint)) { + com.robertx22.library_of_exile.main.Packets.sendToClient(player, + new ThresholdUiPacket(key, rt.id, newVal > 0f, newVal)); + } + if (newVal <= 0f) { + unit.getSpendRuntime().removeActive(rt, key); + } + } + } + } + } + if (player.containerMenu instanceof CraftingStationMenu men) { if (player.tickCount % 5 == 0) { men.be.onTickWhenPlayerWatching(player); 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 39cc5beb2..2888c3032 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,6 +2,7 @@ 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.mine_and_slash.saveclasses.unit.ResourceType; import net.minecraft.server.level.ServerPlayer; @@ -44,7 +45,10 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) { - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked by cooldown")); + 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)" + )); } continue; } @@ -57,7 +61,10 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (debug) { sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked")); } - // UI updates omitted (packet not included in this commit) + // hide UI while locked + if (spec.showUi()) { + com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); + } continue; } @@ -66,7 +73,10 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (threshold <= 0f) continue; int procs = tracker.addAndConsumeForKey(key, type, loss, threshold); - // activity tracking omitted for compatibility + if (loss > 0f && procs == 0) { + unit.getSpendRuntime().markActivity(key, now); + unit.getSpendRuntime().markActive(type, key, spec); + } if (procs > 0) { spec.onProc(sp, procs); spec.startCooldown(unit, now); @@ -74,13 +84,26 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); - // UI/active tracking omitted + // hide UI on proc + if (spec.showUi()) { + com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); + } + 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) + ")"); } - // UI/active tracking omitted + if (spec.showUi()) { + int cint = (int) cur; + if (unit.getSpendRuntime().progressIntChanged(key, cint)) { + boolean show = cur > 0f; + com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, cur)); + } + } + if (cur <= 0f) { + unit.getSpendRuntime().removeActive(type, key); + } } } } @@ -91,4 +114,5 @@ private static void dbg(ServerPlayer sp, String msg) { sp.sendSystemMessage(net.minecraft.network.chat.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 4273a4a1e..d8d6916a9 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 @@ -1,19 +1,35 @@ package com.robertx22.mine_and_slash.mechanics.thresholds; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; + import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; public class SpendThresholdRuntime { // gameTime (ticks) when each key’s cooldown ends private final Map cooldownUntil = new HashMap<>(); - /** Start/refresh cooldown for a key. */ + // last activity tick for each threshold key (progress added) + private final Map lastActivityTick = new HashMap<>(); + + // last decay tick applied for each key (so we decay at most once per second) + private final Map lastDecayTick = new HashMap<>(); + + // last integer progress sent to client (to throttle network updates) + private final Map lastProgressIntSent = new HashMap<>(); + + // Index of active threshold keys by resource (only keys with progress > 0 or recently updated) + private final java.util.EnumMap> activeByResource = new java.util.EnumMap<>(ResourceType.class); + // Quick lookup of spec by key (used for decay threshold value) + private final Map specByKey = new HashMap<>(); + public void startCooldown(String key, long now, int cooldownTicks) { if (cooldownTicks <= 0) return; cooldownUntil.put(key, now + cooldownTicks); } - /** True if now is still before the stored end time. */ public boolean isCoolingDown(String key, long now) { Long until = cooldownUntil.get(key); return until != null && now < until; @@ -27,8 +43,61 @@ public int cooldownRemainingTicks(String key, long now) { return (int) Math.max(0, rem); } - /** Clear a key’s cooldown (optional utility). */ - public void clearCooldown(String key) { - cooldownUntil.remove(key); + // === Activity/Decay tracking === + public void markActivity(String key, long now) { + if (key == null || key.isEmpty()) return; + lastActivityTick.put(key, now); + lastDecayTick.remove(key); + } + + public long getLastActivity(String key) { + return lastActivityTick.getOrDefault(key, 0L); + } + + public long getLastDecay(String key) { + return lastDecayTick.getOrDefault(key, 0L); + } + + public void markDecay(String key, long now) { + if (key == null || key.isEmpty()) return; + lastDecayTick.put(key, now); + } + + public boolean progressIntChanged(String key, int intProgress) { + Integer prev = lastProgressIntSent.get(key); + if (prev == null || prev.intValue() != intProgress) { + lastProgressIntSent.put(key, intProgress); + return true; + } + return false; + } + + // === Active key index === + public void markActive(ResourceType rt, String key, SpendThresholdSpec spec) { + if (rt == null || key == null || key.isEmpty() || spec == null) return; + activeByResource.computeIfAbsent(rt, __ -> new HashSet<>()).add(key); + specByKey.put(key, spec); + } + + public void removeActive(ResourceType rt, String key) { + if (rt == null || key == null || key.isEmpty()) return; + var set = activeByResource.get(rt); + if (set != null) { + set.remove(key); + if (set.isEmpty()) activeByResource.remove(rt); + } + specByKey.remove(key); + lastActivityTick.remove(key); + lastDecayTick.remove(key); + lastProgressIntSent.remove(key); + } + + public Set getActiveKeys(ResourceType rt) { + var s = activeByResource.get(rt); + return s == null ? java.util.Set.of() : java.util.Set.copyOf(s); + } + + public SpendThresholdSpec getSpec(String key) { + return specByKey.get(key); } } From daa6953a41809301181bc8f68e22555811caefc6 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:13:57 -0400 Subject: [PATCH 11/20] onExpire + Ui Implementation --- .../capability/entity/ResourceTracker.java | 28 +++++- .../data/exile_effects/ExileEffect.java | 56 ++++++++++-- .../ExileEffectInstanceData.java | 4 + .../data/spells/components/ComponentPart.java | 46 ++++++++++ .../components/actions/ExileEffectAction.java | 11 ++- .../data/spells/spell_classes/SpellCtx.java | 12 ++- .../event_hooks/ontick/OnServerTick.java | 38 +++++++++ .../gui/overlays/EffectsOverlay.java | 37 +++++++- .../DataDrivenSpendThresholdSpec.java | 59 +++++-------- .../thresholds/SpendThresholdManager.java | 34 ++++++-- .../thresholds/SpendThresholdRuntime.java | 79 +++++++++++++++-- .../datapack/SpendThresholdDef.java | 23 ++--- .../thresholds/ui/ThresholdUiClient.java | 42 +++++++++ .../mine_and_slash/mmorpg/DebugHud.java | 35 ++++++++ .../registers/client/S2CPacketRegister.java | 1 + .../registers/server/CommandRegister.java | 2 + .../effectdatas/ExilePotionEvent.java | 4 + .../new_commands/DebugCommands.java | 85 +++++++++++++++++++ .../vanilla_mc/packets/ThresholdUiPacket.java | 61 +++++++++++++ .../EntityStatusEffectsData.java | 71 ++++++++++++---- .../spend_thresholds/energy_xlvl_wotj.json | 8 +- 21 files changed, 647 insertions(+), 89 deletions(-) create mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ui/ThresholdUiClient.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/mmorpg/DebugHud.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/vanilla_mc/new_commands/DebugCommands.java create mode 100644 src/main/java/com/robertx22/mine_and_slash/vanilla_mc/packets/ThresholdUiPacket.java 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 5dc7d2a1b..b80800811 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 @@ -85,7 +85,7 @@ private float total(java.util.Set types) { private final java.util.EnumMap> keyProgress = new java.util.EnumMap<>(ResourceType.class); - public void clearKey(ResourceType rt, String key) { + public void clearKey(ResourceType rt, String key) { if (key == null || key.isEmpty()) return; var byKey = keyProgress.get(rt); if (byKey == null) return; @@ -119,6 +119,32 @@ public float getKeyProgress(String key, ResourceType rt) { return byKey == null ? 0f : byKey.getOrDefault(key, 0f); } + /** + * Set the exact cursor value for a specific key/resource. + * If value <= EPS the key entry is removed. + */ + 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); + } + + /** + * Decrease the cursor by a fixed amount, clamped at zero. Returns the new value. + */ + 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 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); 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..15e9e2ff4 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 @@ -27,6 +27,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 +253,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 (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE) { + if (target instanceof ServerPlayer sp) { + com.robertx22.mine_and_slash.mmorpg.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) { + com.robertx22.mine_and_slash.mmorpg.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); + // Attach expiring effect id and any per-effect duration overrides carried on the instance + 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 (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && target instanceof ServerPlayer sp2) { + com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp2, "expire_dispatched_" + GUID(), "[EFFECT][EXPIRE] Dispatched attached spell for " + GUID(), 400); + } + + // Apply any leftover datapack-declared on-expire effects that weren't applied by actions (server only) + if (!target.level().isClientSide && data.onExpireEffectDurationTicks != null && !data.onExpireEffectDurationTicks.isEmpty()) { + 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 = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(effId); + if (extraEff != null) { + var unitT = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(target); + var storeT = unitT.getStatusEffectsData(); + var instT = storeT.getOrCreate(extraEff); + instT.stacks = Math.max(instT.stacks, 1); + instT.ticks_left = Math.max(instT.ticks_left, durTicks); + instT.is_infinite = false; + instT.caster_uuid = caster.getStringUUID(); + try { extraEff.onApply(target); } catch (Exception ignored) {} + unitT.equipmentCache.STATUS.setDirty(); + unitT.sync.setDirty(); + if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && target instanceof ServerPlayer spx) { + com.robertx22.mine_and_slash.mmorpg.DebugHud.send(spx, "expire_extra_" + effId, "[EFFECT][EXPIRE] Extra-applied " + effId + " tl=" + instT.ticks_left, 400); + } + } + } + } } } - 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..d7fa15e83 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 @@ -6,7 +6,9 @@ import com.robertx22.mine_and_slash.database.data.spells.components.selectors.TargetSelector; 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 +133,50 @@ 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 == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE) { + if (ctx.caster instanceof ServerPlayer spc) { + com.robertx22.mine_and_slash.mmorpg.DebugHud.send(spc, "expire_action_" + part.type, "[EFFECT][EXPIRE] Action=" + part.type + " targets=" + list.size(), 400); + } + } + + // Fallback: if ON_EXPIRE exile_effects still don't appear, apply directly here + if (!ctx.world.isClientSide && ctx.activation == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE + && part.type.equals(SpellAction.EXILE_EFFECT.GUID())) { + try { + String effId = part.get(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.EXILE_POTION_ID); + Double durD = part.getOrDefault(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.POTION_DURATION, 0D); + Double cntD = part.getOrDefault(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.COUNT, 1D); + int duration = Math.max(1, durD.intValue()); + // If override present from datapack, prefer it + 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 = com.robertx22.mine_and_slash.database.registry.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) { + com.robertx22.mine_and_slash.mmorpg.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..c9485d77e 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,10 @@ public void tryActivate(Collection targets, SpellCtx ctx, MapHolde targets.forEach(t -> { if (RandomUtils.roll(chance)) { + // If ON_EXPIRE, ComponentPart handles direct application to avoid same-tick races + if (ctx.activation == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE) { + return; // no-op here; handled upstream + } 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 +72,12 @@ public void tryActivate(Collection targets, SpellCtx ctx, MapHolde potionEvent.spellid = ctx.calculatedSpellData.getSpell() .GUID(); - potionEvent.Activate(); + // Normal path for non-ON_EXPIRE contexts + 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/ontick/OnServerTick.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/ontick/OnServerTick.java index e65b65747..a0de8f541 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,6 +129,43 @@ 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 : com.robertx22.mine_and_slash.saveclasses.unit.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 + 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 newVal = unit.getResourceTracker().decayKeyProgress(key, rt, decayPerSecond); + int cint = (int) newVal; + if (unit.getSpendRuntime().progressIntChanged(key, cint)) { + com.robertx22.library_of_exile.main.Packets.sendToClient(player, + new ThresholdUiPacket(key, rt.id, newVal > 0f, newVal)); + } + if (newVal <= 0f) { + unit.getSpendRuntime().removeActive(rt, key); + } + } + } + } + } + if (player.containerMenu instanceof CraftingStationMenu men) { if (player.tickCount % 5 == 0) { men.be.onTickWhenPlayerWatching(player); 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..3042e4332 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,40 @@ public static void render(GuiGraphics gui, boolean horizontal) { } } + // Render threshold UI items inline with effects (uses same bg/overlay) + 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; + + // draw frame same as effects + 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); + + // show 0..threshold progress, not current resource value + 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 ca498f77e..d35e248a9 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 @@ -9,13 +9,12 @@ public class DataDrivenSpendThresholdSpec extends SpendThresholdSpec { - public enum ThresholdMode { FLAT, PERCENT_OF_MAX } + public enum ThresholdMode { X_PER_LEVEL, FLAT, PCT_OF_MAX } private final ThresholdMode mode; private final float value; private final boolean multiplyByLevel; - @Nullable private final ResourceType percentMaxOf; - private final boolean showUi; + @Nullable private final ResourceType pctMaxOf; public DataDrivenSpendThresholdSpec( String key, @@ -23,54 +22,40 @@ public DataDrivenSpendThresholdSpec( ThresholdMode mode, float value, boolean multiplyByLevel, - @Nullable ResourceType percentMaxOf, + @Nullable ResourceType pctMaxOf, Set lockWhileEffectIds, int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc, - boolean showUi + boolean resetProgressOnProc ) { - super(resource, 0f, key, - lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, showUi); + super(resource, /*perLevelFactor (unused)*/ 0f, key, + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc); this.mode = mode; this.value = value; this.multiplyByLevel = multiplyByLevel; - this.percentMaxOf = percentMaxOf; - this.showUi = showUi; - } - - // 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 resetProgressOnProc - ) { - this(key, resource, mode, value, multiplyByLevel, percentMaxOf, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false); + this.pctMaxOf = pctMaxOf; } @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); + case X_PER_LEVEL: + base = value * Math.max(1, unit.getLevel()); + break; + case FLAT: + base = value * (multiplyByLevel ? Math.max(1, unit.getLevel()) : 1f); + break; + case PCT_OF_MAX: + ResourceType rt = (pctMaxOf != null) ? pctMaxOf : resource(); + float max = unit.getResources().getMax(unit.getEntity(), rt); base = (value / 100f) * max; - } - case FLAT -> base = value; - default -> base = value; + if (multiplyByLevel) base *= Math.max(1, unit.getLevel()); + break; + default: + base = 0f; } - if (multiplyByLevel) base *= Math.max(1, unit.getLevel()); return Math.max(0f, base); } @@ -78,8 +63,4 @@ public float thresholdFor(EntityData unit) { public void onProc(ServerPlayer sp, int procs) { // No default action here; datapack loader wires actions. } - - public boolean showUi() { return showUi; } - - // Perk lock is handled by callers (anonymous subclass) when needed. } 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 39cc5beb2..2888c3032 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,6 +2,7 @@ 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.mine_and_slash.saveclasses.unit.ResourceType; import net.minecraft.server.level.ServerPlayer; @@ -44,7 +45,10 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) { - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked by cooldown")); + 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)" + )); } continue; } @@ -57,7 +61,10 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (debug) { sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked")); } - // UI updates omitted (packet not included in this commit) + // hide UI while locked + if (spec.showUi()) { + com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); + } continue; } @@ -66,7 +73,10 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (threshold <= 0f) continue; int procs = tracker.addAndConsumeForKey(key, type, loss, threshold); - // activity tracking omitted for compatibility + if (loss > 0f && procs == 0) { + unit.getSpendRuntime().markActivity(key, now); + unit.getSpendRuntime().markActive(type, key, spec); + } if (procs > 0) { spec.onProc(sp, procs); spec.startCooldown(unit, now); @@ -74,13 +84,26 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); - // UI/active tracking omitted + // hide UI on proc + if (spec.showUi()) { + com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); + } + 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) + ")"); } - // UI/active tracking omitted + if (spec.showUi()) { + int cint = (int) cur; + if (unit.getSpendRuntime().progressIntChanged(key, cint)) { + boolean show = cur > 0f; + com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, cur)); + } + } + if (cur <= 0f) { + unit.getSpendRuntime().removeActive(type, key); + } } } } @@ -91,4 +114,5 @@ private static void dbg(ServerPlayer sp, String msg) { sp.sendSystemMessage(net.minecraft.network.chat.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 4273a4a1e..d8d6916a9 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 @@ -1,19 +1,35 @@ package com.robertx22.mine_and_slash.mechanics.thresholds; +import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; + import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; public class SpendThresholdRuntime { // gameTime (ticks) when each key’s cooldown ends private final Map cooldownUntil = new HashMap<>(); - /** Start/refresh cooldown for a key. */ + // last activity tick for each threshold key (progress added) + private final Map lastActivityTick = new HashMap<>(); + + // last decay tick applied for each key (so we decay at most once per second) + private final Map lastDecayTick = new HashMap<>(); + + // last integer progress sent to client (to throttle network updates) + private final Map lastProgressIntSent = new HashMap<>(); + + // Index of active threshold keys by resource (only keys with progress > 0 or recently updated) + private final java.util.EnumMap> activeByResource = new java.util.EnumMap<>(ResourceType.class); + // Quick lookup of spec by key (used for decay threshold value) + private final Map specByKey = new HashMap<>(); + public void startCooldown(String key, long now, int cooldownTicks) { if (cooldownTicks <= 0) return; cooldownUntil.put(key, now + cooldownTicks); } - /** True if now is still before the stored end time. */ public boolean isCoolingDown(String key, long now) { Long until = cooldownUntil.get(key); return until != null && now < until; @@ -27,8 +43,61 @@ public int cooldownRemainingTicks(String key, long now) { return (int) Math.max(0, rem); } - /** Clear a key’s cooldown (optional utility). */ - public void clearCooldown(String key) { - cooldownUntil.remove(key); + // === Activity/Decay tracking === + public void markActivity(String key, long now) { + if (key == null || key.isEmpty()) return; + lastActivityTick.put(key, now); + lastDecayTick.remove(key); + } + + public long getLastActivity(String key) { + return lastActivityTick.getOrDefault(key, 0L); + } + + public long getLastDecay(String key) { + return lastDecayTick.getOrDefault(key, 0L); + } + + public void markDecay(String key, long now) { + if (key == null || key.isEmpty()) return; + lastDecayTick.put(key, now); + } + + public boolean progressIntChanged(String key, int intProgress) { + Integer prev = lastProgressIntSent.get(key); + if (prev == null || prev.intValue() != intProgress) { + lastProgressIntSent.put(key, intProgress); + return true; + } + return false; + } + + // === Active key index === + public void markActive(ResourceType rt, String key, SpendThresholdSpec spec) { + if (rt == null || key == null || key.isEmpty() || spec == null) return; + activeByResource.computeIfAbsent(rt, __ -> new HashSet<>()).add(key); + specByKey.put(key, spec); + } + + public void removeActive(ResourceType rt, String key) { + if (rt == null || key == null || key.isEmpty()) return; + var set = activeByResource.get(rt); + if (set != null) { + set.remove(key); + if (set.isEmpty()) activeByResource.remove(rt); + } + specByKey.remove(key); + lastActivityTick.remove(key); + lastDecayTick.remove(key); + lastProgressIntSent.remove(key); + } + + public Set getActiveKeys(ResourceType rt) { + var s = activeByResource.get(rt); + return s == null ? java.util.Set.of() : java.util.Set.copyOf(s); + } + + public SpendThresholdSpec getSpec(String key) { + return specByKey.get(key); } } 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 7af2dc981..1cd3a7ffb 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 @@ -57,13 +57,17 @@ public SpendThresholdSpec toSpec() { // 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.PCT_OF_MAX; + } else if ("X_PER_LEVEL".equals(rawMode)) { + mode = DataDrivenSpendThresholdSpec.ThresholdMode.X_PER_LEVEL; + } else { + mode = DataDrivenSpendThresholdSpec.ThresholdMode.FLAT; // default + treats legacy values as FLAT + } ResourceType percentOf = null; - if (mode == DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX + if (mode == DataDrivenSpendThresholdSpec.ThresholdMode.PCT_OF_MAX && threshold.percentOf != null && !threshold.percentOf.isEmpty()) { percentOf = parseResource(threshold.percentOf, res); // default to this spec’s resource if bad input } @@ -82,8 +86,7 @@ public SpendThresholdSpec toSpec() { cooldownTicks, locks != null && locks.lockWhileCooldown, locks != null && locks.dropProgressWhileLocked, - locks != null && locks.resetProgressOnProc, - showUi + locks != null && locks.resetProgressOnProc ) { @Override public void onProc(ServerPlayer sp, int procs) { @@ -102,7 +105,7 @@ public void onProc(ServerPlayer sp, int procs) { var inst = EffectUtils.applyEffect(sp, effect, durTicks, stacks); // Attach on-expire duration overrides (ticks directly) - /*if (a.onExpire != null && !a.onExpire.isEmpty()) { + if (a.onExpire != null && !a.onExpire.isEmpty()) { if (inst.onExpireEffectDurationTicks == null) { inst.onExpireEffectDurationTicks = new java.util.HashMap<>(); } @@ -112,7 +115,7 @@ public void onProc(ServerPlayer sp, int procs) { inst.onExpireEffectDurationTicks.put(e.getKey(), ticks); } } - }*/ // TODO: Add back in when onExpire is implemented + } } } @@ -128,7 +131,7 @@ public boolean isLockedFor(EntityData unit) { } return false; } - }.withPriority(priority); + }.withPriority(priority).withShowUi(showUi); } // --- helpers --- 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..11c24f0f7 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/ui/ThresholdUiClient.java @@ -0,0 +1,42 @@ +package com.robertx22.mine_and_slash.mechanics.thresholds.ui; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** Client-only holder for which threshold keys should currently render, their resource ids, and progress. */ +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..0fd3e7dbd --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mmorpg/DebugHud.java @@ -0,0 +1,35 @@ +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() {} + + // Unified flags + public static volatile boolean ON_EXPIRE = false; + + // Simple per-player throttling to avoid spammy chat when debugging + 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..c0f7c15b2 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/vanilla_mc/new_commands/DebugCommands.java @@ -0,0 +1,85 @@ +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) { + + // New unified debug toggle: + // /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); + + // Validate inputs (in addition to tab suggestions) + 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..262e80fb2 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 @@ -35,43 +35,80 @@ 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) { + // Fire onRemove immediately for this key + eff.onRemove(en); + // Decide deletion based on current state after onRemove + ExileEffectInstanceData inst = exileMap.get(entry.getKey()); + if (inst == null || inst.shouldRemove()) { + toDeleteKeys.add(entry.getKey()); + } else if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { + com.robertx22.mine_and_slash.mmorpg.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) { + // Fire onRemove immediately for this key + eff.onRemove(en); + ExileEffectInstanceData inst = exileMap.get(entry.getKey()); + if (inst == null || inst.shouldRemove()) { + toDeleteKeys.add(entry.getKey()); + } else if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { + com.robertx22.mine_and_slash.mmorpg.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); + + + // Now delete expired effects (evaluated after onRemove) + for (String key : toDeleteKeys) { + ExileEffectInstanceData current = exileMap.remove(key); + if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { + if (current != null) { + com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp, "expire_removed_" + key, "[EFFECT][EXPIRE] Removed " + key + " (ticks_left=" + current.ticks_left + ", stacks=" + current.stacks + ") [id=" + System.identityHashCode(current) + "]", 200); + } else { + com.robertx22.mine_and_slash.mmorpg.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 b58cea578..2ed7dbd4c 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 @@ -9,11 +9,11 @@ "value": 10, "multiply_by_level": true }, - "cooldown_seconds": 20, + "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, "reset_progress_on_proc": true }, @@ -21,10 +21,10 @@ { "action": "exile_effect", "exile_potion_id": "wrath_of_the_juggernaut", - "duration_seconds": 10, + "duration_ticks": 200, "stacks": 1, "on_expire": { - "burnout": 10 + "burnout": 200 } } ] From 66fedf4aef14438c4be8fa49079d808fead361ae Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:08:04 -0400 Subject: [PATCH 12/20] Fixed Certain onCast = consumeEffect Fixed effect consumption logic for certain effects like Overheat. --- .../database/data/spells/components/ComponentPart.java | 6 ++++++ .../data/spells/components/actions/ExileEffectAction.java | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) 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 d7fa15e83..e8a57ee2e 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 @@ -141,9 +141,15 @@ public void tryActivate(SpellCtx ctx) { } // Fallback: if ON_EXPIRE exile_effects still don't appear, apply directly here + // Only applies to GIVE_STACKS to avoid resurrecting effects when JSON intends REMOVE_STACKS if (!ctx.world.isClientSide && ctx.activation == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE && part.type.equals(SpellAction.EXILE_EFFECT.GUID())) { try { + String actionType = part.get(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.POTION_ACTION); + if (actionType == null || !actionType.equals(com.robertx22.mine_and_slash.database.data.spells.components.actions.ExileEffectAction.GiveOrTake.GIVE_STACKS.name())) { + // Skip fallback for non-GIVE actions (e.g., REMOVE_STACKS) + continue; + } String effId = part.get(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.EXILE_POTION_ID); Double durD = part.getOrDefault(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.POTION_DURATION, 0D); Double cntD = part.getOrDefault(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.COUNT, 1D); 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 c9485d77e..690151e5a 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,9 +59,11 @@ public void tryActivate(Collection targets, SpellCtx ctx, MapHolde targets.forEach(t -> { if (RandomUtils.roll(chance)) { - // If ON_EXPIRE, ComponentPart handles direct application to avoid same-tick races - if (ctx.activation == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE) { - return; // no-op here; handled upstream + // 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) From 88db75fc64cbe1685d464965245eae6808451169 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:25:08 -0400 Subject: [PATCH 13/20] Threshold-Decay Fixed + Cleanup --- .../event_hooks/my_events/OnResourceLost.java | 9 --------- .../event_hooks/ontick/OnServerTick.java | 7 ------- .../DataDrivenSpendThresholdSpec.java | 7 +------ .../mechanics/thresholds/SpendKeys.java | 13 ------------ .../thresholds/SpendThresholdContributor.java | 9 --------- .../thresholds/SpendThresholdManager.java | 16 --------------- .../thresholds/SpendThresholdSpec.java | 20 +++---------------- .../datapack/SpendThresholdDef.java | 14 ------------- .../spend_thresholds/energy_xlvl_wotj.json | 10 +++------- 9 files changed, 7 insertions(+), 98 deletions(-) delete mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendKeys.java delete mode 100644 src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java diff --git a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java index 3e29a8206..27464f413 100644 --- a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java @@ -6,22 +6,13 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; -/** - * Unified entrypoint for resource LOSS (spend, drains, damage). - * Health damage integration calls this via the LivingDamageEvent handler below. - * - * Debug printing is handled inside SpendThresholdManager and is toggled by - * OnResourceLost.DEBUG_ENABLED. - */ public final class OnResourceLost { private OnResourceLost() {} public enum LossSource { SpendOrDrain, Damage, Other } - /** Toggle SpendThresholdManager debug logs per player. */ public static boolean DEBUG_ENABLED = false; - /** Call this whenever a resource actually goes down. */ public static void trigger(LivingEntity entity, ResourceType type, float loss, LossSource source) { if (loss <= 0f) return; if (!(entity instanceof ServerPlayer sp)) return; 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 a0de8f541..80dbdd747 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,7 +18,6 @@ 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; @@ -138,7 +137,6 @@ public static void onEndTick(ServerPlayer player) { for (var rt : com.robertx22.mine_and_slash.saveclasses.unit.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; @@ -153,11 +151,6 @@ public static void onEndTick(ServerPlayer player) { if (thr <= 0f) continue; float decayPerSecond = thr * 0.15f; float newVal = unit.getResourceTracker().decayKeyProgress(key, rt, decayPerSecond); - int cint = (int) newVal; - if (unit.getSpendRuntime().progressIntChanged(key, cint)) { - com.robertx22.library_of_exile.main.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/mechanics/thresholds/DataDrivenSpendThresholdSpec.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java index ca498f77e..11f92fe31 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 @@ -15,7 +15,6 @@ public enum ThresholdMode { FLAT, PERCENT_OF_MAX } private final float value; private final boolean multiplyByLevel; @Nullable private final ResourceType percentMaxOf; - private final boolean showUi; public DataDrivenSpendThresholdSpec( String key, @@ -32,12 +31,11 @@ public DataDrivenSpendThresholdSpec( boolean showUi ) { super(resource, 0f, key, - lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, showUi); + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc); this.mode = mode; this.value = value; this.multiplyByLevel = multiplyByLevel; this.percentMaxOf = percentMaxOf; - this.showUi = showUi; } // Backward-compatible ctor (defaults showUi=false) @@ -79,7 +77,4 @@ public void onProc(ServerPlayer sp, int procs) { // No default action here; datapack loader wires actions. } - public boolean showUi() { return showUi; } - - // Perk lock is handled by callers (anonymous subclass) when needed. } diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendKeys.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendKeys.java deleted file mode 100644 index ba7bbe834..000000000 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendKeys.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.robertx22.mine_and_slash.mechanics.thresholds; - -import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; -import com.robertx22.mine_and_slash.uncommon.datasaving.Load; -import net.minecraft.server.level.ServerPlayer; - -public final class SpendKeys { - private SpendKeys() {} - public static String key(String nodeId, ResourceType rt) { return "spend." + rt.id + "." + nodeId; } - public static float threshold(ServerPlayer sp, float perLevelFactor) { - return perLevelFactor * Load.Unit(sp).getLevel(); - } -} diff --git a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java deleted file mode 100644 index 9180a6299..000000000 --- a/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdContributor.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.robertx22.mine_and_slash.mechanics.thresholds; - -import com.robertx22.mine_and_slash.capability.entity.EntityData; -import java.util.List; - -public interface SpendThresholdContributor { - /** Return zero or more specs active for this unit (e.g., from allocated talents). */ - List getSpendThresholds(EntityData unit); -} 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 2888c3032..541cd5a63 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,7 +2,6 @@ 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.mine_and_slash.saveclasses.unit.ResourceType; import net.minecraft.server.level.ServerPlayer; @@ -61,10 +60,6 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (debug) { sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked")); } - // hide UI while locked - if (spec.showUi()) { - com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); - } continue; } @@ -84,23 +79,12 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); - // hide UI on proc - if (spec.showUi()) { - com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); - } 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) + ")"); } - if (spec.showUi()) { - int cint = (int) cur; - if (unit.getSpendRuntime().progressIntChanged(key, cint)) { - boolean show = cur > 0f; - com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, cur)); - } - } if (cur <= 0f) { unit.getSpendRuntime().removeActive(type, 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 6653eadcc..9a3a26e47 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 @@ -21,7 +21,6 @@ public abstract class SpendThresholdSpec { private final boolean lockWhileCooldown; // treat cooldown as a lock private final boolean dropProgressWhileLocked; private final boolean resetProgressOnProc; - private final boolean showUi; // whether to render progress HUD for this spec // registry ordering (lower runs first) private int priority = 0; @@ -34,8 +33,8 @@ public SpendThresholdSpec(ResourceType resource, int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc, - boolean showUi) { + boolean resetProgressOnProc + ) { this.resource = resource; this.perLevelFactor = perLevelFactor; this.key = key; @@ -44,20 +43,8 @@ public SpendThresholdSpec(ResourceType resource, this.lockWhileCooldown = lockWhileCooldown; this.dropProgressWhileLocked = dropProgressWhileLocked; this.resetProgressOnProc = resetProgressOnProc; - this.showUi = showUi; } - // Backward-compatible ctor (defaults showUi=false) - public SpendThresholdSpec(ResourceType resource, - float perLevelFactor, - String key, - Set lockWhileEffectIds, - int cooldownTicks, - boolean lockWhileCooldown, - boolean dropProgressWhileLocked, - boolean resetProgressOnProc) { - this(resource, perLevelFactor, key, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false); - } // ===== accessors ===== public ResourceType resource() { return resource; } @@ -68,7 +55,6 @@ public SpendThresholdSpec(ResourceType resource, public boolean resetOnProc() { return resetProgressOnProc; } public int cooldownTicks() { return cooldownTicks; } public int priority() { return priority; } - public boolean showUi() { return showUi; } public SpendThresholdSpec withPriority(int p) { @@ -78,7 +64,7 @@ public SpendThresholdSpec withPriority(int p) { public SpendThresholdSpec withShowUi(boolean on) { - return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc, on) { + return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc) { @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); } 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 7af2dc981..cc91c5ccc 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 @@ -100,20 +100,6 @@ 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); - - // Attach on-expire duration overrides (ticks directly) - /*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); - } - } - }*/ // TODO: Add back in when onExpire is implemented - } } 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 b58cea578..621a996c0 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 @@ -2,14 +2,13 @@ "key": "ENERGY_XLVL_WOTJ", "resource": "energy", "enabled": true, - "show_ui": true, "priority": 0, "threshold": { "mode": "FLAT", "value": 10, "multiply_by_level": true }, - "cooldown_seconds": 20, + "cooldown_ticks": 400, "require_stat": "unlock_wotj", "locks": { "effects": ["wrath_of_the_juggernaut", "burnout"], @@ -21,11 +20,8 @@ { "action": "exile_effect", "exile_potion_id": "wrath_of_the_juggernaut", - "duration_seconds": 10, - "stacks": 1, - "on_expire": { - "burnout": 10 - } + "duration_ticks": 200, + "stacks": 1 } ] } \ No newline at end of file From 589ff6412f5000bdecda0dcb0d0930d8541a34ad Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:09:44 -0400 Subject: [PATCH 14/20] Cleanup + Tidy --- .../capability/entity/EntityLeechData.java | 31 ++------------ .../capability/entity/ResourceTracker.java | 29 ++----------- .../data/exile_effects/ExileEffect.java | 19 ++++----- .../data/spells/components/ComponentPart.java | 26 ++++++------ .../components/actions/ExileEffectAction.java | 1 - .../event_hooks/my_events/EffectUtils.java | 16 +------- .../event_hooks/my_events/OnResourceLost.java | 9 ---- .../my_events/OnResourceRestore.java | 41 ++++--------------- .../event_hooks/ontick/OnServerTick.java | 7 +--- .../gui/overlays/EffectsOverlay.java | 3 -- .../DataDrivenSpendThresholdSpec.java | 41 +++++++++---------- .../thresholds/SpendThresholdManager.java | 8 +--- .../thresholds/SpendThresholdRuntime.java | 8 +--- .../thresholds/SpendThresholdSpec.java | 24 ++--------- .../datapack/SpendThresholdDef.java | 16 +++----- .../thresholds/ui/ThresholdUiClient.java | 1 - .../mine_and_slash/mmorpg/DebugHud.java | 2 - .../new_commands/DebugCommands.java | 2 - .../EntityStatusEffectsData.java | 21 ++++------ .../spend_thresholds/25percent_health.json | 2 +- 20 files changed, 78 insertions(+), 229 deletions(-) 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 b80800811..544224596 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. @@ -10,21 +10,17 @@ public class ResourceTracker { private static final float EPS = 1e-4f; - // 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); @@ -38,16 +34,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; @@ -62,26 +53,23 @@ 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); @@ -95,7 +83,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; @@ -113,16 +100,11 @@ 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 ? 0f : byKey.getOrDefault(key, 0f); } - /** - * Set the exact cursor value for a specific key/resource. - * If value <= EPS the key entry is removed. - */ public void setKeyProgress(String key, ResourceType rt, float value) { if (key == null || key.isEmpty()) return; var byKey = keyProgress.computeIfAbsent(rt, __ -> new java.util.HashMap<>()); @@ -131,9 +113,6 @@ public void setKeyProgress(String key, ResourceType rt, float value) { if (byKey.isEmpty()) keyProgress.remove(rt); } - /** - * Decrease the cursor by a fixed amount, clamped at zero. Returns the new value. - */ 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); @@ -145,12 +124,10 @@ public float decayKeyProgress(String key, ResourceType rt, float amount) { 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 15e9e2ff4..57f4d4306 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,7 @@ 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.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; @@ -258,27 +259,25 @@ public void onRemove(LivingEntity target) { caster = target; } // --- Debug: show expire intent - if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE) { + if (DebugHud.ON_EXPIRE) { if (target instanceof ServerPlayer sp) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp, "expire_onremove_" + GUID(), "[EFFECT][EXPIRE] onRemove(" + GUID() + ") stacks=" + data.stacks + ", ticks_left=" + data.ticks_left + ", infinite=" + data.is_infinite, 200); + 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) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(spc, "expire_onremove_" + GUID(), "[EFFECT][EXPIRE] Trigger from " + GUID() + " on target " + target.getName().getString(), 200); + 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); - // Attach expiring effect id and any per-effect duration overrides carried on the instance 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 (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && target instanceof ServerPlayer sp2) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp2, "expire_dispatched_" + GUID(), "[EFFECT][EXPIRE] Dispatched attached spell for " + GUID(), 400); + if (DebugHud.ON_EXPIRE && target instanceof ServerPlayer sp2) { + DebugHud.send(sp2, "expire_dispatched_" + GUID(), "[EFFECT][EXPIRE] Dispatched attached spell for " + GUID(), 400); } - // Apply any leftover datapack-declared on-expire effects that weren't applied by actions (server only) if (!target.level().isClientSide && data.onExpireEffectDurationTicks != null && !data.onExpireEffectDurationTicks.isEmpty()) { for (var entry : data.onExpireEffectDurationTicks.entrySet()) { String effId = entry.getKey(); @@ -286,7 +285,7 @@ public void onRemove(LivingEntity target) { if (ctx.onExpireApplied != null && ctx.onExpireApplied.contains(effId)) { continue; } - var extraEff = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(effId); + var extraEff = ExileDB.ExileEffects().get(effId); if (extraEff != null) { var unitT = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(target); var storeT = unitT.getStatusEffectsData(); @@ -298,8 +297,8 @@ public void onRemove(LivingEntity target) { try { extraEff.onApply(target); } catch (Exception ignored) {} unitT.equipmentCache.STATUS.setDirty(); unitT.sync.setDirty(); - if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && target instanceof ServerPlayer spx) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(spx, "expire_extra_" + effId, "[EFFECT][EXPIRE] Extra-applied " + effId + " tl=" + instT.ticks_left, 400); + if (DebugHud.ON_EXPIRE && target instanceof ServerPlayer spx) { + DebugHud.send(spx, "expire_extra_" + effId, "[EFFECT][EXPIRE] Extra-applied " + effId + " tl=" + instT.ticks_left, 400); } } } 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 e8a57ee2e..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,6 +4,8 @@ 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; @@ -134,27 +136,23 @@ public void tryActivate(SpellCtx ctx) { } else { action.tryActivate(list, ctx, part); if (DebugHud.ON_EXPIRE - && ctx.activation == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE) { + && ctx.activation == EntityActivation.ON_EXPIRE) { if (ctx.caster instanceof ServerPlayer spc) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(spc, "expire_action_" + part.type, "[EFFECT][EXPIRE] Action=" + part.type + " targets=" + list.size(), 400); + DebugHud.send(spc, "expire_action_" + part.type, "[EFFECT][EXPIRE] Action=" + part.type + " targets=" + list.size(), 400); } } - // Fallback: if ON_EXPIRE exile_effects still don't appear, apply directly here - // Only applies to GIVE_STACKS to avoid resurrecting effects when JSON intends REMOVE_STACKS - if (!ctx.world.isClientSide && ctx.activation == com.robertx22.mine_and_slash.database.data.spells.components.EntityActivation.ON_EXPIRE + if (!ctx.world.isClientSide && ctx.activation == EntityActivation.ON_EXPIRE && part.type.equals(SpellAction.EXILE_EFFECT.GUID())) { try { - String actionType = part.get(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.POTION_ACTION); - if (actionType == null || !actionType.equals(com.robertx22.mine_and_slash.database.data.spells.components.actions.ExileEffectAction.GiveOrTake.GIVE_STACKS.name())) { - // Skip fallback for non-GIVE actions (e.g., REMOVE_STACKS) + String actionType = part.get(MapField.POTION_ACTION); + if (actionType == null || !actionType.equals(ExileEffectAction.GiveOrTake.GIVE_STACKS.name())) { continue; } - String effId = part.get(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.EXILE_POTION_ID); - Double durD = part.getOrDefault(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.POTION_DURATION, 0D); - Double cntD = part.getOrDefault(com.robertx22.mine_and_slash.database.data.spells.map_fields.MapField.COUNT, 1D); + 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 override present from datapack, prefer it if (ctx.onExpireEffectDurationTicks != null && ctx.onExpireEffectDurationTicks.containsKey(effId)) { int override = ctx.onExpireEffectDurationTicks.get(effId); if (override > 0) { @@ -162,7 +160,7 @@ public void tryActivate(SpellCtx ctx) { } } int stacks = Math.max(1, cntD.intValue()); - var effect = com.robertx22.mine_and_slash.database.registry.ExileDB.ExileEffects().get(effId); + 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); @@ -176,7 +174,7 @@ public void tryActivate(SpellCtx ctx) { unit.equipmentCache.STATUS.setDirty(); unit.sync.setDirty(); if (ctx.caster instanceof ServerPlayer spc) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(spc, "expire_fallback_direct_" + effId, "[EFFECT][EXPIRE] Direct-applied " + effId + " tl=" + inst.ticks_left + " x" + inst.stacks, 400); + DebugHud.send(spc, "expire_fallback_direct_" + effId, "[EFFECT][EXPIRE] Direct-applied " + effId + " tl=" + inst.ticks_left + " x" + inst.stacks, 400); } } ctx.onExpireApplied.add(effId); 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 690151e5a..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 @@ -74,7 +74,6 @@ public void tryActivate(Collection targets, SpellCtx ctx, MapHolde potionEvent.spellid = ctx.calculatedSpellData.getSpell() .GUID(); - // Normal path for non-ON_EXPIRE contexts try { potionEvent.Activate(); } catch (Exception ex) { 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..59a21f708 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 @@ -25,15 +25,6 @@ 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; @@ -46,21 +37,18 @@ public static ExileEffectInstanceData applyEffect(ServerPlayer sp, ExileEffect e var unit = Load.Unit(sp); 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 + 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/OnResourceLost.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java index 3e29a8206..27464f413 100644 --- a/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java @@ -6,22 +6,13 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; -/** - * Unified entrypoint for resource LOSS (spend, drains, damage). - * Health damage integration calls this via the LivingDamageEvent handler below. - * - * Debug printing is handled inside SpendThresholdManager and is toggled by - * OnResourceLost.DEBUG_ENABLED. - */ public final class OnResourceLost { private OnResourceLost() {} public enum LossSource { SpendOrDrain, Damage, Other } - /** Toggle SpendThresholdManager debug logs per player. */ public static boolean DEBUG_ENABLED = false; - /** Call this whenever a resource actually goes down. */ public static void trigger(LivingEntity entity, ResourceType type, float loss, LossSource source) { if (loss <= 0f) return; if (!(entity instanceof ServerPlayer sp)) return; 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 a0de8f541..b7759c656 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 @@ -129,12 +129,10 @@ 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 : com.robertx22.mine_and_slash.saveclasses.unit.ResourceType.values()) { for (var key : unit.getSpendRuntime().getActiveKeys(rt)) { var spec = unit.getSpendRuntime().getSpec(key); @@ -145,13 +143,12 @@ public static void onEndTick(ServerPlayer player) { 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); int cint = (int) newVal; if (unit.getSpendRuntime().progressIntChanged(key, cint)) { 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 3042e4332..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 @@ -58,7 +58,6 @@ public static void render(GuiGraphics gui, boolean horizontal) { } } - // Render threshold UI items inline with effects (uses same bg/overlay) var thresholdMap = com.robertx22.mine_and_slash.mechanics.thresholds.ui.ThresholdUiClient.visibleEntries(); if (!thresholdMap.isEmpty()) { for (var e : thresholdMap.entrySet()) { @@ -67,11 +66,9 @@ public static void render(GuiGraphics gui, boolean horizontal) { var rt = com.robertx22.mine_and_slash.saveclasses.unit.ResourceType.ofId(resId); if (rt == null) continue; - // draw frame same as effects 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); - // show 0..threshold progress, not current resource value 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); 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 d35e248a9..26886ac65 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 @@ -9,12 +9,13 @@ public class DataDrivenSpendThresholdSpec extends SpendThresholdSpec { - public enum ThresholdMode { X_PER_LEVEL, FLAT, PCT_OF_MAX } + public enum ThresholdMode { FLAT, PERCENT_OF_MAX } private final ThresholdMode mode; private final float value; private final boolean multiplyByLevel; - @Nullable private final ResourceType pctMaxOf; + @Nullable private final ResourceType percentMaxOf; + private final boolean showUi; public DataDrivenSpendThresholdSpec( String key, @@ -22,45 +23,43 @@ public DataDrivenSpendThresholdSpec( ThresholdMode mode, float value, boolean multiplyByLevel, - @Nullable ResourceType pctMaxOf, + @Nullable ResourceType percentMaxOf, Set lockWhileEffectIds, int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc + boolean resetProgressOnProc, + boolean showUi ) { - super(resource, /*perLevelFactor (unused)*/ 0f, key, - lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc); + super(resource, 0f, key, + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, showUi); this.mode = mode; this.value = value; this.multiplyByLevel = multiplyByLevel; - this.pctMaxOf = pctMaxOf; + this.percentMaxOf = percentMaxOf; + this.showUi = showUi; } + @Override public float thresholdFor(EntityData unit) { float base; switch (mode) { - case X_PER_LEVEL: - base = value * Math.max(1, unit.getLevel()); - break; - case FLAT: - base = value * (multiplyByLevel ? Math.max(1, unit.getLevel()) : 1f); - break; - case PCT_OF_MAX: - ResourceType rt = (pctMaxOf != null) ? pctMaxOf : resource(); - float max = unit.getResources().getMax(unit.getEntity(), rt); + case PERCENT_OF_MAX -> { + ResourceType tgt = (percentMaxOf != null) ? percentMaxOf : resource(); + float max = unit.getResources().getMax(unit.getEntity(), tgt); base = (value / 100f) * max; - if (multiplyByLevel) base *= Math.max(1, unit.getLevel()); - break; - default: - base = 0f; + } + case FLAT -> base = value; + default -> base = value; } + if (multiplyByLevel) base *= Math.max(1, unit.getLevel()); return Math.max(0f, base); } @Override public void onProc(ServerPlayer sp, int procs) { - // No default action here; datapack loader wires actions. } + + public boolean showUi() { return showUi; } } 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 2888c3032..4a481a700 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 @@ -6,12 +6,10 @@ import com.robertx22.mine_and_slash.saveclasses.unit.ResourceType; import net.minecraft.server.level.ServerPlayer; -/** 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 ===== @@ -23,7 +21,7 @@ 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()) { @@ -39,7 +37,6 @@ 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); @@ -53,7 +50,6 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t continue; } - // Locks (effects + optional perk requirement) if (spec.isLockedFor(unit)) { if (spec.dropProgressWhileLocked()) { tracker.clearKey(type, key); @@ -61,7 +57,6 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (debug) { sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked")); } - // hide UI while locked if (spec.showUi()) { com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); } @@ -84,7 +79,6 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); - // hide UI on proc if (spec.showUi()) { com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); } 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 d8d6916a9..1a31a898d 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 @@ -8,21 +8,16 @@ import java.util.Set; public class SpendThresholdRuntime { - // gameTime (ticks) when each key’s cooldown ends + private final Map cooldownUntil = new HashMap<>(); - // last activity tick for each threshold key (progress added) private final Map lastActivityTick = new HashMap<>(); - // last decay tick applied for each key (so we decay at most once per second) private final Map lastDecayTick = new HashMap<>(); - // last integer progress sent to client (to throttle network updates) private final Map lastProgressIntSent = new HashMap<>(); - // Index of active threshold keys by resource (only keys with progress > 0 or recently updated) private final java.util.EnumMap> activeByResource = new java.util.EnumMap<>(ResourceType.class); - // Quick lookup of spec by key (used for decay threshold value) private final Map specByKey = new HashMap<>(); public void startCooldown(String key, long now, int cooldownTicks) { @@ -35,7 +30,6 @@ public boolean isCoolingDown(String key, long now) { return until != null && now < until; } - /** Remaining ticks until ready (0 if no cooldown / already ready). */ public int cooldownRemainingTicks(String key, long now) { Long until = cooldownUntil.get(key); if (until == null) return 0; 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 6653eadcc..48e7ecc77 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,21 +12,19 @@ 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 resetProgressOnProc; - private final boolean showUi; // whether to render progress HUD for this spec + 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, @@ -47,18 +45,6 @@ public SpendThresholdSpec(ResourceType resource, this.showUi = showUi; } - // Backward-compatible ctor (defaults showUi=false) - public SpendThresholdSpec(ResourceType resource, - float perLevelFactor, - String key, - Set lockWhileEffectIds, - int cooldownTicks, - boolean lockWhileCooldown, - boolean dropProgressWhileLocked, - boolean resetProgressOnProc) { - this(resource, perLevelFactor, key, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false); - } - // ===== accessors ===== public ResourceType resource() { return resource; } public String key() { return key; } @@ -85,12 +71,10 @@ public SpendThresholdSpec withShowUi(boolean on) { }.withPriority(this.priority); } - /** Default threshold = perLevelFactor × LVL. Subclasses may override. */ 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(); @@ -105,14 +89,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 1cd3a7ffb..f60fbe52e 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,20 +54,17 @@ 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; if ("PERCENT_OF_MAX".equals(rawMode)) { - mode = DataDrivenSpendThresholdSpec.ThresholdMode.PCT_OF_MAX; - } else if ("X_PER_LEVEL".equals(rawMode)) { - mode = DataDrivenSpendThresholdSpec.ThresholdMode.X_PER_LEVEL; + mode = DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX; } else { mode = DataDrivenSpendThresholdSpec.ThresholdMode.FLAT; // default + treats legacy values as FLAT } ResourceType percentOf = null; - if (mode == DataDrivenSpendThresholdSpec.ThresholdMode.PCT_OF_MAX + if (mode == DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX && threshold.percentOf != null && !threshold.percentOf.isEmpty()) { percentOf = parseResource(threshold.percentOf, res); // default to this spec’s resource if bad input } @@ -86,7 +83,8 @@ public SpendThresholdSpec toSpec() { cooldownTicks, locks != null && locks.lockWhileCooldown, locks != null && locks.dropProgressWhileLocked, - locks != null && locks.resetProgressOnProc + locks != null && locks.resetProgressOnProc, + showUi ) { @Override public void onProc(ServerPlayer sp, int procs) { @@ -104,7 +102,6 @@ public void onProc(ServerPlayer sp, int procs) { int stacks = Math.max(1, a.stacks); var inst = EffectUtils.applyEffect(sp, effect, durTicks, stacks); - // Attach on-expire duration overrides (ticks directly) if (a.onExpire != null && !a.onExpire.isEmpty()) { if (inst.onExpireEffectDurationTicks == null) { inst.onExpireEffectDurationTicks = new java.util.HashMap<>(); @@ -131,7 +128,7 @@ public boolean isLockedFor(EntityData unit) { } return false; } - }.withPriority(priority).withShowUi(showUi); + }.withPriority(priority); } // --- helpers --- @@ -140,7 +137,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 index 11c24f0f7..c8226610f 100644 --- 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 @@ -4,7 +4,6 @@ import java.util.HashMap; import java.util.Map; -/** Client-only holder for which threshold keys should currently render, their resource ids, and progress. */ public final class ThresholdUiClient { private ThresholdUiClient() {} 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 index 0fd3e7dbd..191a552d2 100644 --- a/src/main/java/com/robertx22/mine_and_slash/mmorpg/DebugHud.java +++ b/src/main/java/com/robertx22/mine_and_slash/mmorpg/DebugHud.java @@ -8,10 +8,8 @@ public final class DebugHud { private DebugHud() {} - // Unified flags public static volatile boolean ON_EXPIRE = false; - // Simple per-player throttling to avoid spammy chat when debugging 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) { 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 index c0f7c15b2..198e4ca73 100644 --- 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 @@ -17,7 +17,6 @@ public class DebugCommands { public static void init(CommandDispatcher dispatcher) { - // New unified debug toggle: // /mine_and_slash debug CommandBuilder.of(CommandRefs.ID, dispatcher, x -> { StringWrapper SUBJECT = new StringWrapper("subject", () -> List.of("spend_threshold", "resource_restore", "onexpire")); @@ -31,7 +30,6 @@ public static void init(CommandDispatcher dispatcher) { String subject = SUBJECT.get(e); String state = STATE.get(e); - // Validate inputs (in addition to tab suggestions) 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"); 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 262e80fb2..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; @@ -54,21 +55,17 @@ public void tick(LivingEntity en) { 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 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) { - // Fire onRemove immediately for this key eff.onRemove(en); - // Decide deletion based on current state after onRemove ExileEffectInstanceData inst = exileMap.get(entry.getKey()); if (inst == null || inst.shouldRemove()) { toDeleteKeys.add(entry.getKey()); - } else if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp, "expire_kept_" + entry.getKey(), "[EFFECT][EXPIRE] Kept " + entry.getKey() + " after onRemove (ticks_left=" + inst.ticks_left + ")", 400); + } 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); } } } @@ -78,13 +75,12 @@ public void tick(LivingEntity en) { if (entry.getValue().shouldRemove()) { ExileEffect eff = ExileDB.ExileEffects().get(entry.getKey()); if (eff != null) { - // Fire onRemove immediately for this key eff.onRemove(en); ExileEffectInstanceData inst = exileMap.get(entry.getKey()); if (inst == null || inst.shouldRemove()) { toDeleteKeys.add(entry.getKey()); - } else if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp, "expire_kept_" + entry.getKey(), "[EFFECT][EXPIRE] Kept " + entry.getKey() + " after onRemove (ticks_left=" + inst.ticks_left + ")", 400); + } 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); } } } @@ -93,14 +89,13 @@ public void tick(LivingEntity en) { - // Now delete expired effects (evaluated after onRemove) for (String key : toDeleteKeys) { ExileEffectInstanceData current = exileMap.remove(key); - if (com.robertx22.mine_and_slash.mmorpg.DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { + if (DebugHud.ON_EXPIRE && en instanceof net.minecraft.server.level.ServerPlayer sp) { if (current != null) { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp, "expire_removed_" + key, "[EFFECT][EXPIRE] Removed " + key + " (ticks_left=" + current.ticks_left + ", stacks=" + current.stacks + ") [id=" + System.identityHashCode(current) + "]", 200); + DebugHud.send(sp, "expire_removed_" + key, "[EFFECT][EXPIRE] Removed " + key + " (ticks_left=" + current.ticks_left + ", stacks=" + current.stacks + ") [id=" + System.identityHashCode(current) + "]", 200); } else { - com.robertx22.mine_and_slash.mmorpg.DebugHud.send(sp, "expire_removed_" + key, "[EFFECT][EXPIRE] Removed " + key + " (no current instance)", 200); + DebugHud.send(sp, "expire_removed_" + key, "[EFFECT][EXPIRE] Removed " + key + " (no current instance)", 200); } } } diff --git a/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json index 84ff2d0fc..1c1bcc45c 100644 --- a/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json +++ b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json @@ -9,7 +9,7 @@ "multiply_by_level": false, "percent_of": "health" }, - "cooldown_seconds": 0, + "cooldown_ticks": 0, "locks": { "effects": [], "drop_progress_while_locked": true, From 4618f8da1e4402425682abbdba6e9924bdbc5a58 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:11:50 -0400 Subject: [PATCH 15/20] Quick getter Fix for 25percent_health.json --- .../data/mmorpg/spend_thresholds/25percent_health.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json index 84ff2d0fc..1c1bcc45c 100644 --- a/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json +++ b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json @@ -9,7 +9,7 @@ "multiply_by_level": false, "percent_of": "health" }, - "cooldown_seconds": 0, + "cooldown_ticks": 0, "locks": { "effects": [], "drop_progress_while_locked": true, From 966057779d799dc09ffdb418e297f6b8a09b1f10 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:17:48 -0400 Subject: [PATCH 16/20] Suggested Fixes --- .../capability/entity/ResourceTracker.java | 26 +++++++------------ .../event_hooks/ontick/OnServerTick.java | 2 +- .../DataDrivenSpendThresholdSpec.java | 8 +++--- .../thresholds/SpendThresholdManager.java | 2 +- .../thresholds/SpendThresholdSpec.java | 17 +++--------- .../datapack/SpendThresholdDef.java | 4 +-- .../spend_thresholds/25percent_health.json | 2 +- .../spend_thresholds/energy_xlvl_wotj.json | 2 +- 8 files changed, 24 insertions(+), 39 deletions(-) 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 b80800811..e208d0b41 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 @@ -9,6 +9,7 @@ */ 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); @@ -85,6 +86,10 @@ private float total(java.util.Set types) { 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); @@ -99,8 +104,8 @@ public void clearKey(ResourceType rt, String key) { public int addAndConsumeForKey(String key, ResourceType rt, float add, float threshold) { if (key == null || key.isEmpty() || add <= 0f || threshold <= 0f) return 0; - var byKey = keyProgress.computeIfAbsent(rt, __ -> new java.util.HashMap<>()); - float cur = byKey.getOrDefault(key, 0f) + add; + var byKey = getKeyProgressOrCreate(rt); + float cur = byKey.getOrDefault(key, DEFAULT_KEY_PROGRESS) + add; int procs = 0; while (cur + EPS >= threshold) { @@ -116,20 +121,9 @@ public int addAndConsumeForKey(String key, ResourceType rt, float add, float thr /** Read current cursor for debug/UI. */ public float getKeyProgress(String key, ResourceType rt) { var byKey = keyProgress.get(rt); - return byKey == null ? 0f : byKey.getOrDefault(key, 0f); + return byKey == null ? DEFAULT_KEY_PROGRESS : byKey.getOrDefault(key, DEFAULT_KEY_PROGRESS); } - /** - * Set the exact cursor value for a specific key/resource. - * If value <= EPS the key entry is removed. - */ - 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); - } /** * Decrease the cursor by a fixed amount, clamped at zero. Returns the new value. @@ -137,8 +131,8 @@ public void setKeyProgress(String key, ResourceType rt, float value) { 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 0f; - float cur = byKey.getOrDefault(key, 0f); + if (byKey == null) return DEFAULT_KEY_PROGRESS; + float cur = byKey.getOrDefault(key, DEFAULT_KEY_PROGRESS); float next = Math.max(0f, cur - amount); if (next <= EPS) byKey.remove(key); else byKey.put(key, next); if (byKey.isEmpty()) keyProgress.remove(rt); 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 80dbdd747..e49628ff1 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 @@ -134,7 +134,7 @@ public static void onEndTick(ServerPlayer player) { var unit = Load.Unit(player); if (unit != null) { // Iterate only active keys per resource - for (var rt : com.robertx22.mine_and_slash.saveclasses.unit.ResourceType.values()) { + for (var rt : ResourceType.values()) { for (var key : unit.getSpendRuntime().getActiveKeys(rt)) { var spec = unit.getSpendRuntime().getSpec(key); long lastAct = unit.getSpendRuntime().getLastActivity(key); 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 11f92fe31..0269aeca7 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 @@ -27,11 +27,11 @@ public DataDrivenSpendThresholdSpec( int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc, + boolean dropProgressOnProc, boolean showUi ) { super(resource, 0f, key, - lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc); + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, dropProgressOnProc); this.mode = mode; this.value = value; this.multiplyByLevel = multiplyByLevel; @@ -50,9 +50,9 @@ public DataDrivenSpendThresholdSpec( int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc + boolean dropProgressOnProc ) { - this(key, resource, mode, value, multiplyByLevel, percentMaxOf, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, false); + this(key, resource, mode, value, multiplyByLevel, percentMaxOf, lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, dropProgressOnProc, false); } @Override 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 541cd5a63..5dc8a298f 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 @@ -75,7 +75,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (procs > 0) { spec.onProc(sp, procs); spec.startCooldown(unit, now); - if (spec.resetOnProc()) { + if (spec.dropProgressOnProc()) { tracker.clearKey(type, key); } if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); 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 9a3a26e47..3d2e2afc6 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 @@ -20,7 +20,7 @@ public abstract class SpendThresholdSpec { private final int cooldownTicks; private final boolean lockWhileCooldown; // treat cooldown as a lock private final boolean dropProgressWhileLocked; - private final boolean resetProgressOnProc; + private final boolean dropProgressOnProc; // registry ordering (lower runs first) private int priority = 0; @@ -33,7 +33,7 @@ public SpendThresholdSpec(ResourceType resource, int cooldownTicks, boolean lockWhileCooldown, boolean dropProgressWhileLocked, - boolean resetProgressOnProc + boolean dropProgressOnProc ) { this.resource = resource; this.perLevelFactor = perLevelFactor; @@ -42,7 +42,7 @@ public SpendThresholdSpec(ResourceType resource, this.cooldownTicks = Math.max(0, cooldownTicks); this.lockWhileCooldown = lockWhileCooldown; this.dropProgressWhileLocked = dropProgressWhileLocked; - this.resetProgressOnProc = resetProgressOnProc; + this.dropProgressOnProc = dropProgressOnProc; } @@ -52,7 +52,7 @@ public SpendThresholdSpec(ResourceType resource, public String keyFor(EntityData unit) { return key; } public boolean lockWhileCooldown() { return lockWhileCooldown; } public boolean dropProgressWhileLocked() { return dropProgressWhileLocked; } - public boolean resetOnProc() { return resetProgressOnProc; } + public boolean dropProgressOnProc() { return dropProgressOnProc; } public int cooldownTicks() { return cooldownTicks; } public int priority() { return priority; } @@ -62,15 +62,6 @@ public SpendThresholdSpec withPriority(int p) { return this; } - - public SpendThresholdSpec withShowUi(boolean on) { - return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc) { - @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); - } - /** Default threshold = perLevelFactor × LVL. Subclasses may override. */ public float thresholdFor(EntityData unit) { return Math.max(0f, perLevelFactor * Math.max(1, unit.getLevel())); 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 cc91c5ccc..7e1fdb5b2 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 @@ -31,7 +31,7 @@ public static class Locks { public List effects = new ArrayList<>(); @SerializedName("lock_while_cooldown") public boolean lockWhileCooldown = false; @SerializedName("drop_progress_while_locked") public boolean dropProgressWhileLocked = true; - @SerializedName("reset_progress_on_proc") public boolean resetProgressOnProc = true; + @SerializedName("drop_progress_on_proc") public boolean dropProgressOnProc = true; } public Locks locks = new Locks(); @@ -82,7 +82,7 @@ public SpendThresholdSpec toSpec() { cooldownTicks, locks != null && locks.lockWhileCooldown, locks != null && locks.dropProgressWhileLocked, - locks != null && locks.resetProgressOnProc, + locks != null && locks.dropProgressOnProc, showUi ) { @Override diff --git a/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json index 1c1bcc45c..f484183b4 100644 --- a/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json +++ b/src/main/resources/data/mmorpg/spend_thresholds/25percent_health.json @@ -13,7 +13,7 @@ "locks": { "effects": [], "drop_progress_while_locked": true, - "reset_progress_on_proc": true + "drop_progress_on_proc": true }, "on_proc": [ { 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 621a996c0..5c8d0c3e2 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 @@ -14,7 +14,7 @@ "effects": ["wrath_of_the_juggernaut", "burnout"], "lock_while_cooldown": true, "drop_progress_while_locked": true, - "reset_progress_on_proc": true + "drop_progress_on_proc": true }, "on_proc": [ { From a36d2c99d9897bdab8b795ece31f1559fb2f6c2d Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:28:28 -0400 Subject: [PATCH 17/20] Suggested Fixes: Performance Optimization --- .../thresholds/SpendThresholdRuntime.java | 102 ++++++++++++------ 1 file changed, 67 insertions(+), 35 deletions(-) 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 d8d6916a9..2ae6b3bc0 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 @@ -8,37 +8,37 @@ import java.util.Set; public class SpendThresholdRuntime { - // gameTime (ticks) when each key’s cooldown ends - private final Map cooldownUntil = new HashMap<>(); - // last activity tick for each threshold key (progress added) - private final Map lastActivityTick = new HashMap<>(); - - // last decay tick applied for each key (so we decay at most once per second) - private final Map lastDecayTick = new HashMap<>(); + private static final class KeyState { + long cooldownUntil; + long lastActivityTick; + long lastDecayTick; + int lastProgressIntSent = Integer.MIN_VALUE; + SpendThresholdSpec spec; + } - // last integer progress sent to client (to throttle network updates) - private final Map lastProgressIntSent = new HashMap<>(); + private final Map states = new HashMap<>(); - // Index of active threshold keys by resource (only keys with progress > 0 or recently updated) private final java.util.EnumMap> activeByResource = new java.util.EnumMap<>(ResourceType.class); - // Quick lookup of spec by key (used for decay threshold value) - private final Map specByKey = new HashMap<>(); + private final java.util.EnumMap> activeByResourceReadOnly = new java.util.EnumMap<>(ResourceType.class); public void startCooldown(String key, long now, int cooldownTicks) { if (cooldownTicks <= 0) return; - cooldownUntil.put(key, now + cooldownTicks); + if (key == null || key.isEmpty()) return; + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.cooldownUntil = now + cooldownTicks; } public boolean isCoolingDown(String key, long now) { - Long until = cooldownUntil.get(key); - return until != null && now < until; + if (key == null || key.isEmpty()) return false; + KeyState ks = states.get(key); + return ks != null && now < ks.cooldownUntil; } - /** Remaining ticks until ready (0 if no cooldown / already ready). */ public int cooldownRemainingTicks(String key, long now) { - Long until = cooldownUntil.get(key); - if (until == null) return 0; + if (key == null || key.isEmpty()) return 0; + KeyState ks = states.get(key); + long until = (ks == null) ? 0L : ks.cooldownUntil; long rem = until - now; return (int) Math.max(0, rem); } @@ -46,27 +46,36 @@ public int cooldownRemainingTicks(String key, long now) { // === Activity/Decay tracking === public void markActivity(String key, long now) { if (key == null || key.isEmpty()) return; - lastActivityTick.put(key, now); - lastDecayTick.remove(key); + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.lastActivityTick = now; + ks.lastDecayTick = 0L; } public long getLastActivity(String key) { - return lastActivityTick.getOrDefault(key, 0L); + if (key == null || key.isEmpty()) return 0L; + KeyState ks = states.get(key); + return ks == null ? 0L : ks.lastActivityTick; } public long getLastDecay(String key) { - return lastDecayTick.getOrDefault(key, 0L); + if (key == null || key.isEmpty()) return 0L; + KeyState ks = states.get(key); + return ks == null ? 0L : ks.lastDecayTick; } public void markDecay(String key, long now) { if (key == null || key.isEmpty()) return; - lastDecayTick.put(key, now); + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.lastDecayTick = now; } public boolean progressIntChanged(String key, int intProgress) { - Integer prev = lastProgressIntSent.get(key); - if (prev == null || prev.intValue() != intProgress) { - lastProgressIntSent.put(key, intProgress); + if (key == null || key.isEmpty()) return false; + KeyState ks = states.get(key); + int prev = (ks == null) ? Integer.MIN_VALUE : ks.lastProgressIntSent; + if (prev != intProgress) { + if (ks == null) ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.lastProgressIntSent = intProgress; return true; } return false; @@ -75,8 +84,16 @@ public boolean progressIntChanged(String key, int intProgress) { // === Active key index === public void markActive(ResourceType rt, String key, SpendThresholdSpec spec) { if (rt == null || key == null || key.isEmpty() || spec == null) return; - activeByResource.computeIfAbsent(rt, __ -> new HashSet<>()).add(key); - specByKey.put(key, spec); + Set set = activeByResource.get(rt); + 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); + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.spec = spec; } public void removeActive(ResourceType rt, String key) { @@ -84,20 +101,35 @@ public void removeActive(ResourceType rt, String key) { var set = activeByResource.get(rt); if (set != null) { set.remove(key); - if (set.isEmpty()) activeByResource.remove(rt); + if (set.isEmpty()) { + activeByResource.remove(rt); + activeByResourceReadOnly.remove(rt); + } + } + 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; + ks.lastProgressIntSent = Integer.MIN_VALUE; } - specByKey.remove(key); - lastActivityTick.remove(key); - lastDecayTick.remove(key); - lastProgressIntSent.remove(key); } public Set getActiveKeys(ResourceType rt) { var s = activeByResource.get(rt); - return s == null ? java.util.Set.of() : java.util.Set.copyOf(s); + 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; } public SpendThresholdSpec getSpec(String key) { - return specByKey.get(key); + if (key == null || key.isEmpty()) return null; + KeyState ks = states.get(key); + return ks == null ? null : ks.spec; } } From f8ba63fc449b7294839b97fafe05b5c29b5ac1a6 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:01:49 -0400 Subject: [PATCH 18/20] Suggested Fixes: Cleanup + Tidy --- .../data/exile_effects/ExileEffect.java | 21 +++-- .../event_hooks/my_events/EffectUtils.java | 26 +++-- .../event_hooks/ontick/OnServerTick.java | 5 +- .../thresholds/SpendThresholdManager.java | 16 ++-- .../thresholds/SpendThresholdRuntime.java | 94 +++++++++++++------ .../thresholds/SpendThresholdSpec.java | 1 - 6 files changed, 108 insertions(+), 55 deletions(-) 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 57f4d4306..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,7 @@ 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; @@ -279,6 +280,7 @@ public void onRemove(LivingEntity target) { } 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()); @@ -286,22 +288,23 @@ public void onRemove(LivingEntity target) { continue; } var extraEff = ExileDB.ExileEffects().get(effId); - if (extraEff != null) { - var unitT = com.robertx22.mine_and_slash.uncommon.datasaving.Load.Unit(target); - var storeT = unitT.getStatusEffectsData(); - var instT = storeT.getOrCreate(extraEff); - instT.stacks = Math.max(instT.stacks, 1); - instT.ticks_left = Math.max(instT.ticks_left, durTicks); + 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(); - try { extraEff.onApply(target); } catch (Exception ignored) {} - unitT.equipmentCache.STATUS.setDirty(); - unitT.sync.setDirty(); 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(); + } } } } 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 59a21f708..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. @@ -28,14 +29,22 @@ private EffectUtils() {} 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); @@ -44,8 +53,13 @@ public static ExileEffectInstanceData applyEffect(ServerPlayer sp, ExileEffect e inst.stacks = Math.max(inst.stacks, capped); inst.ticks_left = Math.max(inst.ticks_left, durationTicks); - effect.onApply(sp); - unit.sync.setDirty(); + try { effect.onApply(entity); } catch (Exception ignored) {} + if (markDirty) { + unit.equipmentCache.STATUS.setDirty(); + if (entity instanceof ServerPlayer) { + unit.sync.setDirty(); + } + } return inst; } 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 b7759c656..dc30ebd38 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 @@ -133,7 +133,7 @@ public static void onEndTick(ServerPlayer player) { long now = player.level().getGameTime(); var unit = Load.Unit(player); if (unit != null) { - for (var rt : com.robertx22.mine_and_slash.saveclasses.unit.ResourceType.values()) { + for (var rt : ResourceType.values()) { for (var key : unit.getSpendRuntime().getActiveKeys(rt)) { var spec = unit.getSpendRuntime().getSpec(key); if (!spec.showUi()) continue; @@ -152,8 +152,7 @@ public static void onEndTick(ServerPlayer player) { float newVal = unit.getResourceTracker().decayKeyProgress(key, rt, decayPerSecond); int cint = (int) newVal; if (unit.getSpendRuntime().progressIntChanged(key, cint)) { - com.robertx22.library_of_exile.main.Packets.sendToClient(player, - new ThresholdUiPacket(key, rt.id, newVal > 0f, newVal)); + 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/mechanics/thresholds/SpendThresholdManager.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java index 4a481a700..2d0d0752c 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 @@ -3,8 +3,10 @@ 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; public final class SpendThresholdManager { private SpendThresholdManager() {} @@ -26,7 +28,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t 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)" )); @@ -43,7 +45,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t } if (debug) { long rem = unit.getSpendRuntime().cooldownRemainingTicks(key, now); - sp.sendSystemMessage(net.minecraft.network.chat.Component.literal( + sp.sendSystemMessage(Component.literal( "[SPEND:" + spec.key() + "] locked by cooldown (" + rem + "t ~ " + fmtSec((float) rem) + "s)" )); } @@ -55,10 +57,10 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t 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")); } if (spec.showUi()) { - com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); + Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); } continue; } @@ -80,7 +82,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t } if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); if (spec.showUi()) { - com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); + Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, false, 0)); } unit.getSpendRuntime().removeActive(type, key); } else { @@ -92,7 +94,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t int cint = (int) cur; if (unit.getSpendRuntime().progressIntChanged(key, cint)) { boolean show = cur > 0f; - com.robertx22.library_of_exile.main.Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, cur)); + Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, cur)); } } if (cur <= 0f) { @@ -105,7 +107,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 1a31a898d..0262a01ef 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 @@ -9,30 +9,36 @@ public class SpendThresholdRuntime { - private final Map cooldownUntil = new HashMap<>(); - - private final Map lastActivityTick = new HashMap<>(); - - private final Map lastDecayTick = new HashMap<>(); + private static final class KeyState { + long cooldownUntil; + long lastActivityTick; + long lastDecayTick; + int lastProgressIntSent = Integer.MIN_VALUE; + SpendThresholdSpec spec; + } - private final Map lastProgressIntSent = new HashMap<>(); + private final Map states = new HashMap<>(); private final java.util.EnumMap> activeByResource = new java.util.EnumMap<>(ResourceType.class); - private final Map specByKey = new HashMap<>(); + private final java.util.EnumMap> activeByResourceReadOnly = new java.util.EnumMap<>(ResourceType.class); public void startCooldown(String key, long now, int cooldownTicks) { if (cooldownTicks <= 0) return; - cooldownUntil.put(key, now + cooldownTicks); + if (key == null || key.isEmpty()) return; + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.cooldownUntil = now + cooldownTicks; } public boolean isCoolingDown(String key, long now) { - Long until = cooldownUntil.get(key); - return until != null && now < until; + if (key == null || key.isEmpty()) return false; + KeyState ks = states.get(key); + return ks != null && now < ks.cooldownUntil; } public int cooldownRemainingTicks(String key, long now) { - Long until = cooldownUntil.get(key); - if (until == null) return 0; + if (key == null || key.isEmpty()) return 0; + KeyState ks = states.get(key); + long until = (ks == null) ? 0L : ks.cooldownUntil; long rem = until - now; return (int) Math.max(0, rem); } @@ -40,27 +46,36 @@ public int cooldownRemainingTicks(String key, long now) { // === Activity/Decay tracking === public void markActivity(String key, long now) { if (key == null || key.isEmpty()) return; - lastActivityTick.put(key, now); - lastDecayTick.remove(key); + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.lastActivityTick = now; + ks.lastDecayTick = 0L; } public long getLastActivity(String key) { - return lastActivityTick.getOrDefault(key, 0L); + if (key == null || key.isEmpty()) return 0L; + KeyState ks = states.get(key); + return ks == null ? 0L : ks.lastActivityTick; } public long getLastDecay(String key) { - return lastDecayTick.getOrDefault(key, 0L); + if (key == null || key.isEmpty()) return 0L; + KeyState ks = states.get(key); + return ks == null ? 0L : ks.lastDecayTick; } public void markDecay(String key, long now) { if (key == null || key.isEmpty()) return; - lastDecayTick.put(key, now); + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.lastDecayTick = now; } public boolean progressIntChanged(String key, int intProgress) { - Integer prev = lastProgressIntSent.get(key); - if (prev == null || prev.intValue() != intProgress) { - lastProgressIntSent.put(key, intProgress); + if (key == null || key.isEmpty()) return false; + KeyState ks = states.get(key); + int prev = (ks == null) ? Integer.MIN_VALUE : ks.lastProgressIntSent; + if (prev != intProgress) { + if (ks == null) ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.lastProgressIntSent = intProgress; return true; } return false; @@ -69,8 +84,15 @@ public boolean progressIntChanged(String key, int intProgress) { // === Active key index === public void markActive(ResourceType rt, String key, SpendThresholdSpec spec) { if (rt == null || key == null || key.isEmpty() || spec == null) return; - activeByResource.computeIfAbsent(rt, __ -> new HashSet<>()).add(key); - specByKey.put(key, spec); + Set set = activeByResource.get(rt); + if (set == null) { + set = new HashSet<>(); + activeByResource.put(rt, set); + activeByResourceReadOnly.put(rt, java.util.Collections.unmodifiableSet(set)); + } + set.add(key); + KeyState ks = states.computeIfAbsent(key, __ -> new KeyState()); + ks.spec = spec; } public void removeActive(ResourceType rt, String key) { @@ -78,20 +100,34 @@ public void removeActive(ResourceType rt, String key) { var set = activeByResource.get(rt); if (set != null) { set.remove(key); - if (set.isEmpty()) activeByResource.remove(rt); + if (set.isEmpty()) { + activeByResource.remove(rt); + activeByResourceReadOnly.remove(rt); + } + } + KeyState ks = states.get(key); + if (ks != null) { + ks.spec = null; + ks.lastActivityTick = 0L; + ks.lastDecayTick = 0L; + ks.lastProgressIntSent = Integer.MIN_VALUE; } - specByKey.remove(key); - lastActivityTick.remove(key); - lastDecayTick.remove(key); - lastProgressIntSent.remove(key); } public Set getActiveKeys(ResourceType rt) { var s = activeByResource.get(rt); - return s == null ? java.util.Set.of() : java.util.Set.copyOf(s); + 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; } public SpendThresholdSpec getSpec(String key) { - return specByKey.get(key); + if (key == null || key.isEmpty()) return null; + KeyState ks = states.get(key); + return ks == null ? null : ks.spec; } } 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 48e7ecc77..f99388c65 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 @@ -15,7 +15,6 @@ public abstract class SpendThresholdSpec { private final float perLevelFactor; private final String key; - // gating/cooldown controls private final Set lockWhileEffectIds; private final int cooldownTicks; private final boolean lockWhileCooldown; // Used to lock the threshold while the cooldown is active. **RECOMMENDED FOR DEBUGGING ONLY** From c47e43aacabd83e386033f25dd5a04e119de1851 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:59:02 -0400 Subject: [PATCH 19/20] Cleanup + Fixes --- .../event_hooks/ontick/OnServerTick.java | 4 +--- .../DataDrivenSpendThresholdSpec.java | 3 +-- .../thresholds/SpendThresholdManager.java | 17 +++++++++++------ .../thresholds/SpendThresholdRuntime.java | 16 +++++++++------- .../thresholds/SpendThresholdSpec.java | 8 +++++--- .../spend_thresholds/energy_xlvl_wotj.json | 1 + 6 files changed, 28 insertions(+), 21 deletions(-) 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 9ddbaceb4..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 @@ -137,7 +137,6 @@ public static void onEndTick(ServerPlayer player) { 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; @@ -151,8 +150,7 @@ public static void onEndTick(ServerPlayer player) { if (thr <= 0f) continue; float decayPerSecond = thr * 0.15f; // Decay rate: 15% of Threshold per second float newVal = unit.getResourceTracker().decayKeyProgress(key, rt, decayPerSecond); - int cint = (int) newVal; - if (unit.getSpendRuntime().progressIntChanged(key, cint)) { + if (unit.getSpendRuntime().progressScaledChanged(key, newVal, 10)) { Packets.sendToClient(player, new ThresholdUiPacket(key, rt.id, newVal > 0f, newVal)); } if (newVal <= 0f) { 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 b95d2e2a6..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,7 +31,7 @@ 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; @@ -58,5 +58,4 @@ public float thresholdFor(EntityData unit) { @Override public void onProc(ServerPlayer sp, int procs) { } - public boolean showUi() { return showUi; } } 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 2db94e71b..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 @@ -46,7 +46,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t if (debug) { long rem = unit.getSpendRuntime().cooldownRemainingTicks(key, now); sp.sendSystemMessage(Component.literal( - "[SPEND:" + spec.key() + "] locked by cooldown (" + rem + "t ~ " + fmtSec((float) rem) + "s)" + "[SPEND:" + spec.key() + "] locked by cooldown (" + rem + "t ~ " + fmtSec((float) rem) + "s) ui=" + (spec.showUi() ? "on" : "off") )); } continue; @@ -57,7 +57,7 @@ public static void processSpend(ServerPlayer sp, EntityData unit, ResourceType t tracker.clearKey(type, key); } if (debug) { - sp.sendSystemMessage(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)); @@ -73,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); @@ -80,19 +85,19 @@ 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()) { - int cint = (int) cur; - if (unit.getSpendRuntime().progressIntChanged(key, cint)) { + if (unit.getSpendRuntime().progressScaledChanged(key, cur, 10)) { boolean show = cur > 0f; Packets.sendToClient(sp, new ThresholdUiPacket(key, type.id, show, cur)); } 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 0262a01ef..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; @@ -116,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 c1fc32423..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 @@ -31,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; @@ -41,6 +41,7 @@ public SpendThresholdSpec(ResourceType resource, this.lockWhileCooldown = lockWhileCooldown; this.dropProgressWhileLocked = dropProgressWhileLocked; this.dropProgressOnProc = dropProgressOnProc; + this.showUi = showUi; } // ===== accessors ===== @@ -52,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) { @@ -60,7 +62,7 @@ public SpendThresholdSpec withPriority(int p) { } public SpendThresholdSpec withShowUi(boolean on) { - return new SpendThresholdSpec(this.resource, this.perLevelFactor, this.key, this.lockWhileEffectIds, this.cooldownTicks, this.lockWhileCooldown, this.dropProgressWhileLocked, this.resetProgressOnProc, 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); } 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 979ab5407..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": { From 53064b17a9be1fd6c481fa8eba3358d7316223b7 Mon Sep 17 00:00:00 2001 From: Dubledice <138730151+Dubledice@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:04:36 -0400 Subject: [PATCH 20/20] Culled Duplicate --- .../capability/entity/ResourceTracker.java | 14 -------------- 1 file changed, 14 deletions(-) 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 ada0504f3..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 @@ -111,20 +111,6 @@ public float getKeyProgress(String key, ResourceType rt) { } - /** - * Decrease the cursor by a fixed amount, clamped at zero. Returns the new value. - */ - 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); - 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; - } - public void setKeyProgress(String key, ResourceType rt, float value) { if (key == null || key.isEmpty()) return; var byKey = keyProgress.computeIfAbsent(rt, __ -> new java.util.HashMap<>());