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 opened;
|
||||||
private boolean empty;
|
private boolean empty;
|
||||||
private ItemStack loot;
|
private ItemStack loot;
|
||||||
|
private long lastEmptiedTime;
|
||||||
|
|
||||||
|
private static final long COOLDOWN_MILLIS = 3 * 60 * 1000; // 3 minutes
|
||||||
|
|
||||||
public Chest(Location location) {
|
public Chest(Location location) {
|
||||||
this.id = UUID.randomUUID();
|
this.id = UUID.randomUUID();
|
||||||
|
|
@ -22,6 +25,7 @@ public class Chest {
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
this.empty = false;
|
this.empty = false;
|
||||||
this.loot = null;
|
this.loot = null;
|
||||||
|
this.lastEmptiedTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getId() {
|
public UUID getId() {
|
||||||
|
|
@ -52,6 +56,21 @@ public class Chest {
|
||||||
return !opened && !empty && hasLoot();
|
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() {
|
public ItemStack getLoot() {
|
||||||
return loot;
|
return loot;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,14 +118,8 @@ public class ChestHologram {
|
||||||
|
|
||||||
String status;
|
String status;
|
||||||
if (chest.isEmpty()) {
|
if (chest.isEmpty()) {
|
||||||
long secs = manager.getSecondsUntilNextReset();
|
long secs = chest.getSecondsUntilReset();
|
||||||
if (secs > 60) {
|
status = secs > 0 ? "§7Пусто §8(" + secs + "с)" : "§7Пусто";
|
||||||
status = "§7Пуст §8(" + (secs / 60) + "м)";
|
|
||||||
} else if (secs > 0) {
|
|
||||||
status = "§7Пуст §8(" + secs + "с)";
|
|
||||||
} else {
|
|
||||||
status = "§7Пуст";
|
|
||||||
}
|
|
||||||
} else if (chest.isOpened()) {
|
} else if (chest.isOpened()) {
|
||||||
status = "§6Открывается...";
|
status = "§6Открывается...";
|
||||||
} else if (chest.hasLoot()) {
|
} else if (chest.hasLoot()) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ public class ChestManager {
|
||||||
private final GameConfig gameConfig;
|
private final GameConfig gameConfig;
|
||||||
private ChestLoot chestLoot;
|
private ChestLoot chestLoot;
|
||||||
private BukkitTask lootResetTask;
|
private BukkitTask lootResetTask;
|
||||||
private long lastResetTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig, GameConfig gameConfig) {
|
public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig, GameConfig gameConfig) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
|
@ -36,7 +35,6 @@ public class ChestManager {
|
||||||
List<Location> locations = locationsConfig.getChests();
|
List<Location> locations = locationsConfig.getChests();
|
||||||
for (Location loc : locations) {
|
for (Location loc : locations) {
|
||||||
Chest chest = new Chest(loc);
|
Chest chest = new Chest(loc);
|
||||||
// Populate loot immediately so it's available
|
|
||||||
chest.setLoot(chestLoot.rollLoot());
|
chest.setLoot(chestLoot.rollLoot());
|
||||||
chests.put(chest.getId(), chest);
|
chests.put(chest.getId(), chest);
|
||||||
}
|
}
|
||||||
|
|
@ -44,14 +42,12 @@ public class ChestManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start periodic loot reset timer.
|
* Start per-chest cooldown timer. Each chest refills individually 3 min after being looted.
|
||||||
* Every N minutes (from config), all empty chests get new loot.
|
|
||||||
*/
|
*/
|
||||||
public void startLootResetTimer() {
|
public void startLootResetTimer() {
|
||||||
if (lootResetTask != null) lootResetTask.cancel();
|
if (lootResetTask != null) lootResetTask.cancel();
|
||||||
long intervalTicks = (long) (gameConfig.getChestLootResetMinutes() * 60 * 20);
|
lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::tickCooldowns, 20L, 20L);
|
||||||
lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::resetLoot, intervalTicks, intervalTicks);
|
plugin.getLogger().info("Per-chest cooldown timer started (3 min per chest)");
|
||||||
plugin.getLogger().info("Chest loot reset timer started (every " + gameConfig.getChestLootResetMinutes() + " min)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopLootResetTimer() {
|
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() {
|
private void tickCooldowns() {
|
||||||
int refilled = 0;
|
|
||||||
for (Chest chest : chests.values()) {
|
for (Chest chest : chests.values()) {
|
||||||
// Skip chests currently being opened to avoid race condition
|
|
||||||
if (chest.isOpened()) continue;
|
if (chest.isOpened()) continue;
|
||||||
|
if (chest.isEmpty() && !chest.isOnCooldown()) {
|
||||||
chest.setLoot(chestLoot.rollLoot());
|
chest.setLoot(chestLoot.rollLoot());
|
||||||
chest.setOpened(false);
|
|
||||||
chest.setEmpty(false);
|
chest.setEmpty(false);
|
||||||
refilled++;
|
|
||||||
}
|
}
|
||||||
this.lastResetTime = System.currentTimeMillis();
|
}
|
||||||
plugin.getLogger().info("[Chest] Loot reset — " + refilled + " chests refilled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Chest getChest(UUID id) {
|
public Chest getChest(UUID id) {
|
||||||
|
|
@ -117,7 +109,6 @@ public class ChestManager {
|
||||||
chest.setEmpty(false);
|
chest.setEmpty(false);
|
||||||
chest.setLoot(chestLoot.rollLoot());
|
chest.setLoot(chestLoot.rollLoot());
|
||||||
}
|
}
|
||||||
this.lastResetTime = System.currentTimeMillis();
|
|
||||||
plugin.getLogger().info("Reset all chests");
|
plugin.getLogger().info("Reset all chests");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,13 +131,7 @@ public class ChestManager {
|
||||||
return (int) chests.values().stream().filter(Chest::isAvailable).count();
|
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() {
|
public ChestLoot getChestLoot() {
|
||||||
return chestLoot;
|
return chestLoot;
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,10 @@ public class ChestOpening {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure chest has loot; roll if missing
|
if (!chest.hasLoot() || chest.isEmpty()) {
|
||||||
if (!chest.hasLoot()) {
|
|
||||||
ItemStack rolled = chestLoot.rollLoot();
|
|
||||||
if (rolled == null) {
|
|
||||||
player.sendMessage(messages().get("chest.empty"));
|
player.sendMessage(messages().get("chest.empty"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
chest.setLoot(rolled);
|
|
||||||
chest.setEmpty(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chest.isAvailable()) {
|
if (!chest.isAvailable()) {
|
||||||
player.sendMessage(messages().get("chest.empty"));
|
player.sendMessage(messages().get("chest.empty"));
|
||||||
|
|
@ -152,7 +146,7 @@ public class ChestOpening {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chest.setOpened(false);
|
chest.setOpened(false);
|
||||||
chest.setEmpty(true);
|
chest.onLooted();
|
||||||
chest.setLoot(null); // Clear loot after giving
|
chest.setLoot(null); // Clear loot after giving
|
||||||
chest.closeVisual();
|
chest.closeVisual();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package dev.loki.command;
|
||||||
import dev.loki.mdbd;
|
import dev.loki.mdbd;
|
||||||
import dev.loki.util.SpawnUtil;
|
import dev.loki.util.SpawnUtil;
|
||||||
import dev.loki.util.TextUtil;
|
import dev.loki.util.TextUtil;
|
||||||
|
import dev.loki.config.LocationsConfig;
|
||||||
import dev.loki.config.MessagesConfig;
|
import dev.loki.config.MessagesConfig;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
|
|
@ -70,7 +71,8 @@ public class SetupCommand implements TabExecutor {
|
||||||
return switch (args[0].toLowerCase()) {
|
return switch (args[0].toLowerCase()) {
|
||||||
case "wand" -> giveWand(player);
|
case "wand" -> giveWand(player);
|
||||||
case "set" -> handleSet(player, args);
|
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 "clear-spawns" -> clearSpawns(player);
|
||||||
case "lobby" -> setLobby(player);
|
case "lobby" -> setLobby(player);
|
||||||
default -> { sendUsage(sender); yield true; }
|
default -> { sendUsage(sender); yield true; }
|
||||||
|
|
@ -331,6 +333,147 @@ public class SetupCommand implements TabExecutor {
|
||||||
return true;
|
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) {
|
private boolean clearSpawns(Player player) {
|
||||||
var locations = plugin.getConfigManager().getLocations();
|
var locations = plugin.getConfigManager().getLocations();
|
||||||
locations.clearLocationList("killer-spawn");
|
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-killer-spawn"));
|
||||||
sender.sendMessage(messages().get("usage.setup-survivor-spawn"));
|
sender.sendMessage(messages().get("usage.setup-survivor-spawn"));
|
||||||
sender.sendMessage(messages().get("usage.setup-gate"));
|
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-clear-spawns"));
|
||||||
sender.sendMessage(messages().get("usage.setup-lobby"));
|
sender.sendMessage(messages().get("usage.setup-lobby"));
|
||||||
}
|
}
|
||||||
|
|
@ -373,13 +518,15 @@ public class SetupCommand implements TabExecutor {
|
||||||
@NotNull String label, @NotNull String[] args) {
|
@NotNull String label, @NotNull String[] args) {
|
||||||
if (!sender.hasPermission("mdbd.admin")) return List.of();
|
if (!sender.hasPermission("mdbd.admin")) return List.of();
|
||||||
if (args.length == 1) {
|
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();
|
.filter(s -> s.startsWith(args[0].toLowerCase())).toList();
|
||||||
}
|
}
|
||||||
if (args.length == 2 && args[0].equalsIgnoreCase("set")) {
|
if (args.length == 2) {
|
||||||
return List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn", "gate").stream()
|
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();
|
.filter(s -> s.startsWith(args[1].toLowerCase())).toList();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,11 @@ public class GameConfig {
|
||||||
|
|
||||||
// Endgame
|
// Endgame
|
||||||
setDefault("endgame.gate-open-seconds", 20);
|
setDefault("endgame.gate-open-seconds", 20);
|
||||||
|
setDefault("endgame.pregame-seconds", 30);
|
||||||
|
setDefault("endgame.postgame-seconds", 60);
|
||||||
setDefault("endgame.chest-open-seconds", 3);
|
setDefault("endgame.chest-open-seconds", 3);
|
||||||
setDefault("endgame.gate-interaction-range", 3.0);
|
setDefault("endgame.gate-interaction-range", 3.0);
|
||||||
|
setDefault("endgame.escape-range", 2.0);
|
||||||
|
|
||||||
// Killer notification
|
// Killer notification
|
||||||
setDefault("killer-notification.compass-duration-seconds", 5);
|
setDefault("killer-notification.compass-duration-seconds", 5);
|
||||||
|
|
@ -263,8 +266,11 @@ public class GameConfig {
|
||||||
// Endgame
|
// Endgame
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
public int getGateOpenSeconds() { return config.getInt("endgame.gate-open-seconds", 20); }
|
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 int getChestOpenSeconds() { return config.getInt("endgame.chest-open-seconds", 3); }
|
||||||
public double getGateInteractionRange() { return config.getDouble("endgame.gate-interaction-range", 3.0); }
|
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
|
// Killer notification
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,12 @@ public class LocationsConfig {
|
||||||
return -1;
|
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. */
|
/** Clear all survivor spawn slots. */
|
||||||
public void clearSurvivorSpawns() {
|
public void clearSurvivorSpawns() {
|
||||||
for (int i = 1; i <= 4; i++) {
|
for (int i = 1; i <= 4; i++) {
|
||||||
|
|
@ -219,6 +225,15 @@ public class LocationsConfig {
|
||||||
public void clearLocationList(String path) {
|
public void clearLocationList(String path) {
|
||||||
config.set(path, new ArrayList<>());
|
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) {
|
public void setSingleLocation(String path, Location loc) {
|
||||||
List<String> list = new ArrayList<>();
|
List<String> list = new ArrayList<>();
|
||||||
list.add(locationToString(loc));
|
list.add(locationToString(loc));
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ public class GameSession {
|
||||||
this.ticksElapsed = 0;
|
this.ticksElapsed = 0;
|
||||||
this.roleAssigner = new RoleAssigner(plugin.getConfigManager().getLocations());
|
this.roleAssigner = new RoleAssigner(plugin.getConfigManager().getLocations());
|
||||||
this.generatorManager = new GeneratorManager(
|
this.generatorManager = new GeneratorManager(
|
||||||
plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired());
|
plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired(),
|
||||||
|
plugin.getConfigManager().getGame());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
|
|
|
||||||
|
|
@ -150,9 +150,7 @@ public class ExitGate {
|
||||||
|
|
||||||
public void setPowered(boolean powered) {
|
public void setPowered(boolean powered) {
|
||||||
this.powered = powered;
|
this.powered = powered;
|
||||||
if (powered && state == GateState.CLOSED) {
|
// НЕ меняем статус на OPENING — ворота открываются только через взаимодействие игрока
|
||||||
state = GateState.OPENING;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startOpening() {
|
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). */
|
/** Прямая установка прогресса (для линейного расчёта в GateOpening). */
|
||||||
public void setProgressDirect(double value) {
|
public void setProgressDirect(double value) {
|
||||||
if (state != GateState.OPENING) return;
|
if (state != GateState.OPENING) return;
|
||||||
|
|
@ -204,6 +212,14 @@ public class ExitGate {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isInRange(Location playerLoc, double radius) {
|
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;
|
return location.distance(playerLoc) <= radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,12 @@ public class GateManager {
|
||||||
return (int) gates.stream().filter(ExitGate::isOpen).count();
|
return (int) gates.stream().filter(ExitGate::isOpen).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void openAllGates() {
|
||||||
|
for (ExitGate gate : gates) {
|
||||||
|
gate.forceOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void resetAll() {
|
public void resetAll() {
|
||||||
endgameActive = false;
|
endgameActive = false;
|
||||||
for (ExitGate gate : gates) {
|
for (ExitGate gate : gates) {
|
||||||
|
|
|
||||||
|
|
@ -38,17 +38,22 @@ public class GateOpening {
|
||||||
|
|
||||||
ExitGate gate = gateManager.getGateAt(player.getLocation(), INTERACTION_RADIUS);
|
ExitGate gate = gateManager.getGateAt(player.getLocation(), INTERACTION_RADIUS);
|
||||||
if (gate == null) {
|
if (gate == null) {
|
||||||
player.sendMessage(messages.get("chest.door-nearby"));
|
player.sendMessage(messages.get("gate.door-nearby"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gate.isPowered()) {
|
if (!gate.isPowered()) {
|
||||||
player.sendMessage(messages.get("chest.door-no-power"));
|
player.sendMessage(messages.get("gate.door-no-power"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gate.isClosed() && !gate.isOpening()) {
|
if (gate.isOpening()) {
|
||||||
player.sendMessage(messages.get("chest.door-already"));
|
player.sendMessage(messages.get("gate.door-already"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gate.isOpen()) {
|
||||||
|
player.sendMessage(messages.get("gate.door-already"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +66,7 @@ public class GateOpening {
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
Player player = survivor.getPlayer();
|
Player player = survivor.getPlayer();
|
||||||
if (player != null) {
|
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();
|
Player player = survivor.getPlayer();
|
||||||
if (player == null) return;
|
if (player == null) return;
|
||||||
|
|
||||||
|
// Перевести ворота в режим открытия
|
||||||
|
gate.startOpening();
|
||||||
|
|
||||||
// Восстановить сохранённый прогресс
|
// Восстановить сохранённый прогресс
|
||||||
gate.restoreProgress();
|
gate.restoreProgress();
|
||||||
|
|
||||||
openingPlayers.put(survivor.getUuid(), new OpeningProgress(gate, 0));
|
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);
|
new OpeningTask(survivor, gate, plugin).runTaskTimer(plugin, 0L, 1L);
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +97,7 @@ public class GateOpening {
|
||||||
World world = loc.getWorld();
|
World world = loc.getWorld();
|
||||||
|
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
player.sendMessage(messages.get("chest.door-opened"));
|
player.sendMessage(messages.get("gate.door-opened"));
|
||||||
if (world != null) {
|
if (world != null) {
|
||||||
world.playSound(loc, Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
|
world.playSound(loc, Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f);
|
||||||
// Big particle burst when opened
|
// Big particle burst when opened
|
||||||
|
|
|
||||||
|
|
@ -76,4 +76,6 @@ public class Hook {
|
||||||
public boolean isBasement() { return basement; }
|
public boolean isBasement() { return basement; }
|
||||||
public boolean isDisabled() { return disabled; }
|
public boolean isDisabled() { return disabled; }
|
||||||
public void setDisabled(boolean disabled) { this.disabled = 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; // "повешен" = завершён
|
return true; // "повешен" = завершён
|
||||||
}
|
}
|
||||||
|
|
||||||
Hook nearest = findNearestUnoccupiedHook(killerLoc, 5.0);
|
Hook nearest = findNearestUnoccupiedHook(killerLoc, 4.0);
|
||||||
if (nearest == null) {
|
|
||||||
nearest = findAvailableHook().orElse(null);
|
|
||||||
}
|
|
||||||
if (nearest != null) {
|
if (nearest != null) {
|
||||||
nearest.hookSurvivor(survivor);
|
nearest.hookSurvivor(survivor);
|
||||||
survivor.setState(PlayerState.HOOKED); // Set state to HOOKED
|
survivor.setState(PlayerState.HOOKED); // Set state to HOOKED
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ public class ItemMedkit implements BaseItem {
|
||||||
meta.setDisplayName("§a" + getType().getDisplayName());
|
meta.setDisplayName("§a" + getType().getDisplayName());
|
||||||
meta.setLore(Arrays.asList(
|
meta.setLore(Arrays.asList(
|
||||||
"§7" + getType().getDescription(),
|
"§7" + getType().getDescription(),
|
||||||
"§eCharges: §f" + charges + "/" + getType().getDefaultCharges(),
|
"§eЗаряды: §f" + charges + "/" + getType().getDefaultCharges(),
|
||||||
"§7Right-click to heal"
|
"§7ПКМ — лечиться"
|
||||||
));
|
));
|
||||||
|
|
||||||
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
||||||
|
|
@ -60,7 +60,7 @@ public class ItemMedkit implements BaseItem {
|
||||||
@Override
|
@Override
|
||||||
public void onUse(Player player) {
|
public void onUse(Player player) {
|
||||||
if (charges > 0) {
|
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;
|
package dev.loki.item;
|
||||||
|
|
||||||
public enum ItemType {
|
public enum ItemType {
|
||||||
MEDKIT("Medkit", "Heals one health state", 16),
|
MEDKIT("Медкит", "Лечит одно состояние здоровья", 16),
|
||||||
HOOK_CUTTER("Hook Cutter", "Cuts a survivor off a hook", 1);
|
HOOK_CUTTER("Крюкорез", "Срезает выжившего с крюка", 1);
|
||||||
|
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.scheduler.BukkitRunnable;
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
import org.bukkit.scheduler.BukkitTask;
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
@ -39,6 +41,9 @@ public class GeneratorKick {
|
||||||
private int globalCooldownTicks = 0;
|
private int globalCooldownTicks = 0;
|
||||||
private BukkitTask cooldownTask;
|
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) {
|
public GeneratorKick(Plugin plugin, GameConfig config, GeneratorManager generatorManager) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
@ -135,6 +140,8 @@ public class GeneratorKick {
|
||||||
for (BukkitTask t : chargingTasks.values()) t.cancel();
|
for (BukkitTask t : chargingTasks.values()) t.cancel();
|
||||||
chargingTasks.clear();
|
chargingTasks.clear();
|
||||||
if (cooldownTask != null) { cooldownTask.cancel(); cooldownTask = null; }
|
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)
|
// Unblock generator after kick-duration (separate from global cooldown)
|
||||||
int unblockTicks = config.getKickDuration() * 20;
|
int unblockTicks = config.getKickDuration() * 20;
|
||||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
BukkitTask unblockTask = plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||||
gen.setBlocked(false);
|
gen.setBlocked(false);
|
||||||
}, unblockTicks);
|
}, unblockTicks);
|
||||||
|
pendingUnblockTasks.add(unblockTask);
|
||||||
|
|
||||||
// Start global kick cooldown
|
// Start global kick cooldown
|
||||||
startGlobalCooldown();
|
startGlobalCooldown();
|
||||||
|
|
|
||||||
|
|
@ -228,10 +228,32 @@ public class PlayerInteractListener implements Listener {
|
||||||
// #13: Запрет входа в печки для всех (и маньяка тоже) — MUST be FIRST before any other checks
|
// #13: Запрет входа в печки для всех (и маньяка тоже) — MUST be FIRST before any other checks
|
||||||
if (event.getClickedBlock() != null) {
|
if (event.getClickedBlock() != null) {
|
||||||
String type = event.getClickedBlock().getType().toString();
|
String type = event.getClickedBlock().getType().toString();
|
||||||
|
|
||||||
if (type.contains("FURNACE") || type.contains("SMOKER") || type.contains("BLAST_FURNACE")) {
|
if (type.contains("FURNACE") || type.contains("SMOKER") || type.contains("BLAST_FURNACE")) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
return;
|
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;
|
if (!bp.isKiller() && tryGeneratorInteract(player)) return;
|
||||||
|
|
@ -242,29 +264,15 @@ public class PlayerInteractListener implements Listener {
|
||||||
return;
|
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;
|
if (event.getClickedBlock() == null) return;
|
||||||
|
|
||||||
String type = event.getClickedBlock().getType().toString();
|
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: if survivor clicks on a hook block, attempt rescue
|
||||||
|
// Hook rescue / block protection
|
||||||
if (!bp.isKiller() && onRescueAttempt != null) {
|
if (!bp.isKiller() && onRescueAttempt != null) {
|
||||||
onRescueAttempt.accept(player, true);
|
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);
|
if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.loki.objective;
|
package dev.loki.objective;
|
||||||
|
|
||||||
import dev.loki.data.GeneratorDataManager;
|
import dev.loki.data.GeneratorDataManager;
|
||||||
|
import dev.loki.config.GameConfig;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
|
@ -12,11 +13,15 @@ import java.util.Map;
|
||||||
public class GeneratorManager {
|
public class GeneratorManager {
|
||||||
private final Map<Integer, Generator> generators;
|
private final Map<Integer, Generator> generators;
|
||||||
private final int requiredCount;
|
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.generators = new HashMap<>();
|
||||||
this.requiredCount = requiredCount;
|
|
||||||
loadGenerators(dataManager);
|
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) {
|
private void loadGenerators(GeneratorDataManager dataManager) {
|
||||||
|
|
@ -103,7 +108,7 @@ public class GeneratorManager {
|
||||||
public void tick() {
|
public void tick() {
|
||||||
for (Generator gen : generators.values()) {
|
for (Generator gen : generators.values()) {
|
||||||
if (gen.isUnderRegression()) {
|
if (gen.isUnderRegression()) {
|
||||||
double rate = gen.wasKicked() ? 0.125 : 0.0125;
|
double rate = gen.wasKicked() ? kickRegressionRate : normalRegressionRate;
|
||||||
gen.removeProgress(rate);
|
gen.removeProgress(rate);
|
||||||
}
|
}
|
||||||
gen.decrementRegression();
|
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.mdbd;
|
||||||
import dev.loki.minigames.GeneratorMinigame;
|
import dev.loki.minigames.GeneratorMinigame;
|
||||||
|
import dev.loki.player.BasePlayer;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
|
|
@ -71,8 +72,8 @@ public class GeneratorRepair {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.generator.setCurrentRepairer(null);
|
session.generator.setCurrentRepairer(null);
|
||||||
// Progress is PRESERVED - generator keeps its progress value
|
// Progress is PRESERVED - generator keeps its progress value
|
||||||
// Only unblock if it was blocked due to an ongoing repair (not a kick)
|
// Only unblock if it was blocked due to an ongoing repair (not a kick or block)
|
||||||
if (!session.generator.isUnderRegression()) {
|
if (!session.generator.isUnderRegression() && !session.generator.isBlocked()) {
|
||||||
session.generator.setBlocked(false);
|
session.generator.setBlocked(false);
|
||||||
}
|
}
|
||||||
player.sendActionBar(Component.empty());
|
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) {
|
private void notifyKillerWithCompass(Player failedPlayer, Generator generator) {
|
||||||
|
var session = plugin.getGameManager().getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
int durationTicks = plugin.getConfigManager().getGame().getCompassDurationSeconds() * 20;
|
int durationTicks = plugin.getConfigManager().getGame().getCompassDurationSeconds() * 20;
|
||||||
Location compassTarget = generator.getLocation();
|
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;
|
if (online.getUniqueId().equals(failedPlayer.getUniqueId())) continue;
|
||||||
|
|
||||||
online.sendMessage(plugin.getConfigManager().getMessages().get("generator.kicked.notification"));
|
online.sendMessage(plugin.getConfigManager().getMessages().get("generator.kicked.notification"));
|
||||||
|
|
@ -196,7 +203,6 @@ public class GeneratorRepair {
|
||||||
|
|
||||||
ItemStack prev = online.getInventory().getItemInOffHand();
|
ItemStack prev = online.getInventory().getItemInOffHand();
|
||||||
online.getInventory().setItemInOffHand(compass);
|
online.getInventory().setItemInOffHand(compass);
|
||||||
// Восстанавливаем старый предмет только если в offhand всё ещё компас
|
|
||||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||||
ItemStack current = online.getInventory().getItemInOffHand();
|
ItemStack current = online.getInventory().getItemInOffHand();
|
||||||
if (current.getType() == Material.COMPASS) {
|
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();
|
gateManager = session.getGateManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если ворота уже открыты — игра завершилась по таймеру эндгейма,
|
||||||
|
// показываем результаты сразу
|
||||||
|
if (gateManager != null && gateManager.getOpenGateCount() > 0) {
|
||||||
|
this.complete = true;
|
||||||
|
announceEndgameEnd(session);
|
||||||
|
teleportAllToLobby(session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set up sidebar
|
// Set up sidebar
|
||||||
sidebar = new Sidebar();
|
sidebar = new Sidebar();
|
||||||
for (BasePlayer bp : session.getPlayers().values()) {
|
for (BasePlayer bp : session.getPlayers().values()) {
|
||||||
|
|
@ -101,7 +110,6 @@ public class EndgamePhase implements Phase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGates(session);
|
|
||||||
updateHatch(session);
|
updateHatch(session);
|
||||||
checkWinConditions(session);
|
checkWinConditions(session);
|
||||||
}
|
}
|
||||||
|
|
@ -123,6 +131,10 @@ public class EndgamePhase implements Phase {
|
||||||
hatch.reset();
|
hatch.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gateManager != null) {
|
||||||
|
gateManager.resetAll();
|
||||||
|
}
|
||||||
|
|
||||||
// exit() НЕ вызывает session.stop() — это делает transitionToNextPhase()
|
// exit() НЕ вызывает session.stop() — это делает transitionToNextPhase()
|
||||||
// Телепорт и результаты уже обработаны в enter() или будут в teleportAllToLobby
|
// Телепорт и результаты уже обработаны в enter() или будут в teleportAllToLobby
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ public class GameplayDamageSystem {
|
||||||
private Listener vanillaDamageBlocker;
|
private Listener vanillaDamageBlocker;
|
||||||
private ItemUseListener itemUseListener;
|
private ItemUseListener itemUseListener;
|
||||||
private PlayerJumpListener jumpListener;
|
private PlayerJumpListener jumpListener;
|
||||||
|
private org.bukkit.scheduler.BukkitTask swimmingTask;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize damage and combat system.
|
* 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.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
|
||||||
p.setHealth(20.0);
|
p.setHealth(20.0);
|
||||||
p.setSwimming(true);
|
p.setSwimming(true);
|
||||||
Bukkit.getScheduler().runTaskTimer(plugin, task -> {
|
swimmingTask = new org.bukkit.scheduler.BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
if (!p.isOnline()) {
|
if (!p.isOnline()) {
|
||||||
task.cancel();
|
cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (survivor.getState() != PlayerState.DOWNED) {
|
if (survivor.getState() != PlayerState.DOWNED) {
|
||||||
p.setSwimming(false);
|
p.setSwimming(false);
|
||||||
task.cancel();
|
cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!p.isSwimming()) p.setSwimming(true);
|
if (!p.isSwimming()) p.setSwimming(true);
|
||||||
}, 1L, 5L);
|
}
|
||||||
|
}.runTaskTimer(plugin, 1L, 5L);
|
||||||
});
|
});
|
||||||
|
|
||||||
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
|
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
|
||||||
|
|
@ -138,6 +142,10 @@ public class GameplayDamageSystem {
|
||||||
* Clear all damage system resources.
|
* Clear all damage system resources.
|
||||||
*/
|
*/
|
||||||
public void clear() {
|
public void clear() {
|
||||||
|
if (swimmingTask != null) {
|
||||||
|
swimmingTask.cancel();
|
||||||
|
swimmingTask = null;
|
||||||
|
}
|
||||||
if (jumpListener != null) {
|
if (jumpListener != null) {
|
||||||
jumpListener.clearInGamePlayers();
|
jumpListener.clearInGamePlayers();
|
||||||
jumpListener.unregister();
|
jumpListener.unregister();
|
||||||
|
|
@ -156,7 +164,15 @@ public class GameplayDamageSystem {
|
||||||
*/
|
*/
|
||||||
public void clearManagers() {
|
public void clearManagers() {
|
||||||
if (killerController != null) killerController.shutdown();
|
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.core.GameSession;
|
||||||
import dev.loki.hook.*;
|
import dev.loki.hook.*;
|
||||||
import dev.loki.player.BasePlayer;
|
import dev.loki.player.BasePlayer;
|
||||||
|
import dev.loki.player.PlayerState;
|
||||||
import dev.loki.player.Survivor;
|
import dev.loki.player.Survivor;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -18,6 +20,10 @@ public class GameplayHookHandler {
|
||||||
private final HookSacrifice hookSacrifice;
|
private final HookSacrifice hookSacrifice;
|
||||||
private final HookStruggle hookStruggle;
|
private final HookStruggle hookStruggle;
|
||||||
private final HookRecovery hookRecovery;
|
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,
|
public GameplayHookHandler(HookManager hookManager, HookSacrifice hookSacrifice,
|
||||||
HookStruggle hookStruggle, HookRecovery hookRecovery) {
|
HookStruggle hookStruggle, HookRecovery hookRecovery) {
|
||||||
|
|
@ -27,12 +33,17 @@ public class GameplayHookHandler {
|
||||||
this.hookRecovery = hookRecovery;
|
this.hookRecovery = hookRecovery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSession(GameSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
private int hookTpTick;
|
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() {
|
public void tick() {
|
||||||
|
applyKillerProximity();
|
||||||
checkHookStages();
|
checkHookStages();
|
||||||
hookTpTick++;
|
hookTpTick++;
|
||||||
if (hookTpTick >= 20) {
|
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.
|
* 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) {
|
private void wireMoveListener(GameSession session) {
|
||||||
if (moveListener == null) return;
|
// Escape check is handled in GameplayPhase.checkEscapes() every tick during POSTGAME
|
||||||
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) {
|
private void wireInteractListener(GameSession session) {
|
||||||
|
|
@ -156,12 +142,15 @@ public class GameplayInteraction {
|
||||||
Survivor carried = (Survivor) other;
|
Survivor carried = (Survivor) other;
|
||||||
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
|
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
|
||||||
killer.setCarrying(false);
|
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);
|
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
|
||||||
if (nearest != null) {
|
if (nearest != null) {
|
||||||
Player vp = carried.getPlayer();
|
Player vp = carried.getPlayer();
|
||||||
if (vp != null) vp.teleport(nearest.getLocation());
|
if (vp != null) vp.teleport(nearest.getLocation());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,11 +179,13 @@ public class GameplayInteraction {
|
||||||
Survivor carried = (Survivor) other;
|
Survivor carried = (Survivor) other;
|
||||||
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
|
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
|
||||||
killer.setCarrying(false);
|
killer.setCarrying(false);
|
||||||
|
if (carried.getState() == PlayerState.HOOKED) {
|
||||||
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
|
Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
|
||||||
if (nearest != null) {
|
if (nearest != null) {
|
||||||
Player vp = carried.getPlayer();
|
Player vp = carried.getPlayer();
|
||||||
if (vp != null) vp.teleport(nearest.getLocation());
|
if (vp != null) vp.teleport(nearest.getLocation());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import dev.loki.player.Survivor;
|
||||||
import dev.loki.ui.ActionBar;
|
import dev.loki.ui.ActionBar;
|
||||||
import dev.loki.ui.TitleUI;
|
import dev.loki.ui.TitleUI;
|
||||||
import net.kyori.adventure.text.Component;
|
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.Bukkit;
|
||||||
import org.bukkit.GameMode;
|
import org.bukkit.GameMode;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
|
|
@ -33,15 +35,20 @@ import org.bukkit.Sound;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GameplayPhase implements Phase {
|
public class GameplayPhase implements Phase {
|
||||||
|
|
||||||
|
private enum EndgameState { NONE, PREGAME, POSTGAME }
|
||||||
|
|
||||||
private final GeneratorManager generatorManager;
|
private final GeneratorManager generatorManager;
|
||||||
private int tickCounter;
|
private int tickCounter;
|
||||||
private boolean shouldEnd;
|
private boolean shouldEnd;
|
||||||
private boolean allGensDone;
|
private boolean allGensDone;
|
||||||
|
private EndgameState endgameState = EndgameState.NONE;
|
||||||
|
private int endgameTimer; // секунд до смены состояния
|
||||||
|
|
||||||
// Sub-handlers
|
// Sub-handlers
|
||||||
private GameplayHookHandler hookHandler;
|
private GameplayHookHandler hookHandler;
|
||||||
|
|
@ -81,6 +88,8 @@ public class GameplayPhase implements Phase {
|
||||||
tickCounter = 0;
|
tickCounter = 0;
|
||||||
shouldEnd = false;
|
shouldEnd = false;
|
||||||
allGensDone = false;
|
allGensDone = false;
|
||||||
|
endgameState = EndgameState.NONE;
|
||||||
|
endgameTimer = 0;
|
||||||
winConditions = new GameplayWinConditions();
|
winConditions = new GameplayWinConditions();
|
||||||
|
|
||||||
initManagers(session, plugin, cfg);
|
initManagers(session, plugin, cfg);
|
||||||
|
|
@ -104,7 +113,6 @@ public class GameplayPhase implements Phase {
|
||||||
if (generatorRepair != null) generatorRepair.tick();
|
if (generatorRepair != null) generatorRepair.tick();
|
||||||
if (interaction != null) interaction.tickSurvivors();
|
if (interaction != null) interaction.tickSurvivors();
|
||||||
hookHandler.tick();
|
hookHandler.tick();
|
||||||
boolean timeExpired = ui.tickMatchTimer(session);
|
|
||||||
|
|
||||||
// Particle effects around generators (every 10 ticks)
|
// Particle effects around generators (every 10 ticks)
|
||||||
if (tickCounter % 10 == 0 && generatorManager != null) {
|
if (tickCounter % 10 == 0 && generatorManager != null) {
|
||||||
|
|
@ -119,19 +127,29 @@ public class GameplayPhase implements Phase {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tickCounter % 20 == 0) {
|
if (tickCounter % 20 == 0) {
|
||||||
|
tickEndgame(session);
|
||||||
checkGeneratorProgress(session);
|
checkGeneratorProgress(session);
|
||||||
|
if (endgameState == EndgameState.NONE) {
|
||||||
winConditions.checkWinConditions(session);
|
winConditions.checkWinConditions(session);
|
||||||
if (winConditions.isShouldEnd() || timeExpired) {
|
if (winConditions.isShouldEnd()) {
|
||||||
shouldEnd = true;
|
shouldEnd = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Во время эндгейма игра заканчивается когда все выжившие мертвы/сбежали
|
||||||
|
if (allSurvivorsIncapacitated(session)) {
|
||||||
|
shouldEnd = true;
|
||||||
|
}
|
||||||
|
// Или когда истёк таймер после открытия ворот
|
||||||
|
if (endgameState == EndgameState.POSTGAME && endgameTimer <= 0) {
|
||||||
|
shouldEnd = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
ui.updateSidebar(session, generatorManager);
|
ui.updateSidebar(session, generatorManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeExpired) {
|
// Escape check — каждый тик для мгновенной реакции
|
||||||
showGameResults(session);
|
if (endgameState == EndgameState.POSTGAME) {
|
||||||
teleportAllToLobby(session);
|
checkEscapes(session);
|
||||||
session.stop();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,6 +211,7 @@ public class GameplayPhase implements Phase {
|
||||||
|
|
||||||
hookRescue = new HookRescue(plugin, ab, cfg);
|
hookRescue = new HookRescue(plugin, ab, cfg);
|
||||||
hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery);
|
hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery);
|
||||||
|
hookHandler.setSession(session);
|
||||||
hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim));
|
hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim));
|
||||||
|
|
||||||
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg);
|
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg);
|
||||||
|
|
@ -267,12 +286,30 @@ public class GameplayPhase implements Phase {
|
||||||
// Tick helpers
|
// 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) {
|
private void checkGeneratorProgress(GameSession session) {
|
||||||
if (allGensDone) return;
|
if (allGensDone) return;
|
||||||
if (generatorManager.areAllRequiredCompleted()) {
|
if (generatorManager.areAllRequiredCompleted()) {
|
||||||
allGensDone = true;
|
allGensDone = true;
|
||||||
interaction.setAllGensDone(true);
|
interaction.setAllGensDone(true);
|
||||||
gateManager.activateEndgame();
|
gateManager.activateEndgame();
|
||||||
|
endgameState = EndgameState.PREGAME;
|
||||||
|
endgameTimer = session.getConfig().getPregameSeconds();
|
||||||
Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done"));
|
Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done"));
|
||||||
for (BasePlayer bp : session.getPlayers().values()) {
|
for (BasePlayer bp : session.getPlayers().values()) {
|
||||||
Player p = bp.getPlayer();
|
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) {
|
private void spawnGameBlocks(mdbd plugin, GameConfig cfg) {
|
||||||
// Спавн блоков на местах генераторов
|
// Спавн блоков на местах генераторов
|
||||||
Material genMat = Material.matchMaterial(cfg.getGeneratorBlock());
|
Material genMat = Material.matchMaterial(cfg.getGeneratorBlock());
|
||||||
|
|
@ -333,35 +427,6 @@ public class GameplayPhase implements Phase {
|
||||||
// Cleanup
|
// 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) {
|
private void teleportAllToLobby(GameSession session) {
|
||||||
for (BasePlayer bp : session.getPlayers().values()) {
|
for (BasePlayer bp : session.getPlayers().values()) {
|
||||||
Player p = bp.getPlayer();
|
Player p = bp.getPlayer();
|
||||||
|
|
@ -395,12 +460,11 @@ public class GameplayPhase implements Phase {
|
||||||
if (hookStruggle != null) hookStruggle.clear();
|
if (hookStruggle != null) hookStruggle.clear();
|
||||||
if (chestManager != null) chestManager.stopLootResetTimer();
|
if (chestManager != null) chestManager.stopLootResetTimer();
|
||||||
if (chestHologram != null) chestHologram.stop();
|
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) {
|
if (gateManager != null) {
|
||||||
for (ExitGate gate : gateManager.getAllGates()) {
|
for (ExitGate gate : gateManager.getAllGates()) {
|
||||||
gate.removeGateBlock();
|
gate.removeGateBlock();
|
||||||
}
|
}
|
||||||
gateManager.resetAll();
|
|
||||||
}
|
}
|
||||||
session.getPlugin().getInteractListener().setGeneratorManager(null);
|
session.getPlugin().getInteractListener().setGeneratorManager(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import dev.loki.config.GameConfig;
|
||||||
import dev.loki.mdbd;
|
import dev.loki.mdbd;
|
||||||
import dev.loki.player.PlayerState;
|
import dev.loki.player.PlayerState;
|
||||||
import dev.loki.player.Survivor;
|
import dev.loki.player.Survivor;
|
||||||
|
import dev.loki.util.TextUtil;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
@ -98,6 +98,10 @@ public class SurvivorHealing {
|
||||||
if (targetSurvivor == null) { cancelHealing(); return; }
|
if (targetSurvivor == null) { cancelHealing(); return; }
|
||||||
|
|
||||||
PlayerState targetState = targetSurvivor.getState();
|
PlayerState targetState = targetSurvivor.getState();
|
||||||
|
if (targetState == PlayerState.DEAD || targetState == PlayerState.ESCAPED) {
|
||||||
|
cancelHealing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) {
|
if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) {
|
||||||
completeHealing(targetSurvivor);
|
completeHealing(targetSurvivor);
|
||||||
return;
|
return;
|
||||||
|
|
@ -166,7 +170,7 @@ public class SurvivorHealing {
|
||||||
String bar = renderProgressBar(progress);
|
String bar = renderProgressBar(progress);
|
||||||
String template = mdbd.getInstance().getConfigManager().getMessages().getString(
|
String template = mdbd.getInstance().getConfigManager().getMessages().getString(
|
||||||
isSelfHeal ? "survivor.heal.self-start" : "survivor.heal.other-start", bar);
|
isSelfHeal ? "survivor.heal.self-start" : "survivor.heal.other-start", bar);
|
||||||
Component message = MiniMessage.miniMessage().deserialize(template);
|
Component message = TextUtil.color(template);
|
||||||
healingTarget.sendActionBar(message);
|
healingTarget.sendActionBar(message);
|
||||||
// Also show on healer
|
// Also show on healer
|
||||||
Player healer = controller.getPlayer();
|
Player healer = controller.getPlayer();
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ public class SurvivorMovement {
|
||||||
if (player == null) return;
|
if (player == null) return;
|
||||||
|
|
||||||
if (isCrawling) {
|
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.hook.HookStage;
|
||||||
import dev.loki.player.PlayerState;
|
import dev.loki.player.PlayerState;
|
||||||
import dev.loki.player.Survivor;
|
import dev.loki.player.Survivor;
|
||||||
|
import dev.loki.util.TextUtil;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.TextComponent;
|
import net.kyori.adventure.text.TextComponent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
|
@ -12,7 +13,7 @@ import org.bukkit.entity.Player;
|
||||||
public class ActionBar {
|
public class ActionBar {
|
||||||
|
|
||||||
public void sendMessage(Player player, String message) {
|
public void sendMessage(Player player, String message) {
|
||||||
player.sendActionBar(Component.text(message));
|
player.sendActionBar(TextUtil.color(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendComponent(Player player, Component component) {
|
public void sendComponent(Player player, Component component) {
|
||||||
|
|
@ -26,20 +27,20 @@ public class ActionBar {
|
||||||
|
|
||||||
public void showRepairProgress(Player player, double percent, boolean skillCheckActive) {
|
public void showRepairProgress(Player player, double percent, boolean skillCheckActive) {
|
||||||
Component bar = ProgressBar.create(percent);
|
Component bar = ProgressBar.create(percent);
|
||||||
TextComponent message = Component.text("Repairing ")
|
TextComponent message = Component.text("Починка: ")
|
||||||
.color(NamedTextColor.WHITE)
|
.color(NamedTextColor.WHITE)
|
||||||
.append(bar)
|
.append(bar)
|
||||||
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
||||||
|
|
||||||
if (skillCheckActive) {
|
if (skillCheckActive) {
|
||||||
message = message.append(Component.text(" ⚠ SKILL CHECK!").color(NamedTextColor.YELLOW));
|
message = message.append(Component.text(" ⚠ СКИЛЛ-ЧЕК!").color(NamedTextColor.YELLOW));
|
||||||
}
|
}
|
||||||
|
|
||||||
player.sendActionBar(message);
|
player.sendActionBar(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showHealProgress(Player player, double percent, boolean selfHeal) {
|
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 bar = ProgressBar.create(percent);
|
||||||
Component message = Component.text(label + " ")
|
Component message = Component.text(label + " ")
|
||||||
.color(NamedTextColor.WHITE)
|
.color(NamedTextColor.WHITE)
|
||||||
|
|
@ -50,7 +51,7 @@ public class ActionBar {
|
||||||
|
|
||||||
public void showRescueProgress(Player player, double percent) {
|
public void showRescueProgress(Player player, double percent) {
|
||||||
Component bar = ProgressBar.create(percent);
|
Component bar = ProgressBar.create(percent);
|
||||||
Component message = Component.text("Rescuing ")
|
Component message = Component.text("Спасение: ")
|
||||||
.color(NamedTextColor.WHITE)
|
.color(NamedTextColor.WHITE)
|
||||||
.append(bar)
|
.append(bar)
|
||||||
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
.append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY));
|
||||||
|
|
@ -71,15 +72,15 @@ public class ActionBar {
|
||||||
if (stage == HookStage.STAGE_1) {
|
if (stage == HookStage.STAGE_1) {
|
||||||
stageIcon = StatusIcons.HOOK;
|
stageIcon = StatusIcons.HOOK;
|
||||||
stageColor = NamedTextColor.YELLOW;
|
stageColor = NamedTextColor.YELLOW;
|
||||||
stageName = "Stage 1";
|
stageName = "Стадия 1";
|
||||||
} else if (stage == HookStage.STAGE_2) {
|
} else if (stage == HookStage.STAGE_2) {
|
||||||
stageIcon = StatusIcons.HOOK;
|
stageIcon = StatusIcons.HOOK;
|
||||||
stageColor = NamedTextColor.RED;
|
stageColor = NamedTextColor.RED;
|
||||||
stageName = "Stage 2 - STRUGGLE!";
|
stageName = "Стадия 2 — ВЫРВИСЬ!";
|
||||||
} else {
|
} else {
|
||||||
stageIcon = StatusIcons.DEATH_SKULL;
|
stageIcon = StatusIcons.DEATH_SKULL;
|
||||||
stageColor = NamedTextColor.DARK_RED;
|
stageColor = NamedTextColor.DARK_RED;
|
||||||
stageName = "SACRIFICE IMMINENT";
|
stageName = "ЖЕРТВОПРИНОШЕНИЕ";
|
||||||
}
|
}
|
||||||
|
|
||||||
Component bar = stage == HookStage.STAGE_2
|
Component bar = stage == HookStage.STAGE_2
|
||||||
|
|
@ -91,7 +92,7 @@ public class ActionBar {
|
||||||
.append(bar);
|
.append(bar);
|
||||||
|
|
||||||
if (stage == HookStage.STAGE_2) {
|
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);
|
player.sendActionBar(message);
|
||||||
|
|
@ -108,22 +109,22 @@ public class ActionBar {
|
||||||
private Component formatStatus(PlayerState state) {
|
private Component formatStatus(PlayerState state) {
|
||||||
return switch (state) {
|
return switch (state) {
|
||||||
case HEALTHY -> StatusIcons.HEALTHY_HEART
|
case HEALTHY -> StatusIcons.HEALTHY_HEART
|
||||||
.append(Component.text(" Healthy").color(NamedTextColor.GREEN));
|
.append(Component.text(" Здоров").color(NamedTextColor.GREEN));
|
||||||
case INJURED -> StatusIcons.INJURED_HEART
|
case INJURED -> StatusIcons.INJURED_HEART
|
||||||
.append(Component.text(" Injured").color(NamedTextColor.YELLOW));
|
.append(Component.text(" Ранен").color(NamedTextColor.YELLOW));
|
||||||
case DOWNED -> Component.text("🆘 Downed - need help!").color(NamedTextColor.RED);
|
case DOWNED -> Component.text("🆘 В нокдауне — нужна помощь!").color(NamedTextColor.RED);
|
||||||
case HOOKED -> StatusIcons.HOOK
|
case HOOKED -> StatusIcons.HOOK
|
||||||
.append(Component.text(" Hooked!").color(NamedTextColor.DARK_RED));
|
.append(Component.text(" На крюке!").color(NamedTextColor.DARK_RED));
|
||||||
case CARRIED -> Component.text("🎒 Being carried").color(NamedTextColor.RED);
|
case CARRIED -> Component.text("🎒 Унесен маньяком").color(NamedTextColor.RED);
|
||||||
default -> Component.text("");
|
default -> Component.text("");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showCarrying(Player killer, String survivorName) {
|
public void showCarrying(Player killer, String survivorName) {
|
||||||
Component message = Component.text("Carrying: ")
|
Component message = Component.text("Несет: ")
|
||||||
.color(NamedTextColor.WHITE)
|
.color(NamedTextColor.WHITE)
|
||||||
.append(Component.text(survivorName).color(NamedTextColor.YELLOW))
|
.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);
|
killer.sendActionBar(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,14 +94,14 @@ public class Sidebar {
|
||||||
// Clear ALL old entries in this objective first time only
|
// Clear ALL old entries in this objective first time only
|
||||||
if (lastSeconds == null) {
|
if (lastSeconds == null) {
|
||||||
for (String entry : new ArrayList<>(board.getEntries())) {
|
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);
|
board.resetScores(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a FIXED entry key that never changes - only the score updates
|
// 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
|
// Simply update the score - the entry persists
|
||||||
Score score = timerObj.getScore(timerEntry);
|
Score score = timerObj.getScore(timerEntry);
|
||||||
|
|
@ -124,7 +124,7 @@ public class Sidebar {
|
||||||
|
|
||||||
// Clear entries and hide
|
// Clear entries and hide
|
||||||
for (String entry : board.getEntries()) {
|
for (String entry : board.getEntries()) {
|
||||||
if (entry.contains("⏱") || entry.contains("До конца")) {
|
if (entry.contains("⏱") || entry.contains("Осталось")) {
|
||||||
board.resetScores(entry);
|
board.resetScores(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,19 +155,17 @@ public class Sidebar {
|
||||||
if (endgameSecondsLeft >= 0) {
|
if (endgameSecondsLeft >= 0) {
|
||||||
String timerColor = endgameSecondsLeft <= 30 ? "§c" : "§e";
|
String timerColor = endgameSecondsLeft <= 30 ? "§c" : "§e";
|
||||||
setScore(objective, "§8─────────────────", line--);
|
setScore(objective, "§8─────────────────", line--);
|
||||||
setScore(objective, timerColor + "⏱ До конца: " + endgameSecondsLeft + " сек", line--);
|
setScore(objective, timerColor + "⏱ Осталось: " + endgameSecondsLeft + "с", line--);
|
||||||
setScore(objective, "§8─────────────────", line--);
|
setScore(objective, "§8─────────────────", line--);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generator progress
|
// Generator progress — use actual placed count, not config max
|
||||||
int completedGens = generatorManager != null ? generatorManager.getCompletedCount() : 0;
|
int completedGens = generatorManager != null ? generatorManager.getCompletedCount() : 0;
|
||||||
int requiredGens = cfg.getGeneratorRequired();
|
int totalGens = generatorManager != null ? generatorManager.getTotalCount() : cfg.getGeneratorRequired();
|
||||||
int totalGens = cfg.getGeneratorTotal();
|
|
||||||
|
|
||||||
setScore(objective, "§eГенераторы:", line--);
|
setScore(objective, "§eГенераторы:", line--);
|
||||||
String genBar = buildGenBar(completedGens, requiredGens);
|
String genBar = buildGenBar(completedGens, totalGens);
|
||||||
setScore(objective, " §f" + completedGens + "/" + requiredGens + " " + genBar, line--);
|
setScore(objective, " §f" + completedGens + "/" + totalGens + " " + genBar, line--);
|
||||||
setScore(objective, " §7(всего: " + totalGens + ")", line--);
|
|
||||||
setScore(objective, " ", line--);
|
setScore(objective, " ", line--);
|
||||||
|
|
||||||
// Survivor list with hearts and colors
|
// Survivor list with hearts and colors
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,16 @@ public class TitleUI {
|
||||||
Component subtitle;
|
Component subtitle;
|
||||||
|
|
||||||
if (isKiller) {
|
if (isKiller) {
|
||||||
title = Component.text("KILLER")
|
title = Component.text("МАНЬЯК")
|
||||||
.color(NamedTextColor.DARK_RED)
|
.color(NamedTextColor.DARK_RED)
|
||||||
.decoration(TextDecoration.BOLD, true);
|
.decoration(TextDecoration.BOLD, true);
|
||||||
subtitle = Component.text("Sacrifice them all")
|
subtitle = Component.text("Принеси их в жертву")
|
||||||
.color(NamedTextColor.RED);
|
.color(NamedTextColor.RED);
|
||||||
} else {
|
} else {
|
||||||
title = Component.text("SURVIVOR")
|
title = Component.text("ВЫЖИВШИЙ")
|
||||||
.color(NamedTextColor.DARK_GREEN)
|
.color(NamedTextColor.DARK_GREEN)
|
||||||
.decoration(TextDecoration.BOLD, true);
|
.decoration(TextDecoration.BOLD, true);
|
||||||
subtitle = Component.text("Repair generators and escape")
|
subtitle = Component.text("Чини генераторы и выбирайся")
|
||||||
.color(NamedTextColor.GREEN);
|
.color(NamedTextColor.GREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,17 +57,17 @@ public class TitleUI {
|
||||||
);
|
);
|
||||||
|
|
||||||
Component title = escaped
|
Component title = escaped
|
||||||
? Component.text("ESCAPED!")
|
? Component.text("СБЕЖАЛ!")
|
||||||
.color(NamedTextColor.GREEN)
|
.color(NamedTextColor.GREEN)
|
||||||
.decoration(TextDecoration.BOLD, true)
|
.decoration(TextDecoration.BOLD, true)
|
||||||
: Component.text("VICTORY!")
|
: Component.text("ПОБЕДА!")
|
||||||
.color(NamedTextColor.DARK_RED)
|
.color(NamedTextColor.DARK_RED)
|
||||||
.decoration(TextDecoration.BOLD, true);
|
.decoration(TextDecoration.BOLD, true);
|
||||||
|
|
||||||
Component subtitle = escaped
|
Component subtitle = escaped
|
||||||
? Component.text("You survived the night")
|
? Component.text("Ты пережил ночь")
|
||||||
.color(NamedTextColor.DARK_GREEN)
|
.color(NamedTextColor.DARK_GREEN)
|
||||||
: Component.text("All survivors sacrificed")
|
: Component.text("Все выжившие принесены в жертву")
|
||||||
.color(NamedTextColor.RED);
|
.color(NamedTextColor.RED);
|
||||||
|
|
||||||
player.showTitle(Title.title(title, subtitle, times));
|
player.showTitle(Title.title(title, subtitle, times));
|
||||||
|
|
@ -80,11 +80,11 @@ public class TitleUI {
|
||||||
Duration.ofSeconds(1)
|
Duration.ofSeconds(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
Component title = Component.text("DEFEAT")
|
Component title = Component.text("ПОРАЖЕНИЕ")
|
||||||
.color(NamedTextColor.DARK_RED)
|
.color(NamedTextColor.DARK_RED)
|
||||||
.decoration(TextDecoration.BOLD, true);
|
.decoration(TextDecoration.BOLD, true);
|
||||||
|
|
||||||
Component subtitle = Component.text("The Entity claims all")
|
Component subtitle = Component.text("Сущность забирает всех")
|
||||||
.color(NamedTextColor.RED);
|
.color(NamedTextColor.RED);
|
||||||
|
|
||||||
player.showTitle(Title.title(title, subtitle, times));
|
player.showTitle(Title.title(title, subtitle, times));
|
||||||
|
|
|
||||||
|
|
@ -195,12 +195,21 @@ endgame:
|
||||||
# Время открытия ворот (секунд)
|
# Время открытия ворот (секунд)
|
||||||
gate-open-seconds: 20
|
gate-open-seconds: 20
|
||||||
|
|
||||||
|
# Секунд до открытия ворот (после починки всех ген)
|
||||||
|
pregame-seconds: 30
|
||||||
|
|
||||||
|
# Секунд после открытия ворот до конца игры
|
||||||
|
postgame-seconds: 60
|
||||||
|
|
||||||
# Открытие сундуков (секунд)
|
# Открытие сундуков (секунд)
|
||||||
chest-open-seconds: 3
|
chest-open-seconds: 3
|
||||||
|
|
||||||
# Расстояние для взаимодействия с воротами (блоков)
|
# Расстояние для взаимодействия с воротами (блоков)
|
||||||
gate-interaction-range: 3.0
|
gate-interaction-range: 3.0
|
||||||
|
|
||||||
|
# Расстояние до ворот для побега (блоков)
|
||||||
|
escape-range: 2.0
|
||||||
|
|
||||||
# Уведомление маньяка о генераторе
|
# Уведомление маньяка о генераторе
|
||||||
killer-notification:
|
killer-notification:
|
||||||
# Время действия компаса (секунд)
|
# Время действия компаса (секунд)
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,11 @@ setup:
|
||||||
survivor-spawn-placed: "%setup-prefix%&aСпавн для &6%s &aвыжившего установлен! (всего: %d/4) %s"
|
survivor-spawn-placed: "%setup-prefix%&aСпавн для &6%s &aвыжившего установлен! (всего: %d/4) %s"
|
||||||
wand-point: "%setup-prefix%&a[Wand] Точка %d: %s"
|
wand-point: "%setup-prefix%&a[Wand] Точка %d: %s"
|
||||||
map-cleared: "%setup-prefix%&aКарта очищена!"
|
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Все спавны (киллер + выжившие) очищены!"
|
spawns-cleared: "%setup-prefix%&aВсе спавны (киллер + выжившие) очищены!"
|
||||||
lobby-set: "%setup-prefix%&aЛобби установлено: %s"
|
lobby-set: "%setup-prefix%&aЛобби установлено: %s"
|
||||||
|
|
||||||
|
|
@ -200,7 +205,7 @@ survivor:
|
||||||
actionbar: "&a⚕ %s %d%%"
|
actionbar: "&a⚕ %s %d%%"
|
||||||
done: "%prefix%&a✔ Вы вылечились!"
|
done: "%prefix%&a✔ Вы вылечились!"
|
||||||
cancelled: "%prefix%&eЛечение прервано."
|
cancelled: "%prefix%&eЛечение прервано."
|
||||||
depleted: "&cYour medkit has been depleted!"
|
depleted: "&cМедкит закончился!"
|
||||||
|
|
||||||
# ── Крюки ──────────────────────────────────────
|
# ── Крюки ──────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -228,18 +233,22 @@ hook:
|
||||||
sacrifice:
|
sacrifice:
|
||||||
message: "%prefix%&4Вы были принесены в жертву. Наблюдайте за матчем."
|
message: "%prefix%&4Вы были принесены в жертву. Наблюдайте за матчем."
|
||||||
title: "☠"
|
title: "☠"
|
||||||
subtitle: "&cYou have been sacrificed"
|
subtitle: "&cВы были принесены в жертву"
|
||||||
broadcast: "%s has been sacrificed to the Entity"
|
broadcast: "%s принесен в жертву Сущности"
|
||||||
|
|
||||||
# ── Сундуки ────────────────────────────────────
|
# ── Сундуки ────────────────────────────────────
|
||||||
|
|
||||||
chest:
|
chest:
|
||||||
empty: "&cThis chest is empty!"
|
empty: "&cСундук пуст!"
|
||||||
opening: "&eOpening chest..."
|
opening: "&eОткрытие сундука..."
|
||||||
cancelled: "&cOpening cancelled!"
|
cancelled: "&cОткрытие прервано!"
|
||||||
too-far: "%prefix%&cСлишком далеко!"
|
too-far: "%prefix%&cСлишком далеко!"
|
||||||
actionbar: "&6Открытие: %s %d%%"
|
actionbar: "&6Открытие: %s %d%%"
|
||||||
found: "%prefix%&aВы нашли: %s"
|
found: "%prefix%&aВы нашли: %s"
|
||||||
|
|
||||||
|
# ── Ворота ─────────────────────────────────────
|
||||||
|
|
||||||
|
gate:
|
||||||
door-open: "&aОткрываем ворота... (зажмите ПКМ)"
|
door-open: "&aОткрываем ворота... (зажмите ПКМ)"
|
||||||
door-opened: "&aВорота открыты!"
|
door-opened: "&aВорота открыты!"
|
||||||
door-nearby: "&cНет ворот поблизости!"
|
door-nearby: "&cНет ворот поблизости!"
|
||||||
|
|
@ -307,6 +316,8 @@ usage:
|
||||||
setup-killer-spawn: "&6/dbdsetup set killer-spawn — Спавн киллера (только 1)"
|
setup-killer-spawn: "&6/dbdsetup set killer-spawn — Спавн киллера (только 1)"
|
||||||
setup-survivor-spawn: "&6/dbdsetup set survivor-spawn — Спавн выжившего (до 4)"
|
setup-survivor-spawn: "&6/dbdsetup set survivor-spawn — Спавн выжившего (до 4)"
|
||||||
setup-gate: "&6/dbdsetup set gate A|B — Установить ворота (из точек ванда)"
|
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-clear-spawns: "&6/dbdsetup clear-spawns — Очистить все спавны"
|
||||||
setup-lobby: "&6/dbdsetup lobby — Установить точку лобби"
|
setup-lobby: "&6/dbdsetup lobby — Установить точку лобби"
|
||||||
admin-list-header: "&6=== DBD Admin ==="
|
admin-list-header: "&6=== DBD Admin ==="
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue