feat(progression): unlock stealth from potion invisibility
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package games.dmg.spigotstealth;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
/** Accumulates qualifying potion invisibility using monotonic elapsed time. */
|
||||
public final class QualifyingInvisibilityService {
|
||||
private final StealthStateManager stateManager;
|
||||
private final long unlockThresholdMillis;
|
||||
private final LongSupplier nanoTime;
|
||||
private final Consumer<UUID> unlockNotifier;
|
||||
private final Map<UUID, ActiveInterval> activeIntervals = new HashMap<>();
|
||||
|
||||
public QualifyingInvisibilityService(
|
||||
StealthStateManager stateManager,
|
||||
Duration unlockThreshold,
|
||||
LongSupplier nanoTime,
|
||||
Consumer<UUID> unlockNotifier) {
|
||||
this.stateManager = Objects.requireNonNull(stateManager, "stateManager");
|
||||
this.unlockThresholdMillis = Objects.requireNonNull(unlockThreshold, "unlockThreshold").toMillis();
|
||||
this.nanoTime = Objects.requireNonNull(nanoTime, "nanoTime");
|
||||
this.unlockNotifier = Objects.requireNonNull(unlockNotifier, "unlockNotifier");
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> begin(UUID playerId, String playerName, Instant startedAt) {
|
||||
Objects.requireNonNull(playerId, "playerId");
|
||||
Objects.requireNonNull(startedAt, "startedAt");
|
||||
long nowNanos = nanoTime.getAsLong();
|
||||
ActiveInterval previous = activeIntervals.put(playerId, new ActiveInterval(nowNanos, startedAt, playerName));
|
||||
return record(playerId, playerName, elapsedMillis(previous, nowNanos), startedAt, true);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> checkpoint(UUID playerId) {
|
||||
long nowNanos = nanoTime.getAsLong();
|
||||
ActiveInterval previous = activeIntervals.get(playerId);
|
||||
if (previous == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
long elapsedMillis = elapsedMillis(previous, nowNanos);
|
||||
Instant continuedAt = previous.startedAt().plusMillis(elapsedMillis);
|
||||
activeIntervals.put(playerId, new ActiveInterval(nowNanos, continuedAt, previous.playerName()));
|
||||
return record(playerId, previous.playerName(), elapsedMillis, continuedAt, true);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> stop(UUID playerId) {
|
||||
long nowNanos = nanoTime.getAsLong();
|
||||
ActiveInterval previous = activeIntervals.remove(playerId);
|
||||
if (previous == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
return record(playerId, previous.playerName(), elapsedMillis(previous, nowNanos), null, false);
|
||||
}
|
||||
|
||||
public boolean isQualifying(UUID playerId) {
|
||||
return activeIntervals.containsKey(playerId);
|
||||
}
|
||||
|
||||
public Set<UUID> activePlayerIds() {
|
||||
return Set.copyOf(activeIntervals.keySet());
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> stopAll() {
|
||||
CompletableFuture<?>[] stops = activePlayerIds().stream()
|
||||
.map(this::stop)
|
||||
.toArray(CompletableFuture[]::new);
|
||||
return CompletableFuture.allOf(stops);
|
||||
}
|
||||
|
||||
public long currentAccumulatedMillis(UUID playerId) {
|
||||
long persisted = stateManager.snapshot().player(playerId).accumulatedMillis();
|
||||
return Math.addExact(persisted, elapsedMillis(activeIntervals.get(playerId), nanoTime.getAsLong()));
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> record(
|
||||
UUID playerId,
|
||||
String playerName,
|
||||
long elapsedMillis,
|
||||
Instant qualifyingSince,
|
||||
boolean active) {
|
||||
AtomicBoolean unlockedNow = new AtomicBoolean();
|
||||
CompletableFuture<Void> saved = stateManager.update(state -> {
|
||||
PlayerStealthState player = state.player(playerId);
|
||||
long accumulated = Math.addExact(player.accumulatedMillis(), elapsedMillis);
|
||||
boolean unlocked = player.unlocked() || accumulated >= unlockThresholdMillis;
|
||||
unlockedNow.set(unlocked && !player.unlocked());
|
||||
PlayerStealthState updated = new PlayerStealthState(
|
||||
playerId,
|
||||
playerName,
|
||||
accumulated,
|
||||
unlocked,
|
||||
player.preparedLogin(),
|
||||
player.concealed(),
|
||||
active ? qualifyingSince : null,
|
||||
player.unknownFields());
|
||||
return state.withPlayer(updated);
|
||||
});
|
||||
return saved.thenRun(() -> {
|
||||
if (unlockedNow.get()) {
|
||||
unlockNotifier.accept(playerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static long elapsedMillis(ActiveInterval interval, long nowNanos) {
|
||||
if (interval == null) {
|
||||
return 0L;
|
||||
}
|
||||
long elapsedNanos = Math.max(0L, nowNanos - interval.startedNanos());
|
||||
return Duration.ofNanos(elapsedNanos).toMillis();
|
||||
}
|
||||
|
||||
private record ActiveInterval(long startedNanos, Instant startedAt, String playerName) { }
|
||||
}
|
||||
Reference in New Issue
Block a user