package com.mythlane.gravityflip.physics;
import com.mythlane.gravityflip.region.GravityFlipRegion;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Thread-safe tracker that decides whether fall damage should be suppressed for a given UUID
* based on (a) current in-region membership, (b) post-exit grace window.
*
*
Pure-data service : no Hytale runtime dependency (no ECS, no PhysicsValues), injected
* via constructor into {@link GravityApplier} (populator) and
* {@link FallDamageSuppressorSystem} (consumer) — no static mutable state.
*
*
Precedence rule (multi-region) : when {@link #markInRegion} is called, the LAST
* call for a given UUID within a single tick wins. {@link GravityApplier} takes care of
* passing the FIRST matched region (iteration order of {@code enabledRegions}), so for an
* entity simultaneously in N regions the first-match region's {@code FallDamage}
* and {@code GracePeriodMs} drive suppression.
*
*
State machine :
*
* - {@code markInRegion(uuid, region)} → currentRegion[uuid]=region ; grace entries cleared.
* - {@code markExit(uuid, region, nowMs)} → currentRegion[uuid] removed ; grace entry stored
* (region + timestamp).
* - {@code shouldSuppressFallDamage(uuid, nowMs)} :
*
* - If current region != null AND current region {@code FallDamage==false} → true.
* - Else if grace entry present AND grace region {@code FallDamage==false} AND
* {@code nowMs - exitMs <= gracePeriodMs} → true.
* - Else → false.
*
*
*
*/
public final class FallDamageGuard {
/** Entity currently inside a region (cleared on exit). */
private final ConcurrentHashMap currentRegionByUuid =
new ConcurrentHashMap<>();
/** Timestamp (ms) at which the entity last exited a region. */
private final ConcurrentHashMap exitTimestampByUuid =
new ConcurrentHashMap<>();
/** Region referenced at the moment of exit (used to read FallDamage + GracePeriodMs). */
private final ConcurrentHashMap regionAtExitByUuid =
new ConcurrentHashMap<>();
public void markInRegion(UUID uuid, GravityFlipRegion region) {
if (uuid == null || region == null) return;
currentRegionByUuid.put(uuid, region);
// Re-entry ⇒ discard any stale grace entry (grace is a post-exit concept).
exitTimestampByUuid.remove(uuid);
regionAtExitByUuid.remove(uuid);
}
public void markExit(UUID uuid, GravityFlipRegion region, long nowMs) {
if (uuid == null || region == null) return;
currentRegionByUuid.remove(uuid);
exitTimestampByUuid.put(uuid, nowMs);
regionAtExitByUuid.put(uuid, region);
}
public boolean shouldSuppressFallDamage(UUID uuid, long nowMs) {
if (uuid == null) return false;
GravityFlipRegion current = currentRegionByUuid.get(uuid);
if (current != null) {
return !current.isFallDamage();
}
Long exitMs = exitTimestampByUuid.get(uuid);
GravityFlipRegion exitRegion = regionAtExitByUuid.get(uuid);
if (exitMs == null || exitRegion == null) return false;
if (exitRegion.isFallDamage()) return false; // region allowed fall damage → no grace
return (nowMs - exitMs) <= exitRegion.getGracePeriodMs();
}
// ---- Test hooks / diagnostics (package-private) ----
int trackedInRegionCount() { return currentRegionByUuid.size(); }
int trackedGraceCount() { return exitTimestampByUuid.size(); }
}