diff --git a/docs/optional-modules/games.md b/docs/optional-modules/games.md index 6a9d033..f697c1b 100644 --- a/docs/optional-modules/games.md +++ b/docs/optional-modules/games.md @@ -4,13 +4,71 @@ description: Game state management system for phased minigames. # Games -## GameState Class +The `pluginbase-games` module provides a state machine for minigame phases. Add it as a dependency alongside `pluginbase-core`: + +```xml + + dev.demeng + pluginbase-games + 1.36.1-SNAPSHOT + +``` + +## GameState + +`GameState` is an abstract class representing a single phase of a minigame (lobby, active round, post-game, etc.). Each state has a lifecycle driven by four abstract methods: + +| Method | Visibility | Description | +|---|---|---| +| `onStart()` | `protected` | Called once when `start()` is invoked. | +| `onUpdate()` | `protected` | Called on each tick while the state is active. | +| `onEnd()` | `protected` | Called once when `end()` is invoked. Bound resources are closed before this runs. | +| `getDuration()` | `protected` | Returns the state duration in milliseconds. | + +Public control methods: + +| Method | Returns | Description | +|---|---|---| +| `start()` | `void` | Starts the state. No-op if already started or ended. | +| `update()` | `void` | Ticks the state. Calls `end()` automatically if `isReadyToEnd()` returns true, otherwise calls `onUpdate()`. | +| `end()` | `void` | Ends the state: closes bound resources, then calls `onEnd()`. No-op if not started or already ended. | +| `isStarted()` | `boolean` | Whether `start()` has been called. | +| `isEnded()` | `boolean` | Whether `end()` has been called. | +| `getStartTime()` | `long` | Epoch millis when the state started, or 0 if not started. | +| `getRemainingDuration()` | `long` | Milliseconds remaining based on `getDuration()`, minimum 0. | +| `bind(AutoCloseable)` | `` | Binds a resource for automatic cleanup when the state ends. Returns the same object. | +| `bindModule(TerminableModule)` | `` | Binds a `TerminableModule` for automatic cleanup. Returns the same module. | + +### Duration and end conditions -`GameState` represents a single phase of a minigame (e.g., lobby, active game, post-game). +`isReadyToEnd()` is a `protected` method that `update()` calls each tick to decide whether to end the state. The default implementation returns true when `getRemainingDuration() <= 0`, which means the elapsed time since start has exceeded `getDuration()`. + +If `getDuration()` returns 0, the state will end on its first `update()` call. There is no built-in "infinite duration" sentinel. To create a state that runs indefinitely until manually ended, override `isReadyToEnd()`: ```java -import dev.demeng.pluginbase.games.GameState; +@Override +protected boolean isReadyToEnd() { + return false; +} + +@Override +protected long getDuration() { + return 0; +} +``` + +For custom end conditions (player count, score threshold, etc.), override `isReadyToEnd()`: +```java +@Override +protected boolean isReadyToEnd() { + return alivePlayers.size() <= 1; +} +``` + +### Minimal example + +```java public class ActiveGameState extends GameState { private final Arena arena; @@ -23,7 +81,6 @@ public class ActiveGameState extends GameState { @Override protected void onStart() { - // Called when state begins players.forEach(uuid -> { Player player = Bukkit.getPlayer(uuid); if (player != null) { @@ -34,14 +91,10 @@ public class ActiveGameState extends GameState { } @Override - protected void onUpdate() { - // Called periodically during the state - // Check win conditions, etc. - } + protected void onUpdate() {} @Override protected void onEnd() { - // Called when state ends players.forEach(uuid -> { Player player = Bukkit.getPlayer(uuid); if (player != null) { @@ -52,58 +105,79 @@ public class ActiveGameState extends GameState { @Override protected long getDuration() { - // Duration in milliseconds (0 for infinite) - return 300000; // 5 minutes + return 300_000; // 5 minutes } } ``` -## Using GameState +## Binding resources to a state + +`bind(AutoCloseable)` registers a resource (event subscription, scheduled task, etc.) for automatic cleanup when the state ends. All bound resources are closed before `onEnd()` is called. + +`GameState` does **not** implement `TerminableConsumer`, so you cannot use `.bindWith(this)` inside a state. Instead, wrap the resource with `bind()`: ```java -public class SpleefPlugin extends BasePlugin { +public class ActiveGameState extends GameState { - private GameState currentState; - private final Set players = new HashSet<>(); + @Override + protected void onStart() { + bind(Events.subscribe(BlockBreakEvent.class) + .handler(this::handleBreak)); + + bind(Schedulers.sync().runRepeating(() -> { + updateScoreboard(); + }, 0L, 20L)); + } @Override - protected void enable() { - // Start in lobby state - currentState = new LobbyState(players); - currentState.start(); + protected void onUpdate() {} - // Update state periodically - Schedulers.sync().runRepeating(() -> { - if (currentState != null && currentState.isStarted() && !currentState.isEnded()) { - currentState.update(); - } - }, 0L, 20L).bindWith(this); + @Override + protected void onEnd() {} - // Join command - Lamp handler = createCommandHandler(); - handler.register(new SpleefCommands(this)); + @Override + protected long getDuration() { + return 300_000; } - public void startGame() { - // End current state - if (currentState != null) { - currentState.end(); - } - - // Start active game state - currentState = new ActiveGameState(arena, players); - currentState.start(); + private void handleBreak(BlockBreakEvent event) { + // ... } +} +``` - public void endGame() { - // End active state - if (currentState != null) { - currentState.end(); - } +## ScheduledStateSeries - // Return to lobby - currentState = new LobbyState(players); - currentState.start(); +`ScheduledStateSeries` extends `GameState` and runs a sequence of states one after another. Each child state's `update()` is called on a fixed tick interval. When a child state's `isReadyToEnd()` returns true, it is ended and the next state starts. + +Constructors: + +| Constructor | Description | +|---|---| +| `ScheduledStateSeries(GameState...)` | States in order, 1-tick update interval. | +| `ScheduledStateSeries(List)` | States from a list, 1-tick update interval. | +| `ScheduledStateSeries(long, GameState...)` | Custom interval (in ticks), states in order. | +| `ScheduledStateSeries(long, List)` | Custom interval (in ticks), states from a list. | +| `ScheduledStateSeries()` | Empty series, 1-tick interval. | +| `ScheduledStateSeries(long)` | Empty series, custom interval. | + +The `addNext(GameState...)` and `addNext(List)` methods insert states immediately after the current one, pushing later states back. Useful for dynamic phase injection (overtime, tiebreaker, etc.). + +### Full example + +```java +public class SpleefPlugin extends BasePlugin { + + private final Set players = new HashSet<>(); + + @Override + protected void enable() { + LobbyState lobby = new LobbyState(players); + ActiveGameState active = new ActiveGameState(arena, players); + PostGameState postGame = new PostGameState(); + + ScheduledStateSeries series = new ScheduledStateSeries(lobby, active, postGame); + series.start(); } } @@ -121,74 +195,47 @@ public class LobbyState extends GameState { } @Override - protected void onUpdate() { - // Check if enough players to start - } + protected void onUpdate() {} @Override - protected void onEnd() { - // Cleanup - } + protected void onEnd() {} @Override protected long getDuration() { - return 0; // Infinite - manual transition + return 30_000; // 30 second lobby countdown } } ``` -## Binding Resources +### Manual state management (without ScheduledStateSeries) -Bind resources to the state so they're automatically cleaned up when the state ends: +If you need full control over transitions, drive `update()` yourself: ```java -public class ActiveGameState extends GameState { +public class SpleefPlugin extends BasePlugin { + + private GameState currentState; @Override - protected void onStart() { - // Events bound to this state - Events.subscribe(BlockBreakEvent.class) - .handler(this::handleBreak) - .bindWith(this); + protected void enable() { + currentState = new LobbyState(players); + currentState.start(); - // Tasks bound to this state Schedulers.sync().runRepeating(() -> { - updateScoreboard(); + if (currentState != null && currentState.isStarted() && !currentState.isEnded()) { + currentState.update(); + } }, 0L, 20L).bindWith(this); } - @Override - protected void onEnd() { - // All bound resources are automatically cleaned up + public void transition(GameState next) { + if (currentState != null) { + currentState.end(); + } + currentState = next; + currentState.start(); } - - // ... -} -``` - -## State Lifecycle - -```java -GameState state = new MyGameState(); - -// Start the state -state.start(); // Calls onStart() -state.isStarted(); // true - -// Update state -state.update(); // Calls onUpdate() - -// Check if ready to end -if (state.isReadyToEnd()) { - state.end(); // Calls onEnd() } - -state.isEnded(); // true ``` -## Benefits - -* **Lifecycle management** - Clear start, update, end phases -* **Automatic cleanup** - Bound resources cleaned when state ends -* **Duration control** - States can have fixed durations -* **Resource binding** - Events and tasks automatically terminate with state +Note: `.bindWith(this)` is valid here because `BasePlugin` implements `TerminableConsumer`. Inside a `GameState` subclass, use `bind()` instead.