Files
purpur-tyrant/src/main/java/games/dmg/spigottyrant/GameLifecycleService.java
T
dmg 58e6dec724
Release / release (push) Successful in 2m12s
CI / build (push) Successful in 57s
feat(admin): add game lifecycle controls
2026-08-14 22:42:25 -04:00

138 lines
4.8 KiB
Java

package games.dmg.spigottyrant;
import java.time.Duration;
import java.time.Instant;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public final class GameLifecycleService {
public LifecycleState start(
PersistentState state,
UUID tyrantId,
Optional<UUID> vigilanteId,
Instant now
) {
if (state.game().lifecycle() != GameLifecycle.UNSTARTED) {
throw new IllegalStateException("game has already been started");
}
if (vigilanteId.filter(tyrantId::equals).isPresent()) {
throw new IllegalArgumentException("Tyrant and Vigilante must be different players");
}
GameState game = new GameState(
GameLifecycle.RUNNING,
Optional.of(tyrantId),
vigilanteId,
Optional.empty(),
Optional.empty(),
Optional.empty(),
Duration.ZERO,
0,
1,
Set.of()
);
return new LifecycleState(game, state.players());
}
public LifecycleState pause(PersistentState state, Instant now) {
if (state.game().lifecycle() != GameLifecycle.RUNNING) {
throw new IllegalStateException("only a running game can be paused");
}
GameState current = state.game();
GameState paused = new GameState(
GameLifecycle.PAUSED,
current.tyrantId(),
current.vigilanteId(),
current.pendingTyrant(),
current.pendingVigilante(),
Optional.of(now),
current.accumulatedPausedTime(),
current.tyrantLevel(),
current.unspentChoices(),
current.purchases()
);
return new LifecycleState(paused, state.players());
}
public LifecycleState resume(PersistentState state, Instant now) {
GameState current = state.game();
if (current.lifecycle() != GameLifecycle.PAUSED || current.pausedAt().isEmpty()) {
throw new IllegalStateException("only a paused game can be resumed");
}
Instant pausedAt = current.pausedAt().orElseThrow();
Duration pausedFor = Duration.between(pausedAt, now);
if (pausedFor.isNegative()) {
throw new IllegalArgumentException("resume time must not precede pause time");
}
GameState resumed = new GameState(
GameLifecycle.RUNNING,
current.tyrantId(),
current.vigilanteId(),
shiftSelection(current.pendingTyrant(), pausedFor),
shiftSelection(current.pendingVigilante(), pausedFor),
Optional.empty(),
current.accumulatedPausedTime().plus(pausedFor),
current.tyrantLevel(),
current.unspentChoices(),
current.purchases()
);
Map<UUID, PlayerState> players = new HashMap<>();
for (PlayerState player : state.players().values()) {
players.put(player.playerId(), shift(player, pausedFor));
}
return new LifecycleState(resumed, players);
}
public LifecycleState reset(PersistentState state) {
Map<UUID, PlayerState> players = new HashMap<>();
for (PlayerState player : state.players().values()) {
players.put(player.playerId(), new PlayerState(
player.playerId(),
player.latestName(),
player.lastLogin(),
player.optedOutUntil(),
TyrantClass.NONE,
Optional.empty(),
Map.of(),
Set.of(),
player.capturedMobs()
));
}
return new LifecycleState(GameState.empty(), players);
}
private static Optional<PendingSelection> shiftSelection(
Optional<PendingSelection> selection,
Duration pausedFor
) {
return selection.map(value -> new PendingSelection(
value.candidateId(), value.expiresAt().plus(pausedFor)
));
}
private static PlayerState shift(PlayerState player, Duration pausedFor) {
Map<Ability, Instant> cooldowns = new EnumMap<>(Ability.class);
player.cooldownEnds().forEach((ability, deadline) ->
cooldowns.put(ability, deadline.plus(pausedFor))
);
return new PlayerState(
player.playerId(),
player.latestName(),
shiftInstant(player.lastLogin(), pausedFor),
shiftInstant(player.optedOutUntil(), pausedFor),
player.tyrantClass(),
player.followerOf(),
cooldowns,
player.readyAbilityItems(),
player.capturedMobs()
);
}
private static Optional<Instant> shiftInstant(Optional<Instant> value, Duration duration) {
return value.map(instant -> instant.plus(duration));
}
}