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 1ab033760..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 @@ -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.*; @@ -68,6 +71,8 @@ public class ModEffects implements ExileRegistryInit { public static EffectCtx BLIND = new EffectCtx("blind", "Blind", Elements.Shadow, EffectType.negative); public static EffectCtx STUN = new EffectCtx("stun", "Stun", Elements.Physical, EffectType.negative); public static EffectCtx GALE_FORCE = new EffectCtx("gale_force", "Gale Force", Elements.Physical, EffectType.beneficial); + public static EffectCtx WRATH_OF_THE_JUGGERNAUT = new EffectCtx("wrath_of_the_juggernaut", "Wrath of the Juggernaut", Elements.Physical, EffectType.beneficial); + public static EffectCtx BURNOUT = new EffectCtx("burnout", "Burnout", Elements.Physical, EffectType.negative); // these could be used for map affixes public static EffectCtx SLOW = new EffectCtx("slow", "Lethargy", Elements.Physical, EffectType.negative); @@ -100,6 +105,50 @@ public static List getCurses() { public static int ESSENCE_OF_FROST_MAX_STACKS = 5; + // ---------- Helper ---------- + private static EffectCtx state(String id, String name, Elements elem) { + return new EffectCtx(id, name, elem, EffectType.beneficial); + } + private static EffectCtx statePhysical(String id, String name) { + return state(id, name, Elements.Physical); + } + + // Pretty names for resources (UI text) + private static final Map RES_NAME = Map.of( + ResourceType.health, "Health", + ResourceType.mana, "Mana", + ResourceType.energy, "Energy", + ResourceType.magic_shield, "Magic Shield", + ResourceType.blood, "Blood" + ); + + // Suggested elements per resource (only used for coloring/category) + + // ---------- Generic flags ---------- + public static final EffectCtx LEECHING_STATE = state( + "leeching_state", "Leeching (State)", Elements.Physical + ); + public static final EffectCtx REGEN_STATE = state( + "regen_state", "Regenerating (State)", Elements.Physical + ); + + // ---------- Per-resource flags (generated) ---------- + public static final EnumMap 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 nice = RES_NAME.get(rt); + + LEECHING_STATE_BY_RES.put( + rt, statePhysical("leeching_" + rt.id + "_state", "Leeching " + nice) + ); + REGEN_STATE_BY_RES.put( + rt, statePhysical("regen_" + rt.id + "_state", "Regenerating " + nice) + ); + } + } + public static void init() { } @@ -359,6 +408,22 @@ public void registerAll() { .addTags(EffectTags.song, EffectTags.offensive) .build(); + ExileEffectBuilder.of(WRATH_OF_THE_JUGGERNAUT) + .vanillaStat(VanillaStatData.create(ATTACK_SPEED, 0.30F, ModType.MORE, UUID.fromString("0c7a6e2c-5e5c-4f2f-9e3b-2a8e3c1a1f30"))) + .vanillaStat(VanillaStatData.create(KNOCKBACK_RESISTANCE, 1.0F, ModType.FLAT, UUID.fromString("a9d9c9f2-9f0f-4521-9c3e-9f7a1c2b5e11"))) + .stat(10, 10, DefenseStats.DAMAGE_REDUCTION.get(), ModType.FLAT) + .stat(100, 100, SpellChangeStats.COOLDOWN_REDUCTION_PER_SPELL_TAG.get(SpellTags.weapon_skill), ModType.FLAT) + .spell(SpellBuilder.forEffect() + .buildForEffect()) + .addTags(EffectTags.offensive) + .maxStacks(1) + .build(); + + ExileEffectBuilder.of(BURNOUT) + .maxStacks(1) + .addTags(EffectTags.negative) + .build(); + ExileEffectBuilder.of(REJUVENATE) .maxStacks(5) @@ -385,6 +450,24 @@ public void registerAll() { .tick(20D)) .buildForEffect()) .build(); + + // (NEW) Leeching & Healing + ExileEffectBuilder.of(LEECHING_STATE) + .maxStacks(1) + .build(); + + ExileEffectBuilder.of(REGEN_STATE) + .maxStacks(1) + .build(); + + // Register per-resource flags + for (EffectCtx ctx : LEECHING_STATE_BY_RES.values()) { + ExileEffectBuilder.of(ctx).maxStacks(1).build(); + } + + for (EffectCtx ctx : REGEN_STATE_BY_RES.values()) { + ExileEffectBuilder.of(ctx).maxStacks(1).build(); + } ExileEffectBuilder.of(BLIZZARD_REDUCE_HEAL_STRENGTH) .maxStacks(1) 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/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() { } 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..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 @@ -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; } + + private final SpendThresholdRuntime spendRuntime = new SpendThresholdRuntime(); + public SpendThresholdRuntime getSpendRuntime() { return spendRuntime; } + + public boolean alreadyHit(Entity spellEntity, LivingEntity target) { // this makes sure piercing projectiles hit target only once and then pass through 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..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 @@ -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 capPercentPerSec = data.getUnit() + .getCalculatedStat(ResourceStats.LEECH_CAP.get(rt)) + .getValue() / 100F; + + float maxRes = data.getResources().getMax(data.entity, rt); + float fiveSecs = 5F * capPercentPerSec * maxRes; // “5 seconds worth” reservoir cap + float clamped = MathHelper.clamp(en.getValue(), 0, fiveSecs); + en.setValue(clamped); } - for (Map.Entry 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 capPercentPerSec = data.getUnit() + .getCalculatedStat(ResourceStats.LEECH_CAP.get(rt)) + .getValue() / 100F; + + float maxRes = data.getResources().getMax(data.entity, rt); + float perSecondCap = capPercentPerSec * maxRes; + + // Intended drain this second (bounded by per-second cap and reservoir) + float take = Math.min(reservoir, perSecondCap); + if (take <= EPS) continue; + + // Hook: a future stat could allow full-health leeching + final boolean allowFullLeech = data.getUnit() + .getCalculatedStat(ResourceStats.LEECH_AT_FULL_HEALTH.get()).getValue() > 0; + + // Apply and get what actually landed + float applied = data.getResources().restoreAndReturnApplied( + data.entity, rt, take, + com.robertx22.mine_and_slash.uncommon.effectdatas.rework.RestoreType.leech + ); + + // Full-resource policy: + // - Non-health: never persist at full → discard. + // - Health: persist only if 'leech_at_full_health' is enabled. + // If nothing landed (resource is full), enforce full-resource policy. + if (applied <= EPS) { // use EPS to avoid float noise + boolean keepReservoir = + (rt == ResourceType.health) && allowFullLeech; // only health with talent + + if (!keepReservoir) { + entry.setValue(0f); // discard reservoir } - - addLeech(entry.getKey(), -num); - data.getResources().restore(data.entity, entry.getKey(), num); + continue; // skip draining by 'take' } - } + // Normal path: drain by intended 'take' to preserve ≤5s duration + entry.setValue(reservoir - take); + } + + // 3) Prune empty entries to keep the map small + store.entrySet().removeIf(e -> e.getValue() <= EPS); } } + 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/EffectUtils.java b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java new file mode 100644 index 000000000..6c4f43aee --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/EffectUtils.java @@ -0,0 +1,69 @@ +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.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; + +/** + * 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 + * @return ExileEffectInstanceData for the applied effect, or null if resolve failed. + */ + public static ExileEffectInstanceData applyState(ServerPlayer sp, EffectCtx ctx, int durationTicks, int stacks) { + final ExileEffect effect = resolveEffect(ctx); + if (effect == null) return null; + + return applyEffect(sp, effect, durationTicks, stacks); + } + + 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(); + 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 + 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); + return eff; + } +} 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..3e29a8206 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/event_hooks/my_events/OnResourceLost.java @@ -0,0 +1,36 @@ +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); + } +} + + + 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..e4e6b9fa8 --- /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 = 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); + } + } + + // ====== 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/mechanics/thresholds/DataDrivenSpendThresholdSpec.java b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java new file mode 100644 index 000000000..ca498f77e --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/DataDrivenSpendThresholdSpec.java @@ -0,0 +1,85 @@ +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 { FLAT, PERCENT_OF_MAX } + + private final ThresholdMode mode; + private final float value; + private final boolean multiplyByLevel; + @Nullable private final ResourceType percentMaxOf; + private final boolean showUi; + + 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, + boolean showUi + ) { + super(resource, 0f, key, + lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, showUi); + 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); + } + + @Override + public float thresholdFor(EntityData unit) { + float base; + switch (mode) { + case PERCENT_OF_MAX -> { + // default to this spec's resource if percentOf is null + ResourceType tgt = (percentMaxOf != null) ? percentMaxOf : resource(); + float max = unit.getResources().getMax(unit.getEntity(), tgt); + base = (value / 100f) * max; + } + 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; } + + // 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 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..39cc5beb2 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdManager.java @@ -0,0 +1,94 @@ +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() { + // No-op: thresholds are defined via datapack JSON. + } + + // ===== 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) { + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked by cooldown")); + } + continue; + } + + // Locks (effects + optional perk requirement) + if (spec.isLockedFor(unit)) { + if (spec.dropProgressWhileLocked()) { + tracker.clearKey(type, key); + } + if (debug) { + sp.sendSystemMessage(net.minecraft.network.chat.Component.literal("[SPEND:" + spec.key() + "] locked")); + } + // UI updates omitted (packet not included in this commit) + continue; + } + + + float threshold = spec.thresholdFor(unit); + 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); + if (spec.resetOnProc()) { + tracker.clearKey(type, key); + } + if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")"); + // UI/active tracking omitted + } 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 + } + } + } + + // --- 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); } +} 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..6653eadcc --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/SpendThresholdSpec.java @@ -0,0 +1,118 @@ +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 java.util.Collections; +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; + private final boolean showUi; // whether to render progress HUD for this spec + + // registry ordering (lower runs first) + private int priority = 0; + + // 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, + boolean showUi) { + 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; + 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; } + 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; } + public boolean showUi() { return showUi; } + + + public SpendThresholdSpec withPriority(int p) { + this.priority = 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, 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); + } + + /** 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 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) { + 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/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..7af2dc981 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdDef.java @@ -0,0 +1,148 @@ +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 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.*; + +public class SpendThresholdDef { + public String key; + 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 = "FLAT"; + public float value = 0f; + @SerializedName("multiply_by_level") public boolean multiplyByLevel = false; + @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 = false; + @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_ticks") + public int cooldownTicks = 0; + + @SerializedName("require_stat") + public String requireStatId = ""; + + public static class ProcAction { + public String action; // "apply_effect" + @SerializedName("exile_potion_id") public String effectId; + @SerializedName("duration_ticks") public int durationTicks = 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 = parseResource(resource, ResourceType.energy); + + // Modes supported: FLAT (optionally with multiply_by_level) or PERCENT_OF_MAX + String rawMode = (threshold.mode == null ? "FLAT" : threshold.mode.trim()).toUpperCase(Locale.ROOT); + boolean mult = threshold.multiplyByLevel; + DataDrivenSpendThresholdSpec.ThresholdMode mode = + "PERCENT_OF_MAX".equals(rawMode) + ? DataDrivenSpendThresholdSpec.ThresholdMode.PERCENT_OF_MAX + : DataDrivenSpendThresholdSpec.ThresholdMode.FLAT; // default + treats legacy values as FLAT + + 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 = (locks != null && locks.effects != null) + ? new HashSet<>(locks.effects) : Collections.emptySet(); + + return new DataDrivenSpendThresholdSpec( + key, + res, + mode, + threshold.value, + mult, + percentOf, + lockEff, + cooldownTicks, + 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 store = unit.getStatusEffectsData(); + + for (ProcAction a : onProc) { + if (!"exile_effect".equalsIgnoreCase(a.action) || a.effectId == null) continue; + var effect = ExileDB.ExileEffects().get(a.effectId); + if (effect == null) continue; + + 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 + + } + } + + @Override + public boolean isLockedFor(EntityData unit) { + if (super.isEffectLocked(unit)) return true; + if (requireStatId != null && !requireStatId.isEmpty()) { + var st = 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 new file mode 100644 index 000000000..1b2ccbac0 --- /dev/null +++ b/src/main/java/com/robertx22/mine_and_slash/mechanics/thresholds/datapack/SpendThresholdsReloadListener.java @@ -0,0 +1,51 @@ +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 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(); + + 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/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(); 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/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); 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; + } +} + + 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