fix: 10 bug fixes — healing dead players, kick bypass, gate race, swimming ghost, regression config, dead code cleanup, gate zone detection
This commit is contained in:
parent
b666f1b0fa
commit
d2b174b94c
35 changed files with 587 additions and 773 deletions
|
|
@ -15,6 +15,9 @@ public class Chest {
|
|||
private boolean opened;
|
||||
private boolean empty;
|
||||
private ItemStack loot;
|
||||
private long lastEmptiedTime;
|
||||
|
||||
private static final long COOLDOWN_MILLIS = 3 * 60 * 1000; // 3 minutes
|
||||
|
||||
public Chest(Location location) {
|
||||
this.id = UUID.randomUUID();
|
||||
|
|
@ -22,6 +25,7 @@ public class Chest {
|
|||
this.opened = false;
|
||||
this.empty = false;
|
||||
this.loot = null;
|
||||
this.lastEmptiedTime = 0;
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
|
|
@ -52,6 +56,21 @@ public class Chest {
|
|||
return !opened && !empty && hasLoot();
|
||||
}
|
||||
|
||||
public void onLooted() {
|
||||
this.empty = true;
|
||||
this.lastEmptiedTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public long getSecondsUntilReset() {
|
||||
long elapsed = (System.currentTimeMillis() - lastEmptiedTime) / 1000;
|
||||
long remaining = (COOLDOWN_MILLIS / 1000) - elapsed;
|
||||
return Math.max(0, remaining);
|
||||
}
|
||||
|
||||
public boolean isOnCooldown() {
|
||||
return getSecondsUntilReset() > 0;
|
||||
}
|
||||
|
||||
public ItemStack getLoot() {
|
||||
return loot;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,14 +118,8 @@ public class ChestHologram {
|
|||
|
||||
String status;
|
||||
if (chest.isEmpty()) {
|
||||
long secs = manager.getSecondsUntilNextReset();
|
||||
if (secs > 60) {
|
||||
status = "§7Пуст §8(" + (secs / 60) + "м)";
|
||||
} else if (secs > 0) {
|
||||
status = "§7Пуст §8(" + secs + "с)";
|
||||
} else {
|
||||
status = "§7Пуст";
|
||||
}
|
||||
long secs = chest.getSecondsUntilReset();
|
||||
status = secs > 0 ? "§7Пусто §8(" + secs + "с)" : "§7Пусто";
|
||||
} else if (chest.isOpened()) {
|
||||
status = "§6Открывается...";
|
||||
} else if (chest.hasLoot()) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ public class ChestManager {
|
|||
private final GameConfig gameConfig;
|
||||
private ChestLoot chestLoot;
|
||||
private BukkitTask lootResetTask;
|
||||
private long lastResetTime = System.currentTimeMillis();
|
||||
|
||||
public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig, GameConfig gameConfig) {
|
||||
this.plugin = plugin;
|
||||
|
|
@ -36,7 +35,6 @@ 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);
|
||||
}
|
||||
|
|
@ -44,14 +42,12 @@ public class ChestManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Start periodic loot reset timer.
|
||||
* Every N minutes (from config), all empty chests get new loot.
|
||||
* Start per-chest cooldown timer. Each chest refills individually 3 min after being looted.
|
||||
*/
|
||||
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)");
|
||||
lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::tickCooldowns, 20L, 20L);
|
||||
plugin.getLogger().info("Per-chest cooldown timer started (3 min per chest)");
|
||||
}
|
||||
|
||||
public void stopLootResetTimer() {
|
||||
|
|
@ -62,20 +58,16 @@ public class ChestManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reset loot: refill ALL chests with new random loot.
|
||||
* Every second, check each chest — if its cooldown expired, refill it.
|
||||
*/
|
||||
private void resetLoot() {
|
||||
int refilled = 0;
|
||||
private void tickCooldowns() {
|
||||
for (Chest chest : chests.values()) {
|
||||
// Skip chests currently being opened to avoid race condition
|
||||
if (chest.isOpened()) continue;
|
||||
if (chest.isEmpty() && !chest.isOnCooldown()) {
|
||||
chest.setLoot(chestLoot.rollLoot());
|
||||
chest.setOpened(false);
|
||||
chest.setEmpty(false);
|
||||
refilled++;
|
||||
}
|
||||
this.lastResetTime = System.currentTimeMillis();
|
||||
plugin.getLogger().info("[Chest] Loot reset — " + refilled + " chests refilled");
|
||||
}
|
||||
}
|
||||
|
||||
public Chest getChest(UUID id) {
|
||||
|
|
@ -117,7 +109,6 @@ public class ChestManager {
|
|||
chest.setEmpty(false);
|
||||
chest.setLoot(chestLoot.rollLoot());
|
||||
}
|
||||
this.lastResetTime = System.currentTimeMillis();
|
||||
plugin.getLogger().info("Reset all chests");
|
||||
}
|
||||
|
||||
|
|
@ -140,13 +131,7 @@ public class ChestManager {
|
|||
return (int) chests.values().stream().filter(Chest::isAvailable).count();
|
||||
}
|
||||
|
||||
/** Seconds until the next automatic loot reset. */
|
||||
public long getSecondsUntilNextReset() {
|
||||
long elapsed = (System.currentTimeMillis() - lastResetTime) / 1000;
|
||||
long total = (long) (gameConfig.getChestLootResetMinutes() * 60);
|
||||
long remaining = total - elapsed;
|
||||
return Math.max(0, remaining);
|
||||
}
|
||||
|
||||
|
||||
public ChestLoot getChestLoot() {
|
||||
return chestLoot;
|
||||
|
|
|
|||
|
|
@ -47,16 +47,10 @@ public class ChestOpening {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Ensure chest has loot; roll if missing
|
||||
if (!chest.hasLoot()) {
|
||||
ItemStack rolled = chestLoot.rollLoot();
|
||||
if (rolled == null) {
|
||||
if (!chest.hasLoot() || chest.isEmpty()) {
|
||||
player.sendMessage(messages().get("chest.empty"));
|
||||
return false;
|
||||
}
|
||||
chest.setLoot(rolled);
|
||||
chest.setEmpty(false);
|
||||
}
|
||||
|
||||
if (!chest.isAvailable()) {
|
||||
player.sendMessage(messages().get("chest.empty"));
|
||||
|
|
@ -152,7 +146,7 @@ public class ChestOpening {
|
|||
}
|
||||
}
|
||||
chest.setOpened(false);
|
||||
chest.setEmpty(true);
|
||||
chest.onLooted();
|
||||
chest.setLoot(null); // Clear loot after giving
|
||||
chest.closeVisual();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package dev.loki.command;
|
|||
import dev.loki.mdbd;
|
||||
import dev.loki.util.SpawnUtil;
|
||||
import dev.loki.util.TextUtil;
|
||||
import dev.loki.config.LocationsConfig;
|
||||
import dev.loki.config.MessagesConfig;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
|
|
@ -70,7 +71,8 @@ public class SetupCommand implements TabExecutor {
|
|||
return switch (args[0].toLowerCase()) {
|
||||
case "wand" -> giveWand(player);
|
||||
case "set" -> handleSet(player, args);
|
||||
case "clear" -> clearMap(player);
|
||||
case "clear" -> handleClear(player, args);
|
||||
case "delete" -> handleDelete(player, args);
|
||||
case "clear-spawns" -> clearSpawns(player);
|
||||
case "lobby" -> setLobby(player);
|
||||
default -> { sendUsage(sender); yield true; }
|
||||
|
|
@ -331,6 +333,147 @@ public class SetupCommand implements TabExecutor {
|
|||
return true;
|
||||
}
|
||||
|
||||
// ── Clear ────────────────────────────────────────────────────────
|
||||
|
||||
private boolean handleClear(Player player, String[] args) {
|
||||
var locations = plugin.getConfigManager().getLocations();
|
||||
|
||||
if (args.length >= 2) {
|
||||
String type = args[1].toLowerCase();
|
||||
return clearObjectsByType(player, locations, type);
|
||||
}
|
||||
|
||||
// Clear ALL object data from config (no block destruction)
|
||||
locations.clearLocationList("generators");
|
||||
locations.clearLocationList("hooks");
|
||||
locations.clearLocationList("chests");
|
||||
locations.clearLocationList("killer-spawn");
|
||||
for (String id : List.of("a", "b")) {
|
||||
locations.getConfig().set("gate-" + id, null);
|
||||
locations.getConfig().set("gate-" + id + "-corner1", null);
|
||||
locations.getConfig().set("gate-" + id + "-corner2", null);
|
||||
}
|
||||
locations.clearSurvivorSpawns();
|
||||
plugin.getGeneratorData().clearGenerators();
|
||||
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
|
||||
player.sendMessage(messages().get("setup.cleared"));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearObjectsByType(Player player, LocationsConfig locations, String type) {
|
||||
String path = switch (type) {
|
||||
case "generator" -> "generators";
|
||||
case "hook" -> "hooks";
|
||||
case "chest" -> "chests";
|
||||
case "gate" -> null;
|
||||
case "killer-spawn" -> "killer-spawn";
|
||||
case "survivor-spawn" -> null;
|
||||
default -> { player.sendMessage(messages().get("setup.unknown-type", type)); yield null; }
|
||||
};
|
||||
if (path == null) {
|
||||
if (type.equals("gate")) {
|
||||
for (String id : List.of("a", "b")) {
|
||||
locations.getConfig().set("gate-" + id, null);
|
||||
locations.getConfig().set("gate-" + id + "-corner1", null);
|
||||
locations.getConfig().set("gate-" + id + "-corner2", null);
|
||||
}
|
||||
} else if (type.equals("survivor-spawn")) {
|
||||
locations.clearSurvivorSpawns();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
locations.clearLocationList(path);
|
||||
if (type.equals("generator")) {
|
||||
plugin.getGeneratorData().clearGenerators();
|
||||
}
|
||||
}
|
||||
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
|
||||
player.sendMessage(messages().get("setup.cleared-type", type));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Delete ───────────────────────────────────────────────────────
|
||||
|
||||
private boolean handleDelete(Player player, String[] args) {
|
||||
if (args.length < 3) {
|
||||
player.sendMessage(messages().get("setup.delete-usage"));
|
||||
return true;
|
||||
}
|
||||
|
||||
String type = args[1].toLowerCase();
|
||||
int index;
|
||||
try {
|
||||
index = Integer.parseInt(args[2]);
|
||||
} catch (NumberFormatException e) {
|
||||
player.sendMessage(messages().get("setup.invalid-number", args[2]));
|
||||
return true;
|
||||
}
|
||||
|
||||
var locations = plugin.getConfigManager().getLocations();
|
||||
|
||||
switch (type) {
|
||||
case "generator" -> {
|
||||
var gens = locations.getGenerators();
|
||||
if (index < 0 || index >= gens.size()) {
|
||||
player.sendMessage(messages().get("setup.out-of-range", index));
|
||||
return true;
|
||||
}
|
||||
locations.removeLocationAtIndex("generators", index);
|
||||
plugin.getGeneratorData().removeGenerator(index);
|
||||
player.sendMessage(messages().get("setup.deleted", "generator #" + index));
|
||||
}
|
||||
case "hook" -> {
|
||||
var hooks = locations.getHooks();
|
||||
if (index < 0 || index >= hooks.size()) {
|
||||
player.sendMessage(messages().get("setup.out-of-range", index));
|
||||
return true;
|
||||
}
|
||||
locations.removeLocationAtIndex("hooks", index);
|
||||
player.sendMessage(messages().get("setup.deleted", "hook #" + index));
|
||||
}
|
||||
case "chest" -> {
|
||||
var chests = locations.getChests();
|
||||
if (index < 0 || index >= chests.size()) {
|
||||
player.sendMessage(messages().get("setup.out-of-range", index));
|
||||
return true;
|
||||
}
|
||||
locations.removeLocationAtIndex("chests", index);
|
||||
player.sendMessage(messages().get("setup.deleted", "chest #" + index));
|
||||
}
|
||||
case "killer-spawn" -> {
|
||||
locations.clearLocationList("killer-spawn");
|
||||
player.sendMessage(messages().get("setup.deleted", "killer-spawn"));
|
||||
}
|
||||
case "survivor-spawn" -> {
|
||||
if (index < 1 || index > 4) {
|
||||
player.sendMessage(messages().get("setup.out-of-range", index));
|
||||
return true;
|
||||
}
|
||||
locations.clearSurvivorSpawn(index);
|
||||
player.sendMessage(messages().get("setup.deleted", "survivor-spawn #" + index));
|
||||
}
|
||||
case "gate" -> {
|
||||
String id = index == 1 ? "a" : index == 2 ? "b" : null;
|
||||
if (id == null) {
|
||||
player.sendMessage(messages().get("setup.out-of-range", index));
|
||||
return true;
|
||||
}
|
||||
locations.getConfig().set("gate-" + id, null);
|
||||
locations.getConfig().set("gate-" + id + "-corner1", null);
|
||||
locations.getConfig().set("gate-" + id + "-corner2", null);
|
||||
player.sendMessage(messages().get("setup.deleted", "gate-" + id));
|
||||
}
|
||||
default -> {
|
||||
player.sendMessage(messages().get("setup.unknown-type", type));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig());
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearSpawns(Player player) {
|
||||
var locations = plugin.getConfigManager().getLocations();
|
||||
locations.clearLocationList("killer-spawn");
|
||||
|
|
@ -359,6 +502,8 @@ public class SetupCommand implements TabExecutor {
|
|||
sender.sendMessage(messages().get("usage.setup-killer-spawn"));
|
||||
sender.sendMessage(messages().get("usage.setup-survivor-spawn"));
|
||||
sender.sendMessage(messages().get("usage.setup-gate"));
|
||||
sender.sendMessage(messages().get("usage.setup-clear"));
|
||||
sender.sendMessage(messages().get("usage.setup-delete"));
|
||||
sender.sendMessage(messages().get("usage.setup-clear-spawns"));
|
||||
sender.sendMessage(messages().get("usage.setup-lobby"));
|
||||
}
|
||||
|
|
@ -373,13 +518,15 @@ public class SetupCommand implements TabExecutor {
|
|||
@NotNull String label, @NotNull String[] args) {
|
||||
if (!sender.hasPermission("mdbd.admin")) return List.of();
|
||||
if (args.length == 1) {
|
||||
return List.of("wand", "set", "clear", "clear-spawns", "lobby").stream()
|
||||
return List.of("wand", "set", "clear", "delete", "clear-spawns", "lobby").stream()
|
||||
.filter(s -> s.startsWith(args[0].toLowerCase())).toList();
|
||||
}
|
||||
if (args.length == 2 && args[0].equalsIgnoreCase("set")) {
|
||||
return List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn", "gate").stream()
|
||||
if (args.length == 2) {
|
||||
if (args[0].equalsIgnoreCase("set") || args[0].equalsIgnoreCase("clear") || args[0].equalsIgnoreCase("delete")) {
|
||||
return List.of("generator", "hook", "chest", "killer-spawn", "survivor-spawn", "gate").stream()
|
||||
.filter(s -> s.startsWith(args[1].toLowerCase())).toList();
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,8 +84,11 @@ public class GameConfig {
|
|||
|
||||
// Endgame
|
||||
setDefault("endgame.gate-open-seconds", 20);
|
||||
setDefault("endgame.pregame-seconds", 30);
|
||||
setDefault("endgame.postgame-seconds", 60);
|
||||
setDefault("endgame.chest-open-seconds", 3);
|
||||
setDefault("endgame.gate-interaction-range", 3.0);
|
||||
setDefault("endgame.escape-range", 2.0);
|
||||
|
||||
// Killer notification
|
||||
setDefault("killer-notification.compass-duration-seconds", 5);
|
||||
|
|
@ -263,8 +266,11 @@ public class GameConfig {
|
|||
// Endgame
|
||||
// -------------------------------------------------------------------------
|
||||
public int getGateOpenSeconds() { return config.getInt("endgame.gate-open-seconds", 20); }
|
||||
public int getPregameSeconds() { return config.getInt("endgame.pregame-seconds", 30); }
|
||||
public int getPostgameSeconds() { return config.getInt("endgame.postgame-seconds", 60); }
|
||||
public int getChestOpenSeconds() { return config.getInt("endgame.chest-open-seconds", 3); }
|
||||
public double getGateInteractionRange() { return config.getDouble("endgame.gate-interaction-range", 3.0); }
|
||||
public double getEscapeRange() { return config.getDouble("endgame.escape-range", 2.0); }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Killer notification
|
||||
|
|
|
|||
|
|
@ -130,6 +130,12 @@ public class LocationsConfig {
|
|||
return -1;
|
||||
}
|
||||
|
||||
/** Clear a specific survivor spawn slot (index 1-4). */
|
||||
public void clearSurvivorSpawn(int index) {
|
||||
if (index < 1 || index > 4) throw new IllegalArgumentException("Index must be 1-4");
|
||||
config.set("survivor-spawn-" + index, null);
|
||||
}
|
||||
|
||||
/** Clear all survivor spawn slots. */
|
||||
public void clearSurvivorSpawns() {
|
||||
for (int i = 1; i <= 4; i++) {
|
||||
|
|
@ -219,6 +225,15 @@ public class LocationsConfig {
|
|||
public void clearLocationList(String path) {
|
||||
config.set(path, new ArrayList<>());
|
||||
}
|
||||
/** Remove a location at a specific index from a list. */
|
||||
public void removeLocationAtIndex(String path, int index) {
|
||||
List<String> list = safeGetStringList(path);
|
||||
if (index >= 0 && index < list.size()) {
|
||||
list.remove(index);
|
||||
config.set(path, list);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSingleLocation(String path, Location loc) {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add(locationToString(loc));
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ public class GameSession {
|
|||
this.ticksElapsed = 0;
|
||||
this.roleAssigner = new RoleAssigner(plugin.getConfigManager().getLocations());
|
||||
this.generatorManager = new GeneratorManager(
|
||||
plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired());
|
||||
plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired(),
|
||||
plugin.getConfigManager().getGame());
|
||||
}
|
||||
|
||||
public void start() {
|
||||
|
|
|
|||
|
|
@ -150,9 +150,7 @@ public class ExitGate {
|
|||
|
||||
public void setPowered(boolean powered) {
|
||||
this.powered = powered;
|
||||
if (powered && state == GateState.CLOSED) {
|
||||
state = GateState.OPENING;
|
||||
}
|
||||
// НЕ меняем статус на OPENING — ворота открываются только через взаимодействие игрока
|
||||
}
|
||||
|
||||
public void startOpening() {
|
||||
|
|
@ -174,6 +172,16 @@ public class ExitGate {
|
|||
}
|
||||
}
|
||||
|
||||
/** Принудительно открыть ворота (вызывается после таймера эндгейма). */
|
||||
public void forceOpen() {
|
||||
this.state = GateState.OPEN;
|
||||
this.progress = 1.0;
|
||||
this.savedProgress = 1.0;
|
||||
removeGateStructure();
|
||||
spawnOpenParticles();
|
||||
playOpenedSound();
|
||||
}
|
||||
|
||||
/** Прямая установка прогресса (для линейного расчёта в GateOpening). */
|
||||
public void setProgressDirect(double value) {
|
||||
if (state != GateState.OPENING) return;
|
||||
|
|
@ -204,6 +212,14 @@ public class ExitGate {
|
|||
}
|
||||
|
||||
public boolean isInRange(Location playerLoc, double radius) {
|
||||
if (hasZone) {
|
||||
int px = playerLoc.getBlockX(), py = playerLoc.getBlockY(), pz = playerLoc.getBlockZ();
|
||||
int closestX = Math.max(zoneMinX, Math.min(px, zoneMaxX));
|
||||
int closestY = Math.max(zoneMinY, Math.min(py, zoneMaxY));
|
||||
int closestZ = Math.max(zoneMinZ, Math.min(pz, zoneMaxZ));
|
||||
double dx = px - closestX, dy = py - closestY, dz = pz - closestZ;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz) <= radius;
|
||||
}
|
||||
return location.distance(playerLoc) <= radius;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,12 @@ public class GateManager {
|
|||
return (int) gates.stream().filter(ExitGate::isOpen).count();
|
||||
}
|
||||
|
||||
public void openAllGates() {
|
||||
for (ExitGate gate : gates) {
|
||||
gate.forceOpen();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetAll() {
|
||||
endgameActive = false;
|
||||
for (ExitGate gate : gates) {
|
||||
|
|
|
|||
|
|
@ -38,17 +38,22 @@ public class GateOpening {
|
|||
|
||||
ExitGate gate = gateManager.getGateAt(player.getLocation(), INTERACTION_RADIUS);
|
||||
if (gate == null) {
|
||||
player.sendMessage(messages.get("chest.door-nearby"));
|
||||
player.sendMessage(messages.get("gate.door-nearby"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!gate.isPowered()) {
|
||||
player.sendMessage(messages.get("chest.door-no-power"));
|
||||
player.sendMessage(messages.get("gate.door-no-power"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!gate.isClosed() && !gate.isOpening()) {
|
||||
player.sendMessage(messages.get("chest.door-already"));
|
||||
if (gate.isOpening()) {
|
||||
player.sendMessage(messages.get("gate.door-already"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gate.isOpen()) {
|
||||
player.sendMessage(messages.get("gate.door-already"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +66,7 @@ public class GateOpening {
|
|||
if (progress != null) {
|
||||
Player player = survivor.getPlayer();
|
||||
if (player != null) {
|
||||
player.sendMessage(messages.get("chest.door-interrupted"));
|
||||
player.sendMessage(messages.get("gate.door-interrupted"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -74,11 +79,14 @@ public class GateOpening {
|
|||
Player player = survivor.getPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
// Перевести ворота в режим открытия
|
||||
gate.startOpening();
|
||||
|
||||
// Восстановить сохранённый прогресс
|
||||
gate.restoreProgress();
|
||||
|
||||
openingPlayers.put(survivor.getUuid(), new OpeningProgress(gate, 0));
|
||||
player.sendMessage(messages.get("chest.door-open"));
|
||||
player.sendMessage(messages.get("gate.door-open"));
|
||||
|
||||
new OpeningTask(survivor, gate, plugin).runTaskTimer(plugin, 0L, 1L);
|
||||
}
|
||||
|
|
@ -89,7 +97,7 @@ public class GateOpening {
|
|||
World world = loc.getWorld();
|
||||
|
||||
if (player != null) {
|
||||
player.sendMessage(messages.get("chest.door-opened"));
|
||||
player.sendMessage(messages.get("gate.door-opened"));
|
||||
if (world != null) {
|
||||
world.playSound(loc, Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
|
||||
// Big particle burst when opened
|
||||
|
|
|
|||
|
|
@ -76,4 +76,6 @@ public class Hook {
|
|||
public boolean isBasement() { return basement; }
|
||||
public boolean isDisabled() { return disabled; }
|
||||
public void setDisabled(boolean disabled) { this.disabled = disabled; }
|
||||
public int getStageProgress() { return stageProgress; }
|
||||
public void addStageProgress(int delta) { stageProgress = Math.max(0, stageProgress + delta); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,10 +111,7 @@ public class HookManager {
|
|||
return true; // "повешен" = завершён
|
||||
}
|
||||
|
||||
Hook nearest = findNearestUnoccupiedHook(killerLoc, 5.0);
|
||||
if (nearest == null) {
|
||||
nearest = findAvailableHook().orElse(null);
|
||||
}
|
||||
Hook nearest = findNearestUnoccupiedHook(killerLoc, 4.0);
|
||||
if (nearest != null) {
|
||||
nearest.hookSurvivor(survivor);
|
||||
survivor.setState(PlayerState.HOOKED); // Set state to HOOKED
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ public class ItemMedkit implements BaseItem {
|
|||
meta.setDisplayName("§a" + getType().getDisplayName());
|
||||
meta.setLore(Arrays.asList(
|
||||
"§7" + getType().getDescription(),
|
||||
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||
"§7Right-click to heal"
|
||||
"§eЗаряды: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||
"§7ПКМ — лечиться"
|
||||
));
|
||||
|
||||
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||
|
|
@ -60,7 +60,7 @@ public class ItemMedkit implements BaseItem {
|
|||
@Override
|
||||
public void onUse(Player player) {
|
||||
if (charges > 0) {
|
||||
player.sendMessage("§aYou start healing...");
|
||||
player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package dev.loki.item;
|
||||
|
||||
public enum ItemType {
|
||||
MEDKIT("Medkit", "Heals one health state", 16),
|
||||
HOOK_CUTTER("Hook Cutter", "Cuts a survivor off a hook", 1);
|
||||
MEDKIT("Медкит", "Лечит одно состояние здоровья", 16),
|
||||
HOOK_CUTTER("Крюкорез", "Срезает выжившего с крюка", 1);
|
||||
|
||||
private final String displayName;
|
||||
private final String description;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ import org.bukkit.plugin.Plugin;
|
|||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -39,6 +41,9 @@ public class GeneratorKick {
|
|||
private int globalCooldownTicks = 0;
|
||||
private BukkitTask cooldownTask;
|
||||
|
||||
/** Pending unblock tasks — cancelled on shutdown to avoid ghost tasks */
|
||||
private final List<BukkitTask> pendingUnblockTasks = new ArrayList<>();
|
||||
|
||||
public GeneratorKick(Plugin plugin, GameConfig config, GeneratorManager generatorManager) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
|
|
@ -135,6 +140,8 @@ public class GeneratorKick {
|
|||
for (BukkitTask t : chargingTasks.values()) t.cancel();
|
||||
chargingTasks.clear();
|
||||
if (cooldownTask != null) { cooldownTask.cancel(); cooldownTask = null; }
|
||||
for (BukkitTask t : pendingUnblockTasks) t.cancel();
|
||||
pendingUnblockTasks.clear();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -164,9 +171,10 @@ public class GeneratorKick {
|
|||
|
||||
// Unblock generator after kick-duration (separate from global cooldown)
|
||||
int unblockTicks = config.getKickDuration() * 20;
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
BukkitTask unblockTask = plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
gen.setBlocked(false);
|
||||
}, unblockTicks);
|
||||
pendingUnblockTasks.add(unblockTask);
|
||||
|
||||
// Start global kick cooldown
|
||||
startGlobalCooldown();
|
||||
|
|
|
|||
|
|
@ -228,10 +228,32 @@ public class PlayerInteractListener implements Listener {
|
|||
// #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;
|
||||
}
|
||||
|
||||
// Chest interaction - cancel vanilla GUI for ALL players (including killer)
|
||||
if (type.contains("CHEST")) {
|
||||
event.setCancelled(true);
|
||||
if (onChestInteract != null && !bp.isKiller()) {
|
||||
onChestInteract.accept(player, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Gate opening — проверяем ДО генератора, чтобы ворота можно было открыть рядом с геной
|
||||
if (!bp.isKiller() && onHatchInteract != null) {
|
||||
var s = gameManager.getPlayerSession(player.getUniqueId());
|
||||
if (s != null && s.getGateManager() != null) {
|
||||
var gate = s.getGateManager().getGateAt(player.getLocation(), 3.0);
|
||||
if (gate != null && !gate.isOpen()) {
|
||||
onHatchInteract.accept(player, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bp.isKiller() && tryGeneratorInteract(player)) return;
|
||||
|
|
@ -242,29 +264,15 @@ public class PlayerInteractListener implements Listener {
|
|||
return;
|
||||
}
|
||||
|
||||
// Gate opening via proximity for survivors — works on air clicks too
|
||||
if (!bp.isKiller() && onHatchInteract != null) {
|
||||
onHatchInteract.accept(player, true);
|
||||
}
|
||||
|
||||
if (event.getClickedBlock() == null) return;
|
||||
|
||||
String type = event.getClickedBlock().getType().toString();
|
||||
|
||||
// Chest interaction - cancel vanilla GUI and use plugin chest system
|
||||
if (type.contains("CHEST")) {
|
||||
event.setCancelled(true);
|
||||
if (onChestInteract != null && !bp.isKiller()) {
|
||||
onChestInteract.accept(player, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Hook rescue: if survivor clicks on a hook block, attempt rescue
|
||||
// Hook rescue / block protection
|
||||
if (!bp.isKiller() && onRescueAttempt != null) {
|
||||
onRescueAttempt.accept(player, true);
|
||||
if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true);
|
||||
}
|
||||
// Cancel interaction with hook blocks to prevent block manipulation
|
||||
if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.objective;
|
||||
|
||||
import dev.loki.data.GeneratorDataManager;
|
||||
import dev.loki.config.GameConfig;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
|
|
@ -12,11 +13,15 @@ import java.util.Map;
|
|||
public class GeneratorManager {
|
||||
private final Map<Integer, Generator> generators;
|
||||
private final int requiredCount;
|
||||
private final double normalRegressionRate;
|
||||
private final double kickRegressionRate;
|
||||
|
||||
public GeneratorManager(GeneratorDataManager dataManager, int requiredCount) {
|
||||
public GeneratorManager(GeneratorDataManager dataManager, int requiredCount, GameConfig config) {
|
||||
this.generators = new HashMap<>();
|
||||
this.requiredCount = requiredCount;
|
||||
loadGenerators(dataManager);
|
||||
this.requiredCount = Math.min(requiredCount, generators.size());
|
||||
this.normalRegressionRate = config.getRegressionRate() / 20.0;
|
||||
this.kickRegressionRate = config.getKickRegressionRate() / 20.0;
|
||||
}
|
||||
|
||||
private void loadGenerators(GeneratorDataManager dataManager) {
|
||||
|
|
@ -103,7 +108,7 @@ public class GeneratorManager {
|
|||
public void tick() {
|
||||
for (Generator gen : generators.values()) {
|
||||
if (gen.isUnderRegression()) {
|
||||
double rate = gen.wasKicked() ? 0.125 : 0.0125;
|
||||
double rate = gen.wasKicked() ? kickRegressionRate : normalRegressionRate;
|
||||
gen.removeProgress(rate);
|
||||
}
|
||||
gen.decrementRegression();
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
package dev.loki.objective;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
|
||||
public class GeneratorRegression {
|
||||
private final GameConfig config;
|
||||
|
||||
public GeneratorRegression(GameConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void applyRegression(Generator generator) {
|
||||
if (generator.isCompleted() || generator.isBeingRepaired()) return;
|
||||
double rate = generator.wasKicked() ? getKickRegressionRate() : getNormalRegressionRate();
|
||||
generator.removeProgress(rate);
|
||||
}
|
||||
|
||||
public void onGeneratorKicked(Generator generator) {
|
||||
generator.setUnderRegression(true, config.getKickDuration() * 20);
|
||||
}
|
||||
|
||||
public void tick(Generator generator) {
|
||||
if (!generator.isUnderRegression()) return;
|
||||
applyRegression(generator);
|
||||
generator.decrementRegression();
|
||||
if (generator.getRegressionTicks() <= 0) {
|
||||
generator.setKicked(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void tickAll(GeneratorManager manager) {
|
||||
for (Generator gen : manager.getAllGenerators()) {
|
||||
if (!gen.isCompleted() && !gen.isBeingRepaired()) {
|
||||
tick(gen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double getNormalRegressionRate() {
|
||||
return config.getRegressionRate() / 20.0;
|
||||
}
|
||||
|
||||
private double getKickRegressionRate() {
|
||||
return config.getKickRegressionRate() / 20.0;
|
||||
}
|
||||
|
||||
public static boolean shouldStartRegression(Generator generator) {
|
||||
return !generator.isCompleted()
|
||||
&& !generator.isBeingRepaired()
|
||||
&& generator.getProgress() > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package dev.loki.objective;
|
|||
|
||||
import dev.loki.mdbd;
|
||||
import dev.loki.minigames.GeneratorMinigame;
|
||||
import dev.loki.player.BasePlayer;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
|
|
@ -71,8 +72,8 @@ 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()) {
|
||||
// Only unblock if it was blocked due to an ongoing repair (not a kick or block)
|
||||
if (!session.generator.isUnderRegression() && !session.generator.isBlocked()) {
|
||||
session.generator.setBlocked(false);
|
||||
}
|
||||
player.sendActionBar(Component.empty());
|
||||
|
|
@ -176,12 +177,18 @@ public class GeneratorRepair {
|
|||
}
|
||||
}
|
||||
|
||||
/** Give every online killer a compass pointing to the failed generator for compass-duration-seconds. */
|
||||
/** Give only killers a compass pointing to the failed generator for compass-duration-seconds. */
|
||||
private void notifyKillerWithCompass(Player failedPlayer, Generator generator) {
|
||||
var session = plugin.getGameManager().getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
int durationTicks = plugin.getConfigManager().getGame().getCompassDurationSeconds() * 20;
|
||||
Location compassTarget = generator.getLocation();
|
||||
|
||||
for (Player online : Bukkit.getOnlinePlayers()) {
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
if (!bp.isKiller()) continue;
|
||||
Player online = bp.getPlayer();
|
||||
if (online == null || !online.isOnline()) continue;
|
||||
if (online.getUniqueId().equals(failedPlayer.getUniqueId())) continue;
|
||||
|
||||
online.sendMessage(plugin.getConfigManager().getMessages().get("generator.kicked.notification"));
|
||||
|
|
@ -196,7 +203,6 @@ public class GeneratorRepair {
|
|||
|
||||
ItemStack prev = online.getInventory().getItemInOffHand();
|
||||
online.getInventory().setItemInOffHand(compass);
|
||||
// Восстанавливаем старый предмет только если в offhand всё ещё компас
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
ItemStack current = online.getInventory().getItemInOffHand();
|
||||
if (current.getType() == Material.COMPASS) {
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
package dev.loki.objective;
|
||||
|
||||
import dev.loki.mdbd;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SkillCheck {
|
||||
private final mdbd plugin;
|
||||
private final GeneratorRepair generatorRepair;
|
||||
private final Map<UUID, CheckState> activeChecks;
|
||||
|
||||
private static final double ZONE_SIZE = 0.15;
|
||||
private static final double ROTATION_SPEED = 3.0;
|
||||
|
||||
public SkillCheck(mdbd plugin, GeneratorRepair generatorRepair) {
|
||||
this.plugin = plugin;
|
||||
this.generatorRepair = generatorRepair;
|
||||
this.activeChecks = new HashMap<>();
|
||||
}
|
||||
|
||||
public void start(UUID playerId, Generator generator) {
|
||||
double zoneStart = Math.random() * 0.7;
|
||||
double zoneEnd = zoneStart + ZONE_SIZE;
|
||||
|
||||
CheckState state = new CheckState(
|
||||
playerId,
|
||||
generator,
|
||||
zoneStart,
|
||||
zoneEnd,
|
||||
0.0,
|
||||
ROTATION_SPEED
|
||||
);
|
||||
activeChecks.put(playerId, state);
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
for (CheckState state : new HashMap<>(activeChecks).values()) {
|
||||
state.update();
|
||||
|
||||
if (state.isExpired()) {
|
||||
onFail(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean tryComplete(UUID playerId, boolean isSuccessZone) {
|
||||
CheckState state = activeChecks.get(playerId);
|
||||
if (state == null) return false;
|
||||
|
||||
if (isSuccessZone) {
|
||||
onSuccess(state);
|
||||
} else {
|
||||
onFail(state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSuccess(CheckState state) {
|
||||
activeChecks.remove(state.playerId);
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
generatorRepair.onSkillCheckResult(state.playerId, true, state.generator)
|
||||
);
|
||||
}
|
||||
|
||||
private void onFail(CheckState state) {
|
||||
activeChecks.remove(state.playerId);
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
generatorRepair.onSkillCheckResult(state.playerId, false, state.generator)
|
||||
);
|
||||
}
|
||||
|
||||
public boolean isActive(UUID playerId) {
|
||||
return activeChecks.containsKey(playerId);
|
||||
}
|
||||
|
||||
public CheckState getState(UUID playerId) {
|
||||
return activeChecks.get(playerId);
|
||||
}
|
||||
|
||||
public static class CheckState {
|
||||
final UUID playerId;
|
||||
final Generator generator;
|
||||
final double zoneStart;
|
||||
final double zoneEnd;
|
||||
double pointerPosition;
|
||||
final double rotationSpeed;
|
||||
int ticks;
|
||||
|
||||
CheckState(UUID playerId, Generator generator, double zoneStart, double zoneEnd,
|
||||
double pointerPosition, double rotationSpeed) {
|
||||
this.playerId = playerId;
|
||||
this.generator = generator;
|
||||
this.zoneStart = zoneStart;
|
||||
this.zoneEnd = zoneEnd;
|
||||
this.pointerPosition = pointerPosition;
|
||||
this.rotationSpeed = rotationSpeed;
|
||||
this.ticks = 0;
|
||||
}
|
||||
|
||||
void update() {
|
||||
pointerPosition += rotationSpeed * 0.01;
|
||||
if (pointerPosition >= 1.0) {
|
||||
pointerPosition = 0.0;
|
||||
}
|
||||
ticks++;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return ticks > 100;
|
||||
}
|
||||
|
||||
boolean isInSuccessZone() {
|
||||
return pointerPosition >= zoneStart && pointerPosition <= zoneEnd;
|
||||
}
|
||||
|
||||
public String getVisualRepresentation() {
|
||||
int zoneStartPos = (int) (zoneStart * 20);
|
||||
int zoneEndPos = (int) (zoneEnd * 20);
|
||||
int pointerPos = (int) (pointerPosition * 20);
|
||||
|
||||
StringBuilder sb = new StringBuilder("<white>[");
|
||||
for (int i = 0; i < 20; i++) {
|
||||
if (i == pointerPos) {
|
||||
sb.append("<red>◆");
|
||||
} else if (i >= zoneStartPos && i <= zoneEndPos) {
|
||||
sb.append("<green>█");
|
||||
} else {
|
||||
sb.append("<gray>░");
|
||||
}
|
||||
}
|
||||
sb.append("<white>]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public double getPointerPosition() {
|
||||
return pointerPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
package dev.loki.objective;
|
||||
|
||||
import dev.loki.config.GameConfig;
|
||||
import dev.loki.mdbd;
|
||||
import dev.loki.objective.Generator;
|
||||
import dev.loki.objective.GeneratorRepair;
|
||||
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, Generator generator, GeneratorRepair generatorRepair) {
|
||||
SkillCheckSession session = new SkillCheckSession(player, playerId, skillCheck, generator, generatorRepair);
|
||||
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.generatorRepair.onSkillCheckResult(playerId, success, session.generator);
|
||||
}
|
||||
|
||||
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.generatorRepair.onSkillCheckResult(playerId, false, session.generator);
|
||||
}
|
||||
|
||||
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;
|
||||
final Generator generator;
|
||||
final GeneratorRepair generatorRepair;
|
||||
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, Generator generator, GeneratorRepair generatorRepair) {
|
||||
this.player = player;
|
||||
this.playerId = playerId;
|
||||
this.skillCheck = skillCheck;
|
||||
this.generator = generator;
|
||||
this.generatorRepair = generatorRepair;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
package dev.loki.objective;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class SkillCheckUI {
|
||||
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
|
||||
|
||||
public static void showSkillCheck(Player player, SkillCheck.CheckState state) {
|
||||
String visual = buildVisualDisplay(state);
|
||||
Component title = MINI_MESSAGE.deserialize(visual);
|
||||
Component subtitle = MINI_MESSAGE.deserialize("<gray>Нажмите <yellow>ПКМ</yellow> в зеленой зоне!");
|
||||
|
||||
player.showTitle(Title.title(title, subtitle));
|
||||
}
|
||||
|
||||
public static void showActionBarSkillCheck(Player player, SkillCheck.CheckState state) {
|
||||
String visual = buildCompactVisual(state);
|
||||
player.sendActionBar(MiniMessage.miniMessage().deserialize(visual));
|
||||
}
|
||||
|
||||
public static void showSuccess(Player player) {
|
||||
player.showTitle(Title.title(
|
||||
MINI_MESSAGE.deserialize("<green>✓ Отлично!"),
|
||||
Component.empty(),
|
||||
Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(500), java.time.Duration.ZERO)
|
||||
));
|
||||
}
|
||||
|
||||
public static void showFail(Player player) {
|
||||
player.showTitle(Title.title(
|
||||
MINI_MESSAGE.deserialize("<red>✗ Провал!"),
|
||||
MINI_MESSAGE.deserialize("<gray>Потеря прогресса..."),
|
||||
Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(800), java.time.Duration.ZERO)
|
||||
));
|
||||
}
|
||||
|
||||
public static void clear(Player player) {
|
||||
player.clearTitle();
|
||||
player.sendActionBar(Component.empty());
|
||||
}
|
||||
|
||||
private static String buildVisualDisplay(SkillCheck.CheckState state) {
|
||||
double zoneStart = state.zoneStart;
|
||||
double zoneEnd = state.zoneEnd;
|
||||
double pointer = state.getPointerPosition();
|
||||
|
||||
int displayWidth = 15;
|
||||
int zoneStartPos = (int) (zoneStart * displayWidth);
|
||||
int zoneEndPos = (int) (zoneEnd * displayWidth);
|
||||
int pointerPos = (int) (pointer * displayWidth);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<gray>");
|
||||
|
||||
for (int i = 0; i < displayWidth; i++) {
|
||||
if (i == pointerPos) {
|
||||
sb.append("<white>◆");
|
||||
} else if (i >= zoneStartPos && i <= zoneEndPos) {
|
||||
sb.append("<green>█");
|
||||
} else {
|
||||
sb.append("<dark_gray>▒");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("<gray>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String buildCompactVisual(SkillCheck.CheckState state) {
|
||||
double zoneStart = state.zoneStart;
|
||||
double zoneEnd = state.zoneEnd;
|
||||
double pointer = state.getPointerPosition();
|
||||
|
||||
int displayWidth = 10;
|
||||
int zoneStartPos = (int) (zoneStart * displayWidth);
|
||||
int zoneEndPos = (int) (zoneEnd * displayWidth);
|
||||
int pointerPos = (int) (pointer * displayWidth);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<yellow>QTE: <white>[");
|
||||
|
||||
for (int i = 0; i < displayWidth; i++) {
|
||||
if (i == pointerPos) {
|
||||
sb.append("<red>♦");
|
||||
} else if (i >= zoneStartPos && i <= zoneEndPos) {
|
||||
sb.append("<green>█");
|
||||
} else {
|
||||
sb.append("<gray>░");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("<white>]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String getSkillCheckText(UUID playerId, double pointer, double zoneStart, double zoneEnd) {
|
||||
int width = 12;
|
||||
int pPos = (int) (pointer * width);
|
||||
int zStart = (int) (zoneStart * width);
|
||||
int zEnd = (int) (zoneEnd * width);
|
||||
|
||||
StringBuilder sb = new StringBuilder("<gray>[");
|
||||
for (int i = 0; i < width; i++) {
|
||||
if (i == pPos) {
|
||||
sb.append("<white>◆");
|
||||
} else if (i >= zStart && i <= zEnd) {
|
||||
sb.append("<green>█");
|
||||
} else {
|
||||
sb.append("<dark_gray>░");
|
||||
}
|
||||
}
|
||||
sb.append("<gray>]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +78,15 @@ public class EndgamePhase implements Phase {
|
|||
gateManager = session.getGateManager();
|
||||
}
|
||||
|
||||
// Если ворота уже открыты — игра завершилась по таймеру эндгейма,
|
||||
// показываем результаты сразу
|
||||
if (gateManager != null && gateManager.getOpenGateCount() > 0) {
|
||||
this.complete = true;
|
||||
announceEndgameEnd(session);
|
||||
teleportAllToLobby(session);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up sidebar
|
||||
sidebar = new Sidebar();
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
|
|
@ -101,7 +110,6 @@ public class EndgamePhase implements Phase {
|
|||
}
|
||||
}
|
||||
|
||||
updateGates(session);
|
||||
updateHatch(session);
|
||||
checkWinConditions(session);
|
||||
}
|
||||
|
|
@ -123,6 +131,10 @@ public class EndgamePhase implements Phase {
|
|||
hatch.reset();
|
||||
}
|
||||
|
||||
if (gateManager != null) {
|
||||
gateManager.resetAll();
|
||||
}
|
||||
|
||||
// exit() НЕ вызывает session.stop() — это делает transitionToNextPhase()
|
||||
// Телепорт и результаты уже обработаны в enter() или будут в teleportAllToLobby
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public class GameplayDamageSystem {
|
|||
private Listener vanillaDamageBlocker;
|
||||
private ItemUseListener itemUseListener;
|
||||
private PlayerJumpListener jumpListener;
|
||||
private org.bukkit.scheduler.BukkitTask swimmingTask;
|
||||
|
||||
/**
|
||||
* Initialize damage and combat system.
|
||||
|
|
@ -83,18 +84,21 @@ public class GameplayDamageSystem {
|
|||
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
|
||||
p.setHealth(20.0);
|
||||
p.setSwimming(true);
|
||||
Bukkit.getScheduler().runTaskTimer(plugin, task -> {
|
||||
swimmingTask = new org.bukkit.scheduler.BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!p.isOnline()) {
|
||||
task.cancel();
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (survivor.getState() != PlayerState.DOWNED) {
|
||||
p.setSwimming(false);
|
||||
task.cancel();
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (!p.isSwimming()) p.setSwimming(true);
|
||||
}, 1L, 5L);
|
||||
}
|
||||
}.runTaskTimer(plugin, 1L, 5L);
|
||||
});
|
||||
|
||||
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
|
||||
|
|
@ -138,6 +142,10 @@ public class GameplayDamageSystem {
|
|||
* Clear all damage system resources.
|
||||
*/
|
||||
public void clear() {
|
||||
if (swimmingTask != null) {
|
||||
swimmingTask.cancel();
|
||||
swimmingTask = null;
|
||||
}
|
||||
if (jumpListener != null) {
|
||||
jumpListener.clearInGamePlayers();
|
||||
jumpListener.unregister();
|
||||
|
|
@ -156,7 +164,15 @@ public class GameplayDamageSystem {
|
|||
*/
|
||||
public void clearManagers() {
|
||||
if (killerController != null) killerController.shutdown();
|
||||
if (playerManager != null) playerManager.clear();
|
||||
if (playerManager != null) {
|
||||
for (BasePlayer bp : playerManager.getAllPlayers()) {
|
||||
Player p = bp.getPlayer();
|
||||
if (p != null && p.isOnline()) {
|
||||
p.setSwimming(false);
|
||||
}
|
||||
}
|
||||
playerManager.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import dev.loki.config.GameConfig;
|
|||
import dev.loki.core.GameSession;
|
||||
import dev.loki.hook.*;
|
||||
import dev.loki.player.BasePlayer;
|
||||
import dev.loki.player.PlayerState;
|
||||
import dev.loki.player.Survivor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
|
|
@ -18,6 +20,10 @@ public class GameplayHookHandler {
|
|||
private final HookSacrifice hookSacrifice;
|
||||
private final HookStruggle hookStruggle;
|
||||
private final HookRecovery hookRecovery;
|
||||
private GameSession session;
|
||||
|
||||
private static final double CAMP_REGRESS_RANGE = 7.0;
|
||||
private static final double CAMP_STALL_RANGE = 13.0;
|
||||
|
||||
public GameplayHookHandler(HookManager hookManager, HookSacrifice hookSacrifice,
|
||||
HookStruggle hookStruggle, HookRecovery hookRecovery) {
|
||||
|
|
@ -27,12 +33,17 @@ public class GameplayHookHandler {
|
|||
this.hookRecovery = hookRecovery;
|
||||
}
|
||||
|
||||
public void setSession(GameSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
private int hookTpTick;
|
||||
|
||||
/**
|
||||
* Called every tick - checks hook stage progression and teleports hooked players back.
|
||||
* Called every tick - checks hook stage progression, killer proximity, and teleports hooked players back.
|
||||
*/
|
||||
public void tick() {
|
||||
applyKillerProximity();
|
||||
checkHookStages();
|
||||
hookTpTick++;
|
||||
if (hookTpTick >= 20) {
|
||||
|
|
@ -41,6 +52,45 @@ public class GameplayHookHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the killer is near a hook, slow or regress the hook progression.
|
||||
* < 7 blocks → regress (goes negative)
|
||||
* 7-13 blocks → stall (no progression)
|
||||
* > 13 blocks → normal
|
||||
*/
|
||||
private void applyKillerProximity() {
|
||||
if (session == null) return;
|
||||
|
||||
Location killerLoc = null;
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
if (bp.isKiller()) {
|
||||
Player p = bp.getPlayer();
|
||||
if (p != null && p.isOnline()) {
|
||||
killerLoc = p.getLocation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (killerLoc == null) return;
|
||||
|
||||
for (Hook hook : hookManager.getAllHooks()) {
|
||||
if (!hook.isOccupied()) continue;
|
||||
if (!killerLoc.getWorld().equals(hook.getLocation().getWorld())) continue;
|
||||
|
||||
double dist = killerLoc.distance(hook.getLocation());
|
||||
|
||||
if (dist < CAMP_REGRESS_RANGE) {
|
||||
// Regress: -2 per tick (faster than normal +1)
|
||||
hook.addStageProgress(-2);
|
||||
// If STAGE_1 and progress goes to 0, that's as low as it gets
|
||||
} else if (dist < CAMP_STALL_RANGE) {
|
||||
// Stall: cancel the normal +1 by adding -1
|
||||
hook.addStageProgress(-1);
|
||||
}
|
||||
// else: normal progression (Hook.tick already adds +1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleport hooked survivors back to their hook if they've moved too far.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -110,21 +110,7 @@ public class GameplayInteraction {
|
|||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
});
|
||||
// Escape check is handled in GameplayPhase.checkEscapes() every tick during POSTGAME
|
||||
}
|
||||
|
||||
private void wireInteractListener(GameSession session) {
|
||||
|
|
@ -156,12 +142,15 @@ public class GameplayInteraction {
|
|||
Survivor carried = (Survivor) other;
|
||||
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
|
||||
killer.setCarrying(false);
|
||||
// Only teleport if actually hooked (not dead from exceeding hook limit)
|
||||
if (carried.getState() == PlayerState.HOOKED) {
|
||||
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
|
||||
if (nearest != null) {
|
||||
Player vp = carried.getPlayer();
|
||||
if (vp != null) vp.teleport(nearest.getLocation());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -190,11 +179,13 @@ public class GameplayInteraction {
|
|||
Survivor carried = (Survivor) other;
|
||||
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
|
||||
killer.setCarrying(false);
|
||||
if (carried.getState() == PlayerState.HOOKED) {
|
||||
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
|
||||
if (nearest != null) {
|
||||
Player vp = carried.getPlayer();
|
||||
if (vp != null) vp.teleport(nearest.getLocation());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import dev.loki.player.Survivor;
|
|||
import dev.loki.ui.ActionBar;
|
||||
import dev.loki.ui.TitleUI;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
|
|
@ -33,15 +35,20 @@ import org.bukkit.Sound;
|
|||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GameplayPhase implements Phase {
|
||||
|
||||
private enum EndgameState { NONE, PREGAME, POSTGAME }
|
||||
|
||||
private final GeneratorManager generatorManager;
|
||||
private int tickCounter;
|
||||
private boolean shouldEnd;
|
||||
private boolean allGensDone;
|
||||
private EndgameState endgameState = EndgameState.NONE;
|
||||
private int endgameTimer; // секунд до смены состояния
|
||||
|
||||
// Sub-handlers
|
||||
private GameplayHookHandler hookHandler;
|
||||
|
|
@ -81,6 +88,8 @@ public class GameplayPhase implements Phase {
|
|||
tickCounter = 0;
|
||||
shouldEnd = false;
|
||||
allGensDone = false;
|
||||
endgameState = EndgameState.NONE;
|
||||
endgameTimer = 0;
|
||||
winConditions = new GameplayWinConditions();
|
||||
|
||||
initManagers(session, plugin, cfg);
|
||||
|
|
@ -104,7 +113,6 @@ public class GameplayPhase implements Phase {
|
|||
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) {
|
||||
|
|
@ -119,19 +127,29 @@ public class GameplayPhase implements Phase {
|
|||
}
|
||||
|
||||
if (tickCounter % 20 == 0) {
|
||||
tickEndgame(session);
|
||||
checkGeneratorProgress(session);
|
||||
if (endgameState == EndgameState.NONE) {
|
||||
winConditions.checkWinConditions(session);
|
||||
if (winConditions.isShouldEnd() || timeExpired) {
|
||||
if (winConditions.isShouldEnd()) {
|
||||
shouldEnd = true;
|
||||
}
|
||||
} else {
|
||||
// Во время эндгейма игра заканчивается когда все выжившие мертвы/сбежали
|
||||
if (allSurvivorsIncapacitated(session)) {
|
||||
shouldEnd = true;
|
||||
}
|
||||
// Или когда истёк таймер после открытия ворот
|
||||
if (endgameState == EndgameState.POSTGAME && endgameTimer <= 0) {
|
||||
shouldEnd = true;
|
||||
}
|
||||
}
|
||||
ui.updateSidebar(session, generatorManager);
|
||||
}
|
||||
|
||||
if (timeExpired) {
|
||||
showGameResults(session);
|
||||
teleportAllToLobby(session);
|
||||
session.stop();
|
||||
return;
|
||||
// Escape check — каждый тик для мгновенной реакции
|
||||
if (endgameState == EndgameState.POSTGAME) {
|
||||
checkEscapes(session);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,6 +211,7 @@ public class GameplayPhase implements Phase {
|
|||
|
||||
hookRescue = new HookRescue(plugin, ab, cfg);
|
||||
hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery);
|
||||
hookHandler.setSession(session);
|
||||
hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim));
|
||||
|
||||
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg);
|
||||
|
|
@ -267,12 +286,30 @@ public class GameplayPhase implements Phase {
|
|||
// Tick helpers
|
||||
// =========================================================================
|
||||
|
||||
/** Все выжившие мертвы или сбежали? */
|
||||
private boolean allSurvivorsIncapacitated(GameSession session) {
|
||||
boolean hasSurvivor = false;
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
if (bp.isKiller()) continue;
|
||||
hasSurvivor = true;
|
||||
PlayerState st = bp.getState();
|
||||
if (st != PlayerState.DEAD && st != PlayerState.ESCAPED
|
||||
&& st != PlayerState.DOWNED && st != PlayerState.HOOKED
|
||||
&& st != PlayerState.CARRIED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return hasSurvivor;
|
||||
}
|
||||
|
||||
private void checkGeneratorProgress(GameSession session) {
|
||||
if (allGensDone) return;
|
||||
if (generatorManager.areAllRequiredCompleted()) {
|
||||
allGensDone = true;
|
||||
interaction.setAllGensDone(true);
|
||||
gateManager.activateEndgame();
|
||||
endgameState = EndgameState.PREGAME;
|
||||
endgameTimer = session.getConfig().getPregameSeconds();
|
||||
Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done"));
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
Player p = bp.getPlayer();
|
||||
|
|
@ -281,6 +318,63 @@ public class GameplayPhase implements Phase {
|
|||
}
|
||||
}
|
||||
|
||||
private void tickEndgame(GameSession session) {
|
||||
if (endgameState == EndgameState.NONE) return;
|
||||
|
||||
endgameTimer--;
|
||||
|
||||
if (endgameState == EndgameState.PREGAME) {
|
||||
// Каждую секунду шлём сообщение о начале эндгейма
|
||||
if (endgameTimer > 0) {
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
Player p = bp.getPlayer();
|
||||
if (p == null) continue;
|
||||
p.sendActionBar(net.kyori.adventure.text.Component.text(
|
||||
"§c⚡ Эндгейм! Ворота откроются через §e" + endgameTimer + "с"));
|
||||
}
|
||||
} else {
|
||||
// Таймер истёк — открываем ворота
|
||||
gateManager.openAllGates();
|
||||
endgameState = EndgameState.POSTGAME;
|
||||
endgameTimer = session.getConfig().getPostgameSeconds();
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
Player p = bp.getPlayer();
|
||||
if (p == null) continue;
|
||||
Title title = Title.title(
|
||||
Component.text("ВОРОТА ОТКРЫТЫ!").color(NamedTextColor.GREEN),
|
||||
Component.text("Бегите к выходу! (" + endgameTimer + "с)").color(NamedTextColor.YELLOW),
|
||||
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))
|
||||
);
|
||||
p.showTitle(title);
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
} else if (endgameState == EndgameState.POSTGAME) {
|
||||
if (endgameTimer > 0 && endgameTimer % 10 == 0 && endgameTimer <= 30) {
|
||||
Bukkit.broadcast(Component.text("§c⚠ До конца игры: §e" + endgameTimer + "с"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkEscapes(GameSession session) {
|
||||
double escapeRange = session.getConfig().getEscapeRange();
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
if (bp.isKiller()) continue;
|
||||
if (bp.getState() == PlayerState.ESCAPED) continue;
|
||||
if (bp.getState() != PlayerState.HEALTHY && bp.getState() != PlayerState.INJURED) continue;
|
||||
Survivor survivor = (Survivor) bp;
|
||||
Player p = survivor.getPlayer();
|
||||
if (p == null || !p.isOnline()) continue;
|
||||
ExitGate gate = gateManager.getGateAt(p.getLocation(), escapeRange);
|
||||
if (gate != null && gate.isOpen()) {
|
||||
survivor.setState(PlayerState.ESCAPED);
|
||||
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
|
||||
Bukkit.broadcast(Component.text(
|
||||
"§6[DBD] §a" + p.getName() + " §6сбежал через ворота!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnGameBlocks(mdbd plugin, GameConfig cfg) {
|
||||
// Спавн блоков на местах генераторов
|
||||
Material genMat = Material.matchMaterial(cfg.getGeneratorBlock());
|
||||
|
|
@ -333,35 +427,6 @@ public class GameplayPhase implements Phase {
|
|||
// 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 {
|
||||
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 teleportAllToLobby(GameSession session) {
|
||||
for (BasePlayer bp : session.getPlayers().values()) {
|
||||
Player p = bp.getPlayer();
|
||||
|
|
@ -395,12 +460,11 @@ public class GameplayPhase implements Phase {
|
|||
if (hookStruggle != null) hookStruggle.clear();
|
||||
if (chestManager != null) chestManager.stopLootResetTimer();
|
||||
if (chestHologram != null) chestHologram.stop();
|
||||
// Clean up gate blocks
|
||||
// Clean up gate blocks — don't reset gate state (EndgamePhase needs to know gates were open)
|
||||
if (gateManager != null) {
|
||||
for (ExitGate gate : gateManager.getAllGates()) {
|
||||
gate.removeGateBlock();
|
||||
}
|
||||
gateManager.resetAll();
|
||||
}
|
||||
session.getPlugin().getInteractListener().setGeneratorManager(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import dev.loki.config.GameConfig;
|
|||
import dev.loki.mdbd;
|
||||
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.minimessage.MiniMessage;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
|
@ -98,6 +98,10 @@ public class SurvivorHealing {
|
|||
if (targetSurvivor == null) { cancelHealing(); return; }
|
||||
|
||||
PlayerState targetState = targetSurvivor.getState();
|
||||
if (targetState == PlayerState.DEAD || targetState == PlayerState.ESCAPED) {
|
||||
cancelHealing();
|
||||
return;
|
||||
}
|
||||
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) {
|
||||
completeHealing(targetSurvivor);
|
||||
return;
|
||||
|
|
@ -166,7 +170,7 @@ public class SurvivorHealing {
|
|||
String bar = renderProgressBar(progress);
|
||||
String template = mdbd.getInstance().getConfigManager().getMessages().getString(
|
||||
isSelfHeal ? "survivor.heal.self-start" : "survivor.heal.other-start", bar);
|
||||
Component message = MiniMessage.miniMessage().deserialize(template);
|
||||
Component message = TextUtil.color(template);
|
||||
healingTarget.sendActionBar(message);
|
||||
// Also show on healer
|
||||
Player healer = controller.getPlayer();
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public class SurvivorMovement {
|
|||
if (player == null) return;
|
||||
|
||||
if (isCrawling) {
|
||||
player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 5, 2, false, false));
|
||||
player.addPotionEffect(new PotionEffect(PotionEffectType.SLOW_FALLING, 10, 0, false, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import dev.loki.hook.Hook;
|
|||
import dev.loki.hook.HookStage;
|
||||
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.TextComponent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
|
@ -12,7 +13,7 @@ import org.bukkit.entity.Player;
|
|||
public class ActionBar {
|
||||
|
||||
public void sendMessage(Player player, String message) {
|
||||
player.sendActionBar(Component.text(message));
|
||||
player.sendActionBar(TextUtil.color(message));
|
||||
}
|
||||
|
||||
public void sendComponent(Player player, Component component) {
|
||||
|
|
@ -26,20 +27,20 @@ public class ActionBar {
|
|||
|
||||
public void showRepairProgress(Player player, double percent, boolean skillCheckActive) {
|
||||
Component bar = ProgressBar.create(percent);
|
||||
TextComponent message = Component.text("Repairing ")
|
||||
TextComponent message = Component.text("Починка: ")
|
||||
.color(NamedTextColor.WHITE)
|
||||
.append(bar)
|
||||
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
||||
|
||||
if (skillCheckActive) {
|
||||
message = message.append(Component.text(" ⚠ SKILL CHECK!").color(NamedTextColor.YELLOW));
|
||||
message = message.append(Component.text(" ⚠ СКИЛЛ-ЧЕК!").color(NamedTextColor.YELLOW));
|
||||
}
|
||||
|
||||
player.sendActionBar(message);
|
||||
}
|
||||
|
||||
public void showHealProgress(Player player, double percent, boolean selfHeal) {
|
||||
String label = selfHeal ? "Healing self" : "Healing ally";
|
||||
String label = selfHeal ? "Лечение себя" : "Лечение союзника";
|
||||
Component bar = ProgressBar.create(percent);
|
||||
Component message = Component.text(label + " ")
|
||||
.color(NamedTextColor.WHITE)
|
||||
|
|
@ -50,7 +51,7 @@ public class ActionBar {
|
|||
|
||||
public void showRescueProgress(Player player, double percent) {
|
||||
Component bar = ProgressBar.create(percent);
|
||||
Component message = Component.text("Rescuing ")
|
||||
Component message = Component.text("Спасение: ")
|
||||
.color(NamedTextColor.WHITE)
|
||||
.append(bar)
|
||||
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
||||
|
|
@ -71,15 +72,15 @@ public class ActionBar {
|
|||
if (stage == HookStage.STAGE_1) {
|
||||
stageIcon = StatusIcons.HOOK;
|
||||
stageColor = NamedTextColor.YELLOW;
|
||||
stageName = "Stage 1";
|
||||
stageName = "Стадия 1";
|
||||
} else if (stage == HookStage.STAGE_2) {
|
||||
stageIcon = StatusIcons.HOOK;
|
||||
stageColor = NamedTextColor.RED;
|
||||
stageName = "Stage 2 - STRUGGLE!";
|
||||
stageName = "Стадия 2 — ВЫРВИСЬ!";
|
||||
} else {
|
||||
stageIcon = StatusIcons.DEATH_SKULL;
|
||||
stageColor = NamedTextColor.DARK_RED;
|
||||
stageName = "SACRIFICE IMMINENT";
|
||||
stageName = "ЖЕРТВОПРИНОШЕНИЕ";
|
||||
}
|
||||
|
||||
Component bar = stage == HookStage.STAGE_2
|
||||
|
|
@ -91,7 +92,7 @@ public class ActionBar {
|
|||
.append(bar);
|
||||
|
||||
if (stage == HookStage.STAGE_2) {
|
||||
message = message.append(Component.text(" CROUCH to struggle!").color(NamedTextColor.YELLOW));
|
||||
message = message.append(Component.text(" ПРИСЯДЬ чтобы вырваться!").color(NamedTextColor.YELLOW));
|
||||
}
|
||||
|
||||
player.sendActionBar(message);
|
||||
|
|
@ -108,22 +109,22 @@ public class ActionBar {
|
|||
private Component formatStatus(PlayerState state) {
|
||||
return switch (state) {
|
||||
case HEALTHY -> StatusIcons.HEALTHY_HEART
|
||||
.append(Component.text(" Healthy").color(NamedTextColor.GREEN));
|
||||
.append(Component.text(" Здоров").color(NamedTextColor.GREEN));
|
||||
case INJURED -> StatusIcons.INJURED_HEART
|
||||
.append(Component.text(" Injured").color(NamedTextColor.YELLOW));
|
||||
case DOWNED -> Component.text("🆘 Downed - need help!").color(NamedTextColor.RED);
|
||||
.append(Component.text(" Ранен").color(NamedTextColor.YELLOW));
|
||||
case DOWNED -> Component.text("🆘 В нокдауне — нужна помощь!").color(NamedTextColor.RED);
|
||||
case HOOKED -> StatusIcons.HOOK
|
||||
.append(Component.text(" Hooked!").color(NamedTextColor.DARK_RED));
|
||||
case CARRIED -> Component.text("🎒 Being carried").color(NamedTextColor.RED);
|
||||
.append(Component.text(" На крюке!").color(NamedTextColor.DARK_RED));
|
||||
case CARRIED -> Component.text("🎒 Унесен маньяком").color(NamedTextColor.RED);
|
||||
default -> Component.text("");
|
||||
};
|
||||
}
|
||||
|
||||
public void showCarrying(Player killer, String survivorName) {
|
||||
Component message = Component.text("Carrying: ")
|
||||
Component message = Component.text("Несет: ")
|
||||
.color(NamedTextColor.WHITE)
|
||||
.append(Component.text(survivorName).color(NamedTextColor.YELLOW))
|
||||
.append(Component.text(" - Find a hook!").color(NamedTextColor.GRAY));
|
||||
.append(Component.text(" — Найди крюк!").color(NamedTextColor.GRAY));
|
||||
killer.sendActionBar(message);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,14 +94,14 @@ public class 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("До конца")) {
|
||||
if (timerObj.getScore(entry).getScore() == 0 || entry.contains("⏱") || entry.contains("Time left")) {
|
||||
board.resetScores(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use a FIXED entry key that never changes - only the score updates
|
||||
String timerEntry = "§6⏱ До конца: §c" + seconds + " сек";
|
||||
String timerEntry = "§6⏱ Осталось: §c" + seconds + "с";
|
||||
|
||||
// Simply update the score - the entry persists
|
||||
Score score = timerObj.getScore(timerEntry);
|
||||
|
|
@ -124,7 +124,7 @@ public class Sidebar {
|
|||
|
||||
// Clear entries and hide
|
||||
for (String entry : board.getEntries()) {
|
||||
if (entry.contains("⏱") || entry.contains("До конца")) {
|
||||
if (entry.contains("⏱") || entry.contains("Осталось")) {
|
||||
board.resetScores(entry);
|
||||
}
|
||||
}
|
||||
|
|
@ -155,19 +155,17 @@ public class Sidebar {
|
|||
if (endgameSecondsLeft >= 0) {
|
||||
String timerColor = endgameSecondsLeft <= 30 ? "§c" : "§e";
|
||||
setScore(objective, "§8─────────────────", line--);
|
||||
setScore(objective, timerColor + "⏱ До конца: " + endgameSecondsLeft + " сек", line--);
|
||||
setScore(objective, timerColor + "⏱ Осталось: " + endgameSecondsLeft + "с", line--);
|
||||
setScore(objective, "§8─────────────────", line--);
|
||||
}
|
||||
|
||||
// Generator progress
|
||||
// Generator progress — use actual placed count, not config max
|
||||
int completedGens = generatorManager != null ? generatorManager.getCompletedCount() : 0;
|
||||
int requiredGens = cfg.getGeneratorRequired();
|
||||
int totalGens = cfg.getGeneratorTotal();
|
||||
int totalGens = generatorManager != null ? generatorManager.getTotalCount() : cfg.getGeneratorRequired();
|
||||
|
||||
setScore(objective, "§eГенераторы:", line--);
|
||||
String genBar = buildGenBar(completedGens, requiredGens);
|
||||
setScore(objective, " §f" + completedGens + "/" + requiredGens + " " + genBar, line--);
|
||||
setScore(objective, " §7(всего: " + totalGens + ")", line--);
|
||||
String genBar = buildGenBar(completedGens, totalGens);
|
||||
setScore(objective, " §f" + completedGens + "/" + totalGens + " " + genBar, line--);
|
||||
setScore(objective, " ", line--);
|
||||
|
||||
// Survivor list with hearts and colors
|
||||
|
|
|
|||
|
|
@ -33,16 +33,16 @@ public class TitleUI {
|
|||
Component subtitle;
|
||||
|
||||
if (isKiller) {
|
||||
title = Component.text("KILLER")
|
||||
title = Component.text("МАНЬЯК")
|
||||
.color(NamedTextColor.DARK_RED)
|
||||
.decoration(TextDecoration.BOLD, true);
|
||||
subtitle = Component.text("Sacrifice them all")
|
||||
subtitle = Component.text("Принеси их в жертву")
|
||||
.color(NamedTextColor.RED);
|
||||
} else {
|
||||
title = Component.text("SURVIVOR")
|
||||
title = Component.text("ВЫЖИВШИЙ")
|
||||
.color(NamedTextColor.DARK_GREEN)
|
||||
.decoration(TextDecoration.BOLD, true);
|
||||
subtitle = Component.text("Repair generators and escape")
|
||||
subtitle = Component.text("Чини генераторы и выбирайся")
|
||||
.color(NamedTextColor.GREEN);
|
||||
}
|
||||
|
||||
|
|
@ -57,17 +57,17 @@ public class TitleUI {
|
|||
);
|
||||
|
||||
Component title = escaped
|
||||
? Component.text("ESCAPED!")
|
||||
? Component.text("СБЕЖАЛ!")
|
||||
.color(NamedTextColor.GREEN)
|
||||
.decoration(TextDecoration.BOLD, true)
|
||||
: Component.text("VICTORY!")
|
||||
: Component.text("ПОБЕДА!")
|
||||
.color(NamedTextColor.DARK_RED)
|
||||
.decoration(TextDecoration.BOLD, true);
|
||||
|
||||
Component subtitle = escaped
|
||||
? Component.text("You survived the night")
|
||||
? Component.text("Ты пережил ночь")
|
||||
.color(NamedTextColor.DARK_GREEN)
|
||||
: Component.text("All survivors sacrificed")
|
||||
: Component.text("Все выжившие принесены в жертву")
|
||||
.color(NamedTextColor.RED);
|
||||
|
||||
player.showTitle(Title.title(title, subtitle, times));
|
||||
|
|
@ -80,11 +80,11 @@ public class TitleUI {
|
|||
Duration.ofSeconds(1)
|
||||
);
|
||||
|
||||
Component title = Component.text("DEFEAT")
|
||||
Component title = Component.text("ПОРАЖЕНИЕ")
|
||||
.color(NamedTextColor.DARK_RED)
|
||||
.decoration(TextDecoration.BOLD, true);
|
||||
|
||||
Component subtitle = Component.text("The Entity claims all")
|
||||
Component subtitle = Component.text("Сущность забирает всех")
|
||||
.color(NamedTextColor.RED);
|
||||
|
||||
player.showTitle(Title.title(title, subtitle, times));
|
||||
|
|
|
|||
|
|
@ -195,12 +195,21 @@ endgame:
|
|||
# Время открытия ворот (секунд)
|
||||
gate-open-seconds: 20
|
||||
|
||||
# Секунд до открытия ворот (после починки всех ген)
|
||||
pregame-seconds: 30
|
||||
|
||||
# Секунд после открытия ворот до конца игры
|
||||
postgame-seconds: 60
|
||||
|
||||
# Открытие сундуков (секунд)
|
||||
chest-open-seconds: 3
|
||||
|
||||
# Расстояние для взаимодействия с воротами (блоков)
|
||||
gate-interaction-range: 3.0
|
||||
|
||||
# Расстояние до ворот для побега (блоков)
|
||||
escape-range: 2.0
|
||||
|
||||
# Уведомление маньяка о генераторе
|
||||
killer-notification:
|
||||
# Время действия компаса (секунд)
|
||||
|
|
|
|||
|
|
@ -116,6 +116,11 @@ setup:
|
|||
survivor-spawn-placed: "%setup-prefix%&aСпавн для &6%s &aвыжившего установлен! (всего: %d/4) %s"
|
||||
wand-point: "%setup-prefix%&a[Wand] Точка %d: %s"
|
||||
map-cleared: "%setup-prefix%&aКарта очищена!"
|
||||
cleared: "%setup-prefix%&aВсе объекты удалены из конфига!"
|
||||
cleared-type: "%setup-prefix%&aОбъекты &6%s &aудалены из конфига!"
|
||||
delete-usage: "%setup-prefix%&cИспользование: /dbdsetup delete <тип> <номер>"
|
||||
out-of-range: "%setup-prefix%&cИндекс &6%d &cвне диапазона!"
|
||||
deleted: "%setup-prefix%&aОбъект &6%s &aудалён!"
|
||||
spawns-cleared: "%setup-prefix%&aВсе спавны (киллер + выжившие) очищены!"
|
||||
lobby-set: "%setup-prefix%&aЛобби установлено: %s"
|
||||
|
||||
|
|
@ -200,7 +205,7 @@ survivor:
|
|||
actionbar: "&a⚕ %s %d%%"
|
||||
done: "%prefix%&a✔ Вы вылечились!"
|
||||
cancelled: "%prefix%&eЛечение прервано."
|
||||
depleted: "&cYour medkit has been depleted!"
|
||||
depleted: "&cМедкит закончился!"
|
||||
|
||||
# ── Крюки ──────────────────────────────────────
|
||||
|
||||
|
|
@ -228,18 +233,22 @@ hook:
|
|||
sacrifice:
|
||||
message: "%prefix%&4Вы были принесены в жертву. Наблюдайте за матчем."
|
||||
title: "☠"
|
||||
subtitle: "&cYou have been sacrificed"
|
||||
broadcast: "%s has been sacrificed to the Entity"
|
||||
subtitle: "&cВы были принесены в жертву"
|
||||
broadcast: "%s принесен в жертву Сущности"
|
||||
|
||||
# ── Сундуки ────────────────────────────────────
|
||||
|
||||
chest:
|
||||
empty: "&cThis chest is empty!"
|
||||
opening: "&eOpening chest..."
|
||||
cancelled: "&cOpening cancelled!"
|
||||
empty: "&cСундук пуст!"
|
||||
opening: "&eОткрытие сундука..."
|
||||
cancelled: "&cОткрытие прервано!"
|
||||
too-far: "%prefix%&cСлишком далеко!"
|
||||
actionbar: "&6Открытие: %s %d%%"
|
||||
found: "%prefix%&aВы нашли: %s"
|
||||
|
||||
# ── Ворота ─────────────────────────────────────
|
||||
|
||||
gate:
|
||||
door-open: "&aОткрываем ворота... (зажмите ПКМ)"
|
||||
door-opened: "&aВорота открыты!"
|
||||
door-nearby: "&cНет ворот поблизости!"
|
||||
|
|
@ -307,6 +316,8 @@ usage:
|
|||
setup-killer-spawn: "&6/dbdsetup set killer-spawn — Спавн киллера (только 1)"
|
||||
setup-survivor-spawn: "&6/dbdsetup set survivor-spawn — Спавн выжившего (до 4)"
|
||||
setup-gate: "&6/dbdsetup set gate A|B — Установить ворота (из точек ванда)"
|
||||
setup-clear: "&6/dbdsetup clear [тип] — Очистить объекты (без разрушения блоков)"
|
||||
setup-delete: "&6/dbdsetup delete <тип> <#> — Удалить конкретный объект"
|
||||
setup-clear-spawns: "&6/dbdsetup clear-spawns — Очистить все спавны"
|
||||
setup-lobby: "&6/dbdsetup lobby — Установить точку лобби"
|
||||
admin-list-header: "&6=== DBD Admin ==="
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue