Complete Stage 1: Core Game Loop

- Add GameState enum (LOBBY, ROLE_ASSIGN, IN_GAME, ENDING)
- Add Phase interface with enter/tick/exit/isComplete
- Add GameSession with tick-based phase machine
- Add GameEventBus for internal events
- Add GameManager for session lifecycle
- Add all phases: LobbyPhase, RoleAssignPhase, GameplayPhase, EndgamePhase
- Add role assignment: RoleAssigner, RoleQueue, SpawnPosition
- Add BasePlayer, Survivor, Killer, PlayerState
- Add DBDCommand with /dbd join/leave/create/forcestart/stop
- Update mdbd.java with GameManager and command registration
- Add command definition to paper-plugin.yml
- Mark Stage 1 complete in PLAN.md
This commit is contained in:
loki5512344 2026-05-31 20:43:40 +02:00
parent 53f3f6e9da
commit 82ed455cd2
20 changed files with 884 additions and 8 deletions

View file

@ -0,0 +1,148 @@
package dev.loki.command;
import dev.loki.core.GameManager;
import dev.loki.mdbd;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DBDCommand implements TabExecutor {
private final mdbd plugin;
private final GameManager gameManager;
public DBDCommand(mdbd plugin, GameManager gameManager) {
this.plugin = plugin;
this.gameManager = gameManager;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 0) {
sendUsage(sender);
return true;
}
String sub = args[0].toLowerCase();
return switch (sub) {
case "join" -> handleJoin(sender);
case "leave" -> handleLeave(sender);
case "forcestart" -> handleForceStart(sender);
case "create" -> handleCreate(sender);
case "stop" -> handleStop(sender);
default -> {
sendUsage(sender);
yield true;
}
};
}
private boolean handleJoin(CommandSender sender) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED));
return true;
}
if (gameManager.joinGame(player)) {
player.sendMessage(Component.text("[DBD] Вы присоединились к игре!", NamedTextColor.GREEN));
} else {
player.sendMessage(Component.text("[DBD] Не удалось присоединиться", NamedTextColor.RED));
}
return true;
}
private boolean handleLeave(CommandSender sender) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED));
return true;
}
if (gameManager.leaveGame(player)) {
player.sendMessage(Component.text("[DBD] Вы покинули игру", NamedTextColor.YELLOW));
} else {
player.sendMessage(Component.text("[DBD] Вы не в игре", NamedTextColor.RED));
}
return true;
}
private boolean handleForceStart(CommandSender sender) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
return true;
}
var session = gameManager.getCurrentSession();
if (session == null) {
sender.sendMessage(Component.text("Нет активной игры!", NamedTextColor.RED));
return true;
}
var phase = session.getCurrentPhase();
if (phase instanceof dev.loki.phase.LobbyPhase lobby) {
lobby.forceStart();
sender.sendMessage(Component.text("[DBD] Игра форсирована!", NamedTextColor.GREEN));
} else {
sender.sendMessage(Component.text("Нельзя форсировать сейчас", NamedTextColor.RED));
}
return true;
}
private boolean handleCreate(CommandSender sender) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
return true;
}
if (gameManager.createGame()) {
sender.sendMessage(Component.text("[DBD] Игра создана!", NamedTextColor.GREEN));
} else {
sender.sendMessage(Component.text("[DBD] Игра уже существует", NamedTextColor.RED));
}
return true;
}
private boolean handleStop(CommandSender sender) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
return true;
}
gameManager.endGame();
sender.sendMessage(Component.text("[DBD] Игра остановлена", NamedTextColor.YELLOW));
return true;
}
private void sendUsage(CommandSender sender) {
sender.sendMessage(Component.text("=== DBD Commands ===", NamedTextColor.GOLD));
sender.sendMessage(Component.text("/dbd join - присоединиться", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd leave - покинуть", NamedTextColor.GRAY));
if (sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("/dbd create - создать игру", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd forcestart - форс старт", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd stop - остановить", NamedTextColor.GRAY));
}
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
List<String> completions = new ArrayList<>(Arrays.asList("join", "leave"));
if (sender.hasPermission("mdbd.admin")) {
completions.addAll(Arrays.asList("create", "forcestart", "stop"));
}
return completions.stream()
.filter(s -> s.startsWith(args[0].toLowerCase()))
.toList();
}
return List.of();
}
private Object getCurrentPhase() {
var session = gameManager.getCurrentSession();
return session != null ? session.getState() : null;
}
}

View file

@ -0,0 +1,30 @@
package dev.loki.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class GameEventBus {
private final Map<Class<?>, List<Consumer<Object>>> listeners = new HashMap<>();
@SuppressWarnings("unchecked")
public <T> void on(Class<T> eventClass, Consumer<T> listener) {
listeners.computeIfAbsent(eventClass, k -> new ArrayList<>())
.add((Consumer<Object>) listener);
}
public void emit(Object event) {
List<Consumer<Object>> eventListeners = listeners.get(event.getClass());
if (eventListeners != null) {
for (Consumer<Object> listener : eventListeners) {
listener.accept(event);
}
}
}
public void clear() {
listeners.clear();
}
}

View file

@ -0,0 +1,68 @@
package dev.loki.core;
import dev.loki.mdbd;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class GameManager {
private final mdbd plugin;
private final Map<UUID, GameSession> playerSessions;
private GameSession currentSession;
public GameManager(mdbd plugin) {
this.plugin = plugin;
this.playerSessions = new HashMap<>();
}
public boolean createGame() {
if (currentSession != null) {
return false;
}
currentSession = new GameSession(plugin);
currentSession.start();
return true;
}
public boolean joinGame(Player player) {
if (currentSession == null) {
return false;
}
if (playerSessions.containsKey(player.getUniqueId())) {
return false;
}
playerSessions.put(player.getUniqueId(), currentSession);
return true;
}
public boolean leaveGame(Player player) {
GameSession session = playerSessions.remove(player.getUniqueId());
if (session != null) {
session.removePlayer(player.getUniqueId());
return true;
}
return false;
}
public void endGame() {
if (currentSession != null) {
currentSession.stop();
currentSession = null;
playerSessions.clear();
}
}
public GameSession getCurrentSession() {
return currentSession;
}
public boolean isInGame(UUID uuid) {
return playerSessions.containsKey(uuid);
}
public GameSession getPlayerSession(UUID uuid) {
return playerSessions.get(uuid);
}
}

View file

@ -0,0 +1,128 @@
package dev.loki.core;
import dev.loki.config.GameConfig;
import dev.loki.mdbd;
import dev.loki.phase.LobbyPhase;
import dev.loki.phase.Phase;
import dev.loki.player.BasePlayer;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class GameSession {
private final mdbd plugin;
private final GameEventBus eventBus;
private final Map<UUID, BasePlayer> players;
private Phase currentPhase;
private BukkitTask tickTask;
private int ticksElapsed;
public GameSession(mdbd plugin) {
this.plugin = plugin;
this.eventBus = new GameEventBus();
this.players = new HashMap<>();
this.ticksElapsed = 0;
}
public void start() {
switchPhase(new LobbyPhase(plugin.getConfigManager().getGame()));
this.tickTask = Bukkit.getScheduler().runTaskTimer(plugin, this::tick, 1L, 1L);
plugin.getLogger().info("Game session started");
}
public void stop() {
if (tickTask != null) {
tickTask.cancel();
tickTask = null;
}
if (currentPhase != null) {
currentPhase.exit(this);
}
players.clear();
eventBus.clear();
plugin.getLogger().info("Game session stopped");
}
private void tick() {
ticksElapsed++;
if (currentPhase != null) {
currentPhase.tick(this);
if (currentPhase.isComplete()) {
transitionToNextPhase();
}
}
}
private void transitionToNextPhase() {
currentPhase.exit(this);
Phase nextPhase = determineNextPhase();
if (nextPhase != null) {
switchPhase(nextPhase);
} else {
stop();
}
}
private Phase determineNextPhase() {
return null;
}
public void switchPhase(Phase newPhase) {
if (currentPhase != null) {
currentPhase.exit(this);
}
currentPhase = newPhase;
currentPhase.enter(this);
}
public void addPlayer(BasePlayer player) {
players.put(player.getUuid(), player);
eventBus.emit(new PlayerJoinEvent(player));
}
public void removePlayer(UUID uuid) {
BasePlayer removed = players.remove(uuid);
if (removed != null) {
eventBus.emit(new PlayerLeaveEvent(removed));
}
}
public BasePlayer getPlayer(UUID uuid) {
return players.get(uuid);
}
public Map<UUID, BasePlayer> getPlayers() {
return new HashMap<>(players);
}
public int getPlayerCount() {
return players.size();
}
public GameState getState() {
if (currentPhase instanceof LobbyPhase) return GameState.LOBBY;
return GameState.IN_GAME;
}
public mdbd getPlugin() {
return plugin;
}
public GameConfig getConfig() {
return plugin.getConfigManager().getGame();
}
public int getTicksElapsed() {
return ticksElapsed;
}
public GameEventBus getEventBus() {
return eventBus;
}
public record PlayerJoinEvent(BasePlayer player) {}
public record PlayerLeaveEvent(BasePlayer player) {}
}

View file

@ -0,0 +1,18 @@
package dev.loki.core;
public enum GameState {
LOBBY("Ожидание игроков"),
ROLE_ASSIGN("Назначение ролей"),
IN_GAME("Игра идет"),
ENDING("Завершение");
private final String displayName;
GameState(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View file

@ -1,11 +1,14 @@
package dev.loki;
import dev.loki.command.DBDCommand;
import dev.loki.config.ConfigManager;
import dev.loki.core.GameManager;
import org.bukkit.plugin.java.JavaPlugin;
public final class mdbd extends JavaPlugin {
private static mdbd instance;
private ConfigManager configManager;
private GameManager gameManager;
@Override
public void onEnable() {
@ -15,12 +18,20 @@ public final class mdbd extends JavaPlugin {
this.configManager = new ConfigManager(this);
this.configManager.loadConfigs();
this.gameManager = new GameManager(this);
getCommand("dbd").setExecutor(new DBDCommand(this, gameManager));
getCommand("dbd").setTabCompleter(new DBDCommand(this, gameManager));
getLogger().info("Mdbd Plugin enabled successfully!");
}
@Override
public void onDisable() {
getLogger().info("Mdbd Plugin disabling...");
if (gameManager != null) {
gameManager.endGame();
}
instance = null;
}
@ -31,4 +42,8 @@ public final class mdbd extends JavaPlugin {
public ConfigManager getConfigManager() {
return configManager;
}
public GameManager getGameManager() {
return gameManager;
}
}

View file

@ -0,0 +1,31 @@
package dev.loki.phase;
import dev.loki.core.GameSession;
public class EndgamePhase implements Phase {
private int collapseTicks;
private boolean complete;
@Override
public void enter(GameSession session) {
collapseTicks = session.getConfig().getGame().getInt("endgame.collapse-seconds", 120) * 20;
complete = false;
}
@Override
public void tick(GameSession session) {
collapseTicks--;
if (collapseTicks <= 0) {
complete = true;
}
}
@Override
public void exit(GameSession session) {
}
@Override
public boolean isComplete() {
return complete;
}
}

View file

@ -0,0 +1,36 @@
package dev.loki.phase;
import dev.loki.core.GameSession;
import org.bukkit.Bukkit;
public class GameplayPhase implements Phase {
private int gameTicks;
private boolean shouldEnd;
@Override
public void enter(GameSession session) {
Bukkit.broadcastMessage("[DBD] Игра началась! Выжившие, чините генераторы!");
gameTicks = 0;
shouldEnd = false;
}
@Override
public void tick(GameSession session) {
gameTicks++;
// TODO: Check endgame conditions
}
@Override
public void exit(GameSession session) {
Bukkit.broadcastMessage("[DBD] Игра завершена!");
}
@Override
public boolean isComplete() {
return shouldEnd;
}
public void forceEnd() {
shouldEnd = true;
}
}

View file

@ -0,0 +1,74 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
public class LobbyPhase implements Phase {
private final GameConfig config;
private int countdown;
private int secondsLeft;
private boolean started;
public LobbyPhase(GameConfig config) {
this.config = config;
this.secondsLeft = config.getLobbyCountdown();
this.countdown = 0;
this.started = false;
}
@Override
public void enter(GameSession session) {
Bukkit.broadcast(Component.text("[DBD] Лобби открыто! Используйте /dbd join", NamedTextColor.GREEN));
}
@Override
public void tick(GameSession session) {
int playerCount = session.getPlayerCount();
int minPlayers = config.getMinPlayers();
if (playerCount < minPlayers) {
secondsLeft = config.getLobbyCountdown();
countdown = 0;
started = false;
return;
}
countdown++;
if (countdown >= 20) {
countdown = 0;
secondsLeft--;
if (secondsLeft <= 5 || secondsLeft % 10 == 0) {
Bukkit.broadcast(Component.text(
"[DBD] До начала: " + secondsLeft + " сек. (" + playerCount + "/" + minPlayers + ")",
NamedTextColor.YELLOW));
}
}
if (secondsLeft <= 0) {
started = true;
}
}
@Override
public void exit(GameSession session) {
Bukkit.broadcast(Component.text("[DBD] Игра начинается!", NamedTextColor.GREEN));
}
@Override
public boolean isComplete() {
return started;
}
public void forceStart() {
secondsLeft = 0;
started = true;
}
public int getSecondsLeft() {
return secondsLeft;
}
}

View file

@ -0,0 +1,10 @@
package dev.loki.phase;
import dev.loki.core.GameSession;
public interface Phase {
void enter(GameSession session);
void tick(GameSession session);
void exit(GameSession session);
boolean isComplete();
}

View file

@ -0,0 +1,40 @@
package dev.loki.phase;
import dev.loki.core.GameSession;
import dev.loki.role.RoleAssigner;
import org.bukkit.Bukkit;
public class RoleAssignPhase implements Phase {
private final RoleAssigner assigner;
private int ticksLeft;
private boolean complete;
public RoleAssignPhase(RoleAssigner assigner) {
this.assigner = assigner;
this.complete = false;
}
@Override
public void enter(GameSession session) {
ticksLeft = session.getConfig().getRoleAssignDelay() * 20;
assigner.assignRoles(session);
}
@Override
public void tick(GameSession session) {
ticksLeft--;
if (ticksLeft <= 0) {
complete = true;
}
}
@Override
public void exit(GameSession session) {
Bukkit.broadcastMessage("[DBD] Роли назначены!");
}
@Override
public boolean isComplete() {
return complete;
}
}

View file

@ -0,0 +1,49 @@
package dev.loki.player;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.UUID;
public abstract class BasePlayer {
protected final UUID uuid;
protected PlayerState state;
protected Location spawnLocation;
public BasePlayer(UUID uuid) {
this.uuid = uuid;
this.state = PlayerState.HEALTHY;
}
public UUID getUuid() {
return uuid;
}
public Player getPlayer() {
return Bukkit.getPlayer(uuid);
}
public PlayerState getState() {
return state;
}
public void setState(PlayerState state) {
this.state = state;
}
public Location getSpawnLocation() {
return spawnLocation;
}
public void setSpawnLocation(Location location) {
this.spawnLocation = location;
}
public boolean isOnline() {
Player player = getPlayer();
return player != null && player.isOnline();
}
public abstract boolean isKiller();
}

View file

@ -0,0 +1,25 @@
package dev.loki.player;
import java.util.UUID;
public class Killer extends BasePlayer {
private boolean carrying;
public Killer(UUID uuid) {
super(uuid);
this.carrying = false;
}
@Override
public boolean isKiller() {
return true;
}
public boolean isCarrying() {
return carrying;
}
public void setCarrying(boolean carrying) {
this.carrying = carrying;
}
}

View file

@ -0,0 +1,21 @@
package dev.loki.player;
public enum PlayerState {
HEALTHY("Здоров"),
INJURED("Ранен"),
DOWNED("В нокдауне"),
HOOKED("На крюке"),
CARRIED("Унесен"),
DEAD("Мертв"),
ESCAPED("Сбежал");
private final String displayName;
PlayerState(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View file

@ -0,0 +1,25 @@
package dev.loki.player;
import java.util.UUID;
public class Survivor extends BasePlayer {
private int hooksTaken;
public Survivor(UUID uuid) {
super(uuid);
this.hooksTaken = 0;
}
@Override
public boolean isKiller() {
return false;
}
public int getHooksTaken() {
return hooksTaken;
}
public void incrementHooks() {
hooksTaken++;
}
}

View file

@ -0,0 +1,65 @@
package dev.loki.role;
import dev.loki.config.LocationsConfig;
import dev.loki.core.GameSession;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import java.util.*;
public class RoleAssigner {
private final RoleQueue queue;
private final LocationsConfig locations;
public RoleAssigner(LocationsConfig locations) {
this.queue = new RoleQueue();
this.locations = locations;
}
public void registerPlayer(UUID uuid) {
queue.addPlayer(uuid);
}
public void unregisterPlayer(UUID uuid) {
queue.removePlayer(uuid);
}
public void assignRoles(GameSession session) {
Map<UUID, BasePlayer> players = session.getPlayers();
if (players.isEmpty()) return;
List<UUID> uuids = new ArrayList<>(players.keySet());
UUID killerUuid = selectKiller(uuids);
for (UUID uuid : uuids) {
BasePlayer player;
if (uuid.equals(killerUuid)) {
player = new Killer(uuid);
Location spawn = locations.getKillerSpawn();
player.setSpawnLocation(spawn);
} else {
player = new Survivor(uuid);
}
session.getEventBus().emit(new RoleAssignedEvent(uuid, player));
}
queue.rotate();
}
private UUID selectKiller(List<UUID> uuids) {
UUID queuedKiller = queue.nextKiller();
if (queuedKiller != null && uuids.contains(queuedKiller)) {
return queuedKiller;
}
return uuids.get(new Random().nextInt(uuids.size()));
}
public void resetQueue() {
queue.clear();
}
public record RoleAssignedEvent(UUID uuid, BasePlayer player) {}
}

View file

@ -0,0 +1,59 @@
package dev.loki.role;
import java.util.*;
public class RoleQueue {
private final Map<UUID, Integer> queuePosition;
private final List<UUID> queue;
public RoleQueue() {
this.queuePosition = new HashMap<>();
this.queue = new ArrayList<>();
}
public void addPlayer(UUID uuid) {
if (!queuePosition.containsKey(uuid)) {
queue.add(uuid);
updatePositions();
}
}
public void removePlayer(UUID uuid) {
queue.remove(uuid);
queuePosition.remove(uuid);
updatePositions();
}
public UUID nextKiller() {
if (queue.isEmpty()) return null;
return queue.get(0);
}
public void rotate() {
if (!queue.isEmpty()) {
UUID first = queue.remove(0);
queue.add(first);
updatePositions();
}
}
private void updatePositions() {
queuePosition.clear();
for (int i = 0; i < queue.size(); i++) {
queuePosition.put(queue.get(i), i);
}
}
public int getPosition(UUID uuid) {
return queuePosition.getOrDefault(uuid, -1);
}
public boolean isInQueue(UUID uuid) {
return queuePosition.containsKey(uuid);
}
public void clear() {
queue.clear();
queuePosition.clear();
}
}

View file

@ -0,0 +1,28 @@
package dev.loki.role;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import java.util.List;
import java.util.Random;
public class SpawnPosition {
private final LocationsConfig config;
private final Random random;
public SpawnPosition(LocationsConfig config) {
this.config = config;
this.random = new Random();
}
public Location getRandomSurvivorSpawn() {
List<Location> spawns = config.getSurvivorSpawns();
if (spawns.isEmpty()) return config.getLobby();
return spawns.get(random.nextInt(spawns.size()));
}
public Location getKillerSpawn() {
Location spawn = config.getKillerSpawn();
return spawn != null ? spawn : config.getLobby();
}
}