Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResourceType, Float> lost = new java.util.EnumMap<>(ResourceType.class);
Expand Down Expand Up @@ -85,7 +86,11 @@ private float total(java.util.Set<ResourceType> types) {
private final java.util.EnumMap<ResourceType, java.util.Map<String, Float>> keyProgress =
new java.util.EnumMap<>(ResourceType.class);

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

public void clearKey(ResourceType rt, String key) {
if (key == null || key.isEmpty()) return;
var byKey = keyProgress.get(rt);
if (byKey == null) return;
Expand All @@ -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) {
Expand All @@ -116,7 +121,22 @@ 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);
}


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

/** Optional utility if you want to wipe a resource’s accumulator. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,37 @@ public static void onEndTick(ServerPlayer player) {
playerData.spellCastingData.charges.onTicks(player, 5);
}

// Every second, apply passive decay to inactive threshold progress
if (age % 20 == 0) {
long now = player.level().getGameTime();
var unit = Load.Unit(player);
if (unit != null) {
// Iterate only active keys per resource
for (var rt : ResourceType.values()) {
for (var key : unit.getSpendRuntime().getActiveKeys(rt)) {
var spec = unit.getSpendRuntime().getSpec(key);
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);
if (newVal <= 0f) {
unit.getSpendRuntime().removeActive(rt, key);
}
}
}
}
}

if (player.containerMenu instanceof CraftingStationMenu men) {
if (player.tickCount % 5 == 0) {
men.be.onTickWhenPlayerWatching(player);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,16 +27,15 @@ public DataDrivenSpendThresholdSpec(
int cooldownTicks,
boolean lockWhileCooldown,
boolean dropProgressWhileLocked,
boolean resetProgressOnProc,
boolean dropProgressOnProc,
boolean showUi
) {
super(resource, 0f, key,
lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, resetProgressOnProc, showUi);
lockWhileEffectIds, cooldownTicks, lockWhileCooldown, dropProgressWhileLocked, dropProgressOnProc);
this.mode = mode;
this.value = value;
this.multiplyByLevel = multiplyByLevel;
this.percentMaxOf = percentMaxOf;
this.showUi = showUi;
}

// Backward-compatible ctor (defaults showUi=false)
Expand All @@ -52,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
Expand All @@ -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.
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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;
}
Expand All @@ -57,7 +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"));
}
// UI updates omitted (packet not included in this commit)
continue;
}

Expand All @@ -66,21 +68,26 @@ 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);
if (spec.resetOnProc()) {
if (spec.dropProgressOnProc()) {
tracker.clearKey(type, key);
}
if (debug) dbg(sp, "[SPEND:" + spec.key() + "] " + type.id + " ×" + procs + " (thr=" + fmt(threshold) + ")");
// UI/active tracking omitted
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 (cur <= 0f) {
unit.getSpendRuntime().removeActive(type, key);
}
}
}
}
Expand All @@ -91,4 +98,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); }
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,135 @@
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<String, Long> cooldownUntil = new HashMap<>();

/** Start/refresh cooldown for a key. */
private static final class KeyState {
long cooldownUntil;
long lastActivityTick;
long lastDecayTick;
int lastProgressIntSent = Integer.MIN_VALUE;
SpendThresholdSpec spec;
}

private final Map<String, KeyState> states = new HashMap<>();

private final java.util.EnumMap<ResourceType, Set<String>> activeByResource = new java.util.EnumMap<>(ResourceType.class);
private final java.util.EnumMap<ResourceType, Set<String>> 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;
}

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

/** 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;
KeyState ks = states.computeIfAbsent(key, __ -> new KeyState());
ks.lastActivityTick = now;
ks.lastDecayTick = 0L;
}

public long getLastActivity(String key) {
if (key == null || key.isEmpty()) return 0L;
KeyState ks = states.get(key);
return ks == null ? 0L : ks.lastActivityTick;
}

public long getLastDecay(String key) {
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;
KeyState ks = states.computeIfAbsent(key, __ -> new KeyState());
ks.lastDecayTick = now;
}

public boolean progressIntChanged(String key, int 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;
}

// === Active key index ===
public void markActive(ResourceType rt, String key, SpendThresholdSpec spec) {
if (rt == null || key == null || key.isEmpty() || spec == null) return;
Set<String> 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) {
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);
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;
}
}

public Set<String> 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;
}

public SpendThresholdSpec getSpec(String key) {
if (key == null || key.isEmpty()) return null;
KeyState ks = states.get(key);
return ks == null ? null : ks.spec;
}
}
Loading