This commit is contained in:
loki5512344 2026-06-10 23:00:12 +02:00
parent 89dc9eb00d
commit 957ab4a5a2
149 changed files with 5379 additions and 3263 deletions

View file

@ -4,6 +4,7 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.inventory.ItemStack;
import java.util.UUID;
@ -13,12 +14,14 @@ public class Chest {
private final Location location;
private boolean opened;
private boolean empty;
private ItemStack loot;
public Chest(Location location) {
this.id = UUID.randomUUID();
this.location = location;
this.opened = false;
this.empty = false;
this.loot = null;
}
public UUID getId() {
@ -46,7 +49,19 @@ public class Chest {
}
public boolean isAvailable() {
return !opened && !empty;
return !opened && !empty && hasLoot();
}
public ItemStack getLoot() {
return loot;
}
public void setLoot(ItemStack loot) {
this.loot = loot;
}
public boolean hasLoot() {
return loot != null && loot.getType() != org.bukkit.Material.AIR;
}
public void openVisual() {

View file

@ -0,0 +1,143 @@
package dev.loki.chest;
import dev.loki.mdbd;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.TextDisplay;
import org.bukkit.entity.Display;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Floating labels above chests showing their status.
* Uses TextDisplay (Paper 1.19.4+) with ArmorStand fallback for older versions.
*/
public class ChestHologram {
private final mdbd plugin;
private final ChestManager manager;
private final Map<UUID, Object> displays = new HashMap<>(); // TextDisplay or ArmorStand
private BukkitTask task;
private boolean useTextDisplay;
public ChestHologram(mdbd plugin, ChestManager manager) {
this.plugin = plugin;
this.manager = manager;
this.useTextDisplay = isTextDisplayAvailable();
}
private static boolean isTextDisplayAvailable() {
try {
EntityType.valueOf("TEXT_DISPLAY");
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public void start() {
stop();
spawnAll();
task = plugin.getServer().getScheduler().runTaskTimer(plugin, this::refresh, 20L, 20L);
plugin.getLogger().info("[ChestHologram] Started with " + displays.size() + " holograms (mode=" + (useTextDisplay ? "TextDisplay" : "ArmorStand") + ")");
}
public void stop() {
if (task != null) {
task.cancel();
task = null;
}
for (Object display : displays.values()) {
if (display instanceof TextDisplay td && td.isValid()) {
td.remove();
} else if (display instanceof ArmorStand as && as.isValid()) {
as.remove();
}
}
displays.clear();
}
private void spawnAll() {
double heightOffset = plugin.getConfigManager().getGame().getChestHologramHeightOffset();
int index = 0;
for (Chest chest : manager.getAllChests()) {
Location loc = chest.getLocation();
if (loc.getWorld() == null) {
plugin.getLogger().warning("[ChestHologram] Chest " + chest.getId() + " has null world, skipping");
continue;
}
Location holoLoc = loc.clone().add(0.5, 2.0 + heightOffset, 0.5);
try {
if (useTextDisplay) {
TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> {
d.setBillboard(Display.Billboard.VERTICAL);
d.setSeeThrough(true);
d.setShadowed(true);
d.setDefaultBackground(false);
d.setPersistent(false);
d.setTextOpacity((byte) -1);
d.setViewRange(128.0f);
});
displays.put(chest.getId(), display);
} else {
ArmorStand stand = loc.getWorld().spawn(holoLoc, ArmorStand.class, as -> {
as.setInvisible(true);
as.setInvulnerable(true);
as.setGravity(false);
as.setMarker(true);
as.setCustomNameVisible(true);
as.setPersistent(false);
});
displays.put(chest.getId(), stand);
}
} catch (Exception e) {
plugin.getLogger().warning("[ChestHologram] Failed to spawn hologram for chest " + chest.getId() + ": " + e.getMessage());
}
index++;
}
}
private void refresh() {
double heightOffset = plugin.getConfigManager().getGame().getChestHologramHeightOffset();
int total = manager.getTotalChests();
int visibleIndex = 0;
for (Chest chest : manager.getAllChests()) {
Object display = displays.get(chest.getId());
if (display == null) continue;
boolean valid = (display instanceof TextDisplay td && td.isValid()) ||
(display instanceof ArmorStand as && as.isValid());
if (!valid) continue;
String status;
if (chest.isEmpty()) {
status = "§7Пуст";
} else if (chest.isOpened()) {
status = "§6Открывается...";
} else if (chest.hasLoot()) {
status = "§aДоступен";
} else {
status = "§aДоступен";
}
visibleIndex++;
String text = "§e📦 Сундук §f#" + visibleIndex + "/" + total + "\n§7" + status;
if (display instanceof TextDisplay td) {
td.text(net.kyori.adventure.text.Component.text(text));
Location base = chest.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5);
td.teleport(base);
} else if (display instanceof ArmorStand as) {
as.setCustomName(text);
Location base = chest.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5);
as.teleport(base);
}
}
}
}

View file

@ -1,8 +1,11 @@
package dev.loki.chest;
import dev.loki.config.GameConfig;
import dev.loki.config.LocationsConfig;
import dev.loki.item.ChestLoot;
import org.bukkit.Location;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.ArrayList;
import java.util.HashMap;
@ -15,11 +18,16 @@ public class ChestManager {
private final JavaPlugin plugin;
private final Map<UUID, Chest> chests;
private final LocationsConfig locationsConfig;
private final GameConfig gameConfig;
private ChestLoot chestLoot;
private BukkitTask lootResetTask;
public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig) {
public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig, GameConfig gameConfig) {
this.plugin = plugin;
this.chests = new HashMap<>();
this.locationsConfig = locationsConfig;
this.gameConfig = gameConfig;
this.chestLoot = new ChestLoot(gameConfig);
loadChests();
}
@ -27,9 +35,45 @@ public class ChestManager {
List<Location> locations = locationsConfig.getChests();
for (Location loc : locations) {
Chest chest = new Chest(loc);
// Populate loot immediately so it's available
chest.setLoot(chestLoot.rollLoot());
chests.put(chest.getId(), chest);
}
plugin.getLogger().info("Loaded " + chests.size() + " chests");
plugin.getLogger().info("Loaded " + chests.size() + " chests with loot");
}
/**
* Start periodic loot reset timer.
* Every N minutes (from config), all empty chests get new loot.
*/
public void startLootResetTimer() {
if (lootResetTask != null) lootResetTask.cancel();
long intervalTicks = (long) (gameConfig.getChestLootResetMinutes() * 60 * 20);
lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::resetLoot, intervalTicks, intervalTicks);
plugin.getLogger().info("Chest loot reset timer started (every " + gameConfig.getChestLootResetMinutes() + " min)");
}
public void stopLootResetTimer() {
if (lootResetTask != null) {
lootResetTask.cancel();
lootResetTask = null;
}
}
/**
* Reset loot: refill ALL chests with new random loot.
*/
private void resetLoot() {
int refilled = 0;
for (Chest chest : chests.values()) {
// Skip chests currently being opened to avoid race condition
if (chest.isOpened()) continue;
chest.setLoot(chestLoot.rollLoot());
chest.setOpened(false);
chest.setEmpty(false);
refilled++;
}
plugin.getLogger().info("[Chest] Loot reset — " + refilled + " chests refilled");
}
public Chest getChest(UUID id) {
@ -69,6 +113,7 @@ public class ChestManager {
for (Chest chest : chests.values()) {
chest.setOpened(false);
chest.setEmpty(false);
chest.setLoot(chestLoot.rollLoot());
}
plugin.getLogger().info("Reset all chests");
}
@ -79,6 +124,7 @@ public class ChestManager {
public void addChest(Location location) {
Chest chest = new Chest(location);
chest.setLoot(chestLoot.rollLoot());
chests.put(chest.getId(), chest);
locationsConfig.addLocation("chests", location);
}
@ -90,4 +136,8 @@ public class ChestManager {
public int getRemainingChests() {
return (int) chests.values().stream().filter(Chest::isAvailable).count();
}
public ChestLoot getChestLoot() {
return chestLoot;
}
}

View file

@ -19,7 +19,7 @@ public class ChestOpening {
private static final long OPEN_TIME_TICKS = 60L;
private final JavaPlugin plugin;
private final ChestLoot chestLoot;
private ChestLoot chestLoot;
private final Map<UUID, BukkitRunnable> activeOpenings;
public ChestOpening(JavaPlugin plugin) {
@ -28,6 +28,11 @@ public class ChestOpening {
this.activeOpenings = new HashMap<>();
}
/** Передать конфиг для весов лута */
public void setChestLoot(ChestLoot loot) {
this.chestLoot = loot;
}
public boolean startOpening(Player player, Chest chest) {
UUID playerId = player.getUniqueId();
@ -35,6 +40,17 @@ public class ChestOpening {
return false;
}
// Ensure chest has loot; roll if missing
if (!chest.hasLoot()) {
ItemStack rolled = chestLoot.rollLoot();
if (rolled == null) {
player.sendMessage("§cThis chest is empty!");
return false;
}
chest.setLoot(rolled);
chest.setEmpty(false);
}
if (!chest.isAvailable()) {
player.sendMessage("§cThis chest is empty!");
return false;
@ -46,13 +62,40 @@ public class ChestOpening {
playOpeningSound(player, chest.getLocation());
BukkitRunnable task = new BukkitRunnable() {
int ticks = 0;
@Override
public void run() {
completeOpening(player, chest);
if (!player.isOnline()) {
chest.setOpened(false);
activeOpenings.remove(playerId);
cancel();
return;
}
if (!chest.isInRange(player.getLocation(), OPEN_RANGE)) {
player.sendMessage(net.kyori.adventure.text.Component.text(
"[DBD] Слишком далеко!", net.kyori.adventure.text.format.NamedTextColor.RED));
chest.setOpened(false);
chest.closeVisual();
activeOpenings.remove(playerId);
player.sendActionBar(net.kyori.adventure.text.Component.empty());
cancel();
return;
}
ticks++;
int filled = (int)(8.0 * ticks / OPEN_TIME_TICKS);
int empty = 8 - filled;
int pct = (int)(100.0 * ticks / OPEN_TIME_TICKS);
player.sendActionBar(net.kyori.adventure.text.Component.text(
"§6Открытие: §e" + "█".repeat(filled) + "§7" + "░".repeat(empty) + " " + pct + "%"));
if (ticks >= OPEN_TIME_TICKS) {
player.sendActionBar(net.kyori.adventure.text.Component.empty());
completeOpening(player, chest);
cancel();
}
}
};
task.runTaskLater(plugin, OPEN_TIME_TICKS);
task.runTaskTimer(plugin, 1L, 1L);
activeOpenings.put(playerId, task);
return true;
@ -87,19 +130,34 @@ public class ChestOpening {
return;
}
if (!chest.isInRange(player.getLocation(), OPEN_RANGE)) {
player.sendMessage("§cYou moved too far away!");
// Use pre-set loot from chest, or roll new if none
ItemStack loot = chest.hasLoot() ? chest.getLoot() : chestLoot.rollLoot();
if (loot == null) {
player.sendMessage(net.kyori.adventure.text.Component.text(
"[DBD] Сундук пуст!", net.kyori.adventure.text.format.NamedTextColor.RED));
chest.setOpened(false);
chest.closeVisual();
return;
}
ItemStack loot = chestLoot.rollLoot();
player.getInventory().addItem(loot);
// Check if inventory has space; drop leftovers at player's feet
Map<Integer, ItemStack> leftover = player.getInventory().addItem(loot);
if (!leftover.isEmpty()) {
for (ItemStack item : leftover.values()) {
player.getWorld().dropItemNaturally(player.getLocation(), item);
}
}
chest.setEmpty(true);
chest.setLoot(null); // Clear loot after giving
chest.closeVisual();
player.sendMessage("§aYou found: §f" + loot.getItemMeta().getDisplayName());
// Название предмета через Adventure API
String itemName = loot.hasItemMeta() && loot.getItemMeta().hasDisplayName()
? net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText()
.serialize(loot.getItemMeta().displayName())
: loot.getType().name();
player.sendMessage(net.kyori.adventure.text.Component.text(
"[DBD] Вы нашли: " + itemName, net.kyori.adventure.text.format.NamedTextColor.GREEN));
playSuccessSound(player, chest.getLocation());
}

View file

@ -1,53 +1,27 @@
package dev.loki.command;
import dev.loki.data.GeneratorDataManager;
import dev.loki.config.LocationsConfig;
import dev.loki.mdbd;
import dev.loki.util.TextUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class DBDAdminCommand implements TabExecutor {
private final mdbd plugin;
private final GeneratorDataManager generatorData;
private final Set<UUID> selectingGenerator;
private final MiniMessage mm;
private BukkitTask actionBarTask;
public DBDAdminCommand(mdbd plugin, GeneratorDataManager generatorData) {
public DBDAdminCommand(mdbd plugin) {
this.plugin = plugin;
this.generatorData = generatorData;
this.selectingGenerator = new HashSet<>();
this.mm = MiniMessage.miniMessage();
startActionBarTask();
}
private void startActionBarTask() {
this.actionBarTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
for (UUID uuid : selectingGenerator) {
Player p = Bukkit.getPlayer(uuid);
if (p != null && p.isOnline()) {
p.sendActionBar(mm.deserialize(
"<#FFA500>⚒ Режим установки: <#00FF00>сломайте блок для генератора <#FFA500>⚒"));
}
}
}, 0L, 20L);
}
@Override
@ -69,10 +43,7 @@ public class DBDAdminCommand implements TabExecutor {
}
return switch (args[0].toLowerCase()) {
case "create" -> handleCreate(player, args);
case "list" -> handleList(player);
case "remove" -> handleRemove(player, args);
case "clear" -> handleClear(player);
case "list" -> handleList(player, args);
case "world" -> handleWorld(player, args);
default -> {
sendUsage(sender);
@ -81,92 +52,62 @@ public class DBDAdminCommand implements TabExecutor {
};
}
@SuppressWarnings("UnreachableBranchError")
private boolean handleCreate(Player player, String[] args) {
if (args.length < 2) {
player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin create <generator>"));
return true;
}
private boolean handleList(Player player, String[] args) {
var locations = plugin.getConfigManager().getLocations();
String type = args[1].toLowerCase();
switch (type) {
case "generator" -> {
if (selectingGenerator.contains(player.getUniqueId())) {
selectingGenerator.remove(player.getUniqueId());
player.sendMessage(TextUtil.color("<#FFA500>Режим выбора генератора отключен"));
} else {
selectingGenerator.add(player.getUniqueId());
player.sendMessage(TextUtil.color("<#00FF00>Режим выбора генератора включен!"));
player.sendMessage(TextUtil.color("<#FFD700>Сломайте блок, чтобы установить генератор"));
player.sendMessage(TextUtil.color("<#FFD700>(блок не сломается, а сохранятся его координаты)"));
player.sendMessage(TextUtil.color("<#FFA500>Введите /dbdadmin create generator снова, чтобы отключить режим"));
// Если указан конкретный тип
if (args.length >= 2) {
return switch (args[1].toLowerCase()) {
case "generator" -> listType(player, "Генераторы", locations.getGenerators());
case "hook" -> listType(player, "Крюки", locations.getHooks());
case "chest" -> listType(player, "Сундуки", locations.getChests());
case "killer-spawn", "killerspawn" -> listType(player, "Спавн киллера", locations.getKillerSpawns());
case "survivor-spawn", "survivorspawn" -> listType(player, "Спавн выживших", locations.getSurvivorSpawns());
default -> {
player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + args[1]));
yield true;
}
return true;
}
default -> {
player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + type));
return true;
}
};
}
}
private boolean handleList(Player player) {
var entries = generatorData.getGeneratorEntries();
player.sendMessage(TextUtil.color("<#FFD700>=== Генераторы (" + entries.size() + ") ==="));
if (entries.isEmpty()) {
player.sendMessage(TextUtil.color("<#808000>Нет установленных генераторов"));
} else {
for (int i = 0; i < entries.size(); i++) {
player.sendMessage(TextUtil.color("<#00CED1>[" + i + "] " + entries.get(i)));
}
}
// Показать всё
player.sendMessage(TextUtil.color("<#FF6B00>=== Все установленные объекты ==="));
listType(player, "Генераторы", locations.getGenerators());
listType(player, "Крюки", locations.getHooks());
listType(player, "Сундуки", locations.getChests());
listType(player, "Спавн киллера", locations.getKillerSpawns());
listType(player, "Спавн выживших", locations.getSurvivorSpawns());
return true;
}
private boolean handleRemove(Player player, String[] args) {
if (args.length < 2) {
player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin remove <id>"));
private boolean listType(Player player, String label, List<Location> locations) {
if (locations.isEmpty()) {
player.sendMessage(TextUtil.color("<#808000>" + label + ": нет"));
return true;
}
try {
int id = Integer.parseInt(args[1]);
var entries = generatorData.getGeneratorEntries();
if (id < 0 || id >= entries.size()) {
player.sendMessage(TextUtil.color("<#FF0000>Генератор с ID " + id + " не найден"));
return true;
}
String removed = entries.get(id);
generatorData.removeGenerator(id);
player.sendMessage(TextUtil.color("<#00FF00>Генератор удален: " + removed));
} catch (NumberFormatException e) {
player.sendMessage(TextUtil.color("<#FF0000>ID должен быть числом"));
player.sendMessage(TextUtil.color("<#FFD700>" + label + " (" + locations.size() + "):"));
for (int i = 0; i < locations.size(); i++) {
Location loc = locations.get(i);
player.sendMessage(TextUtil.color(String.format(
"<#00CED1> #%d %s [%.1f, %.1f, %.1f]",
i + 1, loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ()
)));
}
return true;
}
private boolean handleClear(Player player) {
generatorData.clearGenerators();
player.sendMessage(TextUtil.color("<#FFA500>Все генераторы удалены"));
return true;
}
private boolean handleWorld(Player player, String[] args) {
if (args.length < 2) {
player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin world <lobby|game|<название мира>>"));
player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin world <game|название мира>"));
return true;
}
String worldArg = args[1].toLowerCase();
org.bukkit.World target;
if (worldArg.equals("lobby")) {
target = plugin.getLobbyManager().getLobbyWorld();
if (target == null) {
player.sendMessage(TextUtil.color("<#FF0000>Мир lobby не загружен!"));
return true;
}
} else if (worldArg.equals("game")) {
if (worldArg.equals("game")) {
target = plugin.getLobbyManager().getGameWorld();
if (target == null) {
player.sendMessage(TextUtil.color("<#FF0000>Мир game не загружен!"));
@ -187,34 +128,9 @@ public class DBDAdminCommand implements TabExecutor {
private void sendUsage(CommandSender sender) {
sender.sendMessage(TextUtil.color("<#FF6B00>=== DBD Admin ==="));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin create generator — Режим установки генератора"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin list — Список генераторов"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin remove <id> — Удалить генератор"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin clear — Очистить все генераторы"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin world <lobby|game|мир> — Телепорт в мир"));
}
public boolean isSelectingGenerator(UUID uuid) {
return selectingGenerator.contains(uuid);
}
public void toggleGeneratorMode(org.bukkit.entity.Player player) {
if (selectingGenerator.contains(player.getUniqueId())) {
selectingGenerator.remove(player.getUniqueId());
player.sendMessage(TextUtil.color("<#FFA500>Режим выбора генератора отключен"));
} else {
selectingGenerator.add(player.getUniqueId());
player.sendMessage(TextUtil.color("<#00FF00>Режим выбора генератора включен!"));
player.sendMessage(TextUtil.color("<#FFD700>Сломайте блок чтобы установить генератор"));
}
}
public void shutdown() {
if (actionBarTask != null) {
actionBarTask.cancel();
actionBarTask = null;
}
selectingGenerator.clear();
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin list — Список всех объектов"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin list <тип> — Список по типу"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin world <game|мир> — Телепорт в мир"));
}
@Override
@ -223,34 +139,23 @@ public class DBDAdminCommand implements TabExecutor {
if (!sender.hasPermission("mdbd.admin")) return List.of();
if (args.length == 1) {
return Arrays.asList("create", "list", "remove", "clear", "world").stream()
return Arrays.asList("list", "world").stream()
.filter(s -> s.startsWith(args[0].toLowerCase()))
.toList();
}
if (args.length == 2 && args[0].equalsIgnoreCase("create")) {
return List.of("generator").stream()
if (args.length == 2 && args[0].equalsIgnoreCase("list")) {
return Arrays.asList("generator", "hook", "chest", "killer-spawn", "survivor-spawn").stream()
.filter(s -> s.startsWith(args[1].toLowerCase()))
.toList();
}
if (args.length == 2 && args[0].equalsIgnoreCase("world")) {
return Arrays.asList("lobby", "game").stream()
return Arrays.asList("game").stream()
.filter(s -> s.startsWith(args[1].toLowerCase()))
.toList();
}
if (args.length == 2 && args[0].equalsIgnoreCase("remove")) {
var entries = generatorData.getGeneratorEntries();
List<String> ids = new ArrayList<>();
for (int i = 0; i < entries.size(); i++) {
ids.add(String.valueOf(i));
}
return ids.stream()
.filter(s -> s.startsWith(args[1]))
.toList();
}
return List.of();
}
}

View file

@ -0,0 +1,211 @@
package dev.loki.command;
import dev.loki.core.GameManager;
import dev.loki.core.GameSession;
import dev.loki.mdbd;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.util.TextUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.Locale;
/**
* /dbd admin — управление игроками в активной сессии.
*/
public class DBDAdminPlayerCommand {
private final mdbd plugin;
private final GameManager gameManager;
public DBDAdminPlayerCommand(mdbd plugin, GameManager gameManager) {
this.plugin = plugin;
this.gameManager = gameManager;
}
public boolean handle(CommandSender sender, String[] args) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
return true;
}
if (args.length < 2) {
sendUsage(sender);
return true;
}
String action = args[1].toLowerCase(Locale.ROOT);
GameSession session = resolveSession(sender, args);
if (session == null) {
sender.sendMessage(Component.text("[DBD] Нет активной игровой сессии.", NamedTextColor.RED));
return true;
}
return switch (action) {
case "heal" -> heal(sender, session, args);
case "down" -> setState(sender, session, args, PlayerState.DOWNED);
case "unhook", "up" -> setState(sender, session, args, PlayerState.INJURED);
case "state" -> setStateArg(sender, session, args);
case "hooks", "lives" -> setHooks(sender, session, args);
case "kill" -> setState(sender, session, args, PlayerState.DEAD);
case "jump" -> toggleJump(sender, args);
default -> {
sendUsage(sender);
yield true;
}
};
}
private boolean heal(CommandSender sender, GameSession session, String[] args) {
Survivor target = resolveSurvivor(sender, session, args, 2);
if (target == null) return true;
target.setState(PlayerState.HEALTHY);
Player p = target.getPlayer();
if (p != null) {
p.setSwimming(false);
p.setHealth(20.0);
p.sendMessage(Component.text("[DBD] Админ вылечил вас.", NamedTextColor.GREEN));
}
sender.sendMessage(Component.text("[DBD] " + playerName(target) + " → HEALTHY", NamedTextColor.GREEN));
return true;
}
private boolean setState(CommandSender sender, GameSession session, String[] args, PlayerState state) {
Survivor target = resolveSurvivor(sender, session, args, 2);
if (target == null) return true;
target.setState(state);
Player p = target.getPlayer();
if (p != null) {
if (state == PlayerState.DOWNED) p.setSwimming(true);
else p.setSwimming(false);
p.setHealth(20.0);
}
sender.sendMessage(Component.text("[DBD] " + playerName(target) + " → " + state.name(), NamedTextColor.YELLOW));
return true;
}
private boolean setStateArg(CommandSender sender, GameSession session, String[] args) {
if (args.length < 4) {
sender.sendMessage(Component.text("[DBD] /dbd admin state <игрок> <HEALTHY|INJURED|DOWNED|HOOKED|...>", NamedTextColor.RED));
return true;
}
PlayerState state;
try {
state = PlayerState.valueOf(args[3].toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
sender.sendMessage(Component.text("[DBD] Неизвестный статус.", NamedTextColor.RED));
return true;
}
return setState(sender, session, args, state);
}
private boolean setHooks(CommandSender sender, GameSession session, String[] args) {
if (args.length < 4) {
sender.sendMessage(Component.text("[DBD] /dbd admin hooks <игрок> <0-3>", NamedTextColor.RED));
return true;
}
Survivor target = resolveSurvivor(sender, session, args, 2);
if (target == null) return true;
int hooks;
try {
hooks = Integer.parseInt(args[3]);
} catch (NumberFormatException e) {
sender.sendMessage(Component.text("[DBD] Число 0–3.", NamedTextColor.RED));
return true;
}
hooks = Math.max(0, Math.min(3, hooks));
target.setHooksTaken(hooks);
sender.sendMessage(Component.text(
"[DBD] " + playerName(target) + " hooks=" + target.getHooksTaken(), NamedTextColor.GREEN));
return true;
}
private Survivor resolveSurvivor(CommandSender sender, GameSession session, String[] args, int nameIndex) {
if (args.length <= nameIndex) {
if (sender instanceof Player pl) {
BasePlayer bp = session.getPlayer(pl.getUniqueId());
if (bp instanceof Survivor s) return s;
}
sender.sendMessage(Component.text("[DBD] Укажите игрока.", NamedTextColor.RED));
return null;
}
Player target = Bukkit.getPlayer(args[nameIndex]);
if (target == null) {
sender.sendMessage(Component.text("[DBD] Игрок не в сети.", NamedTextColor.RED));
return null;
}
BasePlayer bp = session.getPlayer(target.getUniqueId());
if (!(bp instanceof Survivor s)) {
sender.sendMessage(Component.text("[DBD] Игрок не в этой игре или не выживший.", NamedTextColor.RED));
return null;
}
return s;
}
private boolean toggleJump(CommandSender sender, String[] args) {
boolean newState;
if (args.length >= 3) {
String val = args[2].toLowerCase(Locale.ROOT);
newState = val.equals("on") || val.equals("true") || val.equals("1");
} else {
// Toggle: flip current state
newState = !dev.loki.listener.PlayerJumpListener.isJumpingAllowed();
}
dev.loki.listener.PlayerJumpListener.setJumpingAllowed(newState);
sender.sendMessage(Component.text(
"[DBD] Прыжки " + (newState ? "§aВКЛЮЧЕНЫ" : "§cВЫКЛЮЧЕНЫ"), NamedTextColor.GOLD));
return true;
}
private GameSession resolveSession(CommandSender sender, String[] args) {
if (args.length >= 3) {
Player named = Bukkit.getPlayer(args[2]);
if (named != null) {
GameSession s = gameManager.getPlayerSession(named.getUniqueId());
if (s != null) return s;
}
}
if (sender instanceof Player p) {
GameSession s = gameManager.getPlayerSession(p.getUniqueId());
if (s != null) return s;
}
return gameManager.getCurrentSession();
}
private String playerName(Survivor s) {
Player p = s.getPlayer();
return p != null ? p.getName() : s.getUuid().toString();
}
private void sendUsage(CommandSender sender) {
sender.sendMessage(TextUtil.color("<#FF6B00>=== /dbd admin ==="));
sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin heal [игрок]"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin down [игрок]"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin unhook [игрок]"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin state <игрок> <статус>"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin hooks <игрок> <0-3>"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin kill [игрок]"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin jump [on/off] — вкл/выкл прыжки"));
}
public static java.util.List<String> tabComplete(String[] args) {
if (args.length == 2) {
return Arrays.asList("heal", "down", "unhook", "state", "hooks", "kill", "jump").stream()
.filter(s -> s.startsWith(args[1].toLowerCase())).toList();
}
if (args.length == 3 && args[1].equalsIgnoreCase("jump")) {
return Arrays.asList("on", "off");
}
if (args.length == 4 && args[1].equalsIgnoreCase("state")) {
return Arrays.stream(PlayerState.values()).map(Enum::name)
.filter(s -> s.startsWith(args[3].toUpperCase())).toList();
}
return java.util.List.of();
}
}

View file

@ -1,7 +1,7 @@
package dev.loki.command;
import dev.loki.core.GameState;
import dev.loki.core.GameManager;
import dev.loki.core.GameState;
import dev.loki.mdbd;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@ -13,69 +13,76 @@ 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;
private final DBDAdminPlayerCommand adminPlayerCommand;
public DBDCommand(mdbd plugin, GameManager gameManager) {
this.plugin = plugin;
this.plugin = plugin;
this.gameManager = gameManager;
this.adminPlayerCommand = new DBDAdminPlayerCommand(plugin, 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;
}
if (args.length == 0) { sendUsage(sender); return true; }
String sub = args[0].toLowerCase();
return switch (sub) {
case "join" -> handleJoin(sender);
case "leave" -> handleLeave(sender);
return switch (args[0].toLowerCase()) {
case "join" -> handleJoin(sender, args);
case "leave" -> handleLeave(sender);
case "forcestart" -> handleForceStart(sender);
case "create" -> handleCreate(sender);
case "stop" -> handleStop(sender);
case "reload" -> handleReload(sender);
case "lobby" -> handleLobby(sender);
case "setlobby" -> handleSetLobby(sender);
default -> {
sendUsage(sender);
yield true;
}
case "stop" -> handleStop(sender);
case "reload" -> handleReload(sender);
case "save" -> handleMapSave(sender, args);
case "load" -> handleMapLoad(sender, args);
case "maps" -> handleMapsList(sender);
case "lobby" -> handleLobby(sender);
case "setlobby" -> handleSetLobby(sender);
case "admin" -> adminPlayerCommand.handle(sender, args);
default -> { sendUsage(sender); yield true; }
};
}
private boolean handleJoin(CommandSender sender) {
// -------------------------------------------------------------------------
// /dbd join — присоединиться к игре
// -------------------------------------------------------------------------
private boolean handleJoin(CommandSender sender, String[] args) {
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));
player.sendMessage(Component.text("[DBD] Не удалось присоединиться. Возможно, игра уже идёт.", NamedTextColor.RED));
}
return true;
}
// -------------------------------------------------------------------------
// /dbd leave
// -------------------------------------------------------------------------
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));
player.sendMessage(Component.text("[DBD] Вы покинули игру.", NamedTextColor.YELLOW));
} else {
player.sendMessage(Component.text("[DBD] Вы не в игре", NamedTextColor.RED));
player.sendMessage(Component.text("[DBD] Вы не в игре.", NamedTextColor.RED));
}
return true;
}
// -------------------------------------------------------------------------
// Admin commands
// -------------------------------------------------------------------------
private boolean handleForceStart(CommandSender sender) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
@ -90,21 +97,18 @@ public class DBDCommand implements TabExecutor {
session.forceStart();
sender.sendMessage(Component.text("[DBD] Игра форсирована!", NamedTextColor.GREEN));
} else {
sender.sendMessage(Component.text("Нельзя форсировать сейчас", NamedTextColor.RED));
sender.sendMessage(Component.text("Нельзя форсировать сейчас.", NamedTextColor.RED));
}
return true;
}
private boolean handleCreate(CommandSender sender) {
private boolean handleStop(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));
}
gameManager.endGame();
sender.sendMessage(Component.text("[DBD] Игра остановлена.", NamedTextColor.YELLOW));
return true;
}
@ -118,13 +122,68 @@ public class DBDCommand implements TabExecutor {
return true;
}
// -------------------------------------------------------------------------
// Map save/load
// -------------------------------------------------------------------------
private boolean handleMapSave(CommandSender sender, String[] args) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
return true;
}
if (args.length < 2) {
sender.sendMessage(Component.text("[DBD] Использование: /dbd save <название>", NamedTextColor.RED));
return true;
}
String name = args[1].toLowerCase();
if (plugin.getMapManager().saveMap(name)) {
sender.sendMessage(Component.text("[DBD] Карта «" + name + "» сохранена в plugins/Mdbd/maps/", NamedTextColor.GREEN));
} else {
sender.sendMessage(Component.text("[DBD] Не удалось сохранить карту.", NamedTextColor.RED));
}
return true;
}
private boolean handleMapLoad(CommandSender sender, String[] args) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
return true;
}
if (args.length < 2) {
sender.sendMessage(Component.text("[DBD] Использование: /dbd load <название>", NamedTextColor.RED));
return true;
}
String name = args[1].toLowerCase();
if (plugin.getMapManager().loadMap(name)) {
sender.sendMessage(Component.text("[DBD] Карта «" + name + "» загружена!", NamedTextColor.GREEN));
sender.sendMessage(Component.text("[DBD] Перезагрузите карту или перезапустите сервер для применения.", NamedTextColor.YELLOW));
} else {
sender.sendMessage(Component.text("[DBD] Карта «" + name + "» не найдена.", NamedTextColor.RED));
}
return true;
}
private boolean handleMapsList(CommandSender sender) {
if (!sender.hasPermission("mdbd.admin")) {
sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED));
return true;
}
List<String> maps = plugin.getMapManager().listMaps();
if (maps.isEmpty()) {
sender.sendMessage(Component.text("[DBD] Нет сохранённых карт.", NamedTextColor.YELLOW));
} else {
sender.sendMessage(Component.text("=== Сохранённые карты ===", NamedTextColor.GOLD));
for (String m : maps) sender.sendMessage(Component.text(" §f" + m, NamedTextColor.GRAY));
}
return true;
}
private boolean handleLobby(CommandSender sender) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED));
return true;
}
plugin.getLobbyManager().teleportToLobby(player);
player.sendMessage(Component.text("[DBD] Вы телепортированы в лобби!", NamedTextColor.GREEN));
player.sendMessage(Component.text("[DBD] Телепорт в лобби!", NamedTextColor.GREEN));
return true;
}
@ -140,32 +199,26 @@ public class DBDCommand implements TabExecutor {
plugin.getConfigManager().getLocations().setLobby(player.getLocation());
plugin.getConfigManager().saveConfig("locations.yml",
plugin.getConfigManager().getLocations().getConfig());
player.sendMessage(Component.text("[DBD] Лобби установлено!", NamedTextColor.GREEN));
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));
player.sendMessage(Component.text("[DBD] Точка лобби установлена!", NamedTextColor.GREEN));
return true;
}
// -------------------------------------------------------------------------
// Usage
// -------------------------------------------------------------------------
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));
sender.sendMessage(Component.text("/dbd lobby - телепорт в лобби", NamedTextColor.GRAY));
sender.sendMessage(Component.text("=== DBD Команды ===", NamedTextColor.GOLD));
sender.sendMessage(Component.text("/dbd join — присоединиться к игре", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd leave — покинуть игру", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd lobby — телепорт в лобби", 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));
sender.sendMessage(Component.text("/dbd reload - перезагрузить конфиги", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd lobby - телепорт в лобби", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd setlobby - установить точку лобби", NamedTextColor.GRAY));
sender.sendMessage(Component.text("/dbd admin — управление игроками в матче", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/dbd forcestart — форс старт", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/dbd stop — остановить игру", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/dbd reload — перезагрузить конфиги", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/dbd setlobby — установить точку лобби", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/dbd save — сохранить карту", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/dbd load — загрузить карту", NamedTextColor.YELLOW));
}
}
@ -173,19 +226,15 @@ public class DBDCommand implements TabExecutor {
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", "lobby"));
List<String> subs = new ArrayList<>(List.of("join", "leave", "lobby"));
if (sender.hasPermission("mdbd.admin")) {
completions.addAll(Arrays.asList("create", "forcestart", "stop", "reload", "setlobby"));
subs.addAll(List.of("admin", "forcestart", "stop", "reload", "setlobby", "save", "load", "maps"));
}
return completions.stream()
.filter(s -> s.startsWith(args[0].toLowerCase()))
.toList();
return subs.stream().filter(s -> s.startsWith(args[0].toLowerCase())).toList();
}
if (args.length >= 2 && args[0].equalsIgnoreCase("admin") && sender.hasPermission("mdbd.admin")) {
return DBDAdminPlayerCommand.tabComplete(args);
}
return List.of();
}
private Object getCurrentPhase() {
var session = gameManager.getCurrentSession();
return session != null ? session.getState() : null;
}
}

View file

@ -1,29 +1,55 @@
package dev.loki.command;
import dev.loki.config.LocationsConfig;
import dev.loki.mdbd;
import dev.loki.util.SpawnUtil;
import dev.loki.util.TextUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.*;
/**
* /dbdsetup — настройка карты.
*
* Система:
* 1. Ванд: /dbdsetup wand (ЛКМ=pos1, ПКМ=pos2 — для ворот)
* 2. Обычные объекты: /dbdsetup set <тип> [номер] → сломай блок
* 3. Ворота: /dbdsetup set gate A|B (из pos1/pos2 ванда)
*
* Типы: hook, chest, generator, killer-spawn, survivor-spawn, gate
* Для спавнов можно указать номер: /dbdsetup set killer-spawn 0
*/
public class SetupCommand implements TabExecutor {
private final mdbd plugin;
private final java.util.Map<String, Location> selectionCorners;
private final java.util.Set<java.util.UUID> selectingKillerSpawn = new java.util.HashSet<>();
private final java.util.Set<java.util.UUID> selectingSurvivorSpawn = new java.util.HashSet<>();
// Wand positions for gates (LMB=pos1, RMB=pos2)
private final Map<UUID, Location> pos1 = new HashMap<>();
private final Map<UUID, Location> pos2 = new HashMap<>();
// Setup mode data: mode + optional spawn index
private static final class SetupData {
final String mode;
int spawnIndex; // -1 means "not specified, append"; resets to -1 after first use
SetupData(String mode, int spawnIndex) {
this.mode = mode;
this.spawnIndex = spawnIndex;
}
}
private final Map<UUID, SetupData> setupMode = new HashMap<>();
public SetupCommand(mdbd plugin) {
this.plugin = plugin;
this.selectionCorners = new java.util.HashMap<>();
}
@Override
@ -37,191 +63,306 @@ public class SetupCommand implements TabExecutor {
sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED));
return true;
}
if (args.length == 0) {
sendUsage(sender);
return true;
}
if (args.length == 0) { sendUsage(sender); return true; }
return switch (args[0].toLowerCase()) {
case "wand" -> giveWand(player);
case "pos1" -> setPos(player, 1);
case "pos2" -> setPos(player, 2);
case "set" -> setObject(player, args);
case "build" -> buildMap(player);
case "clear" -> clearMap(player);
case "lobby" -> setLobby(player);
default -> { sendUsage(sender); yield true; }
case "wand" -> giveWand(player);
case "set" -> handleSet(player, args);
case "build" -> buildMap(player);
case "clear" -> clearMap(player);
case "clear-spawns" -> clearSpawns(player);
case "lobby" -> setLobby(player);
default -> { sendUsage(sender); yield true; }
};
}
// ── wand ─────────────────────────────────────────────────────────────────
// ── Wand ─────────────────────────────────────────────────────────────────
private boolean giveWand(Player player) {
var wand = new org.bukkit.inventory.ItemStack(org.bukkit.Material.STICK);
ItemStack wand = new ItemStack(Material.WOODEN_AXE);
wand.editMeta(meta -> {
meta.displayName(TextUtil.color("<#FF6B00>DBD Wand <white>(Выделение)"));
meta.displayName(TextUtil.color("<#FF6B00>DBD Setup Wand"));
meta.lore(List.of(
TextUtil.color("<#FFD700>ЛКМ — Первая точка"),
TextUtil.color("<#FFD700>ПКМ — Вторая точка")
TextUtil.color("<#FFD700>ЛКМ по блоку — Точка 1 (для ворот)"),
TextUtil.color("<#FFD700>ПКМ по блоку — Точка 2 (для ворот)"),
TextUtil.color("<gray>Для остального: /dbdsetup set <тип> и ломай блок")
));
var key = new org.bukkit.NamespacedKey(plugin, "dbd_wand");
meta.getPersistentDataContainer().set(key,
org.bukkit.persistence.PersistentDataType.BYTE, (byte) 1);
});
player.getInventory().addItem(wand);
player.sendMessage(TextUtil.color("<#00FF00>Получена палочка! <gray>ЛКМ=pos1 ПКМ=pos2"));
player.sendMessage(TextUtil.color("<#00FF00>Получен DBD Setup Wand"));
return true;
}
private boolean setPos(Player player, int num) {
String key = player.getUniqueId().toString();
Location loc = player.getLocation().getBlock().getLocation();
selectionCorners.put(key + "_pos" + num, loc);
player.sendMessage(TextUtil.color("<#00FF00>Установлена точка " + num + ": " + formatLoc(loc)));
return true;
}
// ── Set mode ──────────────────────────────────────────────────────────────
/** Called by PlayerInteractListener when wand is used on a block. */
public void setWandPos(Player player, int num, Location loc) {
String key = player.getUniqueId().toString();
selectionCorners.put(key + "_pos" + num, loc);
player.sendMessage(TextUtil.color("<#00FF00>[Wand] Точка " + num + ": " + formatLoc(loc)));
}
// ── set <type> ────────────────────────────────────────────────────────────
private boolean setObject(Player player, String[] args) {
private boolean handleSet(Player player, String[] args) {
if (args.length < 2) {
player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdsetup set <тип>"));
player.sendMessage(TextUtil.color("<#FFD700>Типы: generator, hook, pallet, gate, killer-spawn, survivor-spawn, lobby"));
player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdsetup set <тип> [номер]"));
player.sendMessage(TextUtil.color(
"<#FFD700>Типы: hook, chest, generator, killer-spawn, survivor-spawn, gate"));
player.sendMessage(TextUtil.color(
"<#FFD700>Для спавнов можно указать номер: /dbdsetup set killer-spawn 0"));
return true;
}
String type = args[1].toLowerCase();
var locations = plugin.getConfigManager().getLocations();
UUID uuid = player.getUniqueId();
switch (type) {
case "generator" -> {
plugin.getDbdAdminCommand().toggleGeneratorMode(player);
if (type.equals("gate")) {
if (args.length < 3) {
player.sendMessage(TextUtil.color("<#FF0000>Укажи: /dbdsetup set gate A (или B)"));
return true;
}
case "killer-spawn", "killer_spawn" -> {
if (selectingKillerSpawn.remove(player.getUniqueId())) {
player.sendMessage(TextUtil.color("<#FFA500>Режим спавна киллера отключен"));
} else {
selectingKillerSpawn.add(player.getUniqueId());
player.sendMessage(TextUtil.color("<#FF4444>Режим спавна киллера включен!"));
player.sendMessage(TextUtil.color("<#FFD700>Сломайте блок для установки (блок не сломается)"));
String gateId = args[2].toUpperCase();
if (!gateId.equals("A") && !gateId.equals("B")) {
player.sendMessage(TextUtil.color("<#FF0000>Ворота могут быть только A или B"));
return true;
}
return placeGate(player, gateId);
}
String mode = switch (type) {
case "killer_spawn" -> "killer-spawn";
case "survivor_spawn" -> "survivor-spawn";
default -> type;
};
if (!List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn").contains(mode)) {
player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + type));
return true;
}
// Parse optional spawn index
int spawnIndex = -1;
if (args.length >= 3 && (mode.equals("killer-spawn") || mode.equals("survivor-spawn"))) {
try {
spawnIndex = Integer.parseInt(args[2]);
if (spawnIndex < 0) {
player.sendMessage(TextUtil.color("<#FF0000>Номер спавна должен быть >= 0"));
return true;
}
} catch (NumberFormatException e) {
player.sendMessage(TextUtil.color("<#FF0000>Номер спавна должен быть числом: " + args[2]));
return true;
}
case "survivor-spawn", "survivor_spawn" -> {
if (selectingSurvivorSpawn.remove(player.getUniqueId())) {
player.sendMessage(TextUtil.color("<#FFA500>Режим спавна выжившего отключен"));
} else {
selectingSurvivorSpawn.add(player.getUniqueId());
int current = locations.getSurvivorSpawns().size();
player.sendMessage(TextUtil.color("<#00FF00>Режим спавна выжившего включен! Уже: " + current));
player.sendMessage(TextUtil.color("<#FFD700>Сломайте блок для установки (блок не сломается)"));
}
return true;
}
case "hook" -> {
Location loc = player.getLocation();
locations.addLocation("hooks", loc);
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
player.sendMessage(TextUtil.color("<#00FF00>Крюк установлен: " + formatLoc(loc)));
}
case "pallet" -> {
Location loc = player.getLocation().getBlock().getLocation();
locations.addLocation("pallets", loc);
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
player.sendMessage(TextUtil.color("<#00FF00>Паллет установлен: " + formatLoc(loc)));
}
case "gate" -> {
Location loc = player.getLocation();
locations.addLocation("exitgates", loc);
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
player.sendMessage(TextUtil.color("<#00FF00>Ворота установлены: " + formatLoc(loc)));
}
case "lobby" -> {
Location loc = player.getLocation();
locations.setLobby(loc);
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
player.sendMessage(TextUtil.color("<#00FF00>Лобби установлено: " + formatLoc(loc)));
}
default -> {
player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + type));
player.sendMessage(TextUtil.color("<#FFD700>Типы: generator, hook, pallet, gate, killer-spawn, survivor-spawn, lobby"));
}
}
// Toggle: если уже в этом режиме с тем же типом — выключаем
SetupData currentData = setupMode.get(uuid);
if (currentData != null && mode.equals(currentData.mode)) {
setupMode.remove(uuid);
player.sendMessage(TextUtil.color("<#FFA500>Режим установки <#FFD700>" + mode + " <#FFA500>выключен"));
return true;
}
setupMode.put(uuid, new SetupData(mode, spawnIndex));
String indexMsg = spawnIndex >= 0 ? " #" + spawnIndex : "";
if (mode.contains("spawn")) {
player.sendMessage(TextUtil.color(
"<#00FF00>Режим установки: <#FFD700>" + mode + indexMsg +
"\n<#00FF00>Сломай блок чтобы установить спавн (блок восстановится)" +
"\n<#FFA500>Повторно введи команду для выключения"));
} else {
player.sendMessage(TextUtil.color(
"<#00FF00>Режим установки: <#FFD700>" + mode + indexMsg +
"\n<#00FF00>Сломай блок чтобы установить (блок восстановится)" +
"\n<#FFA500>Введи команду снова чтобы выключить режим"));
}
return true;
}
// ── spawn selection callbacks (called by SetupBlockBreakListener) ─────────
private boolean placeGate(Player player, String gateId) {
Location p1 = pos1.get(player.getUniqueId());
Location p2 = pos2.get(player.getUniqueId());
public boolean isSelectingKillerSpawn(java.util.UUID uuid) {
return selectingKillerSpawn.contains(uuid);
}
if (p1 == null || p2 == null) {
player.sendMessage(TextUtil.color(
"<#FF0000>Сначала выбери оба угла топориком! (ЛКМ=точка 1, ПКМ=точка 2)"));
return true;
}
public boolean isSelectingSurvivorSpawn(java.util.UUID uuid) {
return selectingSurvivorSpawn.contains(uuid);
}
if (!p1.getWorld().equals(p2.getWorld())) {
player.sendMessage(TextUtil.color("<#FF0000>Точки должны быть в одном мире!"));
return true;
}
public void onKillerSpawnSelected(Player player, Location loc) {
plugin.getConfigManager().getLocations().setKillerSpawn(loc);
plugin.getConfigManager().saveConfig("locations.yml",
plugin.getConfigManager().getLocations().getConfig());
player.sendMessage(TextUtil.color("<#FF4444>Спавн киллера установлен: " + formatLoc(loc)));
}
Location center = new Location(
p1.getWorld(),
(p1.getX() + p2.getX()) / 2.0,
Math.min(p1.getY(), p2.getY()),
(p1.getZ() + p2.getZ()) / 2.0,
player.getLocation().getYaw(),
0f
);
public void onSurvivorSpawnSelected(Player player, Location loc) {
var locations = plugin.getConfigManager().getLocations();
locations.addLocation("survivor-spawns", loc);
locations.setGate(gateId, center);
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
int count = locations.getSurvivorSpawns().size();
player.sendMessage(TextUtil.color("<#00FF00>Спавн выжившего #" + count + " добавлен: " + formatLoc(loc)));
player.sendMessage(TextUtil.color(
"<#00FF00>Ворота <#FFFFFF>" + gateId + " <#00FF00>установлены: " + formatLoc(center)));
return true;
}
// ── build / clear / lobby ─────────────────────────────────────────────────
// ── Block break callback ───────────────────────────────────────────────────
/**
* Called when a block is broken in setup mode.
* Saves the location and restores the original block.
*
* @param player The player who broke the block
* @param blockLoc Location of the broken block
* @param restoreBlock The material to restore the block to (null = don't restore)
*/
public void onBlockBroken(Player player, Location blockLoc, Material restoreBlock) {
UUID uuid = player.getUniqueId();
SetupData data = setupMode.get(uuid);
if (data == null) return;
String mode = data.mode;
var locations = plugin.getConfigManager().getLocations();
Location loc = SpawnUtil.blockBreakSpawn(blockLoc, player.getLocation().getYaw());
switch (mode) {
case "hook" -> {
locations.addLocation("hooks", blockLoc.clone().add(0.5, 0, 0.5));
player.sendMessage(TextUtil.color("<#00FF00>Крюк установлен: " + formatLoc(loc)));
}
case "chest" -> {
locations.addLocation("chests", blockLoc.clone().add(0.5, 0, 0.5));
player.sendMessage(TextUtil.color("<#00FF00>Сундук добавлен: " + formatLoc(loc)));
}
case "generator" -> {
locations.addLocation("generators", blockLoc.clone().add(0.5, 0, 0.5));
plugin.getGeneratorData().addGenerator(blockLoc.clone().add(0.5, 0, 0.5));
player.sendMessage(TextUtil.color("<#00FF00>Генератор добавлен: " + formatLoc(loc)));
}
case "killer-spawn" -> {
locations.setSingleLocation("killer-spawn", loc);
player.sendMessage(TextUtil.color("<#FF4444>Спавн киллера установлен: " + formatLoc(loc)));
data.spawnIndex = -1;
}
case "survivor-spawn" -> {
int currentCount = locations.getSurvivorSpawns().size();
if (currentCount >= 4) {
player.sendMessage(TextUtil.color("<#FF0000>Достигнут лимит спавнов выживших (4)! Используй /dbdsetup clear-spawns для сброса."));
break;
}
saveSpawn(locations, "survivor-spawns", loc, data.spawnIndex);
int newCount = locations.getSurvivorSpawns().size();
String label = switch (newCount) {
case 1 -> "первого";
case 2 -> "второго";
case 3 -> "третьего";
case 4 -> "четвёртого";
default -> "#" + newCount;
};
player.sendMessage(TextUtil.color("<#00FF00>Спавн для <#FFD700>" + label + " <#00FF00>выжившего установлен! (всего: " + newCount + "/4) " + formatLoc(loc)));
// Сбрасываем индекс после использования, чтобы следующие блоки добавлялись
data.spawnIndex = -1;
}
}
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
// Restore the block to its original material + send block update to client
if (restoreBlock != null) {
Material finalRestore = restoreBlock;
plugin.getServer().getScheduler().runTask(plugin, () -> {
blockLoc.getBlock().setType(finalRestore, false);
player.sendBlockChange(blockLoc, finalRestore.createBlockData());
});
}
// НЕ выходим из режима — чтобы можно было установить несколько подряд
}
/**
* Saves a spawn location at a specific index, or appends if index is -1.
*/
private void saveSpawn(LocationsConfig locations, String path, Location loc, int index) {
if (index >= 0) {
locations.setLocationAtIndex(path, index, loc);
} else {
locations.addLocation(path, loc);
}
}
// ── Wand position callbacks ────────────────────────────────────────────────
public void setWandPos(Player player, int num, Location loc) {
if (num == 1) pos1.put(player.getUniqueId(), loc);
else pos2.put(player.getUniqueId(), loc);
player.sendMessage(TextUtil.color("<#00FF00>[Wand] Точка " + num + ": " + formatLoc(loc)));
}
public boolean isInSetupMode(UUID uuid) {
return setupMode.containsKey(uuid);
}
public String getSetupMode(UUID uuid) {
SetupData data = setupMode.get(uuid);
return data != null ? data.mode : null;
}
/**
* Returns the spawn index for the current setup mode, or -1 if not specified.
*/
public int getSpawnIndex(UUID uuid) {
SetupData data = setupMode.get(uuid);
return data != null ? data.spawnIndex : -1;
}
// ── Build / clear / lobby ─────────────────────────────────────────────────
private boolean buildMap(Player player) {
player.sendMessage(TextUtil.color("<#FFD700>Строительство карты..."));
plugin.getMapBuilder().buildMap(plugin.getConfigManager().getLocations());
player.sendMessage(TextUtil.color("<#00FF00>Карта построена!"));
return true;
}
private boolean clearMap(Player player) {
player.sendMessage(TextUtil.color("<#FFA500>Очистка карты..."));
plugin.getMapBuilder().clearMap(plugin.getConfigManager().getLocations());
player.sendMessage(TextUtil.color("<#00FF00>Карта очищена!"));
return true;
}
private boolean setLobby(Player player) {
Location loc = player.getLocation();
plugin.getConfigManager().getLocations().setLobby(loc);
plugin.getConfigManager().saveConfig("locations.yml",
plugin.getConfigManager().getLocations().getConfig());
player.sendMessage(TextUtil.color("<#00FF00>Лобби установлено: " + formatLoc(loc)));
private boolean clearSpawns(Player player) {
var locations = plugin.getConfigManager().getLocations();
locations.clearLocationList("killer-spawn");
locations.clearLocationList("survivor-spawns");
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
player.sendMessage(TextUtil.color("<#00FF00>Все спавны (киллер + выжившие) очищены!"));
return true;
}
// ── usage / helpers ───────────────────────────────────────────────────────
private boolean setLobby(Player player) {
plugin.getConfigManager().getLocations().setLobby(player.getLocation());
plugin.getConfigManager().saveConfig("locations.yml",
plugin.getConfigManager().getLocations().getConfig());
player.sendMessage(TextUtil.color("<#00FF00>Лобби установлено: " + formatLoc(player.getLocation())));
return true;
}
// ── Usage / helpers ───────────────────────────────────────────────────────
private void sendUsage(CommandSender sender) {
sender.sendMessage(TextUtil.color("<#FF6B00>=== DBD Setup ==="));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup wand — Получить палочку выделения"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup pos1/pos2 — Установить точки выделения"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set <type> — Установить объект на карте"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup build — Построить карту"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup clear — Очистить карту"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup lobby — Установить спавн лобби"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup wand — Получить ванд (ЛКМ/ПКМ для ворот)"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set hook — Установить крюк (ломай блок)"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set chest — Установить сундук"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set generator — Установить генератор"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set killer-spawn — Спавн киллера (только 1)"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set survivor-spawn — Спавн выжившего (до 4)"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set gate A|B — Установить ворота (из точек ванда)"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup clear-spawns — Очистить все спавны"));
sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup lobby — Установить точку лобби"));
}
private String formatLoc(Location loc) {
return String.format("[%s, %.0f, %.0f, %.0f]",
return String.format("[%s, %.1f, %.1f, %.1f]",
loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ());
}
@ -229,19 +370,13 @@ public class SetupCommand implements TabExecutor {
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("mdbd.admin")) return List.of();
if (args.length == 1) {
return Arrays.asList("wand", "pos1", "pos2", "set", "build", "clear", "lobby")
.stream()
.filter(s -> s.startsWith(args[0].toLowerCase()))
.toList();
return List.of("wand", "set", "build", "clear", "clear-spawns", "lobby").stream()
.filter(s -> s.startsWith(args[0].toLowerCase())).toList();
}
if (args.length == 2 && args[0].equalsIgnoreCase("set")) {
return Arrays.asList("generator", "hook", "pallet", "gate",
"killer-spawn", "survivor-spawn", "lobby")
.stream()
.filter(s -> s.startsWith(args[1].toLowerCase()))
.toList();
return List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn", "gate").stream()
.filter(s -> s.startsWith(args[1].toLowerCase())).toList();
}
return List.of();
}

View file

@ -1,5 +1,6 @@
package dev.loki.config;
import dev.loki.core.GameSession;
import dev.loki.mdbd;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
@ -7,6 +8,8 @@ import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public class ConfigManager {
@ -36,6 +39,22 @@ public class ConfigManager {
plugin.getLogger().info("Configs loaded successfully");
}
/**
* Reload all config files from disk and update all running game sessions with the new config.
*/
public void reloadConfigs() {
loadConfigs();
// Update all active GameSessions with the new GameConfig reference
if (plugin.getGameManager() != null) {
for (GameSession session : plugin.getGameManager().getAllSessions()) {
if (session != null) {
session.refreshConfig();
}
}
}
plugin.getLogger().info("Configs reloaded and applied to all running sessions");
}
public void saveAllConfigs() {
saveConfig("locations.yml", locationsConfig.getConfig());
saveConfig("game.yml", gameConfig.getConfig());
@ -48,7 +67,13 @@ public class ConfigManager {
if (!file.exists()) {
createDefaultConfig(fileName, file);
}
return YamlConfiguration.loadConfiguration(file);
try (var reader = new InputStreamReader(
Files.newInputStream(file.toPath()), StandardCharsets.UTF_8)) {
return YamlConfiguration.loadConfiguration(reader);
} catch (IOException e) {
plugin.getLogger().warning("Could not read config UTF-8, fallback: " + fileName);
return YamlConfiguration.loadConfiguration(file);
}
}
private void createDefaultConfig(String resourceName, File targetFile) {

View file

@ -17,7 +17,7 @@ public class GameConfig {
setDefault("lobby.countdown-4-players", 45);
setDefault("lobby.countdown-5-players", 15);
setDefault("lobby.role-assign-delay", 5);
setDefault("lobby.server-lobby-count", 3);
setDefault("lobby.server-lobby-count", 10);
setDefault("lobby.persist-lobbies", true);
// Players scale
@ -42,7 +42,7 @@ public class GameConfig {
setDefault("hooks.struggle-interval", 2);
setDefault("hooks.max-hooks", 3);
setDefault("hooks.interaction-range", 3.0);
setDefault("hooks.rescue-seconds", 3);
setDefault("hooks.rescue-seconds", 2);
setDefault("hooks.sabotage-seconds", 10);
setDefault("hooks.sabotage-recovery-seconds", 60);
@ -64,17 +64,14 @@ public class GameConfig {
setDefault("healing.heal-range", 3.0);
// Carry / escape
setDefault("carry.escape-jumps", 15);
setDefault("carry.escape-shift-presses", 50);
setDefault("carry.escape-speed-seconds", 3);
setDefault("carry.jump-fill-percent", 6.67);
// Killer
setDefault("killer.attack-cooldown", 2.5);
setDefault("killer.post-hit-cooldown", 4.0);
setDefault("killer.attack-range", 3.0);
setDefault("killer.pallet-stun-seconds", 2.0);
setDefault("killer.pallet-break-seconds", 3.0);
setDefault("killer.terror-radius", 32);
setDefault("killer.pallet-interaction-range", 3.0);
// BossBar
setDefault("bossbar.enabled", true);
@ -84,15 +81,8 @@ public class GameConfig {
setDefault("bossbar.appear-range", 32);
setDefault("bossbar.full-range", 8);
// Window vault
setDefault("window.vault-survivor-fast", 1.0);
setDefault("window.vault-survivor-slow", 1.5);
setDefault("window.vault-killer", 1.5);
setDefault("window.vault-block-seconds", 15);
// Endgame
setDefault("endgame.gate-open-seconds", 20);
setDefault("endgame.collapse-seconds", 120);
setDefault("endgame.chest-open-seconds", 3);
setDefault("endgame.gate-interaction-range", 3.0);
@ -115,6 +105,31 @@ public class GameConfig {
// Hatch
setDefault("hatch.min-survivors-for-open", 1);
setDefault("hatch.require-more-gens-than-survivors", true);
// Block types for spawning
setDefault("blocks.generator", "BLAST_FURNACE");
setDefault("blocks.chest", "CHEST");
setDefault("blocks.hook", "IRON_BARS");
// Holograms
setDefault("holograms.enabled", true);
setDefault("holograms.generator.enabled", true);
setDefault("holograms.generator.height-offset", -1.0);
setDefault("holograms.hook.enabled", true);
setDefault("holograms.hook.height-offset", 0.0);
setDefault("holograms.chest.enabled", true);
setDefault("holograms.chest.height-offset", 0.0);
// Skill check
setDefault("skillcheck.chance", 0.005);
setDefault("skillcheck.win-percent", 5.0);
setDefault("skillcheck.loss-percent", 10.0);
setDefault("skillcheck.success-block", "GREEN_STAINED_GLASS_PANE");
setDefault("skillcheck.fail-block", "RED_STAINED_GLASS_PANE");
setDefault("skillcheck.neutral-block", "GRAY_STAINED_GLASS_PANE");
// Chest loot
setDefault("chest.loot-reset-minutes", 3.0);
}
private void setDefault(String path, Object value) {
@ -193,19 +208,41 @@ public class GameConfig {
// -------------------------------------------------------------------------
// Carry / Escape
// -------------------------------------------------------------------------
public int getCarryEscapeJumps() { return config.getInt("carry.escape-jumps", 15); }
/** Number of Shift presses required to escape from killer's carry (default 50). */
public int getCarryEscapePresses() { return config.getInt("carry.escape-shift-presses", 50); }
public int getCarryEscapeSpeedSeconds() { return config.getInt("carry.escape-speed-seconds", 3); }
public double getCarryJumpFillPercent() { return config.getDouble("carry.jump-fill-percent", 6.67); }
// -------------------------------------------------------------------------
// Killer
// -------------------------------------------------------------------------
public int getAttackCooldown() { return (int)(config.getDouble("killer.attack-cooldown", 2.5) * 20); }
/** Кулдаун после ПОПАДАНИЯ по игроку (3–5 сек). Отличается от swing cooldown. */
public int getPostHitCooldown() { return (int)(config.getDouble("killer.post-hit-cooldown", 4.0) * 20); }
public double getAttackRange() { return config.getDouble("killer.attack-range", 3.0); }
public int getPalletStunTicks() { return (int)(config.getDouble("killer.pallet-stun-seconds", 2.0) * 20); }
public int getPalletBreakTicks() { return (int)(config.getDouble("killer.pallet-break-seconds", 3.0) * 20); }
public int getTerrorRadius() { return config.getInt("killer.terror-radius", 32); }
public double getPalletInteractionRange() { return config.getDouble("killer.pallet-interaction-range", 3.0); }
// -------------------------------------------------------------------------
// Block types (какие блоки спавнятся на местах объектов)
// -------------------------------------------------------------------------
public String getGeneratorBlock() { return config.getString("blocks.generator", "BLAST_FURNACE"); }
public String getChestBlock() { return config.getString("blocks.chest", "CHEST"); }
public String getHookBlock() { return config.getString("blocks.hook", "IRON_BARS"); }
// -------------------------------------------------------------------------
// Holograms
// -------------------------------------------------------------------------
public boolean isHologramsEnabled() { return config.getBoolean("holograms.enabled", true); }
public boolean isGeneratorHologramEnabled() { return config.getBoolean("holograms.generator.enabled", true); }
public double getGeneratorHologramHeightOffset() { return config.getDouble("holograms.generator.height-offset", -1.0); }
public boolean isHookHologramEnabled() { return config.getBoolean("holograms.hook.enabled", true); }
public double getHookHologramHeightOffset() { return config.getDouble("holograms.hook.height-offset", 0.0); }
public boolean isChestHologramEnabled() { return config.getBoolean("holograms.chest.enabled", true); }
public double getChestHologramHeightOffset() { return config.getDouble("holograms.chest.height-offset", 0.0); }
// -------------------------------------------------------------------------
// Chest loot
// -------------------------------------------------------------------------
public double getChestLootResetMinutes() { return config.getDouble("chest.loot-reset-minutes", 3.0); }
// -------------------------------------------------------------------------
// BossBar
@ -217,19 +254,10 @@ public class GameConfig {
public int getBossBarAppearRange() { return config.getInt("bossbar.appear-range", 32); }
public int getBossBarFullRange() { return config.getInt("bossbar.full-range", 8); }
// -------------------------------------------------------------------------
// Window
// -------------------------------------------------------------------------
public int getVaultSurvivorFastTicks() { return (int)(config.getDouble("window.vault-survivor-fast", 1.0) * 20); }
public int getVaultSurvivorSlowTicks() { return (int)(config.getDouble("window.vault-survivor-slow", 1.5) * 20); }
public int getVaultKillerTicks() { return (int)(config.getDouble("window.vault-killer", 1.5) * 20); }
public int getVaultBlockSeconds() { return config.getInt("window.vault-block-seconds", 15); }
// -------------------------------------------------------------------------
// Endgame
// -------------------------------------------------------------------------
public int getGateOpenSeconds() { return config.getInt("endgame.gate-open-seconds", 20); }
public int getCollapseSeconds() { return config.getInt("endgame.collapse-seconds", 120); }
public int getChestOpenSeconds() { return config.getInt("endgame.chest-open-seconds", 3); }
public double getGateInteractionRange() { return config.getDouble("endgame.gate-interaction-range", 3.0); }
@ -248,5 +276,15 @@ public class GameConfig {
public String getColorHealing() { return config.getString("scoreboard.color-healing", "GREEN"); }
public String getColorHooked() { return config.getString("scoreboard.color-hooked", "DARK_RED"); }
// -------------------------------------------------------------------------
// Skill check
// -------------------------------------------------------------------------
public double getSkillCheckChance() { return config.getDouble("skillcheck.chance", 0.005); }
public double getSkillCheckWinPercent() { return config.getDouble("skillcheck.win-percent", 5.0); }
public double getSkillCheckLossPercent() { return config.getDouble("skillcheck.loss-percent", 10.0); }
public String getSkillCheckSuccessBlock() { return config.getString("skillcheck.success-block", "GREEN_STAINED_GLASS_PANE"); }
public String getSkillCheckFailBlock() { return config.getString("skillcheck.fail-block", "RED_STAINED_GLASS_PANE"); }
public String getSkillCheckNeutralBlock() { return config.getString("skillcheck.neutral-block", "GRAY_STAINED_GLASS_PANE"); }
public FileConfiguration getConfig() { return config; }
}

View file

@ -25,31 +25,27 @@ public class LocationsConfig {
config.set("lobby.yaw", 0.0);
config.set("lobby.pitch", 0.0);
}
if (!config.contains("generators")) {
// Безопасная проверка: contains() может вернуть true для пустого списка,
// поэтому также проверяем что getList() не null
if (!config.contains("generators") || config.getList("generators") == null) {
config.set("generators", new ArrayList<>());
}
if (!config.contains("hooks")) {
if (!config.contains("hooks") || config.getList("hooks") == null) {
config.set("hooks", new ArrayList<>());
}
if (!config.contains("pallets")) {
config.set("pallets", new ArrayList<>());
}
if (!config.contains("windows")) {
if (!config.contains("windows") || config.getList("windows") == null) {
config.set("windows", new ArrayList<>());
}
if (!config.contains("chests")) {
if (!config.contains("chests") || config.getList("chests") == null) {
config.set("chests", new ArrayList<>());
}
if (!config.contains("totems")) {
config.set("totems", new ArrayList<>());
}
if (!config.contains("exitgates")) {
if (!config.contains("exitgates") || config.getList("exitgates") == null) {
config.set("exitgates", new ArrayList<>());
}
if (!config.contains("killer-spawn")) {
if (!config.contains("killer-spawn") || config.getList("killer-spawn") == null) {
config.set("killer-spawn", new ArrayList<>());
}
if (!config.contains("survivor-spawns")) {
if (!config.contains("survivor-spawns") || config.getList("survivor-spawns") == null) {
config.set("survivor-spawns", new ArrayList<>());
}
}
@ -84,10 +80,6 @@ public class LocationsConfig {
return loadLocationList("hooks");
}
public List<Location> getPallets() {
return loadLocationList("pallets");
}
public List<Location> getWindows() {
return loadLocationList("windows");
}
@ -100,8 +92,10 @@ public class LocationsConfig {
return loadLocationList("totems");
}
public List<Location> getExitGates() {
return loadLocationList("exitgates");
// getExitGates() — определён в addLocation/setGate блоке
public List<Location> getKillerSpawns() {
return loadLocationList("killer-spawn");
}
public Location getKillerSpawn() {
@ -109,39 +103,35 @@ public class LocationsConfig {
return spawns.isEmpty() ? null : spawns.get(0);
}
public void setKillerSpawn(Location loc) {
// killer-spawn is a single location stored as a list with one entry
List<String> list = new java.util.ArrayList<>();
list.add(String.format("%s,%.2f,%.2f,%.2f,%.1f,%.1f",
loc.getWorld().getName(),
loc.getX(), loc.getY(), loc.getZ(),
loc.getYaw(), loc.getPitch()));
config.set("killer-spawn", list);
}
public List<Location> getSurvivorSpawns() {
return loadLocationList("survivor-spawns");
}
private List<Location> loadLocationList(String path) {
List<Location> locations = new ArrayList<>();
List<?> list = config.getList(path);
if (list == null) return locations;
List<String> list = safeGetStringList(path);
if (list.isEmpty()) return locations;
for (Object obj : list) {
if (obj instanceof String) {
Location loc = parseLocation((String) obj);
if (loc != null) locations.add(loc);
}
for (String s : list) {
Location loc = parseLocation(s);
if (loc != null) locations.add(loc);
}
return locations;
}
private Location parseLocation(String str) {
String[] parts = str.split(",");
// Handle both old format (world,x,y,z,yaw,pitch) and new format (world|x|y|z|yaw|pitch)
// New format uses | as separator to avoid issues with negative coordinates
String[] parts = str.contains("|") ? str.split("\\|") : str.split(",");
if (parts.length < 4) return null;
World world = Bukkit.getWorld(parts[0].trim());
if (world == null) return null;
if (world == null) {
// Try to find by first loaded world as fallback
var loadedWorld = Bukkit.getWorlds().stream().findFirst();
if (loadedWorld.isEmpty()) return null;
world = loadedWorld.get();
}
try {
double x = Double.parseDouble(parts[1].trim());
double y = Double.parseDouble(parts[2].trim());
@ -155,15 +145,106 @@ public class LocationsConfig {
}
public void addLocation(String path, Location loc) {
List<String> list = config.getStringList(path);
list.add(String.format("%s,%.2f,%.2f,%.2f,%.1f,%.1f",
List<String> list = safeGetStringList(path);
list.add(locationToString(loc));
config.set(path, list);
}
/**
* Sets a location at a specific index in the list.
* If the index is beyond the current list size, pads with empty strings.
*/
public void setLocationAtIndex(String path, int index, Location loc) {
List<String> list = safeGetStringList(path);
while (list.size() <= index) {
list.add("");
}
list.set(index, locationToString(loc));
config.set(path, list);
}
/** Безопасная загрузка StringList: использует getList с fallback на toString(). */
private List<String> safeGetStringList(String path) {
List<String> result = new ArrayList<>();
List<?> raw = config.getList(path);
if (raw != null) {
for (Object obj : raw) {
if (obj instanceof String s) {
result.add(s);
} else if (obj != null) {
result.add(obj.toString());
}
}
}
return result;
}
/** Очистить список локаций (используется при загрузке карты). */
public void clearLocationList(String path) {
config.set(path, new ArrayList<>());
}
public void setSingleLocation(String path, Location loc) {
List<String> list = new ArrayList<>();
list.add(locationToString(loc));
config.set(path, list);
}
/** Устанавливает ворота A или B (хранятся как gate-a / gate-b). */
public void setGate(String id, Location loc) {
String key = "gate-" + id.toLowerCase();
config.set(key, locationToString(loc));
}
public Location getGate(String id) {
String key = "gate-" + id.toLowerCase();
String str = config.getString(key);
return str != null ? parseLocation(str) : null;
}
public List<Location> getExitGates() {
// Пробуем новый формат gate-a / gate-b
List<Location> gates = new ArrayList<>();
Location a = getGate("a");
Location b = getGate("b");
if (a != null) gates.add(a);
if (b != null) gates.add(b);
// Фолбэк: старый список exitgates
if (gates.isEmpty()) {
gates.addAll(loadLocationList("exitgates"));
}
return gates;
}
private String locationToString(Location loc) {
// Use | as separator instead of , to avoid issues with negative coordinates in YAML
return String.format("%s|%.2f|%.2f|%.2f|%.1f|%.1f",
loc.getWorld().getName(),
loc.getX(), loc.getY(), loc.getZ(),
loc.getYaw(), loc.getPitch()));
config.set(path, list);
loc.getYaw(), loc.getPitch());
}
public FileConfiguration getConfig() {
return config;
}
/**
* Reload locations from the given FileConfiguration (freshly loaded from disk).
* This clears any stale in-memory state and re-applies defaults.
*/
/** Reload internal FileConfiguration reference (used after config reload from disk). */
public void reload(FileConfiguration newConfig) {
// Replace defaults on new config
if (!newConfig.contains("lobby")) {
newConfig.set("lobby.world", "world");
newConfig.set("lobby.x", 0.0);
newConfig.set("lobby.y", 100.0);
newConfig.set("lobby.z", 0.0);
newConfig.set("lobby.yaw", 0.0);
newConfig.set("lobby.pitch", 0.0);
}
// Copy all data from new config to current config
for (String key : newConfig.getKeys(true)) {
config.set(key, newConfig.get(key));
}
}
}

View file

@ -49,10 +49,6 @@ public class MessagesConfig {
setDefault("survivor.heal.complete", "&#00FF00Вылечено!");
setDefault("survivor.heal.interrupt", "&#FFD700Лечение прервано!");
setDefault("survivor.sprint.exhausted", "&#FF8C00Вы устали...");
setDefault("survivor.pallet.drop", "&#DEB887Вы бросили паллет!");
setDefault("survivor.pallet.stun", "&#FFD700Киллер оглушен!");
setDefault("survivor.vault.fast", "&#00CED1Быстрый прыжок!");
setDefault("survivor.vault.slow", "&#87CEEBМедленный прыжок...");
// Hook messages
setDefault("hook.stage1", "&#FFFF00Крюк: Фаза 1");
@ -66,16 +62,11 @@ public class MessagesConfig {
// Item messages
setDefault("item.found", "&#FFD700Найдено: %s");
setDefault("item.medkit.use", "&#00FA9AИспользуете аптечку...");
setDefault("item.flashlight.blind", "&#FFFF00Ослепили киллера!");
setDefault("item.key.hatch", "&#00CED1Ключ открывает люк!");
setDefault("item.map.reveal", "&#87CEEBОбъекты показаны на карте");
// Endgame
setDefault("endgame.gates.powered", "&#00FF00Ворота запитаны!");
setDefault("endgame.gates.opening", "&#FFD700Открываете ворота...");
setDefault("endgame.hatch.open", "&#4B0082Люк открыт!");
setDefault("endgame.collapse.warning", "&#FF0000⚠ КОЛЛАПС ЧЕРЕЗ %s СЕК!");
setDefault("endgame.collapse.start", "&#8B0000КОЛЛАПС НАЧАЛСЯ!");
setDefault("endgame.escape", "&#00FF00✓ ПОБЕГ!");
// Error messages

View file

@ -11,68 +11,137 @@ import java.util.UUID;
public class GameManager {
private final mdbd plugin;
private final Map<UUID, GameSession> playerSessions;
/** Named sessions created by LobbyRegistry */
private final Map<String, GameSession> namedSessions;
private GameSession currentSession;
public GameManager(mdbd plugin) {
this.plugin = plugin;
this.playerSessions = new HashMap<>();
this.namedSessions = new HashMap<>();
}
// -------------------------------------------------------------------------
// Default single-session API (used by /dbd join without a name)
// -------------------------------------------------------------------------
public boolean createGame() {
if (currentSession != null) {
return false;
}
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;
}
if (currentSession == null) return false;
return joinSession(player, currentSession);
}
// Register the player in the session as a Survivor (role assigned later)
Survivor survivor = new Survivor(player.getUniqueId());
currentSession.addPlayer(survivor);
// -------------------------------------------------------------------------
// Named-session API (used by LobbyRegistry)
// -------------------------------------------------------------------------
playerSessions.put(player.getUniqueId(), currentSession);
// Notify session that player joined (teleport to lobby etc)
currentSession.notifyPlayerJoined(player);
/**
* Create a new named GameSession and store it.
* @return the new session, or null if name already taken
*/
public GameSession createNamedGame(String name) {
String key = name.toLowerCase();
// Remove stale session if it exists (e.g. from previous server run)
GameSession old = namedSessions.get(key);
if (old != null) {
old.stop();
namedSessions.remove(key);
}
GameSession session = new GameSession(plugin);
session.start();
namedSessions.put(key, session);
// Also set as current session for backward-compat if none exists
if (currentSession == null) currentSession = session;
return session;
}
/**
* Join a specific session directly.
* @return true on success
*/
public boolean joinSession(Player player, GameSession session) {
if (session == null) return false;
UUID playerId = player.getUniqueId();
GameSession currentSession = playerSessions.get(playerId);
// If player is already in this session, do nothing (success)
if (currentSession == session) return true;
// If player is in a different session, leave it first
if (currentSession != null) {
// Remove from old session
currentSession.removePlayer(playerId);
playerSessions.remove(playerId);
}
Survivor survivor = new Survivor(playerId);
session.addPlayer(survivor);
playerSessions.put(playerId, session);
session.notifyPlayerJoined(player);
return true;
}
/** Remove a named session (called when lobby is deleted). */
public void removeNamedGame(String name) {
GameSession gs = namedSessions.remove(name.toLowerCase());
if (gs != null) {
if (gs == currentSession) currentSession = null;
// Remove all players in this session from the player map
playerSessions.entrySet().removeIf(e -> e.getValue() == gs);
gs.stop();
}
}
public GameSession getNamedSession(String name) {
return namedSessions.get(name.toLowerCase());
}
// -------------------------------------------------------------------------
// Shared API
// -------------------------------------------------------------------------
public boolean leaveGame(Player player) {
GameSession session = playerSessions.remove(player.getUniqueId());
if (session != null) {
session.removePlayer(player.getUniqueId());
// Teleport after removing from session
plugin.getLobbyManager().teleportToLobby(player);
return true;
}
return false;
if (session == null) return false;
session.removePlayer(player.getUniqueId());
plugin.getLobbyManager().teleportToLobby(player);
return true;
}
public void endGame() {
if (currentSession != null) {
// Remove current session players from map
playerSessions.entrySet().removeIf(e -> e.getValue() == currentSession);
currentSession.stop();
currentSession = null;
playerSessions.clear();
}
}
public GameSession getCurrentSession() {
return currentSession;
public void endAll() {
namedSessions.values().forEach(GameSession::stop);
namedSessions.clear();
if (currentSession != null) { currentSession.stop(); currentSession = null; }
playerSessions.clear();
}
public boolean isInGame(UUID uuid) {
return playerSessions.containsKey(uuid);
}
public GameSession getCurrentSession() { return currentSession; }
public boolean isInGame(UUID uuid) { return playerSessions.containsKey(uuid); }
public GameSession getPlayerSession(UUID uuid) { return playerSessions.get(uuid); }
public GameSession getPlayerSession(UUID uuid) {
return playerSessions.get(uuid);
/**
* Returns all active GameSessions (both current and named).
*/
public java.util.Collection<GameSession> getAllSessions() {
java.util.Set<GameSession> all = new java.util.HashSet<>();
if (currentSession != null) all.add(currentSession);
all.addAll(namedSessions.values());
return all;
}
}

View file

@ -5,7 +5,6 @@ import dev.loki.gate.GateManager;
import dev.loki.hook.HookManager;
import dev.loki.mdbd;
import dev.loki.objective.GeneratorManager;
import dev.loki.pallet.PalletManager;
import dev.loki.phase.EndgamePhase;
import dev.loki.phase.GameplayPhase;
import dev.loki.phase.LobbyPhase;
@ -31,8 +30,8 @@ public class GameSession {
private GeneratorManager generatorManager;
private boolean forceStarted = false;
private Runnable onStopped; // callback для LobbyRegistry
private PalletManager palletManager;
private HookManager hookManager;
private GateManager gateManager;
@ -59,10 +58,20 @@ public class GameSession {
}
if (currentPhase != null) {
currentPhase.exit(this);
currentPhase = null;
}
players.clear();
eventBus.clear();
plugin.getLogger().info("Game session stopped");
if (onStopped != null) {
Runnable cb = onStopped;
onStopped = null; // предотвращаем повторный вызов
Bukkit.getScheduler().runTask(plugin, cb);
}
}
public void setOnStopped(Runnable callback) {
this.onStopped = callback;
}
private void tick() {
@ -76,11 +85,19 @@ public class GameSession {
}
private void transitionToNextPhase() {
Phase oldPhase = currentPhase;
Phase next = determineNextPhase();
if (next != null) {
currentPhase.exit(this);
oldPhase.exit(this);
currentPhase = next;
currentPhase.enter(this);
} else {
// Терминальная фаза завершена — всегда выходим из текущей фазы перед остановкой
if (oldPhase != null) {
oldPhase.exit(this);
currentPhase = null;
}
stop();
}
}
@ -93,11 +110,10 @@ public class GameSession {
if (currentPhase instanceof RoleAssignPhase) {
return new GameplayPhase(generatorManager);
}
if (currentPhase instanceof GameplayPhase) {
return new EndgamePhase(
plugin.getConfigManager().getGame().getCollapseSeconds(),
gateManager, null, null
);
if (currentPhase instanceof GameplayPhase gp) {
// Всегда переходим в EndgamePhase для корректного завершения игры
// (очистка скорборда, телепорт в лобби, показ результатов)
return new EndgamePhase(gateManager, null, null);
}
return null;
}
@ -159,9 +175,6 @@ public class GameSession {
return generatorManager;
}
public PalletManager getPalletManager() { return palletManager; }
public void setPalletManager(PalletManager pm) { this.palletManager = pm; }
public HookManager getHookManager() { return hookManager; }
public void setHookManager(HookManager hm) { this.hookManager = hm; }
@ -179,10 +192,23 @@ public class GameSession {
return ticksElapsed;
}
public String getPhaseName() {
if (currentPhase == null) return "None";
return currentPhase.getClass().getSimpleName();
}
public GameEventBus getEventBus() {
return eventBus;
}
/**
* Refresh the config reference so the session uses the latest values from disk.
*/
public void refreshConfig() {
// The config is fetched fresh each time from the ConfigManager,
// so no cached reference needs updating.
}
public void notifyPlayerJoined(org.bukkit.entity.Player player) {
if (currentPhase != null) {
currentPhase.onPlayerJoined(this, player);

View file

@ -90,7 +90,8 @@ public class GeneratorDataManager {
}
public static String formatLocation(Location loc) {
return String.format("%s:%.0f,%.0f,%.0f",
// Use | as separator to handle negative coordinates properly
return String.format("%s:%.0f|%.0f|%.0f",
loc.getWorld().getName(),
loc.getX(),
loc.getY(),
@ -98,13 +99,14 @@ public class GeneratorDataManager {
}
private Location parseLocation(String str) {
// Format: "world:x|y|z" — using | as separator for coordinates to handle negative numbers
String[] parts = str.split(":");
if (parts.length < 2) return null;
World world = Bukkit.getWorld(parts[0].trim());
if (world == null) return null;
String[] coords = parts[1].split(",");
String[] coords = parts[1].split("\\|");
if (coords.length < 3) return null;
try {

View file

@ -3,7 +3,9 @@ package dev.loki.gate;
import dev.loki.config.GameConfig;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
@ -79,10 +81,17 @@ public class GateOpening {
private void completeOpening(Survivor survivor, ExitGate gate) {
Player player = survivor.getPlayer();
Location loc = gate.getLocation();
World world = loc.getWorld();
if (player != null) {
player.sendMessage("§aВорота открыты!");
player.getWorld().playSound(gate.getLocation(),
Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
if (world != null) {
world.playSound(loc, Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
// Big particle burst when opened
world.spawnParticle(Particle.EXPLOSION_EMITTER, loc.clone().add(0, 1, 0), 1);
world.spawnParticle(Particle.FLAME, loc.clone().add(0, 1.5, 0), 15, 2, 1, 2, 0.1);
}
}
gate.updateProgress(1.0);
openingPlayers.remove(survivor.getUuid());
@ -93,6 +102,17 @@ public class GateOpening {
player.getWorld().playSound(gate.getLocation(),
Sound.BLOCK_METAL_HIT, 0.3f, 1.0f);
}
// Spawn particles around the gate
if (progress % 5 == 0) {
Location loc = gate.getLocation();
World world = loc.getWorld();
if (world != null) {
// Green particles for progress (using CRIT for visibility)
world.spawnParticle(Particle.CRIT,
loc.clone().add(0, 1, 0), 3, 0.5, 0.5, 0.5, 0.1);
}
}
}
private class OpeningProgress {

View file

@ -52,6 +52,12 @@ public class Hook {
stageProgress = 0;
}
/** Force-advance to SACRIFICE stage immediately (used by HookRecovery on 3 fails). */
public void advanceToSacrifice() {
currentStage = HookStage.SACRIFICE;
stageProgress = 0;
}
public double getStageProgressPercent() {
if (currentStage == HookStage.SACRIFICE) return 100;
int stageDuration = currentStage.getDurationSeconds() * 20;

View file

@ -0,0 +1,141 @@
package dev.loki.hook;
import dev.loki.mdbd;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Display;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.TextDisplay;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Floating labels above hooks showing their status.
* Uses TextDisplay (Paper 1.19.4+) with ArmorStand fallback for older versions.
*/
public class HookHologram {
private final mdbd plugin;
private final HookManager manager;
private final Map<UUID, Object> displays = new HashMap<>(); // TextDisplay or ArmorStand
private BukkitTask task;
private boolean useTextDisplay;
public HookHologram(mdbd plugin, HookManager manager) {
this.plugin = plugin;
this.manager = manager;
this.useTextDisplay = isTextDisplayAvailable();
}
private static boolean isTextDisplayAvailable() {
try {
EntityType.valueOf("TEXT_DISPLAY");
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public void start() {
stop();
spawnAll();
task = plugin.getServer().getScheduler().runTaskTimer(plugin, this::refresh, 20L, 20L);
plugin.getLogger().info("[HookHologram] Started with " + displays.size() + " holograms (mode=" + (useTextDisplay ? "TextDisplay" : "ArmorStand") + ")");
}
public void stop() {
if (task != null) {
task.cancel();
task = null;
}
for (Object display : displays.values()) {
if (display instanceof TextDisplay td && td.isValid()) {
td.remove();
} else if (display instanceof ArmorStand as && as.isValid()) {
as.remove();
}
}
displays.clear();
}
private void spawnAll() {
double heightOffset = plugin.getConfigManager().getGame().getHookHologramHeightOffset();
int index = 0;
for (Hook hook : manager.getAllHooks()) {
Location loc = hook.getLocation();
if (loc.getWorld() == null) {
plugin.getLogger().warning("[HookHologram] Hook " + hook.getId() + " has null world, skipping");
continue;
}
Location holoLoc = loc.clone().add(0.5, 2.5 + heightOffset, 0.5);
try {
if (useTextDisplay) {
TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> {
d.setBillboard(Display.Billboard.VERTICAL);
d.setSeeThrough(true);
d.setShadowed(true);
d.setDefaultBackground(false);
d.setPersistent(false);
d.setTextOpacity((byte) -1);
d.setViewRange(128.0f);
});
displays.put(hook.getId(), display);
} else {
ArmorStand stand = loc.getWorld().spawn(holoLoc, ArmorStand.class, as -> {
as.setInvisible(true);
as.setInvulnerable(true);
as.setGravity(false);
as.setMarker(true);
as.setCustomNameVisible(true);
as.setPersistent(false);
});
displays.put(hook.getId(), stand);
}
} catch (Exception e) {
plugin.getLogger().warning("[HookHologram] Failed to spawn hologram for hook " + hook.getId() + ": " + e.getMessage());
}
index++;
}
}
private void refresh() {
int total = manager.getAllHooks().size();
int visibleIndex = 0;
for (Hook hook : manager.getAllHooks()) {
Object display = displays.get(hook.getId());
if (display == null) continue;
boolean valid = (display instanceof TextDisplay td && td.isValid()) ||
(display instanceof ArmorStand as && as.isValid());
if (!valid) continue;
String status;
if (hook.isDisabled()) {
status = "§c✂ Саботирован";
} else if (hook.isOccupied()) {
int pct = (int) hook.getStageProgressPercent();
status = "§4Занят [" + hook.getCurrentStage().name() + " " + pct + "%]";
} else {
status = "§aСвободен";
}
visibleIndex++;
String text = "§c⚓ Крюк §f#" + visibleIndex + "/" + total + "\n§7" + status;
double heightOffset = plugin.getConfigManager().getGame().getHookHologramHeightOffset();
Location base = hook.getLocation().clone().add(0.5, 2.5 + heightOffset, 0.5);
if (display instanceof TextDisplay td) {
td.text(net.kyori.adventure.text.Component.text(text));
td.teleport(base);
} else if (display instanceof ArmorStand as) {
as.setCustomName(text);
as.teleport(base);
}
}
}
}

View file

@ -27,12 +27,18 @@ public class HookManager {
public void loadHooks(LocationsConfig config) {
hooks.clear();
List<Location> hookLocations = config.getHooks();
plugin.getLogger().info("[HookManager] Loading " + hookLocations.size() + " hooks from config");
for (int i = 0; i < hookLocations.size(); i++) {
Location loc = hookLocations.get(i);
if (loc == null || loc.getWorld() == null) {
plugin.getLogger().warning("[HookManager] Hook #" + i + " has invalid location, skipping");
continue;
}
boolean basement = i < 4;
UUID id = UUID.nameUUIDFromBytes(("hook_" + i).getBytes());
hooks.put(id, new Hook(id, loc, basement));
}
plugin.getLogger().info("[HookManager] Loaded " + hooks.size() + " valid hooks");
}
public void startTicking() {
@ -89,6 +95,10 @@ public class HookManager {
}
public boolean tryHookSurvivor(Survivor survivor, Location killerLoc) {
if (survivor.getState() != PlayerState.CARRIED) {
return false;
}
// Проверяем лимит крюков → если превышен, сразу смерть
if (survivor.getHooksTaken() >= maxHooks) {
survivor.setState(PlayerState.DEAD);
@ -101,12 +111,13 @@ public class HookManager {
return true; // "повешен" = завершён
}
Hook nearest = findNearestUnoccupiedHook(killerLoc, 3.0);
Hook nearest = findNearestUnoccupiedHook(killerLoc, 5.0);
if (nearest == null) {
nearest = findAvailableHook().orElse(null);
}
if (nearest != null) {
nearest.hookSurvivor(survivor);
survivor.setState(PlayerState.HOOKED); // Set state to HOOKED
survivor.incrementHooks();
return true;
}

View file

@ -0,0 +1,161 @@
package dev.loki.hook;
import dev.loki.config.GameConfig;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Stage 2 self-unhook mechanic.
* On Stage 2 the survivor can attempt to struggle free by holding Shift.
* Each attempt takes struggle-interval ticks. Success chance increases per attempt.
* 3 failed attempts = immediate SACRIFICE.
*/
public class HookRecovery {
private static final int BASE_CHANCE_PCT = 4; // % за первую попытку
private static final int CHANCE_STEP_PCT = 2; // +2% за каждую следующую
private static final int MAX_ATTEMPTS = 3; // провал после 3 попыток
private final Plugin plugin;
private final GameConfig config;
/** survivorUUID → active struggle attempt task */
private final Map<UUID, BukkitTask> activeTasks = new HashMap<>();
/** survivorUUID → number of failed attempts */
private final Map<UUID, Integer> failedAttempts = new HashMap<>();
public HookRecovery(Plugin plugin, GameConfig config) {
this.plugin = plugin;
this.config = config;
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
/**
* Start a recovery attempt.
* Call when a STAGE_2-hooked survivor holds Shift near their hook.
* @return true if attempt started
*/
public boolean startAttempt(Survivor survivor, Hook hook) {
UUID uuid = survivor.getUuid();
if (activeTasks.containsKey(uuid)) return false;
if (hook.getCurrentStage() != HookStage.STAGE_2) return false;
if (!hook.isOccupied() || !uuid.equals(hook.getHookedSurvivor().getUuid())) return false;
int durationTicks = config.getStruggleInterval() * 20;
BukkitTask task = new BukkitRunnable() {
int elapsed = 0;
@Override
public void run() {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline() || !p.isSneaking()
|| hook.getCurrentStage() != HookStage.STAGE_2) {
cancelAttempt(uuid);
cancel();
return;
}
elapsed++;
double pct = (double) elapsed / durationTicks * 100;
p.sendActionBar(Component.text("§6Попытка вырваться: " + (int) pct + "%"));
if (elapsed >= durationTicks) {
resolveAttempt(survivor, hook);
cancel();
}
}
}.runTaskTimer(plugin, 1L, 1L);
activeTasks.put(uuid, task);
Player p = survivor.getPlayer();
if (p != null) {
p.sendMessage(Component.text(
"[DBD] Держите Shift чтобы вырваться с крюка...", NamedTextColor.YELLOW));
}
return true;
}
/** Cancel active attempt (e.g. survivor released Shift). */
public void cancelAttempt(UUID survivorUuid) {
BukkitTask task = activeTasks.remove(survivorUuid);
if (task != null) {
task.cancel();
Player p = plugin.getServer().getPlayer(survivorUuid);
if (p != null && p.isOnline()) p.sendActionBar(Component.empty());
}
}
public boolean isAttempting(UUID uuid) {
return activeTasks.containsKey(uuid);
}
/** Reset counters when survivor is unhooked for any reason. */
public void onUnhooked(UUID survivorUuid) {
cancelAttempt(survivorUuid);
failedAttempts.remove(survivorUuid);
}
public void shutdown() {
activeTasks.values().forEach(BukkitTask::cancel);
activeTasks.clear();
failedAttempts.clear();
}
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
private void resolveAttempt(Survivor survivor, Hook hook) {
UUID uuid = survivor.getUuid();
activeTasks.remove(uuid);
int failures = failedAttempts.getOrDefault(uuid, 0);
int chance = BASE_CHANCE_PCT + failures * CHANCE_STEP_PCT;
Player p = survivor.getPlayer();
if (Math.random() * 100 < chance) {
// Success — unhook
hook.unhookSurvivor();
survivor.setState(PlayerState.INJURED);
failedAttempts.remove(uuid);
if (p != null && p.isOnline()) {
p.sendActionBar(Component.empty());
p.sendMessage(Component.text("[DBD] Вы вырвались с крюка!", NamedTextColor.GREEN));
}
} else {
// Failure
int newFailures = failures + 1;
failedAttempts.put(uuid, newFailures);
if (p != null && p.isOnline()) {
p.sendActionBar(Component.empty());
p.sendMessage(Component.text(
"[DBD] Попытка не удалась! (" + newFailures + "/" + MAX_ATTEMPTS + ")",
NamedTextColor.RED));
}
if (newFailures >= MAX_ATTEMPTS) {
// Force sacrifice
hook.advanceToSacrifice();
failedAttempts.remove(uuid);
if (p != null && p.isOnline()) {
p.sendMessage(Component.text(
"[DBD] Сущность поглотила вас!", NamedTextColor.DARK_RED));
}
}
}
}
}

View file

@ -27,6 +27,8 @@ public class HookRescue {
private final ActionBar actionBar;
private final GameConfig config;
private final Map<UUID, RescueSession> activeRescues = new HashMap<>();
/** Called when rescue succeeds: (rescuer, victim) */
private java.util.function.BiConsumer<Survivor, Survivor> onRescueComplete;
public HookRescue(mdbd plugin, ActionBar actionBar, GameConfig config) {
this.plugin = plugin;
@ -118,6 +120,10 @@ public class HookRescue {
rescuerPlayer.sendActionBar(Component.text("[DBD] Союзник спасён!", NamedTextColor.GREEN));
rescuerPlayer.sendMessage(Component.text("[DBD] Спасение завершено!", NamedTextColor.GREEN));
}
if (onRescueComplete != null && victim != null) {
onRescueComplete.accept(rescuer, victim);
}
}
public void cancelRescue(UUID uuid) {
@ -134,6 +140,10 @@ public class HookRescue {
public boolean isRescuing(UUID uuid) { return activeRescues.containsKey(uuid); }
public void setOnRescueComplete(java.util.function.BiConsumer<Survivor, Survivor> cb) {
this.onRescueComplete = cb;
}
public void clear() {
for (RescueSession session : activeRescues.values()) session.cancel();
activeRescues.clear();

View file

@ -5,20 +5,56 @@ import org.bukkit.inventory.ItemStack;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
/**
* Генератор случайного лута для сундуков.
* Может выдать: Медкит (разное количество зарядов), Набор для срезания.
*/
public class ChestLoot {
private final Random random;
public ChestLoot() {
// Веса для разных типов лута (из конфига game.yml: loot.*)
private final int medkitWeight;
private final int medkitMinCharges;
private final int medkitMaxCharges;
private final int hookCutterWeight;
/** Config-aware constructor. */
public ChestLoot(dev.loki.config.GameConfig config) {
this.random = ThreadLocalRandom.current();
var cfg = config.getConfig();
this.medkitWeight = cfg.getInt("loot.medkit.weight", 60);
this.medkitMinCharges = cfg.getInt("loot.medkit.min-charges", 12);
this.medkitMaxCharges = cfg.getInt("loot.medkit.max-charges", 16);
this.hookCutterWeight = cfg.getInt("loot.hook-cutter.weight", 40);
}
/** 55% медкит, 45% фонарик. Заряды чуть рандомные. */
/** Fallback — hardcoded defaults. */
public ChestLoot() {
this.random = ThreadLocalRandom.current();
this.medkitWeight = 60;
this.medkitMinCharges = 12;
this.medkitMaxCharges = 16;
this.hookCutterWeight = 40;
}
/**
* Бросает кубик и возвращает случайный предмет.
* Никогда не возвращает null.
*/
public ItemStack rollLoot() {
if (random.nextInt(100) < 55) {
return DBDItems.medkit(12 + random.nextInt(5));
int total = medkitWeight + hookCutterWeight;
if (total <= 0) {
return DBDItems.medkit(12);
}
int roll = random.nextInt(total);
if (roll < medkitWeight) {
int charges = medkitMinCharges + random.nextInt(Math.max(1, medkitMaxCharges - medkitMinCharges + 1));
return DBDItems.medkit(charges);
} else {
return DBDItems.flashlight(6 + random.nextInt(3));
return DBDItems.hookCutter();
}
}
}

View file

@ -10,7 +10,7 @@ import org.bukkit.persistence.PersistentDataType;
import java.util.List;
/**
* Central factory for DBD custom items: Medkit and Flashlight.
* Central factory for DBD custom items: Medkit.
* Items are identified by PersistentDataContainer tag "mdbd:item_type".
* Charges are stored as "mdbd:charges".
*/
@ -19,6 +19,17 @@ public final class DBDItems {
public static final NamespacedKey KEY_TYPE = new NamespacedKey("mdbd", "item_type");
public static final NamespacedKey KEY_CHARGES = new NamespacedKey("mdbd", "charges");
// Custom Model Data IDs (соответствуют ресурспаку)
// Переопределяются через game.yml: items.model-data.medkit / hookcutter
private static int CMD_MEDKIT = 1001;
private static int CMD_HOOKCUTTER = 1003;
/** Вызывать при загрузке плагина / reload, чтобы подхватить значения из конфига. */
public static void loadModelData(org.bukkit.configuration.file.FileConfiguration cfg) {
CMD_MEDKIT = cfg.getInt("items.model-data.medkit", 1001);
CMD_HOOKCUTTER = cfg.getInt("items.model-data.hookcutter", 1003);
}
private DBDItems() {}
// ── Factories ─────────────────────────────────────────────────────────────
@ -26,7 +37,7 @@ public final class DBDItems {
/** Медкит — лечит одно состояние здоровья. */
public static ItemStack medkit(int charges) {
return build(
Material.PAPER,
Material.PAPER, CMD_MEDKIT,
"<#00FF7F>Медкит",
charges, ItemType.MEDKIT.getDefaultCharges(),
List.of(
@ -39,25 +50,25 @@ public final class DBDItems {
public static ItemStack medkit() { return medkit(ItemType.MEDKIT.getDefaultCharges()); }
/** Фонарик — ослепляет киллера при попадании. */
public static ItemStack flashlight(int charges) {
/** Набор для срезания — снимает выжившего с крюка, киллер не может вешать 30 сек. */
public static ItemStack hookCutter() {
return build(
Material.TORCH,
"<#FFD700>Фонарик",
charges, ItemType.FLASHLIGHT.getDefaultCharges(),
Material.SHEARS, CMD_HOOKCUTTER,
"<#FF6B6B>Набор для срезания",
1, 1,
List.of(
"<gray>Ослепляет киллера при попадании.",
"<yellow>ПКМ <gray>— направить луч на киллера"
"<gray>Срезает выжившего с крюка.",
"<yellow>ПКМ <gray>— срезать выжившего с крюка",
"<dark_red>Киллер не сможет вешать на крюк 30 сек"
),
"FLASHLIGHT"
"HOOK_CUTTER"
);
}
public static ItemStack flashlight() { return flashlight(ItemType.FLASHLIGHT.getDefaultCharges()); }
// ── Helpers ───────────────────────────────────────────────────────────────
private static ItemStack build(Material mat, String name, int charges, int maxCharges,
private static ItemStack build(Material mat, int customModelData, String name,
int charges, int maxCharges,
List<String> loreLines, String typeId) {
var item = new ItemStack(mat);
item.editMeta(meta -> {
@ -71,6 +82,11 @@ public final class DBDItems {
lore.add(TextUtil.color(chargesBar(charges, maxCharges)));
meta.lore(lore);
// Custom Model Data для ресурспака (0 = не задан)
if (customModelData > 0) {
meta.setCustomModelData(customModelData);
}
meta.getPersistentDataContainer().set(KEY_TYPE, PersistentDataType.STRING, typeId);
meta.getPersistentDataContainer().set(KEY_CHARGES, PersistentDataType.INTEGER, charges);
});
@ -105,12 +121,11 @@ public final class DBDItems {
if (type == null) return item;
return switch (type) {
case "MEDKIT" -> medkit(newCharges);
case "FLASHLIGHT" -> flashlight(newCharges);
default -> item;
};
}
public static boolean isMedkit(ItemStack item) { return "MEDKIT".equals(getType(item)); }
public static boolean isFlashlight(ItemStack item) { return "FLASHLIGHT".equals(getType(item)); }
public static boolean isHookCutter(ItemStack item) { return "HOOK_CUTTER".equals(getType(item)); }
public static boolean isDBDItem(ItemStack item) { return getType(item) != null; }
}

View file

@ -1,96 +0,0 @@
package dev.loki.item;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
public class ItemFlashlight implements BaseItem {
private static final NamespacedKey ITEM_KEY = new NamespacedKey("mdbd", "item_type");
private static final NamespacedKey CHARGES_KEY = new NamespacedKey("mdbd", "charges");
private final int charges;
public ItemFlashlight() {
this(ItemType.FLASHLIGHT.getDefaultCharges());
}
public ItemFlashlight(int charges) {
this.charges = charges;
}
@Override
public ItemType getType() {
return ItemType.FLASHLIGHT;
}
@Override
public ItemStack createItemStack() {
ItemStack item = new ItemStack(Material.TORCH);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§e" + getType().getDisplayName());
meta.setLore(Arrays.asList(
"§7" + getType().getDescription(),
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
"§7Aim at Killer and click to blind"
));
PersistentDataContainer pdc = meta.getPersistentDataContainer();
pdc.set(ITEM_KEY, PersistentDataType.STRING, getItemId());
pdc.set(CHARGES_KEY, PersistentDataType.INTEGER, charges);
item.setItemMeta(meta);
}
return item;
}
@Override
public boolean canUse(Player player) {
return charges > 0;
}
@Override
public void onUse(Player player) {
if (charges > 0) {
player.sendMessage("§eYou flash your flashlight!");
}
}
@Override
public void onDeplete(Player player) {
player.getInventory().setItemInMainHand(null);
player.sendMessage("§cYour flashlight batteries died!");
}
@Override
public int getCharges() {
return charges;
}
@Override
public void setCharges(int charges) {
}
@Override
public boolean isDepleted() {
return charges <= 0;
}
@Override
public String getItemId() {
return "FLASHLIGHT";
}
public static boolean isFlashlight(ItemStack item) {
if (item == null || !item.hasItemMeta()) return false;
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
return "FLASHLIGHT".equals(pdc.get(ITEM_KEY, PersistentDataType.STRING));
}
}

View file

@ -2,7 +2,7 @@ package dev.loki.item;
public enum ItemType {
MEDKIT("Medkit", "Heals one health state", 16),
FLASHLIGHT("Flashlight", "Blinds the Killer", 8);
HOOK_CUTTER("Hook Cutter", "Cuts a survivor off a hook", 1);
private final String displayName;
private final String description;

View file

@ -6,6 +6,7 @@ import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -17,8 +18,6 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
@ -27,7 +26,7 @@ import java.util.Map;
import java.util.UUID;
/**
* Handles right-click usage of DBD custom items (Medkit, Flashlight) during gameplay.
* Handles right-click usage of DBD custom items (Medkit) during gameplay.
*/
public class ItemUseListener implements Listener {
@ -71,9 +70,13 @@ public class ItemUseListener implements Listener {
String type = DBDItems.getType(hand);
if ("MEDKIT".equals(type)) {
// Медкит: нужен Shift (sneaking) + стоять на месте
if (!player.isSneaking()) {
player.sendMessage(Component.text(
"[DBD] Зажмите Shift и стойте на месте для лечения.", NamedTextColor.YELLOW));
return;
}
useMedkit(player, (Survivor) bp, hand);
} else if ("FLASHLIGHT".equals(type)) {
useFlashlight(player, hand);
}
}
@ -82,32 +85,46 @@ public class ItemUseListener implements Listener {
private void useMedkit(Player player, Survivor survivor, ItemStack item) {
UUID uuid = player.getUniqueId();
// Повторный ПКМ — отмена лечения
// Повторный Shift+ПКМ — отмена лечения
if (activeHeals.containsKey(uuid)) {
cancelHeal(player);
return;
}
if (survivor.getState() != PlayerState.INJURED) {
player.sendMessage(Component.text("Вы не ранены!", NamedTextColor.RED));
player.sendMessage(Component.text("[DBD] Вы не ранены!", NamedTextColor.RED));
return;
}
int charges = DBDItems.getCharges(item);
if (charges <= 0) {
player.sendMessage(Component.text("Медкит пуст!", NamedTextColor.RED));
player.sendMessage(Component.text("[DBD] Медкит пуст!", NamedTextColor.RED));
player.getInventory().setItemInMainHand(null);
return;
}
player.sendMessage(Component.text("Начинаете самолечение...", NamedTextColor.GREEN));
player.sendMessage(Component.text("[DBD] Самолечение... держите Shift и не двигайтесь.", NamedTextColor.GREEN));
// Запоминаем позицию для проверки движения
final Location startLoc = player.getLocation().clone();
BukkitTask task = new BukkitRunnable() {
int ticks = 0;
final int startCharges = charges;
final int healTicks = MEDKIT_TICKS;
@Override
public void run() {
if (!player.isOnline() || survivor.getState() != PlayerState.INJURED) {
if (!player.isOnline()) { cancelHeal(player); cancel(); return; }
if (survivor.getState() != PlayerState.INJURED) { cancelHeal(player); cancel(); return; }
// Прерывание при движении или отпускании Shift
if (!player.isSneaking()) {
cancelHeal(player);
cancel();
return;
}
// Проверка движения (больше 0.5 блока от старта)
if (player.getLocation().distanceSquared(startLoc) > 0.25) {
cancelHeal(player);
cancel();
return;
@ -116,38 +133,24 @@ public class ItemUseListener implements Listener {
ticks++;
// Прогресс в action bar
int filled = (int) (8.0 * ticks / MEDKIT_TICKS);
int empty = 8 - filled;
int pct = (int) (100.0 * ticks / MEDKIT_TICKS);
int filled = (int) (8.0 * ticks / healTicks);
int empty = 8 - filled;
int pct = (int) (100.0 * ticks / healTicks);
player.sendActionBar(Component.text(
"⚕ " + "█".repeat(filled) + "░".repeat(empty) + " " + pct + "%",
NamedTextColor.GREEN));
// Звук каждые 20 тиков
if (ticks % 20 == 0) {
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_HURT, 0.15f, 1.8f);
// Обновляем заряды на предмете
int usedCharges = (int)(1.0 * ticks / MEDKIT_TICKS * startCharges);
int remaining = Math.max(1, startCharges - usedCharges);
player.getInventory().setItemInMainHand(DBDItems.setCharges(
player.getInventory().getItemInMainHand(), remaining));
}
if (ticks >= MEDKIT_TICKS) {
if (ticks >= healTicks) {
survivor.setState(PlayerState.HEALTHY);
player.sendMessage(Component.text("✔ Вы вылечились!", NamedTextColor.GREEN));
player.sendMessage(Component.text("[DBD] ✔ Вы вылечились!", NamedTextColor.GREEN));
player.sendActionBar(Component.empty());
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f);
// Тратим заряды
int remaining = startCharges - 8;
if (remaining <= 0) {
player.getInventory().setItemInMainHand(null);
player.sendMessage(Component.text("Медкит израсходован.", NamedTextColor.RED));
} else {
player.getInventory().setItemInMainHand(DBDItems.medkit(remaining));
}
// Полностью убираем медкит (один раз использован)
player.getInventory().setItemInMainHand(null);
activeHeals.remove(uuid);
cancel();
}
@ -166,49 +169,4 @@ public class ItemUseListener implements Listener {
}
}
// ── Фонарик ───────────────────────────────────────────────────────────────
private void useFlashlight(Player player, ItemStack item) {
int charges = DBDItems.getCharges(item);
if (charges <= 0) {
player.sendMessage(Component.text("Батарейки сели!", NamedTextColor.RED));
return;
}
// Луч 15 блоков
var result = player.getWorld().rayTraceEntities(
player.getEyeLocation(),
player.getEyeLocation().getDirection(),
15.0,
e -> e instanceof Player && !e.equals(player)
);
boolean hitKiller = false;
if (result != null && result.getHitEntity() instanceof Player target) {
BasePlayer bp = session.getPlayer(target.getUniqueId());
if (bp != null && bp.isKiller()) {
target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 60, 1, false, false));
target.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 50, 2, false, false));
target.playSound(target.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f);
target.sendMessage(Component.text("☀ Вас ослепили фонариком!", NamedTextColor.YELLOW));
player.sendMessage(Component.text("✔ Ослепили киллера!", NamedTextColor.YELLOW));
hitKiller = true;
}
}
if (!hitKiller) {
player.sendMessage(Component.text("Мимо...", NamedTextColor.GRAY));
}
// 1 заряд за использование
int remaining = charges - 1;
if (remaining <= 0) {
player.getInventory().setItemInMainHand(null);
player.sendMessage(Component.text("Фонарик разрядился.", NamedTextColor.RED));
} else {
player.getInventory().setItemInMainHand(DBDItems.flashlight(remaining));
}
player.playSound(player.getLocation(), Sound.BLOCK_REDSTONE_TORCH_BURNOUT, 0.3f, 1.5f);
}
}

View file

@ -135,12 +135,13 @@ public class GeneratorKick {
private void executeKick(Killer killer, Generator gen) {
chargingTasks.remove(killer.getUuid());
// Remove progress
// Remove 20% progress (or configured percent) and STOP — no continuous regression
double regPct = config.getKickRegressionPercent();
double current = gen.getProgress();
double newProg = Math.max(0, current - regPct);
gen.setProgress(newProg);
gen.setUnderRegression(true, config.getKickDuration() * 20);
// Block generator from repair for the cooldown duration
gen.setBlocked(true);
Player p = killer.getPlayer();
if (p != null && p.isOnline()) {
@ -149,10 +150,16 @@ public class GeneratorKick {
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_GENERIC_EXPLODE, 0.6f, 1.3f);
}
// Notify all players nearby
Bukkit.broadcast(Component.text("[DBD] ⚡ Маньяк разрушил генератор!", NamedTextColor.DARK_RED));
// Notify all players
Bukkit.broadcast(Component.text("[DBD] ⚡ Маньяк разрушил генератор! -" + (int)regPct + "%", NamedTextColor.DARK_RED));
// Start global cooldown
// Unblock generator after kick-duration (separate from global cooldown)
int unblockTicks = config.getKickDuration() * 20;
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
gen.setBlocked(false);
}, unblockTicks);
// Start global kick cooldown
startGlobalCooldown();
}

View file

@ -19,7 +19,8 @@ public class KillerAttack {
private final PlayerManager playerManager;
private final DamageHandler damageHandler;
private final GameConfig config;
private final Map<UUID, Long> cooldowns;
private final Map<UUID, Long> cooldowns; // swing cooldown (attack-cooldown)
private final Map<UUID, Long> postHitCooldowns; // post-hit cooldown after landing a hit
private BukkitTask cleanupTask;
public KillerAttack(Plugin plugin, PlayerManager playerManager,
@ -29,6 +30,7 @@ public class KillerAttack {
this.damageHandler = damageHandler;
this.config = config;
this.cooldowns = new HashMap<>();
this.postHitCooldowns = new HashMap<>();
startCleanupTask();
}
@ -51,34 +53,57 @@ public class KillerAttack {
return false;
}
// Apply both swing cooldown and post-hit cooldown
applyCooldown(killer);
applyPostHitCooldown(killer);
playAttackEffects(killerPlayer, targetPlayer);
return true;
}
public boolean canAttack(Killer killer) {
Long lastAttack = cooldowns.get(killer.getUuid());
if (lastAttack == null) return true;
int cooldownTicks = config.getAttackCooldown();
long currentTick = Bukkit.getCurrentTick();
return currentTick - lastAttack >= cooldownTicks;
// Check swing cooldown
Long lastSwing = cooldowns.get(killer.getUuid());
if (lastSwing != null && currentTick - lastSwing < config.getAttackCooldown()) {
return false;
}
// Check post-hit cooldown
Long lastHit = postHitCooldowns.get(killer.getUuid());
if (lastHit != null && currentTick - lastHit < config.getPostHitCooldown()) {
return false;
}
return true;
}
private void applyCooldown(Killer killer) {
cooldowns.put(killer.getUuid(), (long) Bukkit.getCurrentTick());
}
private void applyPostHitCooldown(Killer killer) {
postHitCooldowns.put(killer.getUuid(), (long) Bukkit.getCurrentTick());
}
public double getCooldownProgress(Killer killer) {
Long lastAttack = cooldowns.get(killer.getUuid());
if (lastAttack == null) return 1.0;
int cooldownTicks = config.getAttackCooldown();
long currentTick = Bukkit.getCurrentTick();
long elapsed = currentTick - lastAttack;
return Math.min(1.0, (double) elapsed / cooldownTicks);
// Return progress of whichever cooldown is longer
Long lastSwing = cooldowns.get(killer.getUuid());
Long lastHit = postHitCooldowns.get(killer.getUuid());
long swingElapsed = (lastSwing != null) ? currentTick - lastSwing : Integer.MAX_VALUE;
long hitElapsed = (lastHit != null) ? currentTick - lastHit : Integer.MAX_VALUE;
int swingTicks = config.getAttackCooldown();
int hitTicks = config.getPostHitCooldown();
double swingProgress = (lastSwing != null) ? Math.min(1.0, (double) swingElapsed / swingTicks) : 1.0;
double hitProgress = (lastHit != null) ? Math.min(1.0, (double) hitElapsed / hitTicks) : 1.0;
return Math.min(swingProgress, hitProgress);
}
public boolean isOnCooldown(Killer killer) {
@ -112,8 +137,10 @@ public class KillerAttack {
private void startCleanupTask() {
cleanupTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
long currentTick = Bukkit.getCurrentTick();
int cooldownTicks = config.getAttackCooldown();
cooldowns.entrySet().removeIf(e -> currentTick - e.getValue() > cooldownTicks * 2);
int swingTicks = config.getAttackCooldown();
int hitTicks = config.getPostHitCooldown();
cooldowns.entrySet().removeIf(e -> currentTick - e.getValue() > swingTicks * 2);
postHitCooldowns.entrySet().removeIf(e -> currentTick - e.getValue() > hitTicks * 2);
}, 1200L, 1200L);
}
@ -122,5 +149,6 @@ public class KillerAttack {
cleanupTask.cancel();
}
cooldowns.clear();
postHitCooldowns.clear();
}
}
}

View file

@ -8,6 +8,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionEffect;
@ -21,9 +22,10 @@ import java.util.UUID;
/**
* Manages the Killer carrying a downed Survivor.
*
* Escape mechanic: Survivor spams SPACE (jump) to fill the escape bar.
* When the bar reaches 100% the Survivor breaks free, is teleported
* slightly forward and receives Speed I for carry.escape-speed-seconds.
* Survivor sits ON THE KILLER'S HEAD as a passenger.
* Escape mechanic: Survivor spams SHIFT to escape.
* Each shift press ejects survivor briefly, then re-mounts.
* After N presses (default 50, configurable), survivor breaks free permanently.
*/
public class KillerCarry {
private final Plugin plugin;
@ -31,18 +33,16 @@ public class KillerCarry {
private final Map<UUID, CarryData> activeCarries = new HashMap<>();
private BukkitTask carryTask;
// Separate map: killerUUID -> carried survivor UUID (for jump listener)
private final Map<UUID, UUID> killerToSurvivor = new HashMap<>();
/** survivor UUID → shift press count */
private final Map<UUID, Integer> shiftPressCounts = new HashMap<>();
private static class CarryData {
final Killer killer;
final Survivor survivor;
double wiggleProgress; // 0..100
CarryData(Killer killer, Survivor survivor) {
this.killer = killer;
this.killer = killer;
this.survivor = survivor;
this.wiggleProgress = 0;
}
}
@ -51,26 +51,46 @@ public class KillerCarry {
this.config = config;
}
// -------------------------------------------------------------------------
// Called by PlayerJumpListener when a survivor jumps
// -------------------------------------------------------------------------
public void onSurvivorJump(UUID survivorUuid) {
for (CarryData data : activeCarries.values()) {
if (data.survivor.getUuid().equals(survivorUuid)) {
data.wiggleProgress += config.getCarryJumpFillPercent();
showWiggleProgress(data);
if (data.wiggleProgress >= 100) {
Bukkit.getScheduler().runTask(plugin, () -> forceDrop(data.killer));
}
return;
/**
* Called when a carried survivor presses Shift.
* Each press counts toward escape. Survivor is briefly ejected then re-mounted.
*/
public void onSurvivorShift(UUID survivorUuid) {
CarryData data = findCarryBySurvivor(survivorUuid);
if (data == null) return;
int presses = shiftPressCounts.merge(survivorUuid, 1, Integer::sum);
int requiredPresses = config.getCarryEscapePresses();
Player kp = data.killer.getPlayer();
Player sp = data.survivor.getPlayer();
if (kp != null && sp != null && sp.isOnline()) {
// Eject survivor briefly
kp.removePassenger(sp);
// Visual + sound feedback
sp.playSound(sp.getLocation(), Sound.ENTITY_PLAYER_ATTACK_SWEEP, 0.3f, 2.0f);
int remaining = Math.max(0, requiredPresses - presses);
sp.sendActionBar(Component.text(
"§6Вырываетесь: §e" + presses + "/" + requiredPresses + " §7(Shift) §cОсталось: " + remaining)
.color(NamedTextColor.YELLOW));
if (presses >= requiredPresses) {
// Permanent escape!
shiftPressCounts.remove(survivorUuid);
forceDrop(data.killer);
} else {
// Re-mount after 3 ticks (visual: "falls off then gets back on")
Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (isCarrying(data.killer) && sp.isOnline() && kp.isOnline()) {
kp.addPassenger(sp);
}
}, 3L);
}
}
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
public boolean tryPickup(Killer killer, Survivor survivor) {
if (!canPickup(killer, survivor)) return false;
@ -93,16 +113,16 @@ public class KillerCarry {
public boolean drop(Killer killer) {
CarryData data = activeCarries.remove(killer.getUuid());
killerToSurvivor.remove(killer.getUuid());
if (data == null) return false;
shiftPressCounts.remove(data.survivor.getUuid());
stopCarry(data, false);
return true;
}
public void forceDrop(Killer killer) {
CarryData data = activeCarries.remove(killer.getUuid());
killerToSurvivor.remove(killer.getUuid());
if (data == null) return;
shiftPressCounts.remove(data.survivor.getUuid());
stopCarry(data, true);
}
@ -115,9 +135,8 @@ public class KillerCarry {
return data != null ? data.survivor : null;
}
public double getWiggleProgress(Killer killer) {
CarryData data = activeCarries.get(killer.getUuid());
return data != null ? data.wiggleProgress : 0;
public int getShiftPresses(Survivor survivor) {
return shiftPressCounts.getOrDefault(survivor.getUuid(), 0);
}
public void shutdown() {
@ -126,27 +145,40 @@ public class KillerCarry {
stopCarry(data, false);
}
activeCarries.clear();
killerToSurvivor.clear();
shiftPressCounts.clear();
}
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
private CarryData findCarryBySurvivor(UUID survivorUuid) {
for (CarryData data : activeCarries.values()) {
if (data.survivor.getUuid().equals(survivorUuid)) return data;
}
return null;
}
private void startCarry(Killer killer, Survivor survivor) {
killer.setCarrying(true);
survivor.setState(PlayerState.CARRIED);
Player killerPlayer = killer.getPlayer();
Player survivorPlayer = survivor.getPlayer();
if (survivorPlayer != null) {
survivorPlayer.setInvisible(true);
survivorPlayer.sendMessage(Component.text(
"[DBD] Нажимайте ПРОБЕЛ чтобы вырваться!", NamedTextColor.YELLOW));
}
if (killerPlayer == null || survivorPlayer == null) return;
// Survivor sits ON THE KILLER'S HEAD as a passenger
survivorPlayer.setSwimming(false);
killerPlayer.addPassenger(survivorPlayer);
// Slow falling to prevent visual glitches
survivorPlayer.addPotionEffect(
new PotionEffect(PotionEffectType.SLOW_FALLING, Integer.MAX_VALUE, 0, false, false));
shiftPressCounts.put(survivor.getUuid(), 0);
int required = config.getCarryEscapePresses();
survivorPlayer.sendMessage(Component.text(
"[DBD] Спамьте SHIFT чтобы вырваться! (" + required + " нажатий)", NamedTextColor.YELLOW));
CarryData data = new CarryData(killer, survivor);
activeCarries.put(killer.getUuid(), data);
killerToSurvivor.put(killer.getUuid(), survivor.getUuid());
if (carryTask == null) startCarryTask();
}
@ -154,38 +186,47 @@ public class KillerCarry {
private void stopCarry(CarryData data, boolean escaped) {
data.killer.setCarrying(false);
Player killerPlayer = data.killer.getPlayer();
Player killerPlayer = data.killer.getPlayer();
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer != null && survivorPlayer.isOnline()) {
survivorPlayer.setInvisible(false);
if (survivorPlayer != null && survivorPlayer.isValid()) {
// Dismount from killer
if (killerPlayer != null && killerPlayer.isValid()) {
killerPlayer.removePassenger(survivorPlayer);
}
// Remove slow falling
survivorPlayer.removePotionEffect(PotionEffectType.SLOW_FALLING);
}
if (survivorPlayer != null && survivorPlayer.isOnline()) {
if (escaped) {
// Drop survivor slightly forward from killer
Location dropLoc = survivorPlayer.getLocation();
if (killerPlayer != null) {
dropLoc = killerPlayer.getLocation().clone()
.add(killerPlayer.getLocation().getDirection().multiply(2.0));
}
Location dropLoc = killerPlayer != null
? killerPlayer.getLocation().clone()
.add(killerPlayer.getLocation().getDirection().multiply(2.0))
: survivorPlayer.getLocation();
dropLoc.setY(dropLoc.getY() + 1);
survivorPlayer.teleport(dropLoc);
// Speed I
// Speed I after escape
int speedTicks = config.getCarryEscapeSpeedSeconds() * 20;
survivorPlayer.addPotionEffect(
new PotionEffect(PotionEffectType.SPEED, speedTicks, 0, false, true));
survivorPlayer.sendMessage(Component.text(
"[DBD] Вы вырвались!", NamedTextColor.GREEN));
survivorPlayer.playSound(survivorPlayer.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
data.survivor.setState(PlayerState.DOWNED); // still downed, just free
// After escape, survivor is INJURED
data.survivor.setState(PlayerState.INJURED);
} else {
survivorPlayer.teleport(data.killer.getPlayer() != null
? data.killer.getPlayer().getLocation()
survivorPlayer.teleport(killerPlayer != null
? killerPlayer.getLocation()
: survivorPlayer.getLocation());
data.survivor.setState(PlayerState.DOWNED);
}
// Clear action bar progress
survivorPlayer.sendActionBar(Component.empty());
}
@ -197,18 +238,14 @@ public class KillerCarry {
private void startCarryTask() {
carryTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (activeCarries.isEmpty()) return;
for (CarryData data : new HashMap<>(activeCarries).values()) {
if (!isValidCarry(data)) {
activeCarries.remove(data.killer.getUuid());
killerToSurvivor.remove(data.killer.getUuid());
shiftPressCounts.remove(data.survivor.getUuid());
stopCarry(data, false);
continue;
}
updateCarryPosition(data);
showWiggleProgress(data); // refresh bar every tick
}
}, 1L, 1L);
}, 20L, 20L);
}
private boolean isValidCarry(CarryData data) {
@ -216,24 +253,4 @@ public class KillerCarry {
Player sp = data.survivor.getPlayer();
return kp != null && sp != null && kp.isOnline() && sp.isOnline();
}
private void updateCarryPosition(CarryData data) {
Player killerPlayer = data.killer.getPlayer();
Player survivorPlayer = data.survivor.getPlayer();
if (killerPlayer == null || survivorPlayer == null) return;
Location carryLoc = killerPlayer.getLocation().clone().add(0, -0.5, 0);
survivorPlayer.teleport(carryLoc);
}
private void showWiggleProgress(CarryData data) {
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer == null) return;
double pct = Math.min(100, data.wiggleProgress);
int filled = (int) (pct / 10);
int empty = 10 - filled;
String bar = "§e[" + "§a█".repeat(filled) + "§7░".repeat(empty) + "§e]";
survivorPlayer.sendActionBar(Component.text("§6Побег: " + bar + " " + (int) pct + "%"));
}
}

View file

@ -104,8 +104,8 @@ public class KillerController {
return carry.getCarried(killer);
}
public double getWiggleProgress(Killer killer) {
return carry.getWiggleProgress(killer);
public int getShiftPresses(Survivor survivor) {
return carry.getShiftPresses(survivor);
}
public KillerAttack getAttack() {

View file

@ -62,7 +62,7 @@ public class KillerMovement {
}
private void applySpeed(Player player, double multiplier) {
AttributeInstance speedAttr = player.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
AttributeInstance speedAttr = player.getAttribute(Attribute.MOVEMENT_SPEED);
if (speedAttr == null) return;
double newSpeed = BASE_SPEED * multiplier;
@ -70,7 +70,7 @@ public class KillerMovement {
}
private void resetSpeed(Player player) {
AttributeInstance speedAttr = player.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
AttributeInstance speedAttr = player.getAttribute(Attribute.MOVEMENT_SPEED);
if (speedAttr == null) return;
speedAttr.setBaseValue(BASE_SPEED);

View file

@ -1,45 +0,0 @@
package dev.loki.listener;
import dev.loki.command.DBDAdminCommand;
import dev.loki.data.GeneratorDataManager;
import dev.loki.util.TextUtil;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
public class GeneratorSelectListener implements Listener {
private final DBDAdminCommand adminCommand;
private final GeneratorDataManager generatorData;
public GeneratorSelectListener(DBDAdminCommand adminCommand, GeneratorDataManager generatorData) {
this.adminCommand = adminCommand;
this.generatorData = generatorData;
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Player player = event.getPlayer();
if (!adminCommand.isSelectingGenerator(player.getUniqueId())) {
return;
}
event.setCancelled(true);
Location loc = event.getBlock().getLocation();
Location genLoc = new Location(loc.getWorld(),
loc.getX() + 0.5,
loc.getY(),
loc.getZ() + 0.5);
generatorData.addGenerator(genLoc);
player.sendMessage(TextUtil.color("<#00FF00>Генератор установлен!"));
player.sendMessage(TextUtil.color("<#00CED1>Координаты: " + GeneratorDataManager.formatLocation(genLoc)));
player.sendMessage(TextUtil.color("<#FFD700>Всего генераторов: " + generatorData.getCount()));
player.sendMessage(TextUtil.color("<#FFA500>Режим выбора остается активным. Ломайте следующий блок или"));
player.sendMessage(TextUtil.color("<#FFA500>введите /dbdadmin create generator для отключения"));
}
}

View file

@ -25,17 +25,18 @@ import java.util.function.BiConsumer;
public class PlayerInteractListener implements Listener {
private final Plugin plugin;
private final GameManager gameManager;
private final GeneratorManager generatorManager;
private GeneratorManager generatorManager;
private SetupCommand setupCommand;
private BiConsumer<Player, Action> onGeneratorInteract;
private BiConsumer<Player, Boolean> onAttack;
private BiConsumer<Player, Boolean> onPalletInteract;
private BiConsumer<Player, Boolean> onRescueAttempt;
private BiConsumer<Player, Boolean> onChestInteract;
private BiConsumer<Player, Boolean> onHatchInteract;
/** Called when killer toggles sneak: (player, isSneaking) */
private BiConsumer<Player, Boolean> onKillerSneak;
/** Called when a survivor sneaks near another injured/downed survivor: (healer, target) */
private BiConsumer<Player, Player> onHealSneak;
private final Map<UUID, Long> lastClick = new ConcurrentHashMap<>();
private static final long COOLDOWN_MS = 200;
@ -43,6 +44,7 @@ public class PlayerInteractListener implements Listener {
public PlayerInteractListener(Plugin plugin, GameManager gameManager, GeneratorManager generatorManager) {
this.plugin = plugin;
this.gameManager = gameManager;
// Можем инициализировать null — будет переустановлен в GameplayPhase.initSubHandlers()
this.generatorManager = generatorManager;
}
@ -52,21 +54,96 @@ public class PlayerInteractListener implements Listener {
public void setOnGeneratorInteract(BiConsumer<Player, Action> h) { this.onGeneratorInteract = h; }
public void setOnAttack(BiConsumer<Player, Boolean> h) { this.onAttack = h; }
public void setOnPalletInteract(BiConsumer<Player, Boolean> h) { this.onPalletInteract = h; }
public void setOnRescueAttempt(BiConsumer<Player, Boolean> h) { this.onRescueAttempt = h; }
public void setOnChestInteract(BiConsumer<Player, Boolean> h) { this.onChestInteract = h; }
public void setOnHatchInteract(BiConsumer<Player, Boolean> h) { this.onHatchInteract = h; }
public void setOnKillerSneak(BiConsumer<Player, Boolean> h) { this.onKillerSneak = h; }
public void setOnHealSneak(BiConsumer<Player, Player> h) { this.onHealSneak = h; }
private BiConsumer<Player, Boolean> onCarriedShift;
public void setOnCarriedShift(BiConsumer<Player, Boolean> h) { this.onCarriedShift = h; }
public void setGeneratorManager(GeneratorManager manager) {
this.generatorManager = manager;
}
@EventHandler
public void onSneak(PlayerToggleSneakEvent event) {
Player player = event.getPlayer();
if (!gameManager.isInGame(player.getUniqueId())) return;
// For survivors: trigger generator repair when sneaking
if (event.isSneaking()) {
var session = gameManager.getPlayerSession(player.getUniqueId());
if (session != null) {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp != null && !bp.isKiller()) {
// #11: Carried survivor shift-spam to escape
if (bp.getState() == PlayerState.CARRIED && onCarriedShift != null) {
onCarriedShift.accept(player, true);
return;
}
// Only HEALTHY or INJURED survivors can repair/heal
PlayerState st = bp.getState();
if (st != PlayerState.HEALTHY && st != PlayerState.INJURED) return;
// Generator repair takes priority over healing
if (tryGeneratorInteract(player)) return;
// Healing: check if sneaking near an injured/downed survivor
if (onHealSneak != null) {
Player nearestTarget = findNearestHealTarget(player, session);
if (nearestTarget != null) {
onHealSneak.accept(player, nearestTarget);
}
}
}
}
} else {
// When un-sneaking
var session = gameManager.getPlayerSession(player.getUniqueId());
if (session != null) {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp != null && !bp.isKiller()) {
if (bp.getState() == PlayerState.CARRIED && onCarriedShift != null) {
onCarriedShift.accept(player, false);
return;
}
}
}
}
if (onKillerSneak != null) {
onKillerSneak.accept(player, event.isSneaking());
}
}
/**
* Find the nearest injured or downed survivor within heal range.
* Returns null if none found or if the only target is self and not injured.
*/
private Player findNearestHealTarget(Player healer, dev.loki.core.GameSession session) {
if (session == null) return null;
double healRange = session.getConfig().getHealRange();
Player nearest = null;
double nearestDist = healRange + 1.0;
for (BasePlayer other : session.getPlayers().values()) {
if (other.isKiller()) continue;
Player otherPlayer = other.getPlayer();
if (otherPlayer == null || !otherPlayer.isOnline()) continue;
// Самолечение только через медкит, не через шифт
if (otherPlayer.getUniqueId().equals(healer.getUniqueId())) continue;
PlayerState state = other.getState();
if (state != PlayerState.INJURED && state != PlayerState.DOWNED) continue;
double dist = healer.getLocation().distance(otherPlayer.getLocation());
if (dist <= healRange && dist < nearestDist) {
nearest = otherPlayer;
nearestDist = dist;
}
}
return nearest;
}
@EventHandler
public void onInteract(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND) return;
@ -90,8 +167,9 @@ public class PlayerInteractListener implements Listener {
}
/**
* Handles the DBD wand (STICK with custom display name).
* LMB on block = pos1, RMB on block = pos2.
* Handles both old gate wand and new setup wand.
* Old gate wand: LMB on block = pos1, RMB on block = pos2.
* New setup wand: RMB on block = select block.
* Returns true if the event was consumed by the wand.
*/
private boolean handleWand(PlayerInteractEvent event, Player player) {
@ -99,15 +177,13 @@ public class PlayerInteractListener implements Listener {
if (!player.hasPermission("mdbd.admin")) return false;
ItemStack item = event.getItem();
if (item == null || item.getType() != Material.STICK) return false;
if (item == null || item.getType() != Material.WOODEN_AXE) return false;
if (!item.hasItemMeta()) return false;
// Reliable check: look for NBT persistent data tag instead of display name parsing
var meta = item.getItemMeta();
var key = new org.bukkit.NamespacedKey("mdbd", "dbd_wand");
if (!meta.getPersistentDataContainer().has(key, org.bukkit.persistence.PersistentDataType.BYTE)) {
return false;
}
var gateWandKey = new org.bukkit.NamespacedKey("mdbd", "dbd_wand");
if (!meta.getPersistentDataContainer().has(gateWandKey, org.bukkit.persistence.PersistentDataType.BYTE)) return false;
event.setCancelled(true);
@ -116,6 +192,7 @@ public class PlayerInteractListener implements Listener {
? event.getClickedBlock().getLocation()
: player.getLocation().getBlock().getLocation();
// ЛКМ=pos1, ПКМ=pos2
if (action == Action.LEFT_CLICK_BLOCK || action == Action.LEFT_CLICK_AIR) {
setupCommand.setWandPos(player, 1, loc);
} else if (action == Action.RIGHT_CLICK_BLOCK || action == Action.RIGHT_CLICK_AIR) {
@ -125,7 +202,22 @@ public class PlayerInteractListener implements Listener {
}
private boolean isIncapacitated(BasePlayer bp) {
return bp.getState() == PlayerState.DOWNED || bp.getState() == PlayerState.HOOKED;
return bp.getState() == PlayerState.DOWNED
|| bp.getState() == PlayerState.HOOKED
|| bp.getState() == PlayerState.CARRIED;
}
private boolean tryGeneratorInteract(Player player) {
if (onGeneratorInteract == null || generatorManager == null) return false;
var session = gameManager.getPlayerSession(player.getUniqueId());
if (session == null) return false;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return false;
var gen = generatorManager.getNearestGenerator(
player.getLocation(), session.getConfig().getGeneratorInteractionRange());
if (gen == null) return false;
onGeneratorInteract.accept(player, Action.RIGHT_CLICK_AIR);
return true;
}
private boolean isOnCooldown(Player player) {
@ -137,35 +229,44 @@ public class PlayerInteractListener implements Listener {
}
private void handleRightClick(PlayerInteractEvent event, Player player, BasePlayer bp) {
if (event.getClickedBlock() == null) {
if (!bp.isKiller() && onGeneratorInteract != null && generatorManager != null && generatorManager.isInAnyZone(player)) {
onGeneratorInteract.accept(player, event.getAction());
// #13: Запрет входа в печки для всех (и маньяка тоже) — MUST be FIRST before any other checks
if (event.getClickedBlock() != null) {
String type = event.getClickedBlock().getType().toString();
if (type.contains("FURNACE") || type.contains("SMOKER") || type.contains("BLAST_FURNACE")) {
event.setCancelled(true);
return;
}
if (onAttack != null && bp.isKiller()) onAttack.accept(player, true);
}
if (!bp.isKiller() && tryGeneratorInteract(player)) return;
// Killer attack (hook/carry) — fires on both air AND block clicks
if (bp.isKiller() && onAttack != null) {
onAttack.accept(player, true);
return;
}
if (!bp.isKiller() && onGeneratorInteract != null && generatorManager != null && generatorManager.isInAnyZone(player)) {
onGeneratorInteract.accept(player, event.getAction());
return;
}
if (event.getClickedBlock() == null) return;
String type = event.getClickedBlock().getType().toString();
if ((type.contains("FENCE") || type.contains("PLANKS")) && onPalletInteract != null && !bp.isKiller()) onPalletInteract.accept(player, true);
if (!bp.isKiller() && onRescueAttempt != null) onRescueAttempt.accept(player, true);
if (type.contains("CHEST") && onChestInteract != null) onChestInteract.accept(player, true);
// Chest interaction - cancel vanilla GUI and use plugin chest system
if (type.contains("CHEST") && onChestInteract != null && !bp.isKiller()) {
event.setCancelled(true);
onChestInteract.accept(player, true);
return;
}
// Hook rescue: if survivor clicks on a hook block, attempt rescue
if (!bp.isKiller() && onRescueAttempt != null) {
onRescueAttempt.accept(player, true);
if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true);
}
if ((type.contains("HOPPER") || type.contains("PORTAL")) && onHatchInteract != null && !bp.isKiller()) onHatchInteract.accept(player, true);
// Cancel interaction with hook blocks to prevent block manipulation
if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true);
}
private void handleLeftClick(PlayerInteractEvent event, Player player, BasePlayer bp) {
if (event.getClickedBlock() != null) {
String type = event.getClickedBlock().getType().toString();
if ((type.contains("PLANKS") || type.contains("FENCE")) && onPalletInteract != null && bp.isKiller()) {
event.setCancelled(true);
onPalletInteract.accept(player, false);
}
}
if (onAttack != null && bp.isKiller()) { event.setCancelled(true); onAttack.accept(player, false); }
}

View file

@ -0,0 +1,21 @@
package dev.loki.listener;
import dev.loki.world.LobbyManager;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerJoinListener implements Listener {
private final LobbyManager lobbyManager;
public PlayerJoinListener(LobbyManager lobbyManager) {
this.lobbyManager = lobbyManager;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
lobbyManager.teleportToLobby(player);
}
}

View file

@ -1,10 +1,14 @@
package dev.loki.listener;
import dev.loki.player.BasePlayer;
import dev.loki.player.PlayerState;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.plugin.Plugin;
import java.util.HashSet;
@ -13,43 +17,95 @@ import java.util.UUID;
import java.util.function.Consumer;
/**
* Tracks jump events (velocity Y-positive when on ground) to support the
* carry-escape "spam space" mechanic.
* Detects player jumps by monitoring Y-coordinate changes in PlayerMoveEvent.
* Bukkit 1.21 doesn't have PlayerJumpEvent, so we detect jumps manually.
*
* Jumping is DISABLED by default for all in-game players.
* Admin can toggle via /dbd admin jump.
*/
public class PlayerJumpListener implements Listener {
private final Plugin plugin;
private final Set<UUID> inGamePlayers = new HashSet<>();
private Consumer<Player> onJump;
private final Set<UUID> registered = new HashSet<>();
private final Set<UUID> recentJumpers = new HashSet<>();
/** Whether jumping is allowed. Default: false (disabled). */
private static boolean jumpingAllowed = false;
public PlayerJumpListener(Plugin plugin) {
this.plugin = plugin;
}
public void setOnJump(Consumer<Player> callback) {
this.onJump = callback;
}
public static boolean isJumpingAllowed() { return jumpingAllowed; }
public static void setJumpingAllowed(boolean allowed) { jumpingAllowed = allowed; }
public void register() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/** Register player as in-game for jump detection */
public void addInGamePlayer(UUID uuid) { inGamePlayers.add(uuid); }
public void removeInGamePlayer(UUID uuid) { inGamePlayers.remove(uuid); }
public void clearInGamePlayers() { inGamePlayers.clear(); }
public void unregister() {
HandlerList.unregisterAll(this);
onJump = null;
registered.clear();
}
public void setOnJump(Consumer<Player> h) { this.onJump = h; }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(org.bukkit.event.player.PlayerMoveEvent event) {
// Detect jump: player was on ground and now has positive Y velocity
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
if (!player.isOnGround()) return;
// Paper API: check if player jumped (velocity Y > 0 after the tick)
if (player.getVelocity().getY() > 0.1) {
if (onJump != null) {
onJump.accept(player);
if (!inGamePlayers.contains(player.getUniqueId())) return;
Location from = event.getFrom();
Location to = event.getTo();
if (from == null || to == null) return;
// Detect jump: Y increased significantly (> 0.5 blocks)
double yDiff = to.getY() - from.getY();
if (yDiff > 0.5 && to.getY() > from.getY()) {
// Avoid double-triggering
if (recentJumpers.contains(player.getUniqueId())) return;
recentJumpers.add(player.getUniqueId());
// Remove from set after short delay
plugin.getServer().getScheduler().runTaskLater(plugin,
() -> recentJumpers.remove(player.getUniqueId()), 5L);
// Get player state
BasePlayer bp = getBasePlayer(player);
if (bp != null) {
PlayerState state = bp.getState();
// Survivors being carried can jump to try escape (always allowed)
if (!bp.isKiller() && state == PlayerState.CARRIED) {
if (onJump != null) onJump.accept(player);
return;
}
// Blocked states: HOOKED, DOWNED — never allow jumping
if (state == PlayerState.HOOKED || state == PlayerState.DOWNED) {
event.setCancelled(true);
return;
}
// Jumping disabled by default — cancel all jumps except carry escape
if (!jumpingAllowed) {
event.setCancelled(true);
return;
}
}
}
}
}
private BasePlayer getBasePlayer(Player player) {
// Get player from game session via Bukkit
var session = Bukkit.getServer().getPluginManager().getPlugin("Mdbd");
if (session instanceof dev.loki.mdbd mdbd) {
var gm = mdbd.getGameManager();
if (gm != null) {
var s = gm.getPlayerSession(player.getUniqueId());
if (s != null) return s.getPlayer(player.getUniqueId());
}
}
return null;
}
public void register() { plugin.getServer().getPluginManager().registerEvents(this, plugin); }
public void unregister() { org.bukkit.event.HandlerList.unregisterAll(this); }
}

View file

@ -8,6 +8,7 @@ import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
@ -60,6 +61,7 @@ public class PlayerMoveListener implements Listener {
private boolean shouldCancel(BasePlayer bp, Location to, Location from) {
if (bp.getState() == PlayerState.HOOKED) return true;
if (bp.getState() == PlayerState.CARRIED) return true;
if (bp.getState() == PlayerState.DOWNED) {
double dist = Math.sqrt(Math.pow(to.getX() - from.getX(), 2) + Math.pow(to.getZ() - from.getZ(), 2));
return dist > 0.08;
@ -69,6 +71,7 @@ public class PlayerMoveListener implements Listener {
private void handleMovement(Player player, BasePlayer bp, Location to) {
if (onMovement != null) onMovement.accept(player, to);
if (bp.getState() == PlayerState.CARRIED) return; // Don't trigger move events for carried survivors
if (!bp.isKiller() && onSurvivorMove != null) onSurvivorMove.accept((Survivor) bp, to);
else if (bp.isKiller() && onKillerMove != null) onKillerMove.accept((Killer) bp, to);
}

View file

@ -31,17 +31,13 @@ public class PlayerQuitListener implements Listener {
Player player = event.getPlayer();
UUID uuid = player.getUniqueId();
if (!gameManager.isInGame(uuid)) {
return;
}
if (!gameManager.isInGame(uuid)) return;
GameSession session = gameManager.getPlayerSession(uuid);
BasePlayer basePlayer = session.getPlayer(uuid);
if (session == null) { gameManager.leaveGame(player); return; }
if (basePlayer == null) {
gameManager.leaveGame(player);
return;
}
BasePlayer basePlayer = session.getPlayer(uuid);
if (basePlayer == null) { gameManager.leaveGame(player); return; }
if (basePlayer.isKiller()) {
handleKillerDisconnect(session, (Killer) basePlayer);
@ -49,8 +45,17 @@ public class PlayerQuitListener implements Listener {
handleSurvivorDisconnect(session, (Survivor) basePlayer);
}
// Убираем из session и playerSessions
session.removePlayer(uuid);
gameManager.leaveGame(player);
// Гарантируем чистый инвентарь и scale при следующем входе
player.getInventory().clear();
player.setSwimming(false);
try {
var attr = player.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
}
private void handleKillerDisconnect(GameSession session, Killer killer) {
@ -65,14 +70,35 @@ public class PlayerQuitListener implements Listener {
Player player = survivor.getPlayer();
if (player != null && player.isOnline()) {
player.sendMessage(Component.text(
"[DBD] Вы покинули игру и считаетесь мертвым.",
NamedTextColor.RED));
// Очищаем инвентарь сразу при выходе
player.getInventory().clear();
// Сбрасываем scale
try {
var attr = player.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
// Сбрасываем позу нокдауна
player.setSwimming(false);
}
Bukkit.broadcast(Component.text(
"[DBD] Выживший покинул игру. Осталось живых: " + session.getPlayers().size(),
NamedTextColor.YELLOW));
// Считаем живых ПОСЛЕ того как этот игрок помечен DEAD
int alive = 0;
for (BasePlayer bp : session.getPlayers().values()) {
if (!bp.isKiller()
&& bp.getState() != PlayerState.DEAD
&& bp.getState() != PlayerState.ESCAPED) {
// Не считаем самого выходящего — он уже помечен DEAD выше
if (!bp.getUuid().equals(survivor.getUuid())) alive++;
}
}
// Только участникам сессии
Component msg = Component.text(
"[DBD] Выживший покинул игру. Осталось живых: " + alive, NamedTextColor.YELLOW);
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.sendMessage(msg);
}
checkEndgameConditions(session);
}

View file

@ -1,45 +1,72 @@
package dev.loki.listener;
import dev.loki.command.SetupCommand;
import dev.loki.config.GameConfig;
import dev.loki.mdbd;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
/**
* Handles block-break selection for killer-spawn and survivor-spawn setup modes.
* Generator selection is handled by GeneratorSelectListener.
* Handles block-break for setup modes (/dbdsetup set <type>).
*
* When in setup mode:
* 1. The original block type is captured
* 2. The event is cancelled (no drops, no natural break)
* 3. The location is saved by SetupCommand
* 4. The original block is restored after saving
*/
public class SetupBlockBreakListener implements Listener {
private final SetupCommand setupCommand;
private final mdbd plugin;
public SetupBlockBreakListener(SetupCommand setupCommand) {
public SetupBlockBreakListener(SetupCommand setupCommand, mdbd plugin) {
this.setupCommand = setupCommand;
this.plugin = plugin;
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Player player = event.getPlayer();
boolean killerMode = setupCommand.isSelectingKillerSpawn(player.getUniqueId());
boolean survivorMode = setupCommand.isSelectingSurvivorSpawn(player.getUniqueId());
if (!setupCommand.isInSetupMode(player.getUniqueId())) return;
if (!killerMode && !survivorMode) return;
// Capture original block type BEFORE the block breaks
Material originalType = event.getBlock().getType();
Location blockLoc = event.getBlock().getLocation();
String mode = setupCommand.getSetupMode(player.getUniqueId());
if (mode == null) return;
// Cancel the break to prevent drops and physics
event.setCancelled(true);
// Center of block, standing on top
Location loc = event.getBlock().getLocation()
.add(0.5, 1.0, 0.5);
loc.setYaw(player.getLocation().getYaw());
loc.setPitch(0f);
// Determine the configured block type for display blocks (chest, generator, hook)
GameConfig cfg = plugin.getConfigManager().getGame();
Material placeMat = switch (mode) {
case "chest" -> Material.matchMaterial(cfg.getChestBlock());
case "generator" -> Material.matchMaterial(cfg.getGeneratorBlock());
case "hook" -> Material.matchMaterial(cfg.getHookBlock());
default -> null;
};
if (killerMode) {
setupCommand.onKillerSpawnSelected(player, loc);
if (placeMat != null) {
// Place configured block immediately (event is cancelled so block stays)
blockLoc.getBlock().setType(placeMat, false);
// Save the display block type as the "restore" material
setupCommand.onBlockBroken(player, blockLoc, placeMat);
} else {
setupCommand.onSurvivorSpawnSelected(player, loc);
// For spawn types: send block update to prevent client-side desync
// (client may have shown break animation despite event being cancelled)
player.sendBlockChange(blockLoc, originalType.createBlockData());
// Show visual feedback particle at spawn location
player.spawnParticle(org.bukkit.Particle.FLAME,
blockLoc.clone().add(0.5, 1.0, 0.5), 5, 0.3, 0.3, 0.3, 0.02);
// Pass original material for restoration
setupCommand.onBlockBroken(player, blockLoc, originalType);
}
}
}

View file

@ -0,0 +1,66 @@
package dev.loki.listener;
import dev.loki.objective.SkillCheckInventory;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.plugin.Plugin;
import java.util.UUID;
/**
* Listens for clicks on skill check inventory GUI.
* Blocks all item manipulation — only clicking is allowed.
*/
public class SkillCheckListener implements Listener {
private final Plugin plugin;
private final SkillCheckInventory skillCheckGUI;
public SkillCheckListener(Plugin plugin, SkillCheckInventory skillCheckGUI) {
this.plugin = plugin;
this.skillCheckGUI = skillCheckGUI;
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
// Check if this is a skill check inventory (title contains "Skill Check")
String title = event.getView().getTitle();
if (!title.contains("Skill Check")) return;
// Block ALL item manipulation: shift-click, number keys, double-click, etc.
event.setCancelled(true);
if (!(event.getWhoClicked() instanceof Player player)) return;
UUID playerId = player.getUniqueId();
skillCheckGUI.handleClick(event, playerId);
}
@EventHandler
public void onInventoryDrag(InventoryDragEvent event) {
// Block dragging items out of the skill check inventory
String title = event.getView().getTitle();
if (!title.contains("Skill Check")) return;
event.setCancelled(true);
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
// Check if this is a skill check inventory (title contains "Skill Check")
String title = event.getView().getTitle();
if (!title.contains("Skill Check")) return;
skillCheckGUI.onInventoryClose(event);
}
public void register() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
public void unregister() {
org.bukkit.event.HandlerList.unregisterAll(this);
}
}

View file

@ -43,14 +43,6 @@ public class MapBuilder {
placeHook(loc);
}
// Place pallets
List<Location> pallets = config.getPallets();
for (Location loc : pallets) {
if (loc == null) continue;
loc.setWorld(world);
placePallet(loc);
}
// Place chests
List<Location> chests = config.getChests();
for (Location loc : chests) {
@ -59,14 +51,6 @@ public class MapBuilder {
placeChest(loc);
}
// Place totems
List<Location> totems = config.getTotems();
for (Location loc : totems) {
if (loc == null) continue;
loc.setWorld(world);
placeTotem(loc);
}
// Place exit gates
List<Location> gates = config.getExitGates();
for (int i = 0; i < gates.size(); i++) {
@ -100,9 +84,7 @@ public class MapBuilder {
// Clear all objects (set to air)
clearLocations(config.getGenerators());
clearLocations(config.getHooks());
clearLocations(config.getPallets());
clearLocations(config.getChests());
clearLocations(config.getTotems());
clearLocations(config.getExitGates());
}
@ -131,6 +113,10 @@ public class MapBuilder {
return true;
}
public void placeTotem(Location location) {
// Totems removed - no-op
}
public void placeHook(Location location) {
if (templateManager.hasTemplate("hook")) {
templateManager.placeObject("hook", location);
@ -139,14 +125,6 @@ public class MapBuilder {
buildDefaultHook(location);
}
public void placePallet(Location location) {
if (templateManager.hasTemplate("pallet")) {
templateManager.placeObject("pallet", location);
return;
}
buildDefaultPallet(location);
}
public void placeChest(Location location) {
if (templateManager.hasTemplate("chest")) {
templateManager.placeObject("chest", location);
@ -155,14 +133,6 @@ public class MapBuilder {
location.getBlock().setType(org.bukkit.Material.CHEST);
}
public void placeTotem(Location location) {
if (templateManager.hasTemplate("totem")) {
templateManager.placeObject("totem", location);
return;
}
buildDefaultTotem(location);
}
public void placeExitGate(Location location, int id) {
if (templateManager.hasTemplate("exitgate")) {
templateManager.placeObject("exitgate", location);
@ -204,27 +174,6 @@ public class MapBuilder {
world.getBlockAt(loc.clone().add(0, 3, 0)).setType(org.bukkit.Material.TRIPWIRE_HOOK);
}
private void buildDefaultPallet(Location loc) {
World world = loc.getWorld();
if (world == null) return;
// Horizontal pallet: 3 oak planks
for (int x = -1; x <= 1; x++) {
world.getBlockAt(loc.clone().add(x, 1, 0)).setType(org.bukkit.Material.OAK_PLANKS);
}
// Supports: Fences
world.getBlockAt(loc.clone().add(-1, 0, 0)).setType(org.bukkit.Material.OAK_FENCE);
world.getBlockAt(loc.clone().add(1, 0, 0)).setType(org.bukkit.Material.OAK_FENCE);
}
private void buildDefaultTotem(Location loc) {
World world = loc.getWorld();
if (world == null) return;
// Base: Mossy stone
world.getBlockAt(loc).setType(org.bukkit.Material.MOSSY_COBBLESTONE);
// Skull on top
world.getBlockAt(loc.clone().add(0, 1, 0)).setType(org.bukkit.Material.PLAYER_HEAD);
}
private void buildDefaultExitGate(Location loc, int id) {
World world = loc.getWorld();
if (world == null) return;

View file

@ -0,0 +1,182 @@
package dev.loki.map;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a saved map configuration (generators, hooks, pallets, etc.)
* stored in plugins/Mdbd/maps/<name>.yml
*
* Save: /dbd save <name> — saves current locations.yml data to maps/<name>.yml
* Load: /dbd load <name> — loads map data into locations.yml and reloads
*/
public class MapConfig {
private final String name;
private final File file;
private FileConfiguration cfg;
public MapConfig(File mapsFolder, String name) {
this.name = name;
this.file = new File(mapsFolder, name + ".yml");
this.cfg = new YamlConfiguration();
}
public String getName() { return name; }
public boolean exists() { return file.exists(); }
// -------------------------------------------------------------------------
// Save
// -------------------------------------------------------------------------
public void saveFromLocations(dev.loki.config.LocationsConfig locs) {
cfg = new YamlConfiguration();
cfg.set("name", name);
// Gates A/B
saveLocation("gate-a", locs.getGate("a"));
saveLocation("gate-b", locs.getGate("b"));
// Killer spawns (list)
saveLocationList("killer-spawn", locs.getKillerSpawns());
// Survivor spawns (list)
saveLocationList("survivor-spawns", locs.getSurvivorSpawns());
// Generators (list)
saveLocationList("generators", locs.getGenerators());
// Hooks (list)
saveLocationList("hooks", locs.getHooks());
// Chests (list)
saveLocationList("chests", locs.getChests());
write();
}
// -------------------------------------------------------------------------
// Load
// -------------------------------------------------------------------------
public boolean load() {
if (!file.exists()) return false;
cfg = YamlConfiguration.loadConfiguration(file);
return true;
}
public void applyToLocations(dev.loki.config.LocationsConfig locs) {
// Gates
Location ga = loadLocation("gate-a");
Location gb = loadLocation("gate-b");
if (ga != null) locs.setGate("a", ga);
if (gb != null) locs.setGate("b", gb);
// Killer spawns
List<String> killerRaw = cfg.getStringList("killer-spawn");
if (!killerRaw.isEmpty()) {
locs.clearLocationList("killer-spawn");
for (String s : killerRaw) {
Location l = parseLocation(s);
if (l != null) locs.addLocation("killer-spawn", l);
}
}
// Lists — clear by resetting to empty, then add each
applyList("survivor-spawns", locs);
applyList("generators", locs);
applyList("hooks", locs);
applyList("chests", locs);
applyList("windows", locs);
}
// -------------------------------------------------------------------------
// List of maps in folder
// -------------------------------------------------------------------------
public static List<String> listMaps(File mapsFolder) {
List<String> names = new ArrayList<>();
if (!mapsFolder.exists()) return names;
File[] files = mapsFolder.listFiles((d, n) -> n.endsWith(".yml"));
if (files == null) return names;
for (File f : files) names.add(f.getName().replace(".yml", ""));
return names;
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
private void saveLocation(String key, Location loc) {
if (loc == null || loc.getWorld() == null) return;
cfg.set(key, locToString(loc));
}
private void saveLocationList(String key, List<Location> locs) {
List<String> list = new ArrayList<>();
for (Location l : locs) {
if (l != null && l.getWorld() != null) list.add(locToString(l));
}
cfg.set(key, list);
}
private Location loadLocation(String key) {
String s = cfg.getString(key);
return s != null ? parseLocation(s) : null;
}
private void applyList(String key, dev.loki.config.LocationsConfig locs) {
List<String> raw = cfg.getStringList(key);
// Reset the key to empty, then add each entry
locs.clearLocationList(key);
for (String s : raw) {
Location l = parseLocation(s);
if (l != null) locs.addLocation(key, l);
}
}
private void write() {
try {
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
cfg.save(file);
} catch (IOException e) {
Bukkit.getLogger().warning("[MapConfig] Could not save map «" + name + "»: " + e.getMessage());
}
}
private String locToString(Location loc) {
// Use | as separator instead of , to avoid issues with negative coordinates in YAML
return String.format("%s|%.2f|%.2f|%.2f|%.1f|%.1f",
loc.getWorld().getName(),
loc.getX(), loc.getY(), loc.getZ(),
loc.getYaw(), loc.getPitch());
}
private Location parseLocation(String s) {
// Handle both old format (world,x,y,z,yaw,pitch) and new format (world|x|y|z|yaw|pitch)
// New format uses | as separator to avoid issues with negative coordinates
String[] p = s.contains("|") ? s.split("\\|") : s.split(",");
if (p.length < 4) return null;
World w = Bukkit.getWorld(p[0].trim());
if (w == null) return null;
try {
double x = Double.parseDouble(p[1].trim());
double y = Double.parseDouble(p[2].trim());
double z = Double.parseDouble(p[3].trim());
float yaw = p.length > 4 ? Float.parseFloat(p[4].trim()) : 0f;
float pitch = p.length > 5 ? Float.parseFloat(p[5].trim()) : 0f;
return new Location(w, x, y, z, yaw, pitch);
} catch (NumberFormatException e) {
return null;
}
}
}

View file

@ -0,0 +1,65 @@
package dev.loki.map;
import dev.loki.config.ConfigManager;
import dev.loki.mdbd;
import java.io.File;
import java.util.List;
/**
* Manages named map configs stored in plugins/Mdbd/maps/
* Save: captures current locations.yml into maps/<name>.yml
* Load: reads maps/<name>.yml and applies it to locations.yml, then saves
*/
public class MapManager {
private final mdbd plugin;
private final File mapsFolder;
public MapManager(mdbd plugin) {
this.plugin = plugin;
this.mapsFolder = new File(plugin.getDataFolder(), "maps");
if (!mapsFolder.exists()) mapsFolder.mkdirs();
}
/**
* Save current map layout (all locations) to maps/<name>.yml
*/
public boolean saveMap(String name) {
try {
MapConfig mc = new MapConfig(mapsFolder, name);
mc.saveFromLocations(plugin.getConfigManager().getLocations());
plugin.getLogger().info("[MapManager] Saved map «" + name + "»");
return true;
} catch (Exception e) {
plugin.getLogger().warning("[MapManager] Could not save map «" + name + "»: " + e.getMessage());
return false;
}
}
/**
* Load a map from maps/<name>.yml into locations.yml and persist
*/
public boolean loadMap(String name) {
MapConfig mc = new MapConfig(mapsFolder, name);
if (!mc.exists()) return false;
if (!mc.load()) return false;
mc.applyToLocations(plugin.getConfigManager().getLocations());
// Persist the updated locations
plugin.getConfigManager().saveConfig("locations.yml",
plugin.getConfigManager().getLocations().getConfig());
plugin.getLogger().info("[MapManager] Loaded map «" + name + "»");
return true;
}
public boolean mapExists(String name) {
return new MapConfig(mapsFolder, name).exists();
}
public List<String> listMaps() {
return MapConfig.listMaps(mapsFolder);
}
public File getMapsFolder() { return mapsFolder; }
}

View file

@ -80,18 +80,10 @@ public class TemplateManager {
presets.put("hook_default", new PresetMap("hook_default",
"hook", new Location(null, 1, 0, 1)));
// Pallet preset (3x2x1)
presets.put("pallet_default", new PresetMap("pallet_default",
"pallet", new Location(null, 1, 0, 0)));
// Chest preset (1x1x1)
presets.put("chest_default", new PresetMap("chest_default",
"chest", new Location(null, 0, 0, 0)));
// Totem preset (1x2x1)
presets.put("totem_default", new PresetMap("totem_default",
"totem", new Location(null, 0, 0, 0)));
// Hatch preset (2x1x2)
presets.put("hatch_default", new PresetMap("hatch_default",
"hatch", new Location(null, 1, 0, 1)));

View file

@ -6,11 +6,12 @@ import dev.loki.command.SetupCommand;
import dev.loki.config.ConfigManager;
import dev.loki.core.GameManager;
import dev.loki.data.GeneratorDataManager;
import dev.loki.listener.GeneratorSelectListener;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerJoinListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.listener.PlayerQuitListener;
import dev.loki.map.MapBuilder;
import dev.loki.map.MapManager;
import dev.loki.map.TemplateManager;
import dev.loki.world.LobbyManager;
import org.bukkit.plugin.java.JavaPlugin;
@ -20,6 +21,7 @@ public final class mdbd extends JavaPlugin {
private ConfigManager configManager;
private GameManager gameManager;
private TemplateManager templateManager;
private MapManager mapManager;
private MapBuilder mapBuilder;
private GeneratorDataManager generatorData;
private DBDCommand dbdCommand;
@ -37,17 +39,21 @@ public final class mdbd extends JavaPlugin {
this.configManager = new ConfigManager(this);
this.configManager.loadConfigs();
// Загружаем Custom Model Data для предметов
dev.loki.item.DBDItems.loadModelData(this.configManager.getGame().getConfig());
this.templateManager = new TemplateManager(this);
this.templateManager.loadAllTemplates();
this.templateManager.loadDefaultPresets();
this.mapManager = new MapManager(this);
this.mapBuilder = new MapBuilder(this, templateManager);
this.generatorData = new GeneratorDataManager(this);
this.lobbyManager = new LobbyManager(this);
this.lobbyManager.createLobbyWorld();
this.lobbyManager.createGameWorld();
this.lobbyManager.loadWorlds();
this.gameManager = new GameManager(this);
@ -59,16 +65,15 @@ public final class mdbd extends JavaPlugin {
getCommand("dbdsetup").setExecutor(setupCommand);
getCommand("dbdsetup").setTabCompleter(setupCommand);
this.dbdAdminCommand = new DBDAdminCommand(this, generatorData);
this.dbdAdminCommand = new DBDAdminCommand(this);
getCommand("dbdadmin").setExecutor(dbdAdminCommand);
getCommand("dbdadmin").setTabCompleter(dbdAdminCommand);
getServer().getPluginManager().registerEvents(
new GeneratorSelectListener(dbdAdminCommand, generatorData), this);
getServer().getPluginManager().registerEvents(
new dev.loki.listener.SetupBlockBreakListener(setupCommand), this);
new dev.loki.listener.SetupBlockBreakListener(setupCommand, this), this);
getServer().getPluginManager().registerEvents(
new PlayerQuitListener(this, gameManager), this);
getServer().getPluginManager().registerEvents(
new PlayerJoinListener(lobbyManager), this);
this.interactListener = new PlayerInteractListener(this, gameManager, null);
interactListener.setSetupCommand(setupCommand);
getServer().getPluginManager().registerEvents(interactListener, this);
@ -85,7 +90,7 @@ public final class mdbd extends JavaPlugin {
public void onDisable() {
getLogger().info("Mdbd Plugin disabling...");
if (gameManager != null) {
gameManager.endGame();
gameManager.endAll();
}
if (configManager != null) {
configManager.saveAllConfigs();
@ -112,6 +117,10 @@ public final class mdbd extends JavaPlugin {
return templateManager;
}
public MapManager getMapManager() {
return mapManager;
}
public MapBuilder getMapBuilder() {
return mapBuilder;
}
@ -138,6 +147,7 @@ public final class mdbd extends JavaPlugin {
public void reloadConfigs() {
configManager.loadConfigs();
dev.loki.item.DBDItems.loadModelData(configManager.getGame().getConfig());
templateManager.loadAllTemplates();
generatorData.load();
getLogger().info("All configs reloaded!");

View file

@ -133,7 +133,7 @@ public class Generator {
}
public boolean canBeRepaired() {
return !isCompleted && !isBlocked;
return !isCompleted && !isBlocked && !isUnderRegression();
}
@Deprecated

View file

@ -0,0 +1,137 @@
package dev.loki.objective;
import dev.loki.mdbd;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Display;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.TextDisplay;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
/**
* Floating labels above generators (name + repair progress).
* Uses TextDisplay (Paper 1.19.4+) with ArmorStand fallback for older versions.
*/
public class GeneratorHologram {
private final mdbd plugin;
private final GeneratorManager manager;
private final Map<Integer, Object> displays = new HashMap<>(); // TextDisplay or ArmorStand
private BukkitTask task;
private boolean useTextDisplay;
public GeneratorHologram(mdbd plugin, GeneratorManager manager) {
this.plugin = plugin;
this.manager = manager;
this.useTextDisplay = isTextDisplayAvailable();
}
private static boolean isTextDisplayAvailable() {
try {
EntityType.valueOf("TEXT_DISPLAY");
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public void start() {
stop();
spawnAll();
task = plugin.getServer().getScheduler().runTaskTimer(plugin, this::refresh, 20L, 20L);
plugin.getLogger().info("[GeneratorHologram] Started with " + displays.size() + " holograms (mode=" + (useTextDisplay ? "TextDisplay" : "ArmorStand") + ")");
}
public void stop() {
if (task != null) {
task.cancel();
task = null;
}
for (Object display : displays.values()) {
if (display instanceof TextDisplay td && td.isValid()) {
td.remove();
} else if (display instanceof ArmorStand as && as.isValid()) {
as.remove();
}
}
displays.clear();
}
private void spawnAll() {
double heightOffset = plugin.getConfigManager().getGame().getGeneratorHologramHeightOffset();
for (Generator gen : manager.getAllGenerators()) {
Location loc = gen.getLocation();
if (loc.getWorld() == null) continue;
Location holoLoc = loc.clone().add(0.5, 2.0 + heightOffset, 0.5);
try {
if (useTextDisplay) {
TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> {
d.setBillboard(Display.Billboard.VERTICAL);
d.setSeeThrough(false);
d.setShadowed(true);
d.setDefaultBackground(false);
d.setPersistent(false);
d.setTextOpacity((byte) -1);
});
displays.put(gen.getId(), display);
} else {
ArmorStand stand = loc.getWorld().spawn(holoLoc, ArmorStand.class, as -> {
as.setInvisible(true);
as.setInvulnerable(true);
as.setGravity(false);
as.setMarker(true);
as.setCustomNameVisible(true);
as.setPersistent(false);
});
displays.put(gen.getId(), stand);
}
} catch (Exception e) {
plugin.getLogger().warning("[GeneratorHologram] Failed to spawn hologram for generator " + gen.getId() + ": " + e.getMessage());
}
}
}
private void refresh() {
double heightOffset = plugin.getConfigManager().getGame().getGeneratorHologramHeightOffset();
int total = manager.getTotalCount();
for (Generator gen : manager.getAllGenerators()) {
Object display = displays.get(gen.getId());
if (display == null) continue;
boolean valid = (display instanceof TextDisplay td && td.isValid()) ||
(display instanceof ArmorStand as && as.isValid());
if (!valid) continue;
int pct = (int) gen.getProgress();
String bar = buildBar(pct);
String text;
if (gen.isCompleted()) {
text = "§6⚙ Генератор §f#" + (gen.getId() + 1) + "/" + total + "\n§a✔ Починен!";
} else if (gen.isUnderRegression()) {
text = "§6⚙ Генератор §f#" + (gen.getId() + 1) + "/" + total + "\n§c↓ " + bar + " §f" + pct + "%";
} else {
text = "§6⚙ Генератор §f#" + (gen.getId() + 1) + "/" + total + "\n§e" + bar + " §f" + pct + "%";
}
Location base = gen.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5);
if (display instanceof TextDisplay td) {
td.text(net.kyori.adventure.text.Component.text(text));
td.teleport(base);
} else if (display instanceof ArmorStand as) {
as.setCustomName(text);
as.teleport(base);
}
}
}
private String buildBar(int pct) {
int filled = pct / 10;
int empty = 10 - filled;
return "§e" + "█".repeat(Math.max(0, filled)) + "§7" + "░".repeat(Math.max(0, empty));
}
}

View file

@ -20,6 +20,7 @@ public class GeneratorRepair {
private final GeneratorManager manager;
private final Map<UUID, RepairSession> sessions;
private final SkillCheck skillCheck;
private final SkillCheckInventory skillCheckGUI; // GUI-based skill check
private static final double REPAIR_PER_TICK = 100.0 / 1600.0;
private static final double REPAIR_RANGE = 3.0;
@ -29,14 +30,22 @@ public class GeneratorRepair {
this.manager = manager;
this.sessions = new HashMap<>();
this.skillCheck = new SkillCheck(plugin, this);
this.skillCheckGUI = new SkillCheckInventory(plugin);
}
public void startRepair(Player player, Generator generator) {
UUID uuid = player.getUniqueId();
// If player already repairing same generator, just return (they're holding crouch)
if (sessions.containsKey(uuid)) {
stopRepair(player);
return;
RepairSession existing = sessions.get(uuid);
if (existing.generator.getId() == generator.getId()) {
// Already repairing this one, nothing to do
return;
} else {
// Switching generators, stop current repair
stopRepair(player);
}
}
if (!generator.canBeRepaired()) {
@ -54,7 +63,8 @@ public class GeneratorRepair {
sessions.put(uuid, session);
// Do NOT set blocked here - blocked means nobody can repair it (e.g. after a kick)
player.sendMessage(MiniMessage.miniMessage().deserialize("<gray>Зажмите <yellow>ПКМ</yellow> для починки"));
double currentProgress = generator.getProgress();
// Hint message removed per user request — repair works via sneak silently
}
public void stopRepair(Player player) {
@ -63,6 +73,7 @@ public class GeneratorRepair {
if (session != null) {
session.generator.setCurrentRepairer(null);
// Progress is PRESERVED - generator keeps its progress value
// Only unblock if it was blocked due to an ongoing repair (not a kick)
if (!session.generator.isUnderRegression()) {
session.generator.setBlocked(false);
@ -98,7 +109,9 @@ public class GeneratorRepair {
if (session.skillCheckCooldown > 0) {
session.skillCheckCooldown--;
} else if (shouldTriggerSkillCheck()) {
skillCheck.start(player.getUniqueId(), generator);
// Start GUI-based skill check
Player p = session.player;
skillCheckGUI.startSkillCheck(p, player.getUniqueId(), skillCheck);
session.skillCheckCooldown = 100 + (int)(Math.random() * 200);
return true;
}
@ -151,7 +164,8 @@ public class GeneratorRepair {
}
private boolean shouldTriggerSkillCheck() {
return Math.random() < 0.02;
double chance = plugin.getConfigManager().getGame().getSkillCheckChance();
return Math.random() < chance;
}
public void onSkillCheckResult(UUID playerId, boolean success, Generator generator) {
@ -159,24 +173,23 @@ public class GeneratorRepair {
if (player == null) return;
if (success) {
generator.addProgress(5.0);
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Отлично! +5%"));
double winPercent = plugin.getConfigManager().getGame().getSkillCheckWinPercent();
generator.addProgress(winPercent);
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>✓ Отлично! +" + (int)winPercent + "%"));
player.playSound(player.getLocation(), org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.2f);
} else {
generator.removeProgress(10.0);
generator.setUnderRegression(true, 600);
player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Ошибка! Потеря прогресса!"));
stopRepair(player);
notifyKillerWithCompass(player);
double lossPercent = plugin.getConfigManager().getGame().getSkillCheckLossPercent();
generator.removeProgress(lossPercent);
player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>✗ Ошибка! -" + (int)lossPercent + "%. Продолжай чинить."));
player.playSound(player.getLocation(), org.bukkit.Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f);
notifyKillerWithCompass(player, generator);
}
}
/** Give every online killer a compass pointing to the failed generator for compass-duration-seconds. */
private void notifyKillerWithCompass(Player failedPlayer) {
private void notifyKillerWithCompass(Player failedPlayer, Generator generator) {
int durationTicks = plugin.getConfigManager().getGame().getCompassDurationSeconds() * 20;
RepairSession rs = sessions.get(failedPlayer.getUniqueId());
if (rs == null) return;
Location compassTarget = rs.generator.getLocation();
Location compassTarget = generator.getLocation();
for (Player online : Bukkit.getOnlinePlayers()) {
if (online.getUniqueId().equals(failedPlayer.getUniqueId())) continue;
@ -194,8 +207,13 @@ public class GeneratorRepair {
ItemStack prev = online.getInventory().getItemInOffHand();
online.getInventory().setItemInOffHand(compass);
plugin.getServer().getScheduler().runTaskLater(plugin, () ->
online.getInventory().setItemInOffHand(prev), durationTicks);
// Восстанавливаем старый предмет только если в offhand всё ещё компас
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
ItemStack current = online.getInventory().getItemInOffHand();
if (current.getType() == Material.COMPASS) {
online.getInventory().setItemInOffHand(prev);
}
}, durationTicks);
}
}
@ -207,6 +225,15 @@ public class GeneratorRepair {
return sessions.get(uuid);
}
public SkillCheckInventory getSkillCheckGUI() {
return skillCheckGUI;
}
public void shutdown() {
skillCheckGUI.clear();
sessions.clear();
}
public static class RepairSession {
final Player player;
final Generator generator;

View file

@ -0,0 +1,240 @@
package dev.loki.objective;
import dev.loki.config.GameConfig;
import dev.loki.mdbd;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.*;
/**
* GUI-based Skill Check minigame — ROULETTE style.
*
* Green block moves sequentially left-to-right (0→1→2→...→8→0→...).
* Starts FAST (2-3 ticks), gradually slows down (8-12 ticks).
* ~12-18 switches, then stops on final slot.
* Player must click the GREEN block. Clicking gray = fail. Timeout = fail.
*/
public class SkillCheckInventory {
private final mdbd plugin;
private final Map<UUID, SkillCheckSession> activeSessions = new HashMap<>();
private static final int TIMEOUT_TICKS = 60; // 3 seconds — не выбрал = проигрыш -10%
private static final int MIN_SWITCHES = 5;
private static final int MAX_SWITCHES = 8;
private final Material successMaterial;
private final Material failMaterial;
private final Material neutralMaterial;
public SkillCheckInventory(mdbd plugin) {
this.plugin = plugin;
GameConfig cfg = plugin.getConfigManager().getGame();
this.successMaterial = getMaterial(cfg.getSkillCheckSuccessBlock(), Material.GREEN_STAINED_GLASS_PANE);
this.failMaterial = getMaterial(cfg.getSkillCheckFailBlock(), Material.RED_STAINED_GLASS_PANE);
this.neutralMaterial = getMaterial(cfg.getSkillCheckNeutralBlock(), Material.GRAY_STAINED_GLASS_PANE);
}
private static Material getMaterial(String name, Material fallback) {
Material m = Material.getMaterial(name);
return m != null ? m : fallback;
}
public void startSkillCheck(Player player, UUID playerId, SkillCheck skillCheck) {
// Start from slot 0, roulette animation
SkillCheckSession session = new SkillCheckSession(player, playerId, skillCheck);
activeSessions.put(playerId, session);
Inventory inv = Bukkit.createInventory(null, 9, Component.text("⚡ Skill Check!").color(NamedTextColor.YELLOW));
fillInventory(inv, session.successSlot);
player.openInventory(inv);
// Schedule timeout
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
if (activeSessions.containsKey(playerId)) {
onTimeout(playerId);
}
}, TIMEOUT_TICKS);
// Start roulette animation
runRoulette(playerId);
}
/**
* Roulette animation: green block moves left-to-right sequentially,
* slowing down over time like a real roulette wheel.
*/
private void runRoulette(UUID playerId) {
SkillCheckSession session = activeSessions.get(playerId);
if (session == null || !session.player.isOnline()) {
activeSessions.remove(playerId);
return;
}
// Check if timeout happened
if (session.totalTicks >= TIMEOUT_TICKS) {
onTimeout(playerId);
return;
}
// Move green sequentially to next slot
session.lastSuccessSlot = session.successSlot;
session.successSlot = (session.successSlot + 1) % 9;
session.switchesDone++;
// Update inventory visually
Inventory inv = session.player.getOpenInventory().getTopInventory();
if (inv != null && inv.getSize() == 9) {
inv.setItem(session.lastSuccessSlot, createNeutralGlass());
inv.setItem(session.successSlot, createSuccessGlass());
// Play tick sound for roulette feel
session.player.playSound(session.player.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 0.5f, 1.5f);
}
// Calculate next delay — roulette slows down
int nextDelay;
int done = session.switchesDone;
int total = session.totalSwitches;
if (done < total * 0.4) {
// Fast phase: 40% of switches
nextDelay = 2 + (int)(Math.random() * 2); // 2-3 ticks
} else if (done < total * 0.8) {
// Medium phase: 40% of switches
nextDelay = 4 + (int)(Math.random() * 3); // 4-6 ticks
} else {
// Slow phase: final 20% of switches
nextDelay = 8 + (int)(Math.random() * 5); // 8-12 ticks
}
session.totalTicks += nextDelay;
// Stop roulette after totalSwitches — green stays on final slot
if (done >= total) {
// Roulette stopped — player must click now (timeout still ticking)
return;
}
// Continue roulette
plugin.getServer().getScheduler().runTaskLater(plugin, () -> runRoulette(playerId), nextDelay);
}
/**
* Called when the skill check inventory is closed (e.g. player presses ESC).
* Auto-fail: treats close as timeout.
*/
public void onInventoryClose(InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) return;
UUID playerId = player.getUniqueId();
if (activeSessions.containsKey(playerId)) {
onTimeout(playerId);
}
}
public void handleClick(InventoryClickEvent event, UUID playerId) {
// event.setCancelled(true) is already called in SkillCheckListener before this
SkillCheckSession session = activeSessions.remove(playerId);
if (session == null) return;
Player player = event.getWhoClicked() instanceof Player ? (Player) event.getWhoClicked() : null;
if (player == null) return;
player.closeInventory();
int clickedSlot = event.getRawSlot();
boolean success = (clickedSlot == session.successSlot);
if (success) {
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f);
} else {
player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f);
}
session.skillCheck.tryComplete(playerId, success);
}
private void onTimeout(UUID playerId) {
SkillCheckSession session = activeSessions.remove(playerId);
if (session == null) return;
Player player = session.player;
if (player != null && player.isOnline()) {
player.closeInventory();
player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f);
}
// Timeout = failure
session.skillCheck.tryComplete(playerId, false);
}
private void fillInventory(Inventory inv, int greenSlot) {
for (int i = 0; i < 9; i++) {
inv.setItem(i, i == greenSlot ? createSuccessGlass() : createNeutralGlass());
}
}
private ItemStack createSuccessGlass() {
ItemStack glass = new ItemStack(successMaterial);
ItemMeta meta = glass.getItemMeta();
if (meta != null) {
meta.displayName(Component.text("✓ Нажми!").color(NamedTextColor.GREEN));
glass.setItemMeta(meta);
}
return glass;
}
private ItemStack createFailGlass() {
ItemStack glass = new ItemStack(failMaterial);
ItemMeta meta = glass.getItemMeta();
if (meta != null) {
meta.displayName(Component.text("✗").color(NamedTextColor.RED));
glass.setItemMeta(meta);
}
return glass;
}
private ItemStack createNeutralGlass() {
ItemStack glass = new ItemStack(neutralMaterial);
ItemMeta meta = glass.getItemMeta();
if (meta != null) {
meta.displayName(Component.text("-").color(NamedTextColor.GRAY));
glass.setItemMeta(meta);
}
return glass;
}
public void clear() {
for (UUID uuid : new HashSet<>(activeSessions.keySet())) {
SkillCheckSession session = activeSessions.remove(uuid);
if (session.player != null && session.player.isOnline()) {
session.player.closeInventory();
}
}
activeSessions.clear();
}
private static class SkillCheckSession {
final Player player;
final UUID playerId;
final SkillCheck skillCheck;
int successSlot = 0;
int lastSuccessSlot = -1;
int switchesDone = 0;
final int totalSwitches = MIN_SWITCHES + new Random().nextInt(MAX_SWITCHES - MIN_SWITCHES + 1);
int totalTicks = 0;
SkillCheckSession(Player player, UUID playerId, SkillCheck skillCheck) {
this.player = player;
this.playerId = playerId;
this.skillCheck = skillCheck;
}
}
}

View file

@ -1,124 +0,0 @@
package dev.loki.pallet;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.block.Block;
public class Pallet {
public enum PalletState {
STANDING,
DROPPED,
BROKEN
}
private final Location center; // The OAK_LOG position saved in locations.yml
private Block leftBlock;
private Block centerBlock;
private Block rightBlock;
private PalletState state;
public Pallet(Location center) {
this.center = center.clone();
this.state = PalletState.STANDING;
detectStructure();
}
private boolean isPlanks(Block block) {
return block.getType() == Material.OAK_PLANKS;
}
/**
* Detects which axis the PLP structure lies on (X or Z) by checking
* neighboring blocks. Falls back to X axis if neither looks right.
*/
private void detectStructure() {
Block c = center.getBlock();
centerBlock = c;
Block xLeft = center.clone().add(-1, 0, 0).getBlock();
Block xRight = center.clone().add( 1, 0, 0).getBlock();
Block zLeft = center.clone().add( 0, 0, -1).getBlock();
Block zRight = center.clone().add( 0, 0, 1).getBlock();
// Prefer the axis where both neighbours are OAK_PLANKS
boolean xPlpValid = isPlanks(xLeft) && isPlanks(xRight);
boolean zPlpValid = isPlanks(zLeft) && isPlanks(zRight);
if (xPlpValid) {
leftBlock = xLeft;
rightBlock = xRight;
} else if (zPlpValid) {
leftBlock = zLeft;
rightBlock = zRight;
} else {
// Fallback: use X axis regardless (blocks may have been loaded before
// the world is fully available; state ops will still work).
leftBlock = xLeft;
rightBlock = xRight;
}
}
// -------------------------------------------------------------------------
// State transitions
// -------------------------------------------------------------------------
public void setDropped() {
if (state != PalletState.STANDING) return;
state = PalletState.DROPPED;
// Replace the OAK_LOG center with OAK_PLANKS (sides stay as-is)
centerBlock.setType(Material.OAK_PLANKS);
// Sound + particles at center
Location loc = center.clone().add(0.5, 0.5, 0.5);
if (loc.getWorld() != null) {
loc.getWorld().playSound(loc, Sound.BLOCK_WOOD_BREAK, 1.0f, 0.7f);
loc.getWorld().spawnParticle(Particle.BLOCK, loc, 30, 0.5, 0.3, 0.5, 0.1,
Material.OAK_PLANKS.createBlockData());
}
}
public void setBroken() {
state = PalletState.BROKEN;
leftBlock.setType(Material.AIR);
centerBlock.setType(Material.AIR);
rightBlock.setType(Material.AIR);
}
public void reset() {
state = PalletState.STANDING;
// Restore PLP structure
leftBlock.setType(Material.OAK_PLANKS);
centerBlock.setType(Material.OAK_LOG);
rightBlock.setType(Material.OAK_PLANKS);
}
// -------------------------------------------------------------------------
// Accessors
// -------------------------------------------------------------------------
public Location getLocation() {
return center.clone();
}
/** Centre of the pallet block for distance calculations. */
public Location getCenter() {
return center.clone().add(0.5, 0.5, 0.5);
}
public PalletState getState() {
return state;
}
public boolean isStanding() { return state == PalletState.STANDING; }
public boolean isDropped() { return state == PalletState.DROPPED; }
public boolean isBroken() { return state == PalletState.BROKEN; }
public double distanceTo(Location loc) {
if (!getCenter().getWorld().equals(loc.getWorld())) return Double.MAX_VALUE;
return getCenter().distance(loc);
}
}

View file

@ -1,135 +0,0 @@
package dev.loki.pallet;
import dev.loki.config.GameConfig;
import dev.loki.player.Killer;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class PalletBreak {
private final int breakTicks;
private final Map<UUID, BreakingProgress> breakingKillers;
public PalletBreak(GameConfig config) {
this.breakTicks = config.getPalletBreakTicks();
this.breakingKillers = new HashMap<>();
}
public boolean startBreaking(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
if (!canBreak(killer, pallet)) return false;
Player player = killer.getPlayer();
if (player == null) return false;
BreakingProgress progress = new BreakingProgress(pallet, breakTicks);
breakingKillers.put(killer.getUuid(), progress);
player.sendMessage("§cЛомаем паллет...");
playBreakStartEffect(player, pallet);
new BreakTask(killer, progress, plugin).runTaskTimer(plugin, 0L, 1L);
return true;
}
public void stopBreaking(Killer killer) {
BreakingProgress progress = breakingKillers.remove(killer.getUuid());
if (progress != null) {
Player player = killer.getPlayer();
if (player != null) {
player.sendMessage("§7Прервано!");
}
}
}
public boolean isBreaking(Killer killer) {
return breakingKillers.containsKey(killer.getUuid());
}
private boolean canBreak(Killer killer, Pallet pallet) {
if (!pallet.isDropped()) return false;
if (isBreaking(killer)) return false;
Player player = killer.getPlayer();
if (player == null) return false;
return pallet.distanceTo(player.getLocation()) <= 3.0;
}
private void playBreakStartEffect(Player player, Pallet pallet) {
player.getWorld().playSound(pallet.getLocation(),
Sound.BLOCK_WOOD_HIT, 0.5f, 1.0f);
}
private void playBreakEffect(Player player, Pallet pallet, int progress) {
if (progress % 10 == 0) {
player.getWorld().playSound(pallet.getLocation(),
Sound.BLOCK_WOOD_HIT, 0.5f, 0.8f + (progress / (float) breakTicks) * 0.4f);
}
}
private void completeBreak(Killer killer, Pallet pallet) {
Player player = killer.getPlayer();
if (player != null) {
player.getWorld().playSound(pallet.getLocation(),
Sound.BLOCK_WOOD_BREAK, 1.0f, 0.6f);
player.sendMessage("§cПаллет сломан!");
}
pallet.setBroken();
breakingKillers.remove(killer.getUuid());
}
private class BreakingProgress {
final Pallet pallet;
final int totalTicks;
int currentTicks;
BreakingProgress(Pallet pallet, int totalTicks) {
this.pallet = pallet;
this.totalTicks = totalTicks;
this.currentTicks = 0;
}
boolean tick() {
currentTicks++;
return currentTicks >= totalTicks;
}
}
private class BreakTask extends BukkitRunnable {
private final Killer killer;
private final BreakingProgress progress;
BreakTask(Killer killer, BreakingProgress progress, org.bukkit.plugin.Plugin plugin) {
this.killer = killer;
this.progress = progress;
}
@Override
public void run() {
Player player = killer.getPlayer();
if (player == null || !player.isOnline()) {
stopBreaking(killer);
cancel();
return;
}
if (!canBreak(killer, progress.pallet)) {
stopBreaking(killer);
cancel();
return;
}
playBreakEffect(player, progress.pallet, progress.currentTicks);
if (progress.tick()) {
completeBreak(killer, progress.pallet);
cancel();
}
}
}
}

View file

@ -1,72 +0,0 @@
package dev.loki.pallet;
import dev.loki.player.Survivor;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public class PalletDrop {
private static final double DROP_RADIUS = 2.5;
private static final double INTERACTION_RADIUS = 3.0;
private final PalletManager palletManager;
public PalletDrop(PalletManager palletManager) {
this.palletManager = palletManager;
}
public boolean tryDrop(Survivor survivor) {
Player player = survivor.getPlayer();
if (player == null) return false;
Location loc = player.getLocation();
Pallet pallet = palletManager.findNearestStandingPallet(loc, INTERACTION_RADIUS);
if (pallet == null) {
player.sendMessage("§cНет паллета поблизости!");
return false;
}
if (!canDropFromPosition(player, pallet)) {
player.sendMessage("§cНеправильная сторона паллета!");
return false;
}
dropPallet(survivor, pallet);
return true;
}
private boolean canDropFromPosition(Player player, Pallet pallet) {
Location playerLoc = player.getLocation();
Location palletLoc = pallet.getLocation();
return playerLoc.distance(palletLoc) <= INTERACTION_RADIUS;
}
private void dropPallet(Survivor survivor, Pallet pallet) {
Player player = survivor.getPlayer();
if (player == null) return;
pallet.setDropped();
player.getWorld().playSound(pallet.getLocation(), Sound.BLOCK_WOOD_BREAK, 1.0f, 0.8f);
player.getWorld().spawnParticle(org.bukkit.Particle.BLOCK,
pallet.getCenter(), 20, 0.5, 0.5, 0.5, 0.1,
org.bukkit.Material.OAK_PLANKS.createBlockData());
player.sendMessage("§aВы бросили паллет!");
applyExhaustion(player);
}
private void applyExhaustion(Player player) {
player.addPotionEffect(new PotionEffect(
PotionEffectType.SLOWNESS, 20, 2, false, false));
}
public boolean isNearDroppedPallet(Player player) {
return palletManager.getAllPallets().stream()
.filter(Pallet::isDropped)
.anyMatch(p -> p.distanceTo(player.getLocation()) <= DROP_RADIUS);
}
}

View file

@ -1,80 +0,0 @@
package dev.loki.pallet;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public class PalletManager {
private final List<Pallet> pallets;
public PalletManager(LocationsConfig locations) {
this.pallets = new ArrayList<>();
loadPallets(locations);
}
private void loadPallets(LocationsConfig locations) {
for (Location loc : locations.getPallets()) {
pallets.add(new Pallet(loc));
}
}
public void addPallet(Pallet pallet) {
pallets.add(pallet);
}
public void removePallet(Pallet pallet) {
pallets.remove(pallet);
}
public List<Pallet> getAllPallets() {
return new ArrayList<>(pallets);
}
public Pallet getPalletAt(Location location, double radius) {
for (Pallet pallet : pallets) {
if (pallet.distanceTo(location) <= radius) {
return pallet;
}
}
return null;
}
public Pallet findNearestStandingPallet(Location location, double maxRadius) {
Pallet nearest = null;
double minDistance = maxRadius;
for (Pallet pallet : pallets) {
if (!pallet.isStanding()) continue;
double dist = pallet.distanceTo(location);
if (dist < minDistance) {
minDistance = dist;
nearest = pallet;
}
}
return nearest;
}
public void resetAll() {
for (Pallet pallet : pallets) {
pallet.reset();
}
}
public void clearBroken() {
pallets.removeIf(Pallet::isBroken);
}
public int getTotalCount() {
return pallets.size();
}
public int getDroppedCount() {
return (int) pallets.stream().filter(Pallet::isDropped).count();
}
public int getBrokenCount() {
return (int) pallets.stream().filter(Pallet::isBroken).count();
}
}

View file

@ -1,74 +0,0 @@
package dev.loki.pallet;
import dev.loki.config.GameConfig;
import dev.loki.player.Killer;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class PalletStun {
private final int stunTicks;
private final Set<UUID> stunnedKillers;
public PalletStun(GameConfig config) {
this.stunTicks = config.getPalletStunTicks();
this.stunnedKillers = new HashSet<>();
}
public boolean isStunned(Killer killer) {
return stunnedKillers.contains(killer.getUuid());
}
public void stun(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
if (isStunned(killer)) return;
Player player = killer.getPlayer();
if (player == null) return;
stunnedKillers.add(killer.getUuid());
player.addPotionEffect(new PotionEffect(
PotionEffectType.SLOWNESS, stunTicks, 10, false, false));
player.addPotionEffect(new PotionEffect(
PotionEffectType.BLINDNESS, stunTicks, 1, false, false));
player.getWorld().playSound(player.getLocation(),
Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
player.sendTitle("§c§lСТАН!", "§7Паллет сбил вас с ног", 0, stunTicks, 10);
new BukkitRunnable() {
@Override
public void run() {
stunnedKillers.remove(killer.getUuid());
if (player.isOnline()) {
player.sendMessage("§aВы восстановились!");
}
}
}.runTaskLater(plugin, stunTicks);
}
public void stunIfHitByDropped(Killer killer, Pallet pallet, org.bukkit.plugin.Plugin plugin) {
if (!pallet.isDropped()) return;
Player player = killer.getPlayer();
if (player == null) return;
if (isInStunZone(player, pallet)) {
stun(killer, pallet, plugin);
}
}
private boolean isInStunZone(Player player, Pallet pallet) {
return pallet.distanceTo(player.getLocation()) <= 2.0;
}
public void clearStun(Killer killer) {
stunnedKillers.remove(killer.getUuid());
}
}

View file

@ -6,7 +6,6 @@ import dev.loki.hatch.Hatch;
import dev.loki.hatch.HatchCondition;
import dev.loki.hatch.HatchEscape;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.Sidebar;
@ -23,27 +22,22 @@ import java.util.ArrayList;
import java.util.List;
public class EndgamePhase implements Phase {
private int collapseSeconds;
private final int openTicks;
private int remainingTicks;
private boolean complete;
private boolean gatesActivated;
private boolean killerWin; // true если маньяк убил всех — пропускаем геймплей эндгейма
private GateManager gateManager;
private Hatch hatch;
private HatchCondition hatchCondition;
private HatchEscape hatchEscape;
private Sidebar sidebar;
/** Default constructor — reads collapseSeconds from config on enter() */
public EndgamePhase() {
this.collapseSeconds = -1; // resolved in enter()
this.openTicks = 20 * 20;
}
public EndgamePhase(int collapseSeconds, GateManager gateManager,
public EndgamePhase(GateManager gateManager,
Hatch hatch, HatchCondition hatchCondition) {
this.collapseSeconds = collapseSeconds;
this.openTicks = 20 * 20;
this.gateManager = gateManager;
this.hatch = hatch;
@ -57,20 +51,33 @@ public class EndgamePhase implements Phase {
@Override
public void enter(GameSession session) {
// Read collapseSeconds from config if not explicitly set
if (collapseSeconds < 0) {
collapseSeconds = session.getConfig().getCollapseSeconds();
}
this.remainingTicks = collapseSeconds * 20;
this.complete = false;
this.gatesActivated = false;
this.killerWin = false;
// Проверяем: если все выжившие мертвы/на крюке — это killer win,
// показываем результаты сразу, без геймплея эндгейма
int aliveCount = 0;
for (BasePlayer bp : session.getPlayers().values()) {
if (!bp.isKiller() && bp.getState() != PlayerState.DEAD && bp.getState() != PlayerState.ESCAPED) {
aliveCount++;
}
}
if (aliveCount == 0) {
this.killerWin = true;
this.complete = true;
// Показываем результаты и телепортируем всех в лобби
announceEndgameEnd(session);
teleportAllToLobby(session);
return;
}
// Pick up GateManager from session if not injected via constructor
if (gateManager == null) {
gateManager = session.getGateManager();
}
// Set up sidebar with endgame timer
// Set up sidebar
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
@ -83,12 +90,8 @@ public class EndgamePhase implements Phase {
@Override
public void tick(GameSession session) {
remainingTicks--;
int secondsLeft = remainingTicks / 20;
// Update sidebar every second
if (remainingTicks % 20 == 0 && sidebar != null) {
sidebar.setEndgameTimer(secondsLeft);
// Update sidebar stats every 40 ticks
if (session.getTicksElapsed() % 40 == 0 && sidebar != null) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
@ -100,32 +103,61 @@ public class EndgamePhase implements Phase {
updateGates(session);
updateHatch(session);
checkWinConditions(session);
sendCountdown(session, secondsLeft);
if (remainingTicks <= 0) {
triggerCollapse(session);
complete = true;
}
}
@Override
public void exit(GameSession session) {
if (sidebar != null) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
sidebar.disableTimerObjective(p);
}
}
sidebar.clear();
sidebar = null;
}
announceEndgameEnd(session);
if (hatch != null) {
hatch.reset();
}
// Для killer_win: результаты уже показаны и телепорт выполнен в enter(),
// здесь просто останавливаем сессию
if (killerWin) {
session.stop();
return;
}
// Нормальный эндгейм: показываем результаты и телепортируем всех в лобби
announceEndgameEnd(session);
teleportAllToLobby(session);
}
/**
* Телепортирует всех игроков в лобби и останавливает сессию.
*/
private void teleportAllToLobby(GameSession session) {
List<BasePlayer> playersToTeleport = new ArrayList<>(session.getPlayers().values());
new BukkitRunnable() {
@Override
public void run() {
for (BasePlayer bp : playersToTeleport) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
p.setGameMode(org.bukkit.GameMode.ADVENTURE);
p.getInventory().clear();
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
session.getPlugin().getLobbyManager().teleportToLobby(p);
}
}
session.stop();
}
}.runTaskLater(session.getPlugin(), 100L);
}.runTaskLater(session.getPlugin(), 60L);
}
@Override
@ -133,10 +165,6 @@ public class EndgamePhase implements Phase {
return complete;
}
public int getRemainingSeconds() {
return remainingTicks / 20;
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
@ -175,7 +203,7 @@ public class EndgamePhase implements Phase {
if (p == null) continue;
Title title = Title.title(
Component.text("ЭНДГЕЙМ!").color(TextColor.color(0xCC0000)),
Component.text("Коллапс через " + collapseSeconds + " сек!").color(NamedTextColor.RED),
Component.text("Ворота открыты! Бегите!").color(NamedTextColor.GREEN),
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
);
p.showTitle(title);
@ -214,42 +242,6 @@ public class EndgamePhase implements Phase {
}
}
private void sendCountdown(GameSession session, int secondsLeft) {
if (secondsLeft > 0 && secondsLeft <= 10) {
for (BasePlayer player : session.getPlayers().values()) {
Player p = player.getPlayer();
if (p == null) continue;
Title t = Title.title(
Component.text(String.valueOf(secondsLeft)).color(TextColor.color(0xFF2222)),
Component.empty(),
Title.Times.times(Duration.ZERO, Duration.ofSeconds(1), Duration.ofMillis(200))
);
p.showTitle(t);
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 0.5f, 1.0f);
}
}
}
private void triggerCollapse(GameSession session) {
for (BasePlayer player : session.getPlayers().values()) {
if (player.getState() == PlayerState.DEAD || player.getState() == PlayerState.ESCAPED) continue;
Player p = player.getPlayer();
if (p == null) continue;
if (!player.isKiller()) {
p.setHealth(0);
player.setState(PlayerState.DEAD);
} else {
((Killer) player).setCarrying(false);
}
Title t = Title.title(
Component.text("КОЛЛАПС!").color(TextColor.color(0xFF0000)),
Component.text("Карта разрушена!").color(NamedTextColor.RED),
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
);
p.showTitle(t);
}
}
private void announceEndgameEnd(GameSession session) {
List<String> results = new ArrayList<>();
results.add("§6§l=== РЕЗУЛЬТАТЫ ===");

View file

@ -0,0 +1,181 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.damage.AttackListener;
import dev.loki.damage.DamageHandler;
import dev.loki.item.ItemUseListener;
import dev.loki.killer.KillerController;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerJumpListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.mdbd;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerManager;
import dev.loki.player.PlayerState;
import dev.loki.state.StateMachine;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
/**
* Handles damage system, combat, and item usage during gameplay.
*/
public class GameplayDamageSystem {
private PlayerManager playerManager;
private DamageHandler damageHandler;
private KillerController killerController;
private AttackListener attackListener;
private Listener vanillaDamageBlocker;
private ItemUseListener itemUseListener;
private PlayerJumpListener jumpListener;
/**
* Initialize damage and combat system.
*/
public void init(GameSession session, mdbd plugin, GameConfig cfg) {
playerManager = new PlayerManager();
for (BasePlayer bp : session.getPlayers().values()) {
playerManager.register(bp);
}
damageHandler = new DamageHandler(new StateMachine());
damageHandler.setOnSurvivorInjured(survivor -> {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) return;
p.showTitle(Title.title(
Component.text("РАНЕН!").color(TextColor.color(0xFF8C00)),
Component.text("Найдите аптечку или союзника").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.8f);
p.getWorld().spawnParticle(Particle.DAMAGE_INDICATOR, p.getLocation().add(0, 1, 0), 12, 0.4, 0.5, 0.4, 0);
p.setHealth(20.0);
Bukkit.getScheduler().runTaskTimer(plugin, task -> {
if (!p.isOnline() || survivor.getState() != PlayerState.INJURED) {
task.cancel();
return;
}
p.getWorld().spawnParticle(Particle.HEART, p.getLocation().add(0, 1.2, 0), 1, 0.2, 0.2, 0.2, 0);
}, 40L, 40L);
});
damageHandler.setOnSurvivorDowned(survivor -> {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) return;
p.showTitle(Title.title(
Component.text("НОКДАУН!").color(TextColor.color(0xFF2222)),
Component.text("Ждите помощи...").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
p.setHealth(20.0);
p.setSwimming(true);
Bukkit.getScheduler().runTaskTimer(plugin, task -> {
if (!p.isOnline()) {
task.cancel();
return;
}
if (survivor.getState() != PlayerState.DOWNED) {
p.setSwimming(false);
task.cancel();
return;
}
if (!p.isSwimming()) p.setSwimming(true);
}, 1L, 5L);
});
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
attackListener = new AttackListener(playerManager, damageHandler,
(killer, survivor) -> killerController.tryAttack(killer, survivor));
attackListener.register(plugin);
vanillaDamageBlocker = new VanillaDamageBlocker(session);
Bukkit.getPluginManager().registerEvents(vanillaDamageBlocker, plugin);
killerController.initialize();
itemUseListener = new ItemUseListener(plugin, session);
itemUseListener.register();
// Jump listener
jumpListener = new PlayerJumpListener(plugin);
for (BasePlayer bp : session.getPlayers().values()) {
jumpListener.addInGamePlayer(bp.getUuid());
}
// Carry escape is now handled via Shift (onCarriedShift) in GameplayInteraction
jumpListener.register();
}
/**
* Wire jump listener to session.
*/
public void wireJumpListener(GameSession session, mdbd plugin) {
// Already done in init
}
public KillerController getKillerController() {
return killerController;
}
public PlayerManager getPlayerManager() {
return playerManager;
}
/**
* Clear all damage system resources.
*/
public void clear() {
if (jumpListener != null) {
jumpListener.clearInGamePlayers();
jumpListener.unregister();
jumpListener = null;
}
if (attackListener != null) attackListener.unregister();
if (vanillaDamageBlocker != null) {
HandlerList.unregisterAll(vanillaDamageBlocker);
vanillaDamageBlocker = null;
}
if (itemUseListener != null) itemUseListener.unregister();
}
/**
* Clear manager resources.
*/
public void clearManagers() {
if (killerController != null) killerController.shutdown();
if (playerManager != null) playerManager.clear();
}
// =========================================================================
// Inner class: block vanilla damage
// =========================================================================
private static class VanillaDamageBlocker implements Listener {
private final GameSession session;
VanillaDamageBlocker(GameSession session) {
this.session = session;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player p)) return;
if (!session.getPlayers().containsKey(p.getUniqueId())) return;
event.setCancelled(true);
p.setHealth(20.0);
}
}
}

View file

@ -0,0 +1,78 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.hook.*;
import dev.loki.player.BasePlayer;
import dev.loki.player.Survivor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Handles hook-related logic during gameplay phase.
* Manages hook stage progression, struggle, and sacrifice.
*/
public class GameplayHookHandler {
private final HookManager hookManager;
private final HookSacrifice hookSacrifice;
private final HookStruggle hookStruggle;
private final HookRecovery hookRecovery;
public GameplayHookHandler(HookManager hookManager, HookSacrifice hookSacrifice,
HookStruggle hookStruggle, HookRecovery hookRecovery) {
this.hookManager = hookManager;
this.hookSacrifice = hookSacrifice;
this.hookStruggle = hookStruggle;
this.hookRecovery = hookRecovery;
}
/**
* Called every tick - checks hook stage progression.
*/
public void tick() {
// HookManager handles its own ticking via startTicking()
// We just need to check stage transitions here
checkHookStages();
}
/**
* Check all hooked survivors for stage progression.
*/
private void checkHookStages() {
for (Hook hook : hookManager.getAllHooks()) {
if (!hook.isOccupied()) continue;
Survivor hooked = hook.getHookedSurvivor();
if (hooked == null) continue;
HookStage stage = hook.getCurrentStage();
if (stage == HookStage.STAGE_2) {
// Stage 2 - start struggle if not already started
if (hookStruggle.getStruggleProgress(hooked.getUuid()) == 0) {
hookStruggle.startStruggle(hooked, hook);
}
} else if (stage == HookStage.SACRIFICE) {
// Sacrifice - kill the survivor
hookSacrifice.sacrifice(hooked, hook);
}
}
}
/**
* Called when a survivor is rescued from hook.
*/
public void onRescued(Survivor survivor) {
hookStruggle.stopStruggle(survivor);
// hookSacrifice doesn't have cancel - it handles sacrifice when stage reaches SACRIFICE
}
/**
* Clear all hook-related states.
*/
public void clear() {
hookStruggle.clear();
hookRecovery.shutdown();
// hookSacrifice doesn't have clear() - it handles individual sacrifices
}
}

View file

@ -0,0 +1,308 @@
package dev.loki.phase;
import dev.loki.core.GameSession;
import dev.loki.gate.ExitGate;
import dev.loki.gate.GateManager;
import dev.loki.gate.GateOpening;
import dev.loki.hook.Hook;
import dev.loki.hook.HookManager;
import dev.loki.hook.HookRescue;
import dev.loki.hook.HookSabotage;
import dev.loki.killer.GeneratorKick;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.survivor.SurvivorController;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Handles listener wiring for player interactions.
*/
public class GameplayInteraction {
private PlayerMoveListener moveListener;
private PlayerInteractListener interactListener;
private GateManager gateManager;
private HookManager hookManager;
private HookRescue hookRescue;
private HookSabotage hookSabotage;
private GeneratorKick generatorKick;
private GameplayDamageSystem damageSystem;
private dev.loki.chest.ChestManager chestManager;
private dev.loki.chest.ChestOpening chestOpening;
private GateOpening gateOpening;
private dev.loki.objective.GeneratorRepair generatorRepair;
private boolean allGensDone;
private final Map<UUID, Long> killerLastSneakMs = new HashMap<>();
private final Map<UUID, SurvivorController> survivorControllers = new HashMap<>();
/**
* Wire all listeners.
*/
public void wire(GameSession session) {
wireMoveListener(session);
wireInteractListener(session);
wireSurvivorHealing(session);
}
/**
* Initialize SurvivorControllers with a survivorLookup so healing
* can find ANY player's Survivor instance (for healing other survivors).
*/
private void wireSurvivorHealing(GameSession session) {
survivorControllers.clear();
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
Survivor survivor = (Survivor) bp;
SurvivorController controller = new SurvivorController(survivor, session.getConfig());
controller.getHealing().setSurvivorLookup(player -> {
BasePlayer found = session.getPlayer(player.getUniqueId());
if (found == null || found.isKiller()) return null;
return (Survivor) found;
});
survivorControllers.put(survivor.getUuid(), controller);
}
}
public void setMoveListener(PlayerMoveListener moveListener) {
this.moveListener = moveListener;
}
public void setInteractListener(PlayerInteractListener interactListener) {
this.interactListener = interactListener;
}
public void setManagers(GateManager gateManager,
HookManager hookManager, HookRescue hookRescue,
HookSabotage hookSabotage, GeneratorKick generatorKick,
GameplayDamageSystem damageSystem,
dev.loki.chest.ChestManager chestManager,
dev.loki.chest.ChestOpening chestOpening,
GateOpening gateOpening,
dev.loki.objective.GeneratorRepair generatorRepair) {
this.gateManager = gateManager;
this.hookManager = hookManager;
this.hookRescue = hookRescue;
this.hookSabotage = hookSabotage;
this.generatorKick = generatorKick;
this.damageSystem = damageSystem;
this.chestManager = chestManager;
this.chestOpening = chestOpening;
this.gateOpening = gateOpening;
this.generatorRepair = generatorRepair;
}
public void setAllGensDone(boolean allGensDone) {
this.allGensDone = allGensDone;
}
private void wireMoveListener(GameSession session) {
if (moveListener == null) return;
moveListener.setOnSurvivorMove((survivor, loc) -> {
if (!allGensDone) return;
if (survivor.getState() == PlayerState.ESCAPED) return;
ExitGate gate = gateManager.getGateAt(loc, 3.0);
if (gate != null && gate.isOpen()) {
survivor.setState(PlayerState.ESCAPED);
Player p = survivor.getPlayer();
if (p != null) {
p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
Bukkit.broadcast(Component.text(
"[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD));
}
}
});
}
private void wireInteractListener(GameSession session) {
if (interactListener == null) return;
// Rescue
interactListener.setOnRescueAttempt((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor rescuer = (Survivor) bp;
// Cannot rescue if you're hooked, downed, or carried
if (rescuer.getState() == PlayerState.HOOKED ||
rescuer.getState() == PlayerState.DOWNED ||
rescuer.getState() == PlayerState.CARRIED) return;
Hook hook = hookManager.findNearestOccupiedHook(player.getLocation(), 3.0);
if (hook != null) hookRescue.startRescue(rescuer, hook);
});
// Right-click attack (hook carrying / pickup downed)
interactListener.setOnAttack((player, isRightClick) -> {
if (!isRightClick) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || !bp.isKiller()) return;
Killer killer = (Killer) bp;
if (killer.isCarrying()) {
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearest != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearest.getLocation());
}
}
break;
}
}
} else {
Survivor target = damageSystem.getPlayerManager().getNearestSurvivorInState(
player, PlayerState.DOWNED, 3.0);
if (target != null) {
boolean picked = damageSystem.getKillerController().tryPickup(killer, target);
if (!picked) player.sendMessage(Component.text(
"[DBD] Не удалось подобрать выжившего.", NamedTextColor.RED));
}
}
});
// Sneak: killer → Hook / GeneratorKick / carry pickup, survivor → Sabotage
interactListener.setOnKillerSneak((player, isSneaking) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (bp.isKiller()) {
Killer killer = (Killer) bp;
if (isSneaking) {
// #1: Hook carried survivor if near a hook
if (killer.isCarrying()) {
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearest != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearest.getLocation());
}
return;
}
break;
}
}
return;
}
// #2: Double-sneak to pickup downed survivor
long now = System.currentTimeMillis();
Long last = killerLastSneakMs.get(player.getUniqueId());
if (last != null && now - last < 450 && !killer.isCarrying()) {
Survivor target = damageSystem.getPlayerManager().getNearestSurvivorInState(
player, PlayerState.DOWNED, 3.0);
if (target != null) {
damageSystem.getKillerController().tryPickup(killer, target);
}
}
killerLastSneakMs.put(player.getUniqueId(), now);
generatorKick.startCharging(killer);
} else {
generatorKick.stopCharging(killer.getUuid());
}
} else {
Survivor surv = (Survivor) bp;
if (surv.getState() == PlayerState.HOOKED) {
// Hook recovery is handled elsewhere
} else {
if (isSneaking) hookSabotage.startSabotage(surv);
else hookSabotage.stopSabotage(surv.getUuid());
}
}
});
// Generator repair
interactListener.setOnGeneratorInteract((player, action) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
if (generatorRepair == null) return;
var genManager = session.getGeneratorManager();
if (genManager == null) return;
double range = session.getConfig().getGeneratorInteractionRange();
var generator = genManager.getNearestGenerator(player.getLocation(), range);
if (generator != null) {
generatorRepair.startRepair(player, generator);
}
});
// Chest
interactListener.setOnChestInteract((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
var chest = chestManager.getNearestChest(player.getLocation(), 3.0);
if (chest != null) chestOpening.startOpening(player, chest);
});
// Healing: survivor sneaks near injured/downed survivor
interactListener.setOnHealSneak((healer, target) -> {
SurvivorController controller = survivorControllers.get(healer.getUniqueId());
if (controller != null) {
controller.onHealAttempt(target);
}
});
// #11: Carried survivor shift-spam to escape from killer
interactListener.setOnCarriedShift((player, isSneaking) -> {
if (!isSneaking) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor survivor = (Survivor) bp;
if (survivor.getState() != PlayerState.CARRIED) return;
damageSystem.getKillerController().getCarry().onSurvivorShift(survivor.getUuid());
});
// Gate (uses hatch callback - triggered on PORTAL/HOPPER interaction)
interactListener.setOnHatchInteract((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor survivor = (Survivor) bp;
ExitGate gate = gateManager.getGateAt(player.getLocation(), 3.0);
if (gate != null && !gate.isOpen() && gateOpening != null) {
gateOpening.tryStartOpening(survivor, session.getPlugin());
}
});
}
/**
* Tick all survivor controllers (healing, movement, etc.)
*/
public void tickSurvivors() {
for (SurvivorController controller : survivorControllers.values()) {
controller.tick();
}
}
/**
* Clear all listener callbacks.
*/
public void clear() {
survivorControllers.clear();
if (moveListener != null) moveListener.setOnSurvivorMove(null);
if (interactListener != null) {
interactListener.setOnRescueAttempt(null);
interactListener.setOnChestInteract(null);
interactListener.setOnAttack(null);
interactListener.setOnKillerSneak(null);
interactListener.setOnHealSneak(null);
interactListener.setOnCarriedShift(null);
interactListener.setOnGeneratorInteract(null);
interactListener.setOnHatchInteract(null);
}
}
}

View file

@ -4,54 +4,34 @@ import dev.loki.chest.ChestManager;
import dev.loki.chest.ChestOpening;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.damage.AttackListener;
import dev.loki.damage.DamageHandler;
import dev.loki.gate.ExitGate;
import dev.loki.gate.GateManager;
import dev.loki.hook.Hook;
import dev.loki.gate.GateOpening;
import dev.loki.hook.HookManager;
import dev.loki.hook.HookRecovery;
import dev.loki.hook.HookRescue;
import dev.loki.hook.HookSabotage;
import dev.loki.hook.HookSacrifice;
import dev.loki.hook.HookStage;
import dev.loki.hook.HookStruggle;
import dev.loki.item.ChestLoot;
import dev.loki.killer.GeneratorKick;
import dev.loki.killer.KillerController;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerJumpListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.mdbd;
import dev.loki.objective.GeneratorManager;
import dev.loki.pallet.Pallet;
import dev.loki.pallet.PalletBreak;
import dev.loki.pallet.PalletDrop;
import dev.loki.pallet.PalletManager;
import dev.loki.pallet.PalletStun;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerManager;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.state.StateMachine;
import dev.loki.ui.ActionBar;
import dev.loki.ui.BossBarManager;
import dev.loki.ui.Sidebar;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import java.time.Duration;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.List;
public class GameplayPhase implements Phase {
@ -60,221 +40,98 @@ public class GameplayPhase implements Phase {
private boolean shouldEnd;
private boolean allGensDone;
// Managers initialised in enter()
private PalletManager palletManager;
// Sub-handlers
private GameplayHookHandler hookHandler;
private GameplayWinConditions winConditions;
private GameplayDamageSystem damageSystem;
private GameplayUI ui;
private GameplayInteraction interaction;
// Managers
private HookManager hookManager;
private GateManager gateManager;
private GateOpening gateOpening;
private HookRescue hookRescue;
private HookSacrifice hookSacrifice;
private PalletDrop palletDrop;
private PalletBreak palletBreak;
@SuppressWarnings("unused")
private PalletStun palletStun;
private HookSabotage hookSabotage;
private HookStruggle hookStruggle;
private TitleUI titleUI;
private ChestManager chestManager;
private ChestOpening chestOpening;
private GeneratorKick generatorKick;
private HookSabotage hookSabotage;
private BossBarManager bossBarManager;
private Sidebar sidebar;
// Damage system
private PlayerManager playerManager;
private StateMachine stateMachine;
private DamageHandler damageHandler;
private KillerController killerController;
private AttackListener attackListener;
private Listener vanillaDamageBlocker;
private dev.loki.item.ItemUseListener itemUseListener;
// Listeners
private PlayerMoveListener moveListener;
private PlayerInteractListener interactListener;
private PlayerJumpListener jumpListener;
private dev.loki.objective.GeneratorRepair generatorRepair;
private dev.loki.objective.GeneratorHologram generatorHologram;
private dev.loki.hook.HookHologram hookHologram;
private dev.loki.chest.ChestHologram chestHologram;
public GameplayPhase(GeneratorManager generatorManager) {
this.generatorManager = generatorManager;
}
// -------------------------------------------------------------------------
// =========================================================================
// Phase lifecycle
// -------------------------------------------------------------------------
// =========================================================================
@Override
public void enter(GameSession session) {
mdbd plugin = session.getPlugin();
GameConfig cfg = session.getConfig();
tickCounter = 0;
shouldEnd = false;
shouldEnd = false;
allGensDone = false;
winConditions = new GameplayWinConditions();
// --- Managers ---
palletManager = new PalletManager(plugin.getConfigManager().getLocations());
hookManager = new HookManager(plugin);
hookManager.loadHooks(plugin.getConfigManager().getLocations());
hookManager.setMaxHooks(cfg.getMaxHooks());
gateManager = new GateManager(plugin.getConfigManager().getLocations());
ActionBar ab = new ActionBar();
hookRescue = new HookRescue(plugin, ab, cfg);
hookSacrifice = new HookSacrifice(plugin, null);
palletDrop = new PalletDrop(palletManager);
palletBreak = new PalletBreak(cfg);
palletStun = new PalletStun(cfg);
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations());
chestOpening = new ChestOpening(plugin);
initManagers(session, plugin, cfg);
initSubHandlers(session, plugin, cfg);
initListeners(session, plugin);
// GeneratorKick for the killer
generatorKick = new GeneratorKick(plugin, cfg, generatorManager);
// HookSabotage for survivors
hookSabotage = new HookSabotage(plugin, cfg, hookManager);
session.setPalletManager(palletManager);
session.setHookManager(hookManager);
session.setGateManager(gateManager);
// --- Damage system ---
playerManager = new PlayerManager();
for (BasePlayer bp : session.getPlayers().values()) {
playerManager.register(bp);
}
stateMachine = new StateMachine();
damageHandler = new DamageHandler(stateMachine);
damageHandler.setOnSurvivorInjured(survivor -> {
Player p = survivor.getPlayer();
if (p != null && p.isOnline()) {
p.showTitle(Title.title(
Component.text("РАНЕН!").color(TextColor.color(0xFF8C00)),
Component.text("Найдите аптечку или союзника").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.8f);
p.setHealth(20.0);
}
});
damageHandler.setOnSurvivorDowned(survivor -> {
Player p = survivor.getPlayer();
if (p != null && p.isOnline()) {
p.showTitle(Title.title(
Component.text("НОКДАУН!").color(TextColor.color(0xFF2222)),
Component.text("Ждите помощи...").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
p.setHealth(20.0);
}
});
// KillerController — AttackListener зарегистрирован ТОЛЬКО здесь
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
attackListener = new AttackListener(playerManager, damageHandler,
(killer, survivor) -> killerController.tryAttack(killer, survivor));
attackListener.register(plugin);
// Block vanilla damage
vanillaDamageBlocker = new VanillaDamageBlocker(session);
Bukkit.getPluginManager().registerEvents(vanillaDamageBlocker, plugin);
// KillerController.initialize() does NOT re-register AttackListener
killerController.initialize();
// --- BossBar ---
Collection<Survivor> survivors = session.getPlayers().values().stream()
.filter(bp -> !bp.isKiller())
.map(bp -> (Survivor) bp)
.collect(Collectors.toList());
Killer killer = (Killer) session.getPlayers().values().stream()
.filter(BasePlayer::isKiller)
.findFirst().orElse(null);
bossBarManager = new BossBarManager(plugin, cfg);
if (killer != null) bossBarManager.setTargets(killer, survivors);
if (cfg.isBossBarEnabled()) bossBarManager.start();
// --- Sidebar ---
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) sidebar.setupPlayer(p);
// Устанавливаем ночь на всех мирах
for (var world : Bukkit.getWorlds()) {
world.setGameRule(org.bukkit.GameRule.DO_DAYLIGHT_CYCLE, false);
world.setTime(18000); // Полночь
world.setStorm(false);
}
// --- Jump listener for carry escape ---
jumpListener = new PlayerJumpListener(plugin);
jumpListener.setOnJump(player -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
killerController.getCarry().onSurvivorJump(player.getUniqueId());
});
jumpListener.register();
// --- Items ---
itemUseListener = new dev.loki.item.ItemUseListener(plugin, session);
itemUseListener.register();
giveStartingItems(session);
// Apply player scales via Attribute API
applyPlayerScales(session, cfg);
// --- Wire listeners ---
moveListener = plugin.getMoveListener();
interactListener = plugin.getInteractListener();
wireMoveListener(session);
wireInteractListener(session);
Bukkit.broadcast(Component.text("[DBD] Игра началась! Выжившие, чините генераторы!",
NamedTextColor.GREEN));
Bukkit.broadcast(Component.text(
"[DBD] Игра началась! Выжившие, чините генераторы!", NamedTextColor.GREEN));
}
@Override
public void tick(GameSession session) {
tickCounter++;
generatorManager.tick();
tickHooks(session);
if (generatorRepair != null) generatorRepair.tick();
if (interaction != null) interaction.tickSurvivors();
hookHandler.tick();
boolean timeExpired = ui.tickMatchTimer(session);
// Particle effects around generators (every 10 ticks)
if (tickCounter % 10 == 0 && generatorManager != null) {
for (var gen : generatorManager.getAllGenerators()) {
if (gen.isCompleted()) continue;
Location gloc = gen.getLocation().clone().add(0.5, 0.5, 0.5);
gloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK,
gloc, 2, 0.8, 0.5, 0.8, 0.01);
gloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD,
gloc, 1, 0.6, 0.3, 0.6, 0.01);
}
}
if (tickCounter % 20 == 0) {
checkGeneratorProgress(session);
checkWinConditions(session);
updateSidebar(session);
winConditions.checkWinConditions(session);
if (winConditions.isShouldEnd() || timeExpired) shouldEnd = true;
ui.updateSidebar(session, generatorManager);
}
}
@Override
public void exit(GameSession session) {
if (moveListener != null) {
moveListener.setOnSurvivorMove(null);
}
if (interactListener != null) {
interactListener.setOnPalletInteract(null);
interactListener.setOnRescueAttempt(null);
interactListener.setOnChestInteract(null);
interactListener.setOnAttack(null);
interactListener.setOnKillerSneak(null);
}
if (jumpListener != null) {
jumpListener.unregister();
jumpListener = null;
}
if (hookManager != null) hookManager.clear();
if (hookRescue != null) hookRescue.clear();
if (chestOpening != null) chestOpening.cancelAll();
if (attackListener != null) attackListener.unregister();
if (vanillaDamageBlocker != null) {
HandlerList.unregisterAll(vanillaDamageBlocker);
vanillaDamageBlocker = null;
}
if (killerController != null) killerController.shutdown();
if (generatorKick != null) generatorKick.shutdown();
if (hookSabotage != null) hookSabotage.shutdown();
if (bossBarManager != null) bossBarManager.stop();
if (sidebar != null) { sidebar.clear(); sidebar = null; }
if (itemUseListener != null) itemUseListener.unregister();
if (playerManager != null) playerManager.clear();
// Restore modes and HP
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
p.setHealth(20.0);
p.setGameMode(GameMode.ADVENTURE);
}
}
clearListeners();
clearManagers(session);
ui.clear(session);
ui.resetPlayers(session);
damageSystem.clear();
damageSystem.clearManagers();
Bukkit.broadcast(Component.text("[DBD] Игровая фаза завершена!", NamedTextColor.GOLD));
}
@ -284,336 +141,207 @@ public class GameplayPhase implements Phase {
return shouldEnd;
}
// -------------------------------------------------------------------------
// Attribute-based player scaling
// -------------------------------------------------------------------------
private void applyPlayerScales(GameSession session, GameConfig cfg) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
double scale = bp.isKiller() ? cfg.getKillerScale() : cfg.getSurvivorScale();
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.GENERIC_SCALE);
if (attr != null) attr.setBaseValue(scale);
} catch (Exception ignored) {
// Attribute may not exist on older Paper builds — skip silently
}
}
}
private void resetPlayerScales(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.GENERIC_SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
}
}
// -------------------------------------------------------------------------
// Sidebar update
// -------------------------------------------------------------------------
private void updateSidebar(GameSession session) {
if (sidebar == null) return;
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
sidebar.updateGameStats(p, session, generatorManager);
}
}
}
// -------------------------------------------------------------------------
// Starting items
// -------------------------------------------------------------------------
private void giveStartingItems(GameSession session) {
var loot = new dev.loki.item.ChestLoot();
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
p.getInventory().clear();
p.getInventory().setItem(0, loot.rollLoot());
p.sendMessage(Component.text("[DBD] Вы получили стартовый предмет!", NamedTextColor.YELLOW));
}
}
// -------------------------------------------------------------------------
// Hook ticking
// -------------------------------------------------------------------------
private void tickHooks(GameSession session) {
for (Hook hook : hookManager.getAllHooks()) {
if (!hook.isOccupied()) continue;
hook.tick();
Survivor victim = hook.getHookedSurvivor();
if (victim == null) continue;
Player vp = victim.getPlayer();
if (vp != null && vp.isOnline()) {
double pct = hook.getStageProgressPercent();
String label = hook.getCurrentStage() == HookStage.STAGE_2
? "§cStage 2 — вырывайтесь!" : "§eНа крюке";
vp.sendActionBar(Component.text(label + " " + (int) pct + "%"));
}
if (hook.getCurrentStage() == HookStage.SACRIFICE) {
hookSacrifice.sacrificeIfExpired(victim, hook);
// Перевод в SPECTATOR после смерти обрабатывается через setState
}
}
}
// -------------------------------------------------------------------------
// Spectator mode on death / after sacrifice
// -------------------------------------------------------------------------
/**
* Converts a dead survivor to SPECTATOR so they can watch the game.
* Returns the reason the gameplay phase ended, or null if not yet ended.
*/
private void onSurvivorDied(Survivor survivor, GameSession session) {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) return;
p.setGameMode(GameMode.SPECTATOR);
p.sendMessage(Component.text("[DBD] Вы мертвы. Наблюдайте за матчем.", NamedTextColor.DARK_RED));
// Let spectator follow a random alive player
for (BasePlayer other : session.getPlayers().values()) {
if (other.getUuid().equals(survivor.getUuid())) continue;
Player op = other.getPlayer();
if (op != null && op.isOnline() && other.getState() != PlayerState.DEAD) {
p.setSpectatorTarget(op);
break;
}
}
public String getWinReason() {
return winConditions != null ? winConditions.getWinReason() : null;
}
// -------------------------------------------------------------------------
// Generator / win-condition checks
// -------------------------------------------------------------------------
// =========================================================================
// Init helpers
// =========================================================================
private void initManagers(GameSession session, mdbd plugin, GameConfig cfg) {
hookManager = new HookManager(plugin);
hookManager.loadHooks(plugin.getConfigManager().getLocations());
hookManager.setMaxHooks(cfg.getMaxHooks());
hookManager.startTicking();
gateManager = new GateManager(plugin.getConfigManager().getLocations());
gateOpening = new GateOpening(gateManager, cfg);
ActionBar ab = new ActionBar();
HookRecovery hookRecovery = new HookRecovery(plugin, cfg);
titleUI = new TitleUI(plugin);
hookStruggle = new HookStruggle(plugin, titleUI, cfg);
HookSacrifice hookSacrifice = new HookSacrifice(plugin, null);
hookSabotage = new HookSabotage(plugin, cfg, hookManager);
hookRescue = new HookRescue(plugin, ab, cfg);
hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery);
hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim));
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg);
chestManager.startLootResetTimer();
chestOpening = new ChestOpening(plugin);
chestOpening.setChestLoot(new ChestLoot(cfg));
spawnGameBlocks(plugin, cfg);
generatorKick = new GeneratorKick(plugin, cfg, generatorManager);
generatorRepair = new dev.loki.objective.GeneratorRepair(plugin, generatorManager);
// Голограммы ВСЕГДА включены
generatorHologram = new dev.loki.objective.GeneratorHologram(plugin, generatorManager);
generatorHologram.start();
plugin.getLogger().info("[GameplayPhase] Generator holograms started");
hookHologram = new dev.loki.hook.HookHologram(plugin, hookManager);
hookHologram.start();
plugin.getLogger().info("[GameplayPhase] Hook holograms: " + hookManager.getAllHooks().size());
chestHologram = new dev.loki.chest.ChestHologram(plugin, chestManager);
chestHologram.start();
plugin.getLogger().info("[GameplayPhase] Chest holograms: " + chestManager.getAllChests().size());
session.setHookManager(hookManager);
session.setGateManager(gateManager);
}
private void initSubHandlers(GameSession session, mdbd plugin, GameConfig cfg) {
damageSystem = new GameplayDamageSystem();
damageSystem.init(session, plugin, cfg);
ui = new GameplayUI();
ui.init(session, plugin, cfg);
ui.setWinConditions(winConditions);
interaction = new GameplayInteraction();
interaction.setMoveListener(plugin.getMoveListener());
interaction.setInteractListener(plugin.getInteractListener());
// ВАЖНО: переустановить generatorManager, поскольку он может быть null после exit()
plugin.getInteractListener().setGeneratorManager(generatorManager);
interaction.setManagers(gateManager, hookManager, hookRescue,
hookSabotage, generatorKick, damageSystem,
chestManager, chestOpening, gateOpening, generatorRepair);
}
private void initListeners(GameSession session, mdbd plugin) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.getInventory().clear();
}
// Register skill check GUI listener
dev.loki.listener.SkillCheckListener skillCheckListener =
new dev.loki.listener.SkillCheckListener(plugin,
generatorRepair.getSkillCheckGUI());
skillCheckListener.register();
interaction.wire(session);
}
// =========================================================================
// Tick helpers
// =========================================================================
private void checkGeneratorProgress(GameSession session) {
if (allGensDone) return;
if (generatorManager.areAllRequiredCompleted()) {
allGensDone = true;
interaction.setAllGensDone(true);
gateManager.activateEndgame();
Bukkit.broadcast(Component.text(
"[DBD] Все генераторы починены! Открывайте ворота!", NamedTextColor.GREEN));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null) {
p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f);
}
if (p != null) p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f);
}
}
}
private void checkWinConditions(GameSession session) {
int aliveSurvivors = 0;
int escapedSurvivors = 0;
int totalSurvivors = 0;
private void spawnGameBlocks(mdbd plugin, GameConfig cfg) {
// Спавн блоков на местах генераторов
Material genMat = Material.matchMaterial(cfg.getGeneratorBlock());
if (genMat == null) genMat = Material.BLAST_FURNACE;
for (var gen : generatorManager.getAllGenerators()) {
Location loc = gen.getLocation();
if (loc.getWorld() == null) continue;
Block b = loc.getBlock();
if (b.getType() != genMat) b.setType(genMat, false);
}
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
totalSurvivors++;
PlayerState st = bp.getState();
if (st == PlayerState.ESCAPED) {
escapedSurvivors++;
} else if (st != PlayerState.DEAD) {
aliveSurvivors++;
}
// Переводим мёртвых в SPECTATOR (если ещё не переведены)
if (st == PlayerState.DEAD) {
Player p = bp.getPlayer();
if (p != null && p.isOnline() && p.getGameMode() != GameMode.SPECTATOR) {
onSurvivorDied((Survivor) bp, session);
}
// Спавн блоков на местах сундуков + лут (resetAllChests уже устанавливает лут)
Material chestMat = Material.matchMaterial(cfg.getChestBlock());
if (chestMat == null) chestMat = Material.CHEST;
chestManager.resetAllChests();
// Убедимся, что у каждого сундука есть лут (не null и не AIR)
for (dev.loki.chest.Chest chest : chestManager.getAllChests()) {
Location loc = chest.getLocation();
if (loc.getWorld() == null) continue;
Block block = loc.getBlock();
if (block.getType() != chestMat) block.setType(chestMat, false);
if (!chest.hasLoot()) {
ChestLoot chestLoot = new ChestLoot(cfg);
chest.setLoot(chestLoot.rollLoot());
}
}
if (totalSurvivors == 0) return;
plugin.getLogger().info("[GameplayPhase] Spawned " + chestManager.getTotalChests() + " chests with loot");
// Маньяк побеждает — все мертвы и никто не сбежал
boolean killerWin = (aliveSurvivors == 0 && escapedSurvivors == 0);
// Выжившие побеждают — хотя бы один сбежал
boolean survivorsWin = (escapedSurvivors >= 1);
if (killerWin) {
announceKillerWin(session);
shouldEnd = true;
} else if (survivorsWin && aliveSurvivors == 0) {
// Все либо сбежали либо мертвы — конец матча
announceSurvivorWin(session);
shouldEnd = true;
// Спавн блоков на местах крюков
Material hookMat = Material.matchMaterial(cfg.getHookBlock());
if (hookMat == null) hookMat = Material.IRON_BARS;
for (var hook : hookManager.getAllHooks()) {
Location loc = hook.getLocation();
if (loc.getWorld() == null) continue;
Block b = loc.getBlock();
if (b.getType() != hookMat) b.setType(hookMat, false);
}
plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + hookManager.getAllHooks().size() + " hooks");
}
private void announceKillerWin(GameSession session) {
Bukkit.broadcast(Component.text("☠ КИЛЛЕР ПОБЕДИЛ! ☠", NamedTextColor.DARK_RED));
Bukkit.broadcast(Component.text("Все выжившие уничтожены.", NamedTextColor.RED));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (bp.isKiller()) {
p.showTitle(Title.title(
Component.text("ПОБЕДА!").color(TextColor.color(0xFF2222)),
Component.text("Ты уничтожил всех выживших").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f);
// =========================================================================
// Cleanup
// =========================================================================
private void showGameResults(GameSession session) {
List<String> results = new ArrayList<>();
results.add("§6§l=== РЕЗУЛЬТАТЫ ===");
int escaped = 0, died = 0;
for (BasePlayer player : session.getPlayers().values()) {
if (player.isKiller()) {
Player p = player.getPlayer();
String name = p != null ? p.getName() : "Unknown";
results.add("§c☠ " + name + " — Маньяк");
continue;
}
Player p = player.getPlayer();
String name = p != null ? p.getName() : "Unknown";
if (player.getState() == PlayerState.ESCAPED) {
results.add("§a✔ " + name + " — Сбежал");
escaped++;
} else {
p.showTitle(Title.title(
Component.text("ПОРАЖЕНИЕ").color(TextColor.color(0x888888)),
Component.text("Киллер победил").color(NamedTextColor.GRAY)
));
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f);
results.add("§c✘ " + name + " — Погиб");
died++;
}
}
results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died);
String msg = String.join("\n", results);
for (BasePlayer player : session.getPlayers().values()) {
Player p = player.getPlayer();
if (p != null) p.sendMessage(msg);
}
}
private void announceSurvivorWin(GameSession session) {
Bukkit.broadcast(Component.text("✔ ВЫЖИВШИЕ ПОБЕДИЛИ! ✔", NamedTextColor.GREEN));
Bukkit.broadcast(Component.text("Выжившие сбежали!", NamedTextColor.YELLOW));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (!bp.isKiller()) {
p.showTitle(Title.title(
Component.text("ПОБЕГ!").color(TextColor.color(0x00FF00)),
Component.text("Вы выбрались живыми!").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.2f);
} else {
p.showTitle(Title.title(
Component.text("ПОРАЖЕНИЕ").color(TextColor.color(0x888888)),
Component.text("Выжившие сбежали").color(NamedTextColor.GRAY)
));
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f);
}
}
private void clearListeners() {
if (interaction != null) interaction.clear();
}
// -------------------------------------------------------------------------
// Listener wiring
// -------------------------------------------------------------------------
private void wireMoveListener(GameSession session) {
if (moveListener == null) return;
moveListener.setOnSurvivorMove((survivor, loc) -> {
if (!allGensDone) return;
if (survivor.getState() == PlayerState.ESCAPED) return;
ExitGate gate = gateManager.getGateAt(loc, 3.0);
if (gate != null && gate.isOpen()) {
survivor.setState(PlayerState.ESCAPED);
Player p = survivor.getPlayer();
if (p != null) {
p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
Bukkit.broadcast(Component.text(
"[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD));
}
}
});
private void clearManagers(GameSession session) {
if (hookHandler != null) hookHandler.clear();
if (hookRescue != null) hookRescue.clear();
if (hookSabotage != null) hookSabotage.shutdown();
if (hookManager != null) hookManager.clear();
if (chestOpening != null) chestOpening.cancelAll();
if (chestManager != null) chestManager.resetAllChests();
if (generatorKick != null) generatorKick.shutdown();
if (generatorHologram != null) generatorHologram.stop();
if (hookHologram != null) hookHologram.stop();
if (titleUI != null) titleUI.clearSkillChecks();
if (hookStruggle != null) hookStruggle.clear();
if (chestManager != null) chestManager.stopLootResetTimer();
if (chestHologram != null) chestHologram.stop();
session.getPlugin().getInteractListener().setGeneratorManager(null);
}
private void wireInteractListener(GameSession session) {
if (interactListener == null) return;
interactListener.setOnPalletInteract((player, isRightClick) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (isRightClick && !bp.isKiller()) {
palletDrop.tryDrop((Survivor) bp);
} else if (!isRightClick && bp.isKiller()) {
Killer k = (Killer) bp;
Pallet nearby = palletManager.getPalletAt(player.getLocation(), 3.0);
if (nearby != null && nearby.isDropped()) {
palletBreak.startBreaking(k, nearby, session.getPlugin());
}
}
});
interactListener.setOnRescueAttempt((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor rescuer = (Survivor) bp;
if (rescuer.getState() == PlayerState.HOOKED) return;
Hook hook = hookManager.findNearestOccupiedHook(player.getLocation(), 3.0);
if (hook != null) hookRescue.startRescue(rescuer, hook);
});
interactListener.setOnAttack((player, isRightClick) -> {
if (!isRightClick) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || !bp.isKiller()) return;
Killer killer = (Killer) bp;
if (!killer.isCarrying()) return;
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
Hook nearestHook = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearestHook != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearestHook.getLocation());
}
}
break;
}
}
});
// Sneak: killer → GeneratorKick, survivor → HookSabotage
interactListener.setOnKillerSneak((player, isSneaking) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (bp.isKiller()) {
Killer killer = (Killer) bp;
if (isSneaking) {
generatorKick.startCharging(killer);
} else {
generatorKick.stopCharging(killer.getUuid());
}
} else {
// Survivor sneak → hook sabotage
Survivor surv = (Survivor) bp;
if (isSneaking) {
hookSabotage.startSabotage(surv);
} else {
hookSabotage.stopSabotage(surv.getUuid());
}
}
});
interactListener.setOnChestInteract((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
var chest = chestManager.getNearestChest(player.getLocation(), 3.0);
if (chest != null) chestOpening.startOpening(player, chest);
});
}
// -------------------------------------------------------------------------
// Inner class: blocks all vanilla damage for players in the session
// -------------------------------------------------------------------------
private static class VanillaDamageBlocker implements Listener {
private final GameSession session;
VanillaDamageBlocker(GameSession session) { this.session = session; }
@EventHandler(priority = EventPriority.HIGHEST)
public void onDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player p)) return;
if (!session.getPlayers().containsKey(p.getUniqueId())) return;
event.setCancelled(true);
p.setHealth(20.0);
}
}
}
}

View file

@ -0,0 +1,158 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.mdbd;
import dev.loki.objective.GeneratorManager;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.Survivor;
import dev.loki.ui.BossBarManager;
import dev.loki.ui.Sidebar;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.stream.Collectors;
/**
* Handles UI elements during gameplay: BossBar, sidebar, match timer.
*/
public class GameplayUI {
private static final int MATCH_DURATION_TICKS = 15 * 60 * 20;
private BossBarManager bossBarManager;
private Sidebar sidebar;
private BossBar matchTimerBar;
private int matchTimerTicks;
private GameplayWinConditions winConditions;
/**
* Initialize UI system.
*/
public void init(GameSession session, mdbd plugin, GameConfig cfg) {
// Proximity BossBar (killer distance)
Collection<Survivor> survivors = session.getPlayers().values().stream()
.filter(bp -> !bp.isKiller()).map(bp -> (Survivor) bp).collect(Collectors.toList());
Killer killer = (Killer) session.getPlayers().values().stream()
.filter(BasePlayer::isKiller).findFirst().orElse(null);
bossBarManager = new BossBarManager(plugin, cfg);
if (killer != null) bossBarManager.setTargets(killer, survivors);
if (cfg.isBossBarEnabled()) bossBarManager.start();
// Sidebar
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) sidebar.setupPlayer(p);
}
// Match timer
matchTimerTicks = MATCH_DURATION_TICKS;
matchTimerBar = BossBar.bossBar(
Component.text("[TIMER] 15:00").color(NamedTextColor.GREEN),
1.0f, BossBar.Color.GREEN, BossBar.Overlay.PROGRESS);
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isDeadOrEscaped()) continue;
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.showBossBar(matchTimerBar);
}
// Apply scales
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
double scale = bp.isKiller() ? cfg.getKillerScale() : cfg.getSurvivorScale();
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(scale);
} catch (Exception ignored) {}
}
}
public void setWinConditions(GameplayWinConditions winConditions) {
this.winConditions = winConditions;
}
/**
* Update match timer each tick.
* @return true if time expired
*/
public boolean tickMatchTimer(GameSession session) {
if (matchTimerTicks <= 0 || matchTimerBar == null) return false;
matchTimerTicks--;
if (matchTimerTicks % 20 != 0) return false;
int secondsLeft = matchTimerTicks / 20;
int mins = secondsLeft / 60;
int secs = secondsLeft % 60;
float progress = (float) matchTimerTicks / MATCH_DURATION_TICKS;
BossBar.Color color = secondsLeft > 120 ? BossBar.Color.GREEN
: secondsLeft > 60 ? BossBar.Color.YELLOW : BossBar.Color.RED;
matchTimerBar.name(Component.text("[TIMER] " + String.format("%d:%02d", mins, secs)).color(
secondsLeft > 120 ? NamedTextColor.GREEN
: secondsLeft > 60 ? NamedTextColor.YELLOW : NamedTextColor.RED));
matchTimerBar.progress(Math.max(0.001f, progress));
matchTimerBar.color(color);
if (matchTimerTicks <= 0) {
Bukkit.broadcast(Component.text("[DBD] Время вышло! Маньяк победил!", NamedTextColor.DARK_RED));
if (winConditions != null) winConditions.announceKillerWin(session);
return true;
}
return false;
}
/**
* Update sidebar stats.
*/
public void updateSidebar(GameSession session, GeneratorManager generatorManager) {
if (sidebar == null) return;
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) sidebar.updateGameStats(p, session, generatorManager);
}
}
/**
* Clear all UI resources.
*/
public void clear(GameSession session) {
if (bossBarManager != null) bossBarManager.stop();
if (matchTimerBar != null) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.hideBossBar(matchTimerBar);
}
matchTimerBar = null;
}
if (sidebar != null) {
sidebar.clear();
sidebar = null;
}
}
/**
* Reset player attributes (scale, etc).
*/
public void resetPlayers(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
p.setHealth(20.0);
p.setSwimming(false);
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
}
}
}

View file

@ -0,0 +1,136 @@
package dev.loki.phase;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.hook.*;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.ui.TitleUI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
/**
* Handles win condition checking during gameplay phase.
*/
public class GameplayWinConditions {
private boolean shouldEnd;
private String winReason; // "killer_win", "survivors_win", null
public boolean isShouldEnd() {
return shouldEnd;
}
public String getWinReason() {
return winReason;
}
/**
* Check all win conditions each second (20 ticks).
*/
public void checkWinConditions(GameSession session) {
int aliveSurvivors = countAliveSurvivors(session);
Killer killer = getKiller(session);
// Killer disconnect: if killer player is null/offline, survivors win
if (killer == null || killer.getPlayer() == null || !killer.getPlayer().isOnline()) {
winReason = "survivors_win";
Bukkit.broadcast(Component.text("[DBD] Маньяк покинул игру! Выжившие победили!", NamedTextColor.GREEN));
announceSurvivorsWin(session);
shouldEnd = true;
return;
}
// Killer wins: all survivors are downed/hooked/carried/dead
if (allSurvivorsIncapacitated(session)) {
winReason = "killer_win";
Bukkit.broadcast(Component.text("[DBD] Все выжившие обезврежены!", NamedTextColor.DARK_RED));
announceKillerWin(session);
shouldEnd = true;
return;
}
// Survivors win: all generators done and at least one escaped
if (hasEscapedSurvivor(session)) {
winReason = "survivors_win";
announceSurvivorsWin(session);
shouldEnd = true;
}
}
/** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, or DEAD). */
private boolean allSurvivorsIncapacitated(GameSession session) {
boolean hasSurvivors = false;
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
hasSurvivors = true;
PlayerState st = bp.getState();
if (st != PlayerState.DOWNED && st != PlayerState.HOOKED
&& st != PlayerState.CARRIED && st != PlayerState.DEAD) {
return false; // at least one survivor can still act
}
}
return hasSurvivors; // true if there are survivors and all are incapacitated
}
private int countAliveSurvivors(GameSession session) {
int count = 0;
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
Survivor s = (Survivor) bp;
if (!s.isDeadOrEscaped()) count++;
}
return count;
}
private boolean hasEscapedSurvivor(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) continue;
if (bp.getState() == PlayerState.ESCAPED) return true;
}
return false;
}
private Killer getKiller(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
if (bp.isKiller()) return (Killer) bp;
}
return null;
}
public void announceKillerWin(GameSession session) {
winReason = "killer_win";
Bukkit.broadcast(Component.text("[DBD] Маньяк победил! Все выжившие мертвы!", NamedTextColor.DARK_RED));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (!bp.isKiller() && bp.getState() == PlayerState.DEAD) {
p.setGameMode(GameMode.SPECTATOR);
} else {
p.setGameMode(GameMode.SURVIVAL);
}
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 1.0f, 1.0f);
}
// Teleport handled by EndgamePhase.exit()
}
public void announceSurvivorsWin(GameSession session) {
winReason = "survivors_win";
Bukkit.broadcast(Component.text("[DBD] Выжившие победили!", NamedTextColor.GREEN));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
p.setGameMode(GameMode.SURVIVAL);
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
}
// Teleport handled by EndgamePhase.exit()
}
// Removed scheduleTeleportToLobby — teleport now happens only in EndgamePhase.exit()
}

View file

@ -31,12 +31,14 @@ public class LobbyPhase implements Phase {
@Override
public void enter(GameSession session) {
Bukkit.broadcast(Component.text("[DBD] Лобби открыто! Используйте /dbd join", NamedTextColor.GREEN));
// Телепортируем тех кто уже в сессии (если есть)
var lobbyManager = session.getPlugin().getLobbyManager();
for (var player : session.getPlayers().values()) {
var bukkitPlayer = player.getPlayer();
if (bukkitPlayer != null && bukkitPlayer.isOnline()) {
lobbyManager.teleportToLobby(bukkitPlayer);
var p = player.getPlayer();
if (p != null && p.isOnline()) {
p.setGameMode(org.bukkit.GameMode.ADVENTURE);
lobbyManager.teleportToLobby(p);
p.sendMessage(Component.text("[DBD] Лобби открыто! Ожидание игроков...", NamedTextColor.GREEN));
}
}
}
@ -95,16 +97,11 @@ public class LobbyPhase implements Phase {
}
}
// ActionBar всем
int total = (playerCount >= maxPlayers)
? config.getCountdown5Players()
: config.getCountdown4Players();
String bar = buildCountdownBar(secondsLeft, total);
// ActionBar всем (на русском, без прогресс-бара)
NamedTextColor barColor = (playerCount >= maxPlayers)
? NamedTextColor.GREEN : NamedTextColor.YELLOW;
sendActionBarToAll(session,
Component.text("▶ Старт через: " + secondsLeft + "с " + bar +
" (" + playerCount + "/" + minPlayers + ")", barColor));
Component.text(">>> Начало через: " + secondsLeft + "с (" + playerCount + "/" + minPlayers + ")", barColor));
if (secondsLeft <= 0) {
started = true;
@ -123,7 +120,7 @@ public class LobbyPhase implements Phase {
private String buildCountdownBar(int current, int total) {
int filled = total > 0 ? (int) (10.0 * current / total) : 0;
int empty = 10 - filled;
return "§a" + "█".repeat(Math.max(0, filled)) + "§7" + "░".repeat(Math.max(0, empty));
return "§a" + "#".repeat(Math.max(0, filled)) + "§7" + "-".repeat(Math.max(0, empty));
}
@Override
@ -137,13 +134,18 @@ public class LobbyPhase implements Phase {
@Override
public void onPlayerJoined(GameSession session, Player player) {
player.setGameMode(org.bukkit.GameMode.ADVENTURE);
session.getPlugin().getLobbyManager().teleportToLobby(player);
int playerCount = session.getPlayerCount();
int minPlayers = config.getMinPlayers();
Bukkit.broadcast(Component.text(
"[DBD] " + player.getName() + " присоединился к очереди! (" +
playerCount + "/" + minPlayers + ")",
NamedTextColor.YELLOW));
// Сообщаем только участникам этой сессии, не всему серверу
Component msg = Component.text(
"[DBD] " + player.getName() + " присоединился! (" + playerCount + "/" + minPlayers + ")",
NamedTextColor.YELLOW);
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) p.sendMessage(msg);
}
}
@Override

View file

@ -46,4 +46,9 @@ public abstract class BasePlayer {
}
public abstract boolean isKiller();
/** Check if player is dead or escaped (not participating anymore) */
public boolean isDeadOrEscaped() {
return state == PlayerState.DEAD || state == PlayerState.ESCAPED;
}
}

View file

@ -104,4 +104,22 @@ public class PlayerManager {
}
return nearest;
}
}
public Survivor getNearestSurvivorInState(Player from, PlayerState state, double maxDistance) {
Survivor nearest = null;
double nearestDist = maxDistance;
for (Survivor survivor : getSurvivors()) {
if (survivor.getState() != state) continue;
Player sPlayer = survivor.getPlayer();
if (sPlayer == null || !sPlayer.isOnline()) continue;
double dist = from.getLocation().distance(sPlayer.getLocation());
if (dist < nearestDist) {
nearestDist = dist;
nearest = survivor;
}
}
return nearest;
}
}

View file

@ -22,4 +22,8 @@ public class Survivor extends BasePlayer {
public void incrementHooks() {
hooksTaken++;
}
public void setHooksTaken(int hooksTaken) {
this.hooksTaken = Math.max(0, hooksTaken);
}
}

View file

@ -6,8 +6,10 @@ import dev.loki.core.GameSession;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import dev.loki.player.Survivor;
import dev.loki.util.SpawnUtil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player;
@ -41,28 +43,65 @@ public class RoleAssigner {
List<UUID> uuids = new ArrayList<>(players.keySet());
UUID killerUuid = selectKiller(uuids);
Location killerSpawn = locations.getKillerSpawn();
List<Location> killerSpawns = locations.getKillerSpawns();
List<Location> survivorSpawns = locations.getSurvivorSpawns();
if (killerSpawn == null) {
session.getPlugin().getLogger().warning("[RoleAssigner] killer-spawn not set!");
// Фильтруем null и битые локации
killerSpawns = killerSpawns.stream()
.filter(loc -> loc != null)
.filter(loc -> loc.getWorld() != null)
.collect(java.util.stream.Collectors.toList());
survivorSpawns = survivorSpawns.stream()
.filter(loc -> loc != null)
.filter(loc -> loc.getWorld() != null)
.collect(java.util.stream.Collectors.toList());
if (killerSpawns.isEmpty()) {
session.getPlugin().getLogger().warning("[RoleAssigner] killer-spawn не установлен! Игроки-киллеры будут спавниться в лобби.");
} else {
session.getPlugin().getLogger().info("[RoleAssigner] Загружено " + killerSpawns.size() + " спавнов киллера");
}
if (survivorSpawns.isEmpty()) {
session.getPlugin().getLogger().warning("[RoleAssigner] survivor-spawns not set!");
session.getPlugin().getLogger().warning("[RoleAssigner] survivor-spawns не установлены! Выжившие будут спавниться в лобби.");
} else {
session.getPlugin().getLogger().info("[RoleAssigner] Загружено " + survivorSpawns.size() + " спавнов выживших");
}
int spawnIndex = 0;
// Fix world references to use game world
World rawGameWorld = session.getPlugin().getLobbyManager().getGameWorld();
if (rawGameWorld == null) rawGameWorld = Bukkit.getWorlds().get(0);
final World gameWorld = rawGameWorld;
// Helper to create location with correct world
java.util.function.Function<Location, Location> fixWorld = loc ->
new Location(gameWorld, loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
// Ensure chunks are loaded at spawn locations before teleporting
Random random = new Random();
for (Location spawn : killerSpawns) {
if (spawn != null && spawn.getWorld() != null) {
spawn.getWorld().loadChunk(spawn.getBlockX() >> 4, spawn.getBlockZ() >> 4);
}
}
for (Location spawn : survivorSpawns) {
if (spawn != null && spawn.getWorld() != null) {
spawn.getWorld().loadChunk(spawn.getBlockX() >> 4, spawn.getBlockZ() >> 4);
}
}
for (UUID uuid : uuids) {
BasePlayer newPlayer;
if (uuid.equals(killerUuid)) {
newPlayer = new Killer(uuid);
if (killerSpawn != null) newPlayer.setSpawnLocation(killerSpawn);
if (!killerSpawns.isEmpty()) {
Location spawn = fixWorld.apply(killerSpawns.get(random.nextInt(killerSpawns.size())));
newPlayer.setSpawnLocation(spawn);
}
} else {
newPlayer = new Survivor(uuid);
if (!survivorSpawns.isEmpty()) {
newPlayer.setSpawnLocation(survivorSpawns.get(spawnIndex % survivorSpawns.size()));
spawnIndex++;
Location spawn = fixWorld.apply(survivorSpawns.get(random.nextInt(survivorSpawns.size())));
newPlayer.setSpawnLocation(spawn);
}
}
@ -70,6 +109,8 @@ public class RoleAssigner {
Player bukkit = Bukkit.getPlayer(uuid);
if (bukkit != null && bukkit.isOnline()) {
bukkit.setGameMode(org.bukkit.GameMode.ADVENTURE);
if (newPlayer.isKiller()) {
bukkit.showTitle(net.kyori.adventure.title.Title.title(
net.kyori.adventure.text.Component.text("КИЛЛЕР")
@ -94,14 +135,18 @@ public class RoleAssigner {
applyScale(bukkit, config.getSurvivorScale());
}
Location spawn = newPlayer.getSpawnLocation();
if (spawn != null) {
bukkit.teleport(spawn);
session.getPlugin().getLogger().info("[RoleAssigner] Teleported " + bukkit.getName());
Location spawnLoc = newPlayer.getSpawnLocation();
if (spawnLoc != null) {
Location adjustedSpawn = SpawnUtil.resolveStandingLocation(spawnLoc);
bukkit.teleport(adjustedSpawn);
session.getPlugin().getLogger().info("[RoleAssigner] Teleported " + bukkit.getName() +
" to " + adjustedSpawn.getBlockX() + ", " + adjustedSpawn.getBlockY() + ", " + adjustedSpawn.getBlockZ());
} else {
var gameWorld = session.getPlugin().getLobbyManager().getGameWorld();
session.getPlugin().getLogger().warning("[RoleAssigner] spawnLoc is null for " + bukkit.getName() +
" (role=" + (newPlayer.isKiller() ? "killer" : "survivor") + "), using fallback");
if (gameWorld != null) {
bukkit.teleport(gameWorld.getSpawnLocation());
Location fallback = SpawnUtil.resolveStandingLocation(gameWorld.getSpawnLocation());
bukkit.teleport(fallback);
} else {
session.getPlugin().getLogger().severe(
"[RoleAssigner] Game world is null! Cannot teleport " + bukkit.getName());
@ -118,7 +163,7 @@ public class RoleAssigner {
/** Apply GENERIC_SCALE attribute (Paper 1.20.5+). Silently skips if unavailable. */
private void applyScale(Player player, double scale) {
try {
var attr = player.getAttribute(Attribute.GENERIC_SCALE);
var attr = player.getAttribute(Attribute.SCALE);
if (attr != null) attr.setBaseValue(scale);
} catch (Exception ignored) {}
}

View file

@ -8,6 +8,7 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Player;
import java.util.UUID;
import java.util.function.Function;
/**
* Handles healing another survivor (shift + proximity, 15 sec by default)
@ -26,6 +27,17 @@ public class SurvivorHealing {
// Per-survivor heal counter (tracks heals used this match)
private int healsUsed = 0;
/** Lookup function to find ANY player's Survivor (for healing others). */
private Function<Player, Survivor> survivorLookup;
public void setSurvivorLookup(Function<Player, Survivor> lookup) {
this.survivorLookup = lookup;
}
public boolean hasSurvivorLookup() {
return survivorLookup != null;
}
public SurvivorHealing(SurvivorController controller, GameConfig config) {
this.controller = controller;
this.config = config;
@ -171,12 +183,14 @@ public class SurvivorHealing {
}
private Survivor getSurvivorFromPlayer(Player player) {
// Self: return controller's survivor
if (controller.getPlayer() != null && controller.getPlayer().equals(player)) {
return controller.getSurvivor();
}
// For healing other survivors the target won't be this controller's survivor.
// The target state check is done via the Survivor object that must be passed in.
// This method is used for both self and other.
// Other survivor: use lookup function
if (survivorLookup != null) {
return survivorLookup.apply(player);
}
return null;
}
}

View file

@ -1,52 +0,0 @@
package dev.loki.totem;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import java.util.UUID;
public class DullTotem extends Totem {
private static final String TOTEM_SKULL_TEXTURE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTBiNzE5ZTNlODUwMTRlMDEyZGM1ZTMxNTU2OGIwZjI0N2Y1ZDhkZjE5YjcyOWEyNTEwYmNjY2ViYzdhNmIifX19";
public DullTotem(Location location) {
super(location);
this.state = TotemState.DULL;
}
@Override
public void updateVisuals() {
if (display == null || display.isDead()) return;
display.setCustomName(state.getColorCode() + state.getDisplayName());
display.getEquipment().setHelmet(createTotemItem());
}
private ItemStack createTotemItem() {
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) {
meta.setDisplayName(state.getDisplayName());
skull.setItemMeta(meta);
}
return skull;
}
@Override
public void onInteract(UUID playerId) {
if (state == TotemState.DULL) {
setState(TotemState.CLEANSED);
}
}
public boolean canBecomeHex() {
return state == TotemState.DULL;
}
public void convertToHex(UUID hexPerkId) {
if (canBecomeHex()) {
setState(TotemState.HEX);
setLinkedPerkId(hexPerkId);
}
}
}

View file

@ -1,85 +0,0 @@
package dev.loki.totem;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import java.util.UUID;
public class HexTotem extends Totem {
private final String hexName;
private boolean active;
public HexTotem(Location location, String hexName) {
super(location);
this.state = TotemState.HEX;
this.hexName = hexName;
this.active = true;
}
@Override
public void spawn() {
super.spawn();
startParticleEffect();
}
@Override
public void updateVisuals() {
if (display == null || display.isDead()) return;
String name = state.getColorCode() + "Порча: " + hexName;
display.setCustomName(name);
display.getEquipment().setHelmet(createHexItem());
}
private ItemStack createHexItem() {
ItemStack skull = new ItemStack(Material.WITHER_SKELETON_SKULL);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) {
meta.setDisplayName("§cHex Totem");
skull.setItemMeta(meta);
}
return skull;
}
private void startParticleEffect() {
if (location.getWorld() == null) return;
location.getWorld().spawnParticle(Particle.SOUL_FIRE_FLAME,
location.clone().add(0, 1.5, 0),
2, 0.3, 0.5, 0.3, 0.01);
}
@Override
public void onInteract(UUID playerId) {
if (state == TotemState.HEX && active) {
cleanse(playerId);
}
}
public void cleanse(UUID playerId) {
active = false;
setState(TotemState.CLEANSED);
spawnCleanseEffect();
}
private void spawnCleanseEffect() {
if (location.getWorld() == null) return;
location.getWorld().spawnParticle(Particle.END_ROD,
location.clone().add(0, 1, 0),
20, 0.5, 0.5, 0.5, 0.1);
location.getWorld().playSound(location, org.bukkit.Sound.BLOCK_BEACON_DEACTIVATE, 1.0f, 0.5f);
}
public boolean isActive() {
return active;
}
public String getHexName() {
return hexName;
}
public Class<?> getHexType() {
return null;
}
}

View file

@ -1,86 +0,0 @@
package dev.loki.totem;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.EntityType;
import java.util.UUID;
public abstract class Totem {
protected final UUID id;
protected final Location location;
protected ArmorStand display;
protected TotemState state;
protected UUID linkedPerkId;
public Totem(Location location) {
this.id = UUID.randomUUID();
this.location = location;
this.state = TotemState.DULL;
this.linkedPerkId = null;
}
public void spawn() {
if (display != null && !display.isDead()) {
return;
}
display = (ArmorStand) location.getWorld().spawnEntity(location, EntityType.ARMOR_STAND);
display.setVisible(false);
display.setGravity(false);
display.setSmall(true);
display.setMarker(true);
display.setCustomNameVisible(true);
updateVisuals();
}
public void remove() {
if (display != null && !display.isDead()) {
display.remove();
}
}
public abstract void updateVisuals();
public abstract void onInteract(UUID playerId);
public UUID getId() {
return id;
}
public Location getLocation() {
return location;
}
public TotemState getState() {
return state;
}
public void setState(TotemState state) {
this.state = state;
updateVisuals();
}
public UUID getLinkedPerkId() {
return linkedPerkId;
}
public void setLinkedPerkId(UUID linkedPerkId) {
this.linkedPerkId = linkedPerkId;
}
public boolean isHexTotem() {
return state == TotemState.HEX;
}
public boolean isBlessedTotem() {
return state == TotemState.BLESSED;
}
public boolean isCleansed() {
return state == TotemState.CLEANSED;
}
public double distanceTo(Location other) {
return location.distance(other);
}
}

View file

@ -1,127 +0,0 @@
package dev.loki.totem;
import org.bukkit.Location;
import java.util.*;
public class TotemManager {
private static final int DEFAULT_TOTEM_COUNT = 5;
private final List<Totem> totems = new ArrayList<>();
private final Map<UUID, Totem> totemById = new HashMap<>();
public void initializeTotems(List<Location> locations) {
clearTotems();
int count = Math.min(locations.size(), DEFAULT_TOTEM_COUNT);
for (int i = 0; i < count; i++) {
DullTotem totem = new DullTotem(locations.get(i));
addTotem(totem);
totem.spawn();
}
}
public void addTotem(Totem totem) {
totems.add(totem);
totemById.put(totem.getId(), totem);
}
public void removeTotem(UUID id) {
Totem totem = totemById.remove(id);
if (totem != null) {
totem.remove();
totems.remove(totem);
}
}
public void clearTotems() {
for (Totem totem : totems) {
totem.remove();
}
totems.clear();
totemById.clear();
}
public Totem getTotem(UUID id) {
return totemById.get(id);
}
public Totem getNearestTotem(Location location) {
Totem nearest = null;
double minDist = Double.MAX_VALUE;
for (Totem totem : totems) {
double dist = totem.distanceTo(location);
if (dist < minDist) {
minDist = dist;
nearest = totem;
}
}
return nearest;
}
public Totem getNearestDullTotem(Location location) {
Totem nearest = null;
double minDist = Double.MAX_VALUE;
for (Totem totem : totems) {
if (totem.getState() != TotemState.DULL) continue;
double dist = totem.distanceTo(location);
if (dist < minDist) {
minDist = dist;
nearest = totem;
}
}
return nearest;
}
public List<Totem> getAllTotems() {
return new ArrayList<>(totems);
}
public List<Totem> getHexTotems() {
List<Totem> hexTotems = new ArrayList<>();
for (Totem totem : totems) {
if (totem.isHexTotem()) {
hexTotems.add(totem);
}
}
return hexTotems;
}
public List<Totem> getCleansedTotems() {
List<Totem> cleansed = new ArrayList<>();
for (Totem totem : totems) {
if (totem.isCleansed()) {
cleansed.add(totem);
}
}
return cleansed;
}
public int getActiveHexCount() {
int count = 0;
for (Totem totem : totems) {
if (totem.isHexTotem() && totem instanceof HexTotem hex && hex.isActive()) {
count++;
}
}
return count;
}
public boolean hasActiveHexes() {
return getActiveHexCount() > 0;
}
public void convertToHex(UUID totemId, String hexName) {
Totem totem = totemById.get(totemId);
if (totem instanceof DullTotem dull && dull.canBecomeHex()) {
Location loc = totem.getLocation();
removeTotem(totemId);
HexTotem hexTotem = new HexTotem(loc, hexName);
hexTotem.setLinkedPerkId(totemId);
addTotem(hexTotem);
hexTotem.spawn();
}
}
public static int getDefaultTotemCount() {
return DEFAULT_TOTEM_COUNT;
}
}

View file

@ -1,24 +0,0 @@
package dev.loki.totem;
public enum TotemState {
DULL("Тусклый тотем", "§7"),
HEX("§cПроклятый тотем", "§c"),
BLESSED("§dБлагословенный тотем", "§d"),
CLEANSED("Очищенный тотем", "§a");
private final String displayName;
private final String colorCode;
TotemState(String displayName, String colorCode) {
this.displayName = displayName;
this.colorCode = colorCode;
}
public String getDisplayName() {
return displayName;
}
public String getColorCode() {
return colorCode;
}
}

View file

@ -8,11 +8,11 @@ import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -20,11 +20,17 @@ import java.util.UUID;
/**
* Manages the sidebar scoreboard for each player.
* Displays: generator progress, survivor status (with hearts), endgame timer.
*
* Optimization: Uses separate Objective for timer to prevent fade-in animation
* on every update. Caches last displayed second per player to update only on change.
*/
public class Sidebar {
private static final String OBJECTIVE_NAME = "dbd_stats";
private static final String TIMER_OBJECTIVE_NAME = "dbd_timer";
private final Map<UUID, Scoreboard> scoreboards = new HashMap<>();
private final Map<UUID, Integer> lastDisplayedSeconds = new HashMap<>();
private final Map<UUID, Integer> lastTimerSeconds = new HashMap<>();
// Endgame timer (seconds), -1 = not active
private int endgameSecondsLeft = -1;
@ -41,8 +47,18 @@ public class Sidebar {
);
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
// Create separate timer objective for endgame (shown during collapse only)
Objective timerObjective = board.registerNewObjective(
TIMER_OBJECTIVE_NAME,
Criteria.DUMMY,
Component.text("⏱ Эндгейм").color(NamedTextColor.GOLD)
);
// Don't set DisplaySlot yet - it will be shown when endgame starts
player.setScoreboard(board);
scoreboards.put(player.getUniqueId(), board);
lastDisplayedSeconds.put(player.getUniqueId(), -1);
lastTimerSeconds.put(player.getUniqueId(), -1);
}
/** Set/update the endgame countdown. Pass -1 to disable. */
@ -50,6 +66,73 @@ public class Sidebar {
this.endgameSecondsLeft = seconds;
}
/**
* Update only the endgame timer objective without reloading the entire sidebar.
* Keeps a persistent entry that only gets its score value updated.
* This prevents the entry from disappearing and reappearing each second.
*/
public void updateEndgameTimerOnly(Player player, int seconds) {
UUID playerId = player.getUniqueId();
// Cache check: only update if seconds actually changed for this player
Integer lastSeconds = lastTimerSeconds.get(playerId);
if (lastSeconds != null && lastSeconds == seconds) {
return; // No change, skip update
}
Scoreboard board = scoreboards.get(playerId);
if (board == null) return;
Objective timerObj = board.getObjective(TIMER_OBJECTIVE_NAME);
if (timerObj == null) return;
// Make sure the timer objective is displayed
if (timerObj.getDisplaySlot() == null) {
timerObj.setDisplaySlot(DisplaySlot.SIDEBAR);
}
// Clear ALL old entries in this objective first time only
if (lastSeconds == null) {
for (String entry : new ArrayList<>(board.getEntries())) {
if (timerObj.getScore(entry).getScore() == 0 || entry.contains("⏱") || entry.contains("До конца")) {
board.resetScores(entry);
}
}
}
// Use a FIXED entry key that never changes - only the score updates
String timerEntry = "§6⏱ До конца: §c" + seconds + " сек";
// Simply update the score - the entry persists
Score score = timerObj.getScore(timerEntry);
score.setScore(1);
// Cache the displayed value
lastTimerSeconds.put(playerId, seconds);
}
/**
* Disable the timer objective display (called when endgame ends).
*/
public void disableTimerObjective(Player player) {
UUID playerId = player.getUniqueId();
Scoreboard board = scoreboards.get(playerId);
if (board == null) return;
Objective timerObj = board.getObjective(TIMER_OBJECTIVE_NAME);
if (timerObj == null) return;
// Clear entries and hide
for (String entry : board.getEntries()) {
if (entry.contains("⏱") || entry.contains("До конца")) {
board.resetScores(entry);
}
}
timerObj.setDisplaySlot(null);
lastTimerSeconds.put(playerId, -1);
}
public void updateGameStats(Player player, GameSession session) {
updateGameStats(player, session, null);
}
@ -96,7 +179,15 @@ public class Sidebar {
String name = survPlayer != null ? survPlayer.getName() : "???";
String colorCode = getColorCode(surv.getState(), cfg);
String hearts = buildHearts(surv.getHooksTaken(), cfg.getMaxHooks(), cfg);
// Special handling for CARRIED state — don't show hearts, show that they're being carried
String hearts;
if (surv.getState() == PlayerState.CARRIED) {
hearts = "§5🎒"; // Backpack icon to show they're being carried
} else {
hearts = buildHearts(surv.getHooksTaken(), cfg.getMaxHooks(), cfg);
}
String statusIcon = getStatusIcon(surv.getState());
setScore(objective, " " + colorCode + name + " " + hearts + " " + statusIcon, line--);
@ -202,7 +293,10 @@ public class Sidebar {
}
public void removePlayer(Player player) {
Scoreboard board = scoreboards.remove(player.getUniqueId());
UUID playerId = player.getUniqueId();
Scoreboard board = scoreboards.remove(playerId);
lastDisplayedSeconds.remove(playerId);
lastTimerSeconds.remove(playerId);
if (board != null) {
ScoreboardManager manager = Bukkit.getScoreboardManager();
if (manager != null) {
@ -220,5 +314,7 @@ public class Sidebar {
}
}
scoreboards.clear();
lastDisplayedSeconds.clear();
lastTimerSeconds.clear();
}
}

View file

@ -0,0 +1,64 @@
package dev.loki.util;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
/**
* Resolves spawn locations so players stand on top of solid ground.
*/
public final class SpawnUtil {
private SpawnUtil() {}
/**
* Places the player on the highest solid block at X/Z.
* Handles both configured Y (if on solid ground) and fallback to world's highest block.
* Prevents spawning in air by ensuring player stands on solid block.
*/
public static Location resolveStandingLocation(Location raw) {
if (raw == null || raw.getWorld() == null) return raw;
Location loc = raw.clone();
World world = loc.getWorld();
int x = loc.getBlockX();
int z = loc.getBlockZ();
int y = (int) Math.floor(loc.getY());
// Check if there's solid ground below the configured Y position
Block below = world.getBlockAt(x, y - 1, z);
if (!below.getType().isAir() && below.getType().isSolid()) {
// Ground is solid at configured Y — use it as is
loc.setX(x + 0.5);
loc.setZ(z + 0.5);
loc.setY(y + 1.0);
return loc;
}
// No solid ground at configured Y — find the actual highest solid block
int highest = world.getHighestBlockYAt(x, z);
// Fallback: если чанк не загружен или void, используем сконфигурированную Y
if (!world.isChunkLoaded(x >> 4, z >> 4) || highest <= world.getMinHeight()) {
highest = Math.max((int) raw.getY(), 64);
}
// Ensure we're standing on top of the highest block (not inside it)
// getHighestBlockYAt returns the Y of the block, so we add 1 to stand on top
// Use world.getMinHeight() to support negative Y coordinates
int spawnY = Math.max(world.getMinHeight() + 1, highest + 1);
loc.setX(x + 0.5);
loc.setZ(z + 0.5);
loc.setY((double) spawnY);
return loc;
}
/** Center of broken block, standing on top (setup mode). */
public static Location blockBreakSpawn(Location blockLoc, float yaw) {
Location loc = blockLoc.clone().add(0.5, 1.0, 0.5);
loc.setYaw(yaw);
loc.setPitch(0f);
return loc;
}
}

View file

@ -1,72 +0,0 @@
package dev.loki.window;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
public class Window {
private final Location bottomLocation;
private final Location topLocation;
private boolean blocked;
public Window(Location bottomLocation) {
this.bottomLocation = bottomLocation;
this.topLocation = bottomLocation.clone().add(0, 1, 0);
this.blocked = false;
}
public Location getBottomLocation() {
return bottomLocation;
}
public Location getTopLocation() {
return topLocation;
}
public Location getVaultTarget(boolean killerVault) {
if (killerVault) {
return bottomLocation.clone().add(0, -0.5, 0);
}
return topLocation.clone().add(0, 0.5, 0);
}
public boolean isBlocked() {
return blocked;
}
public void setBlocked(boolean blocked) {
this.blocked = blocked;
}
public boolean isValid() {
Block bottom = bottomLocation.getBlock();
Block top = topLocation.getBlock();
return bottom.getType().isSolid() || bottom.getType() == Material.AIR;
}
public boolean isVaultable(PlayerContext ctx) {
if (blocked && !ctx.isKiller()) return false;
if (!isValid()) return false;
Location playerLoc = ctx.getLocation();
return getDistance(playerLoc) <= 2.5;
}
public double getDistance(Location location) {
return bottomLocation.distance(location);
}
public void reset() {
this.blocked = false;
}
public record PlayerContext(boolean isKiller, Location location) {
public boolean isKiller() {
return isKiller;
}
public Location getLocation() {
return location;
}
}
}

View file

@ -1,120 +0,0 @@
package dev.loki.window;
import dev.loki.player.BasePlayer;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class WindowFastVault {
private static final double FAST_VAULT_TIME = 1.0;
private static final double SLOW_VAULT_TIME = 1.5;
private static final long VAULT_COOLDOWN_MS = 3000;
private final Map<UUID, Long> lastVaultTimes;
private final Map<UUID, Integer> vaultCounts;
public WindowFastVault() {
this.lastVaultTimes = new HashMap<>();
this.vaultCounts = new HashMap<>();
}
public double getVaultTime(BasePlayer player, Window window, boolean isChasing) {
if (player.isKiller()) {
return 1.5;
}
if (isFastVaultEligible(player, isChasing)) {
return FAST_VAULT_TIME;
}
return SLOW_VAULT_TIME;
}
private boolean isFastVaultEligible(BasePlayer player, boolean isChasing) {
UUID id = player.getUuid();
long now = System.currentTimeMillis();
Long lastVault = lastVaultTimes.get(id);
if (lastVault == null) {
return true;
}
if (now - lastVault > VAULT_COOLDOWN_MS) {
vaultCounts.remove(id);
return true;
}
int count = vaultCounts.getOrDefault(id, 0);
return count < 2 && isChasing;
}
public void recordVault(BasePlayer player) {
UUID id = player.getUuid();
long now = System.currentTimeMillis();
lastVaultTimes.put(id, now);
vaultCounts.merge(id, 1, Integer::sum);
}
public void resetForPlayer(BasePlayer player) {
UUID id = player.getUuid();
lastVaultTimes.remove(id);
vaultCounts.remove(id);
}
public void resetAll() {
lastVaultTimes.clear();
vaultCounts.clear();
}
public boolean performFastVault(BasePlayer player, Window window,
org.bukkit.plugin.Plugin plugin, boolean isChasing) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return false;
double vaultTime = getVaultTime(player, window, isChasing);
int ticks = (int) (vaultTime * 20);
Location target = window.getVaultTarget(false);
bukkitPlayer.sendMessage(vaultTime <= FAST_VAULT_TIME
? "§aБыстрый волт!"
: "§7Медленный волт...");
new FastVaultTask(player, target, vaultTime, this, plugin).runTaskLater(plugin, ticks);
return true;
}
private static class FastVaultTask extends BukkitRunnable {
private final BasePlayer player;
private final Location target;
private final double vaultTime;
private final WindowFastVault vaultSystem;
FastVaultTask(BasePlayer player, Location target, double vaultTime,
WindowFastVault vaultSystem, org.bukkit.plugin.Plugin plugin) {
this.player = player;
this.target = target;
this.vaultTime = vaultTime;
this.vaultSystem = vaultSystem;
}
@Override
public void run() {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return;
bukkitPlayer.teleport(target);
bukkitPlayer.getWorld().playSound(target, Sound.BLOCK_LADDER_STEP, 0.5f, 0.8f);
vaultSystem.recordVault(player);
if (vaultTime > FAST_VAULT_TIME) {
bukkitPlayer.sendMessage("§cВы устали — следующий волт будет медленным!");
}
}
}
}

View file

@ -1,82 +0,0 @@
package dev.loki.window;
import dev.loki.config.LocationsConfig;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public class WindowManager {
private final List<Window> windows;
public WindowManager(LocationsConfig locations) {
this.windows = new ArrayList<>();
loadWindows(locations);
}
private void loadWindows(LocationsConfig locations) {
for (Location loc : locations.getWindows()) {
windows.add(new Window(loc));
}
}
public void addWindow(Window window) {
windows.add(window);
}
public void removeWindow(Window window) {
windows.remove(window);
}
public List<Window> getAllWindows() {
return new ArrayList<>(windows);
}
public Window findNearestWindow(Location location, double maxRadius) {
Window nearest = null;
double minDistance = maxRadius;
for (Window window : windows) {
double dist = window.getDistance(location);
if (dist < minDistance) {
minDistance = dist;
nearest = window;
}
}
return nearest;
}
public Window findVaultableWindow(Window.PlayerContext ctx, double maxRadius) {
Window nearest = null;
double minDistance = maxRadius;
for (Window window : windows) {
if (!window.isVaultable(ctx)) continue;
double dist = window.getDistance(ctx.getLocation());
if (dist < minDistance) {
minDistance = dist;
nearest = window;
}
}
return nearest;
}
public void resetAll() {
for (Window window : windows) {
window.reset();
}
}
public int getTotalCount() {
return windows.size();
}
public int getValidCount() {
return (int) windows.stream().filter(Window::isValid).count();
}
public void clearInvalid() {
windows.removeIf(w -> !w.isValid());
}
}

View file

@ -1,112 +0,0 @@
package dev.loki.window;
import dev.loki.player.BasePlayer;
import dev.loki.player.Killer;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class WindowVault {
private final WindowManager windowManager;
private final Set<UUID> vaultingPlayers;
public WindowVault(WindowManager windowManager) {
this.windowManager = windowManager;
this.vaultingPlayers = new HashSet<>();
}
public boolean tryVault(BasePlayer player, org.bukkit.plugin.Plugin plugin) {
if (isVaulting(player)) return false;
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return false;
Window.PlayerContext ctx = new Window.PlayerContext(
player.isKiller(),
bukkitPlayer.getLocation()
);
Window window = windowManager.findVaultableWindow(ctx, 3.0);
if (window == null) {
bukkitPlayer.sendMessage("§cНет окна поблизости!");
return false;
}
double vaultTime = player.isKiller() ? 1.5 : getSurvivorVaultTime(player, window);
performVault(player, window, vaultTime, plugin);
return true;
}
public boolean isVaulting(BasePlayer player) {
return vaultingPlayers.contains(player.getUuid());
}
private double getSurvivorVaultTime(BasePlayer player, Window window) {
if (isFastVaultAvailable(player, window)) {
return 1.0;
}
return 1.5;
}
private boolean isFastVaultAvailable(BasePlayer player, Window window) {
return !hasRecentVault(player.getUuid());
}
private boolean hasRecentVault(UUID playerId) {
return false;
}
private void performVault(BasePlayer player, Window window, double seconds, org.bukkit.plugin.Plugin plugin) {
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return;
vaultingPlayers.add(player.getUuid());
int ticks = (int) (seconds * 20);
Location target = window.getVaultTarget(player.isKiller());
bukkitPlayer.addPotionEffect(new PotionEffect(
PotionEffectType.SLOWNESS, ticks, 10, false, false));
bukkitPlayer.addPotionEffect(new PotionEffect(
PotionEffectType.JUMP_BOOST, ticks, -10, false, false));
bukkitPlayer.getWorld().playSound(bukkitPlayer.getLocation(),
Sound.BLOCK_LADDER_STEP, 0.5f, 1.0f);
new VaultTask(player, target, plugin).runTaskLater(plugin, ticks);
}
private void completeVault(BasePlayer player, Location target) {
vaultingPlayers.remove(player.getUuid());
Player bukkitPlayer = player.getPlayer();
if (bukkitPlayer == null) return;
bukkitPlayer.teleport(target);
bukkitPlayer.getWorld().playSound(target, Sound.BLOCK_LADDER_STEP, 0.5f, 0.8f);
bukkitPlayer.removePotionEffect(PotionEffectType.SLOWNESS);
bukkitPlayer.removePotionEffect(PotionEffectType.JUMP_BOOST);
}
private class VaultTask extends BukkitRunnable {
private final BasePlayer player;
private final Location target;
VaultTask(BasePlayer player, Location target, org.bukkit.plugin.Plugin plugin) {
this.player = player;
this.target = target;
}
@Override
public void run() {
completeVault(player, target);
}
}
}

View file

@ -1,154 +1,69 @@
package dev.loki.world;
import dev.loki.mdbd;
import dev.loki.util.SpawnUtil;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import java.io.File;
public class LobbyManager {
private final mdbd plugin;
private World lobbyWorld;
private World gameWorld;
public LobbyManager(mdbd plugin) {
this.plugin = plugin;
}
public void createLobbyWorld() {
World existing = Bukkit.getWorld("lobby");
if (existing != null) {
lobbyWorld = existing;
plugin.getLogger().info("Lobby world already loaded, using existing");
return;
}
boolean isNew = !worldFolderExists("lobby");
WorldCreator creator = new WorldCreator("lobby");
creator.generator(new VoidWorldGenerator());
creator.environment(World.Environment.NORMAL);
creator.generateStructures(false);
lobbyWorld = creator.createWorld();
if (lobbyWorld == null) {
plugin.getLogger().severe("Failed to create lobby world!");
return;
}
lobbyWorld.setAutoSave(false);
lobbyWorld.setMonsterSpawnLimit(0);
lobbyWorld.setAnimalSpawnLimit(0);
lobbyWorld.setGameRule(GameRule.DO_MOB_SPAWNING, false);
lobbyWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
lobbyWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
lobbyWorld.setGameRule(GameRule.KEEP_INVENTORY, true);
lobbyWorld.setGameRule(GameRule.DO_FIRE_TICK, false);
lobbyWorld.setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
lobbyWorld.setTime(6000);
if (isNew) {
buildPlatform();
}
lobbyWorld.setSpawnLocation(0, 65, 0);
if (isNew) {
plugin.getLogger().info("Lobby world created with stone platform!");
} else {
plugin.getLogger().info("Lobby world loaded (platform already exists)");
}
}
public void createGameWorld() {
World existing = Bukkit.getWorld("game");
if (existing != null) {
gameWorld = existing;
plugin.getLogger().info("Game world already loaded, using existing");
return;
}
WorldCreator creator = new WorldCreator("game");
creator.environment(World.Environment.NORMAL);
creator.generateStructures(false);
gameWorld = creator.createWorld();
public void loadWorlds() {
// Only game world — lobby is a location inside it
gameWorld = Bukkit.getWorld("game");
if (gameWorld == null) {
plugin.getLogger().severe("Failed to create game world!");
return;
}
gameWorld.setAutoSave(false);
gameWorld.setMonsterSpawnLimit(0);
gameWorld.setAnimalSpawnLimit(0);
gameWorld.setGameRule(GameRule.DO_MOB_SPAWNING, false);
gameWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
gameWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
gameWorld.setGameRule(GameRule.KEEP_INVENTORY, true);
gameWorld.setGameRule(GameRule.DO_FIRE_TICK, false);
gameWorld.setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
gameWorld.setTime(6000);
if (worldFolderExists("game")) {
plugin.getLogger().info("Game world loaded");
} else {
plugin.getLogger().info("Game world created!");
}
}
private boolean worldFolderExists(String worldName) {
File worldDir = new File(Bukkit.getWorldContainer(), worldName);
return worldDir.isDirectory();
}
private void buildPlatform() {
int y = 64;
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
lobbyWorld.getBlockAt(x, y, z).setType(Material.STONE);
WorldCreator creator = new WorldCreator("game");
creator.environment(World.Environment.NORMAL);
creator.generateStructures(false);
gameWorld = creator.createWorld();
if (gameWorld != null) {
plugin.getLogger().info("Game world created");
} else {
plugin.getLogger().severe("Failed to create game world!");
}
}
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
lobbyWorld.getBlockAt(x, y - 1, z).setType(Material.BLACK_STAINED_GLASS);
}
if (gameWorld != null) {
applyWorldSettings(gameWorld);
}
for (int x = -4; x <= 4; x++) {
lobbyWorld.getBlockAt(x, y, -4).setType(Material.OAK_FENCE);
lobbyWorld.getBlockAt(x, y, 4).setType(Material.OAK_FENCE);
}
for (int z = -4; z <= 4; z++) {
lobbyWorld.getBlockAt(-4, y, z).setType(Material.OAK_FENCE);
lobbyWorld.getBlockAt(4, y, z).setType(Material.OAK_FENCE);
}
lobbyWorld.getBlockAt(0, y + 1, 0).setType(Material.GLOWSTONE);
}
public World getLobbyWorld() {
return lobbyWorld;
private void applyWorldSettings(World world) {
world.setAutoSave(false);
world.setMonsterSpawnLimit(0);
world.setAnimalSpawnLimit(0);
world.setGameRule(GameRule.DO_MOB_SPAWNING, false);
world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
world.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
world.setGameRule(GameRule.KEEP_INVENTORY, true);
world.setGameRule(GameRule.DO_FIRE_TICK, false);
world.setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
}
public World getGameWorld() {
return gameWorld;
}
/**
* Teleport player to the lobby location (inside game world).
* Uses configured lobby point from /dbd setlobby, or world spawn as fallback.
*/
public void teleportToLobby(org.bukkit.entity.Player player) {
if (lobbyWorld != null) {
// Use the configured lobby location from locations.yml if available,
// otherwise fall back to spawn location on the platform
org.bukkit.Location configLobby = plugin.getConfigManager().getLocations().getLobby();
if (configLobby != null && configLobby.getWorld() != null) {
player.teleport(configLobby);
} else {
player.teleport(lobbyWorld.getSpawnLocation());
}
player.setGameMode(org.bukkit.GameMode.ADVENTURE);
org.bukkit.Location configLobby = plugin.getConfigManager().getLocations().getLobby();
if (configLobby != null && configLobby.getWorld() != null) {
// Remap lobby location to game world (fixes world mismatch)
configLobby = configLobby.clone();
configLobby.setWorld(gameWorld);
player.teleport(SpawnUtil.resolveStandingLocation(configLobby));
} else if (gameWorld != null) {
player.teleport(SpawnUtil.resolveStandingLocation(gameWorld.getSpawnLocation()));
}
}
}

View file

@ -0,0 +1,367 @@
package dev.loki.world;
import dev.loki.core.GameManager;
import dev.loki.core.GameSession;
import dev.loki.mdbd;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.*;
public class LobbyRegistry {
private static final String FILE_NAME = "lobbies.yml";
private final mdbd plugin;
private final GameManager gameManager;
/** name (lower-case) → LobbySession */
private final Map<String, LobbySession> lobbies = new LinkedHashMap<>();
/** name → backing GameSession */
private final Map<String, GameSession> sessions = new HashMap<>();
/** ownerUUID → lobby name — лимит 1 лобби на игрока */
private final Map<UUID, String> ownerToLobby = new HashMap<>();
private File dataFile;
public LobbyRegistry(mdbd plugin, GameManager gameManager) {
this.plugin = plugin;
this.gameManager = gameManager;
}
// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------
public void load() {
dataFile = new File(plugin.getDataFolder(), FILE_NAME);
if (!dataFile.exists()) return;
boolean persist = plugin.getConfigManager().getGame().isPersistLobbies();
FileConfiguration cfg = YamlConfiguration.loadConfiguration(dataFile);
var section = cfg.getConfigurationSection("lobbies");
if (section == null) return;
for (String key : section.getKeys(false)) {
String name = section.getString(key + ".name", key);
String ownerStr = section.getString(key + ".owner", null);
String password = section.getString(key + ".password", null);
boolean priv = section.getBoolean(key + ".private", false);
int maxPlayers = section.getInt(key + ".max-players", 5);
UUID ownerUuid = parseUUID(ownerStr);
if (ownerUuid == null) continue;
String lobbyKey = name.toLowerCase();
LobbySession ls = new LobbySession(name, ownerUuid, password, priv, maxPlayers);
lobbies.put(lobbyKey, ls);
ownerToLobby.put(ownerUuid, lobbyKey);
if (persist) {
recreateSession(lobbyKey, ls.getName());
}
}
plugin.getLogger().info("[LobbyRegistry] Loaded " + lobbies.size() + " lobbies.");
}
private void recreateSession(String key, String displayName) {
GameSession session = gameManager.createNamedGame(key);
if (session == null) {
plugin.getLogger().warning("[LobbyRegistry] Could not restore session for: " + displayName);
return;
}
session.setOnStopped(() -> onSessionFinished(displayName));
sessions.put(key, session);
}
public void save() {
if (dataFile == null) dataFile = new File(plugin.getDataFolder(), FILE_NAME);
FileConfiguration cfg = new YamlConfiguration();
int i = 0;
for (LobbySession ls : lobbies.values()) {
String base = "lobbies." + i++;
cfg.set(base + ".name", ls.getName());
cfg.set(base + ".owner", ls.getOwnerUuid().toString());
cfg.set(base + ".password", ls.getPassword());
cfg.set(base + ".private", ls.isPrivate());
cfg.set(base + ".max-players", ls.getMaxPlayers());
}
try { cfg.save(dataFile); }
catch (IOException e) {
plugin.getLogger().warning("[LobbyRegistry] Could not save: " + e.getMessage());
}
}
// -------------------------------------------------------------------------
// Player commands
// -------------------------------------------------------------------------
/** /dbd create <name> [password] [private] */
public boolean createLobby(Player player, String[] args) {
if (args.length < 1) {
player.sendMessage(Component.text(
"[DBD] Использование: /dbd create <имя> [пароль] [private]", NamedTextColor.RED));
return false;
}
// Лимит: 1 лобби на игрока
if (ownerToLobby.containsKey(player.getUniqueId())) {
player.sendMessage(Component.text(
"[DBD] У вас уже есть лобби «" + ownerToLobby.get(player.getUniqueId()) +
"». Удалите его командой /dbd delete <имя>.", NamedTextColor.RED));
return false;
}
// Check server lobby capacity
int serverLobbyCount = plugin.getConfigManager().getGame().getServerLobbyCount();
if (lobbies.size() >= serverLobbyCount) {
player.sendMessage(Component.text("[DBD] Достигнут лимит лобби на сервере (" + serverLobbyCount + ").", NamedTextColor.RED));
return false;
}
String name = args[0].toLowerCase();
if (name.length() > 24) {
player.sendMessage(Component.text("[DBD] Имя лобби слишком длинное (макс. 24).", NamedTextColor.RED));
return false;
}
if (!name.matches("[a-z0-9_\\-]+")) {
player.sendMessage(Component.text(
"[DBD] Имя может содержать только a-z, 0-9, _ и -", NamedTextColor.RED));
return false;
}
if (lobbies.containsKey(name)) {
player.sendMessage(Component.text("[DBD] Лобби с таким именем уже существует.", NamedTextColor.RED));
return false;
}
String password = (args.length >= 2 && !args[1].equalsIgnoreCase("private")) ? args[1] : null;
boolean priv = (args.length >= 2 && args[args.length - 1].equalsIgnoreCase("private"));
int maxPlayers = plugin.getConfigManager().getGame().getMaxPlayers();
LobbySession ls = new LobbySession(args[0], player.getUniqueId(), password, priv, maxPlayers);
lobbies.put(name, ls);
ownerToLobby.put(player.getUniqueId(), name);
GameSession session = gameManager.createNamedGame(name);
if (session == null) {
lobbies.remove(name);
ownerToLobby.remove(player.getUniqueId());
// Log why it failed — likely a stale session with the same name key
GameSession stale = gameManager.getNamedSession(name);
if (stale != null) {
plugin.getLogger().warning("[LobbyRegistry] createNamedGame returned null for '" + name +
"' because a session with that key already exists (stale session? phase=" +
stale.getPhaseName() + ", players=" + stale.getPlayerCount() + ")");
} else {
plugin.getLogger().warning("[LobbyRegistry] createNamedGame returned null for '" + name +
"' — GameSession constructor failed or start() threw an exception");
}
player.sendMessage(Component.text("[DBD] Не удалось создать сессию.", NamedTextColor.RED));
return false;
}
// Регистрируем callback: пересоздаём сессию после завершения игры
session.setOnStopped(() -> onSessionFinished(name));
sessions.put(name, session);
// Автоматически добавляем создателя в лобби
gameManager.joinSession(player, session);
save();
// Только создателю — без broadcast
player.sendMessage(Component.text(
"[DBD] Лобби §f" + args[0] + "§a создано!" +
(password != null ? " (пароль: " + password + ")" : ""),
NamedTextColor.GREEN));
return true;
}
/** /dbd join <name> [password] */
public boolean joinLobby(Player player, String[] args) {
if (args.length < 1) {
player.sendMessage(Component.text(
"[DBD] Использование: /dbd join <имя> [пароль]", NamedTextColor.RED));
return false;
}
String name = args[0].toLowerCase();
LobbySession ls = lobbies.get(name);
if (ls == null) {
player.sendMessage(Component.text("[DBD] Лобби «" + args[0] + "» не найдено.", NamedTextColor.RED));
return false;
}
if (ls.isPrivate() && !player.getUniqueId().equals(ls.getOwnerUuid())
&& !player.hasPermission("mdbd.admin")) {
player.sendMessage(Component.text("[DBD] Это лобби приватное.", NamedTextColor.RED));
return false;
}
if (ls.hasPassword()) {
String provided = args.length >= 2 ? args[1] : "";
if (!ls.checkPassword(provided)) {
player.sendMessage(Component.text("[DBD] Неверный пароль.", NamedTextColor.RED));
return false;
}
}
GameSession session = sessions.get(name);
if (session == null) {
recreateSession(name, ls.getName());
session = sessions.get(name);
}
if (session == null) {
player.sendMessage(Component.text(
"[DBD] Сессия не найдена. Попросите создателя удалить и пересоздать лобби.",
NamedTextColor.RED));
return false;
}
GameSession current = gameManager.getPlayerSession(player.getUniqueId());
if (current == session) {
player.sendMessage(Component.text(
"[DBD] Вы уже в лобби §f" + ls.getName(), NamedTextColor.YELLOW));
return true;
}
// Если игрок в другой сессии, сначала выходим оттуда
if (current != null) {
gameManager.leaveGame(player);
}
if (gameManager.joinSession(player, session)) {
player.sendMessage(Component.text("[DBD] Вы вошли в лобби §f" + ls.getName(), NamedTextColor.GREEN));
return true;
}
player.sendMessage(Component.text(
"[DBD] Не удалось войти (возможно, игра уже идёт или вы уже в другом лобби).",
NamedTextColor.RED));
return false;
}
/** List public lobbies. */
public void listLobbies(Player player) {
List<LobbySession> visible = lobbies.values().stream()
.filter(ls -> !ls.isPrivate()
|| player.getUniqueId().equals(ls.getOwnerUuid())
|| player.hasPermission("mdbd.admin"))
.toList();
if (visible.isEmpty()) {
player.sendMessage(Component.text("[DBD] Нет доступных лобби.", NamedTextColor.YELLOW));
return;
}
player.sendMessage(Component.text("=== Лобби DBD ===", NamedTextColor.GOLD));
for (LobbySession ls : visible) {
GameSession gs = sessions.get(ls.getName().toLowerCase());
int count = gs != null ? gs.getPlayerCount() : 0;
String lock = ls.hasPassword() ? " §6[🔒]" : "";
String priv = ls.isPrivate() ? " §8[private]" : "";
player.sendMessage(Component.text(
" §f" + ls.getName() + lock + priv + " §7(" + count + "/" + ls.getMaxPlayers() + ")",
NamedTextColor.GRAY));
}
}
/** Delete a lobby (owner or admin). */
public boolean deleteLobby(Player player, String name) {
String key = name.toLowerCase();
LobbySession ls = lobbies.get(key);
if (ls == null) {
player.sendMessage(Component.text("[DBD] Лобби не найдено.", NamedTextColor.RED));
return false;
}
if (!ls.getOwnerUuid().equals(player.getUniqueId())
&& !player.hasPermission("mdbd.admin")) {
player.sendMessage(Component.text(
"[DBD] Только владелец или admin может удалить лобби.", NamedTextColor.RED));
return false;
}
forceRemoveLobby(key);
save();
player.sendMessage(Component.text("[DBD] Лобби §f" + name + "§a удалено.", NamedTextColor.GREEN));
return true;
}
// -------------------------------------------------------------------------
// Internal
// -------------------------------------------------------------------------
/**
* Called when a GameSession finishes (stops).
* Recreates a fresh LobbyPhase session so the lobby can be reused.
*/
private void onSessionFinished(String lobbyName) {
String key = lobbyName.toLowerCase();
LobbySession ls = lobbies.get(key);
if (ls == null) return;
// Удаляем старую сессию из GameManager
gameManager.removeNamedGame(key);
sessions.remove(key);
// Пересоздаём свежую сессию в лобби-фазе
GameSession fresh = gameManager.createNamedGame(key);
if (fresh == null) {
plugin.getLogger().warning("[LobbyRegistry] Could not recreate session for lobby: " + lobbyName);
return;
}
fresh.setOnStopped(() -> onSessionFinished(lobbyName));
sessions.put(key, fresh);
plugin.getLogger().info("[LobbyRegistry] Lobby «" + lobbyName + "» reset for new game.");
}
/** Forcibly remove lobby from all maps and stop its session. */
private void forceRemoveLobby(String key) {
LobbySession ls = lobbies.remove(key);
if (ls != null) ownerToLobby.remove(ls.getOwnerUuid());
GameSession gs = sessions.remove(key);
if (gs != null) gs.stop();
gameManager.removeNamedGame(key);
}
public LobbySession getLobby(String name) {
return lobbies.get(name.toLowerCase());
}
public Collection<LobbySession> getAllLobbies() {
return Collections.unmodifiableCollection(lobbies.values());
}
/** Lobbies that have a live session (for tab-complete). */
public Collection<LobbySession> getJoinableLobbies() {
return lobbies.values().stream()
.filter(ls -> sessions.containsKey(ls.getName().toLowerCase()))
.toList();
}
/** Called when a player leaves a named lobby session. */
public void onPlayerLeftSession(GameSession session, org.bukkit.entity.Player player) {
if (session.getPlayerCount() > 0) return;
// Remove empty lobby entirely
for (String key : new java.util.ArrayList<>(sessions.keySet())) {
if (sessions.get(key) == session) {
plugin.getLogger().info("[LobbyRegistry] Lobby «" + key + "» empty, removing");
forceRemoveLobby(key);
save();
return;
}
}
}
private UUID parseUUID(String s) {
if (s == null) return null;
try { return UUID.fromString(s); } catch (Exception e) { return null; }
}
}

View file

@ -0,0 +1,46 @@
package dev.loki.world;
import java.util.UUID;
/**
* Represents a named game lobby that players can join by name (and optional password).
* Lobbies can be public or private.
*/
public class LobbySession {
private final String name;
private final UUID ownerUuid;
private String password; // null = no password
private boolean privateMode;
private final int maxPlayers;
public LobbySession(String name, UUID ownerUuid, String password,
boolean privateMode, int maxPlayers) {
this.name = name;
this.ownerUuid = ownerUuid;
this.password = (password != null && !password.isEmpty()) ? password : null;
this.privateMode = privateMode;
this.maxPlayers = maxPlayers;
}
public String getName() { return name; }
public UUID getOwnerUuid() { return ownerUuid; }
public String getPassword() { return password; }
public boolean isPrivate() { return privateMode; }
public int getMaxPlayers() { return maxPlayers; }
public boolean hasPassword() { return password != null; }
public boolean checkPassword(String input) {
if (!hasPassword()) return true;
return password.equals(input);
}
public void setPassword(String password) {
this.password = (password != null && !password.isEmpty()) ? password : null;
}
public void setPrivate(boolean privateMode) {
this.privateMode = privateMode;
}
}

View file

@ -1,14 +0,0 @@
package dev.loki.world;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.WorldInfo;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
public class VoidWorldGenerator extends ChunkGenerator {
@Override
public void generateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int chunkX, int chunkZ, @NotNull ChunkData chunkData) {
}
}