diff --git a/src/main/java/com/mythlane/gravityflip/physics/FallDamageGuard.java b/src/main/java/com/mythlane/gravityflip/physics/FallDamageGuard.java
new file mode 100644
index 0000000..3b7c890
--- /dev/null
+++ b/src/main/java/com/mythlane/gravityflip/physics/FallDamageGuard.java
@@ -0,0 +1,85 @@
+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(); }
+}
diff --git a/src/test/java/com/mythlane/gravityflip/physics/FallDamageGuardTest.java b/src/test/java/com/mythlane/gravityflip/physics/FallDamageGuardTest.java
new file mode 100644
index 0000000..44c197f
--- /dev/null
+++ b/src/test/java/com/mythlane/gravityflip/physics/FallDamageGuardTest.java
@@ -0,0 +1,97 @@
+package com.mythlane.gravityflip.physics;
+
+import com.hypixel.hytale.math.shape.Box;
+import com.hypixel.hytale.math.vector.Vector3d;
+import com.mythlane.gravityflip.region.GravityFlipRegion;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Pure-data tests for {@link FallDamageGuard} — no Hytale runtime dependency.
+ * Covers entry / in-region / exit / grace-window / re-entry / FallDamage=true override.
+ */
+class FallDamageGuardTest {
+
+ @Test
+ void notInRegionReturnsFalse() {
+ FallDamageGuard guard = new FallDamageGuard();
+ assertFalse(guard.shouldSuppressFallDamage(UUID.randomUUID(), 1000L));
+ }
+
+ @Test
+ void inRegionFallDamageTrueReturnsFalse() {
+ FallDamageGuard guard = new FallDamageGuard();
+ UUID uuid = UUID.randomUUID();
+ GravityFlipRegion region = region(true, 2500);
+ guard.markInRegion(uuid, region);
+ assertFalse(guard.shouldSuppressFallDamage(uuid, 1000L));
+ }
+
+ @Test
+ void inRegionFallDamageFalseReturnsTrue() {
+ FallDamageGuard guard = new FallDamageGuard();
+ UUID uuid = UUID.randomUUID();
+ GravityFlipRegion region = region(false, 2500);
+ guard.markInRegion(uuid, region);
+ assertTrue(guard.shouldSuppressFallDamage(uuid, 1000L));
+ }
+
+ @Test
+ void afterExitWithinGraceReturnsTrue() {
+ FallDamageGuard guard = new FallDamageGuard();
+ UUID uuid = UUID.randomUUID();
+ GravityFlipRegion region = region(false, 2500);
+ guard.markInRegion(uuid, region);
+ long t0 = 1000L;
+ guard.markExit(uuid, region, t0);
+ assertTrue(guard.shouldSuppressFallDamage(uuid, t0 + 2499));
+ }
+
+ @Test
+ void afterExitBeyondGraceReturnsFalse() {
+ FallDamageGuard guard = new FallDamageGuard();
+ UUID uuid = UUID.randomUUID();
+ GravityFlipRegion region = region(false, 2500);
+ guard.markInRegion(uuid, region);
+ long t0 = 1000L;
+ guard.markExit(uuid, region, t0);
+ assertFalse(guard.shouldSuppressFallDamage(uuid, t0 + 2501));
+ }
+
+ @Test
+ void reEntryAfterExitResetsTracking() {
+ FallDamageGuard guard = new FallDamageGuard();
+ UUID uuid = UUID.randomUUID();
+ GravityFlipRegion region = region(false, 2500);
+ guard.markInRegion(uuid, region);
+ guard.markExit(uuid, region, 1000L);
+ guard.markInRegion(uuid, region); // re-enter
+ // In-region again with FallDamage=false → immediate suppression, grace reset.
+ assertTrue(guard.shouldSuppressFallDamage(uuid, 1500L));
+ }
+
+ @Test
+ void fallDamageTrueRegionNeverSuppressesEvenDuringGrace() {
+ FallDamageGuard guard = new FallDamageGuard();
+ UUID uuid = UUID.randomUUID();
+ GravityFlipRegion suppressed = region(false, 2500);
+ GravityFlipRegion allowed = region(true, 2500);
+ guard.markInRegion(uuid, suppressed);
+ guard.markExit(uuid, suppressed, 1000L);
+ // New region has FallDamage=true → override immediately.
+ guard.markInRegion(uuid, allowed);
+ assertFalse(guard.shouldSuppressFallDamage(uuid, 1500L));
+ }
+
+ private static GravityFlipRegion region(boolean fallDamage, int graceMs) {
+ GravityFlipRegion r = new GravityFlipRegion(
+ "t", new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)), true);
+ r.setFallDamage(fallDamage);
+ r.setGracePeriodMs(graceMs);
+ return r;
+ }
+}