diff --git a/src/main/java/dev/loki/command/DBDAdminCommand.java b/src/main/java/dev/loki/command/DBDAdminCommand.java index 194cd61..99d298f 100644 --- a/src/main/java/dev/loki/command/DBDAdminCommand.java +++ b/src/main/java/dev/loki/command/DBDAdminCommand.java @@ -58,6 +58,7 @@ public class DBDAdminCommand implements TabExecutor { if (args.length >= 2) { return switch (args[1].toLowerCase()) { case "generator" -> listType(player, "Генераторы", locations.getGenerators()); + case "computer" -> listType(player, "Компьютеры", plugin.getComputerData().getComputers()); case "hook" -> listType(player, "Крюки", locations.getHooks()); case "chest" -> listType(player, "Сундуки", locations.getChests()); case "killer-spawn", "killerspawn" -> listType(player, "Спавн киллера", locations.getKillerSpawns()); @@ -73,6 +74,7 @@ public class DBDAdminCommand implements TabExecutor { player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin-list.header")); listType(player, "Генераторы", locations.getGenerators()); + listType(player, "Компьютеры", plugin.getComputerData().getComputers()); listType(player, "Крюки", locations.getHooks()); listType(player, "Сундуки", locations.getChests()); listType(player, "Спавн киллера", locations.getKillerSpawns()); @@ -174,7 +176,7 @@ public class DBDAdminCommand implements TabExecutor { } if (args.length == 2 && args[0].equalsIgnoreCase("list")) { - return Arrays.asList("generator", "hook", "chest", "killer-spawn", "survivor-spawn").stream() + return Arrays.asList("generator", "computer", "hook", "chest", "killer-spawn", "survivor-spawn").stream() .filter(s -> s.startsWith(args[1].toLowerCase())) .toList(); } diff --git a/src/main/java/dev/loki/command/SetupCommand.java b/src/main/java/dev/loki/command/SetupCommand.java index 197f195..53b0b3b 100644 --- a/src/main/java/dev/loki/command/SetupCommand.java +++ b/src/main/java/dev/loki/command/SetupCommand.java @@ -132,7 +132,7 @@ public class SetupCommand implements TabExecutor { default -> type; }; - if (!List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn").contains(mode)) { + if (!List.of("hook", "chest", "generator", "computer", "killer-spawn", "survivor-spawn").contains(mode)) { player.sendMessage(messages().get("setup.unknown-type", type)); return true; } @@ -264,6 +264,17 @@ public class SetupCommand implements TabExecutor { plugin.getGeneratorData().addGenerator(gLoc); player.sendMessage(messages().get("setup.generator-placed", formatLoc(loc))); } + case "computer" -> { + Location cLoc = blockLoc.clone().add(0.5, 0, 0.5); + if (locations.hasLocation("computers", cLoc)) { + player.sendMessage(messages().get("setup.duplicate")); + break; + } + locations.addLocation("computers", cLoc); + plugin.getComputerData().addComputer(cLoc); + player.sendMessage(plugin.getConfigManager().getMessages().get("setup.computer-placed", + formatLoc(loc))); + } case "killer-spawn" -> { if (locations.getKillerSpawn() != null) { player.sendMessage(messages().get("setup.killer-spawn-exists")); @@ -351,6 +362,7 @@ public class SetupCommand implements TabExecutor { // Clear ALL object data from config (no block destruction) locations.clearLocationList("generators"); + locations.clearLocationList("computers"); locations.clearLocationList("hooks"); locations.clearLocationList("chests"); locations.clearLocationList("killer-spawn"); @@ -361,6 +373,7 @@ public class SetupCommand implements TabExecutor { } locations.clearSurvivorSpawns(); plugin.getGeneratorData().clearGenerators(); + plugin.getComputerData().clearComputers(); plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); player.sendMessage(messages().get("setup.cleared")); return true; @@ -369,6 +382,7 @@ public class SetupCommand implements TabExecutor { private boolean clearObjectsByType(Player player, LocationsConfig locations, String type) { String path = switch (type) { case "generator" -> "generators"; + case "computer" -> "computers"; case "hook" -> "hooks"; case "chest" -> "chests"; case "gate" -> null; @@ -392,6 +406,8 @@ public class SetupCommand implements TabExecutor { locations.clearLocationList(path); if (type.equals("generator")) { plugin.getGeneratorData().clearGenerators(); + } else if (type.equals("computer")) { + plugin.getComputerData().clearComputers(); } } plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); @@ -423,6 +439,18 @@ public class SetupCommand implements TabExecutor { plugin.getGeneratorData().removeGenerator(index); player.sendMessage(messages().get("setup.deleted", "generator #" + index)); } + case "computer" -> { + int index = parseInt(args[2], player); + if (index < 0) return true; + var comps = plugin.getComputerData().getComputers(); + if (index >= comps.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("computers", index); + plugin.getComputerData().removeComputer(index); + player.sendMessage(messages().get("setup.deleted", "computer #" + index)); + } case "hook" -> { int index = parseInt(args[2], player); if (index < 0) return true; @@ -515,6 +543,7 @@ public class SetupCommand implements TabExecutor { sender.sendMessage(messages().get("usage.setup-hook")); sender.sendMessage(messages().get("usage.setup-chest")); sender.sendMessage(messages().get("usage.setup-generator")); + sender.sendMessage(messages().get("usage.setup-computer")); sender.sendMessage(messages().get("usage.setup-killer-spawn")); sender.sendMessage(messages().get("usage.setup-survivor-spawn")); sender.sendMessage(messages().get("usage.setup-gate")); @@ -539,7 +568,7 @@ public class SetupCommand implements TabExecutor { } if (args.length == 2) { if (args[0].equalsIgnoreCase("set") || args[0].equalsIgnoreCase("clear") || args[0].equalsIgnoreCase("delete")) { - return List.of("generator", "hook", "chest", "killer-spawn", "survivor-spawn", "gate").stream() + return List.of("computer", "generator", "hook", "chest", "killer-spawn", "survivor-spawn", "gate").stream() .filter(s -> s.startsWith(args[1].toLowerCase())).toList(); } } diff --git a/src/main/java/dev/loki/config/GameConfig.java b/src/main/java/dev/loki/config/GameConfig.java index 64787e4..2b62100 100644 --- a/src/main/java/dev/loki/config/GameConfig.java +++ b/src/main/java/dev/loki/config/GameConfig.java @@ -28,8 +28,6 @@ public class GameConfig { setDefault("generators.total-count", 7); setDefault("generators.required-count", 5); setDefault("generators.repair-seconds", 90); - setDefault("generators.regression-rate", 0.25); - setDefault("generators.kick-regression-rate", 2.5); setDefault("generators.kick-regression-percent", 20.0); setDefault("generators.kick-duration-seconds", 30); setDefault("generators.kick-cooldown-seconds", 30); @@ -37,6 +35,12 @@ public class GameConfig { setDefault("generators.interaction-range", 3.0); setDefault("generators.minigame", "gui"); + // Computers + setDefault("computers.total-count", 2); + setDefault("computers.required-count", 2); + setDefault("computers.repair-seconds", 90); + setDefault("computers.interaction-range", 3.0); + // Hooks setDefault("hooks.stage1-seconds", 60); setDefault("hooks.stage2-seconds", 60); @@ -111,6 +115,7 @@ public class GameConfig { // Block types for spawning setDefault("blocks.generator", "BLAST_FURNACE"); + setDefault("blocks.computer", "LECTERN"); setDefault("blocks.chest", "CHEST"); setDefault("blocks.hook", "IRON_BARS"); @@ -118,6 +123,8 @@ public class GameConfig { setDefault("holograms.enabled", true); setDefault("holograms.generator.enabled", true); setDefault("holograms.generator.height-offset", -1.0); + setDefault("holograms.computer.enabled", true); + setDefault("holograms.computer.height-offset", -1.0); setDefault("holograms.hook.enabled", true); setDefault("holograms.hook.height-offset", -1.0); setDefault("holograms.chest.enabled", true); @@ -133,6 +140,22 @@ public class GameConfig { // Chest loot setDefault("chest.loot-reset-minutes", 3.0); + + // Loot weights + setDefault("loot.medkit.weight", 25); + setDefault("loot.medkit.min-charges", 12); + setDefault("loot.medkit.max-charges", 16); + setDefault("loot.bandage.weight", 25); + setDefault("loot.full-medkit.weight", 10); + setDefault("loot.soda.weight", 20); + setDefault("loot.wrench.weight", 20); + + // Item model data + setDefault("items.model-data.medkit", 1001); + setDefault("items.model-data.bandage", 1004); + setDefault("items.model-data.fullmedkit", 1005); + setDefault("items.model-data.soda", 1006); + setDefault("items.model-data.wrench", 1007); } private void setDefault(String path, Object value) { @@ -176,6 +199,20 @@ public class GameConfig { public double getGeneratorInteractionRange() { return config.getDouble("generators.interaction-range", 3.0); } public String getGeneratorMinigame() { return config.getString("generators.minigame", "gui"); } + // ------------------------------------------------------------------------- + // Computers + // ------------------------------------------------------------------------- + public int getComputerTotal() { return config.getInt("computers.total-count", 5); } + public int getComputerRequired() { return config.getInt("computers.required-count", 3); } + public int getComputerRepairSeconds() { return config.getInt("computers.repair-seconds", 90); } + public double getComputerRegressionRate() { return config.getDouble("computers.regression-rate", 0.25); } + public double getComputerKickRegressionRate() { return config.getDouble("computers.kick-regression-rate", 2.5); } + public double getComputerKickRegressionPercent() { return config.getDouble("computers.kick-regression-percent", 20.0); } + public int getComputerKickDuration() { return config.getInt("computers.kick-duration-seconds", 30); } + public int getComputerKickCooldownSeconds() { return config.getInt("computers.kick-cooldown-seconds", 30); } + public int getComputerKickChargeSeconds() { return config.getInt("computers.kick-charge-seconds", 3); } + public double getComputerInteractionRange() { return config.getDouble("computers.interaction-range", 3.0); } + // ------------------------------------------------------------------------- // Hooks // ------------------------------------------------------------------------- @@ -229,6 +266,7 @@ public class GameConfig { // Block types (какие блоки спавнятся на местах объектов) // ------------------------------------------------------------------------- public String getGeneratorBlock() { return config.getString("blocks.generator", "BLAST_FURNACE"); } + public String getComputerBlock() { return config.getString("blocks.computer", "LECTERN"); } public String getChestBlock() { return config.getString("blocks.chest", "CHEST"); } public String getHookBlock() { return config.getString("blocks.hook", "IRON_BARS"); } @@ -238,6 +276,8 @@ public class GameConfig { public boolean isHologramsEnabled() { return config.getBoolean("holograms.enabled", true); } public boolean isGeneratorHologramEnabled() { return config.getBoolean("holograms.generator.enabled", true); } public double getGeneratorHologramHeightOffset() { return config.getDouble("holograms.generator.height-offset", -1.0); } + public boolean isComputerHologramEnabled() { return config.getBoolean("holograms.computer.enabled", true); } + public double getComputerHologramHeightOffset() { return config.getDouble("holograms.computer.height-offset", -1.0); } public boolean isHookHologramEnabled() { return config.getBoolean("holograms.hook.enabled", true); } public double getHookHologramHeightOffset() { return config.getDouble("holograms.hook.height-offset", 0.0); } public boolean isChestHologramEnabled() { return config.getBoolean("holograms.chest.enabled", true); } diff --git a/src/main/java/dev/loki/config/LocationsConfig.java b/src/main/java/dev/loki/config/LocationsConfig.java index 4e986a6..7bf719c 100644 --- a/src/main/java/dev/loki/config/LocationsConfig.java +++ b/src/main/java/dev/loki/config/LocationsConfig.java @@ -31,6 +31,9 @@ public class LocationsConfig { if (!config.contains("generators") || config.getList("generators") == null) { config.set("generators", new ArrayList<>()); } + if (!config.contains("computers") || config.getList("computers") == null) { + config.set("computers", new ArrayList<>()); + } if (!config.contains("hooks") || config.getList("hooks") == null) { config.set("hooks", new ArrayList<>()); } diff --git a/src/main/java/dev/loki/core/GameSession.java b/src/main/java/dev/loki/core/GameSession.java index 46e2a57..9960d4a 100644 --- a/src/main/java/dev/loki/core/GameSession.java +++ b/src/main/java/dev/loki/core/GameSession.java @@ -5,6 +5,7 @@ import dev.loki.gate.GateManager; import dev.loki.hook.HookManager; import dev.loki.mdbd; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.phase.EndgamePhase; import dev.loki.phase.GameplayPhase; import dev.loki.phase.LobbyPhase; @@ -28,6 +29,7 @@ public class GameSession { private int ticksElapsed; private RoleAssigner roleAssigner; private GeneratorManager generatorManager; + private ComputerManager computerManager; private boolean forceStarted = false; private Runnable onStopped; // callback для LobbyRegistry @@ -44,6 +46,9 @@ public class GameSession { this.generatorManager = new GeneratorManager( plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired(), plugin.getConfigManager().getGame()); + this.computerManager = new ComputerManager( + plugin.getComputerData(), plugin.getConfigManager().getGame().getComputerRequired(), + plugin.getConfigManager().getGame()); } public void start() { @@ -110,7 +115,7 @@ public class GameSession { return new RoleAssignPhase(roleAssigner, skip); } if (currentPhase instanceof RoleAssignPhase) { - return new GameplayPhase(generatorManager); + return new GameplayPhase(generatorManager, computerManager); } if (currentPhase instanceof GameplayPhase gp) { // Всегда переходим в EndgamePhase для корректного завершения игры @@ -177,6 +182,10 @@ public class GameSession { return generatorManager; } + public ComputerManager getComputerManager() { + return computerManager; + } + public HookManager getHookManager() { return hookManager; } public void setHookManager(HookManager hm) { this.hookManager = hm; } diff --git a/src/main/java/dev/loki/data/ComputerDataManager.java b/src/main/java/dev/loki/data/ComputerDataManager.java new file mode 100644 index 0000000..4dce0e4 --- /dev/null +++ b/src/main/java/dev/loki/data/ComputerDataManager.java @@ -0,0 +1,148 @@ +package dev.loki.data; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ComputerDataManager { + + private static final double ZONE_RADIUS = 3.0; + + private final JavaPlugin plugin; + private final File dataFile; + private FileConfiguration data; + + public ComputerDataManager(JavaPlugin plugin) { + this.plugin = plugin; + this.dataFile = new File(plugin.getDataFolder(), "computers.yml"); + load(); + } + + public void load() { + if (!dataFile.exists()) { + try { + dataFile.createNewFile(); + } catch (IOException e) { + plugin.getLogger().warning("Could not create computers.yml"); + } + } + this.data = YamlConfiguration.loadConfiguration(dataFile); + } + + public void save() { + try { + data.save(dataFile); + } catch (IOException e) { + plugin.getLogger().warning("Could not save computers.yml"); + } + } + + public void addComputer(Location location) { + List list = data.getStringList("computers"); + String entry = formatLocation(location); + if (!list.contains(entry)) { + list.add(entry); + data.set("computers", list); + save(); + } + } + + public void removeComputer(int index) { + List list = data.getStringList("computers"); + if (index >= 0 && index < list.size()) { + list.remove(index); + data.set("computers", list); + save(); + } + } + + public void clearComputers() { + data.set("computers", null); + save(); + } + + public List getComputers() { + List result = new ArrayList<>(); + List list = data.getStringList("computers"); + for (String entry : list) { + Location loc = parseLocation(entry); + if (loc != null) { + result.add(loc); + } + } + return result; + } + + public List getComputerEntries() { + return data.getStringList("computers"); + } + + public int getCount() { + return data.getStringList("computers").size(); + } + + public static String formatLocation(Location loc) { + return String.format("%s:%.0f|%.0f|%.0f", + loc.getWorld().getName(), + loc.getX(), + loc.getY(), + loc.getZ()); + } + + private Location parseLocation(String str) { + String[] parts = str.split(":"); + if (parts.length < 2) return null; + + World world = Bukkit.getWorld(parts[0].trim()); + if (world == null) return null; + + String[] coords = parts[1].split("\\|"); + if (coords.length < 3) return null; + + try { + double x = Double.parseDouble(coords[0].trim()); + double y = Double.parseDouble(coords[1].trim()); + double z = Double.parseDouble(coords[2].trim()); + return new Location(world, x, y, z); + } catch (NumberFormatException e) { + return null; + } + } + + public static double getZoneRadius() { + return ZONE_RADIUS; + } + + public boolean isInZone(Location location, List computers) { + for (Location compLoc : computers) { + if (!compLoc.getWorld().equals(location.getWorld())) continue; + if (compLoc.distance(location) <= ZONE_RADIUS) { + return true; + } + } + return false; + } + + public Location getNearestComputer(Location location, List computers) { + Location nearest = null; + double nearestDist = ZONE_RADIUS; + + for (Location compLoc : computers) { + if (!compLoc.getWorld().equals(location.getWorld())) continue; + double dist = compLoc.distance(location); + if (dist <= nearestDist) { + nearestDist = dist; + nearest = compLoc; + } + } + return nearest; + } +} diff --git a/src/main/java/dev/loki/listener/PlayerInteractListener.java b/src/main/java/dev/loki/listener/PlayerInteractListener.java index 0985957..e236438 100644 --- a/src/main/java/dev/loki/listener/PlayerInteractListener.java +++ b/src/main/java/dev/loki/listener/PlayerInteractListener.java @@ -3,6 +3,7 @@ package dev.loki.listener; import dev.loki.command.SetupCommand; import dev.loki.core.GameManager; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; import org.bukkit.Location; @@ -26,9 +27,11 @@ public class PlayerInteractListener implements Listener { private final Plugin plugin; private final GameManager gameManager; private GeneratorManager generatorManager; + private ComputerManager computerManager; private SetupCommand setupCommand; private BiConsumer onGeneratorInteract; + private BiConsumer onComputerInteract; private BiConsumer onAttack; private BiConsumer onRescueAttempt; private BiConsumer onChestInteract; @@ -53,6 +56,7 @@ public class PlayerInteractListener implements Listener { } public void setOnGeneratorInteract(BiConsumer h) { this.onGeneratorInteract = h; } + public void setOnComputerInteract(BiConsumer h) { this.onComputerInteract = h; } public void setOnAttack(BiConsumer h) { this.onAttack = h; } public void setOnRescueAttempt(BiConsumer h) { this.onRescueAttempt = h; } public void setOnChestInteract(BiConsumer h) { this.onChestInteract = h; } @@ -69,6 +73,10 @@ public class PlayerInteractListener implements Listener { this.generatorManager = manager; } + public void setComputerManager(ComputerManager manager) { + this.computerManager = manager; + } + @EventHandler public void onSneak(PlayerToggleSneakEvent event) { Player player = event.getPlayer(); @@ -96,8 +104,10 @@ public class PlayerInteractListener implements Listener { if (st != PlayerState.HEALTHY && st != PlayerState.INJURED) return; if (event.isSneaking()) { - // Generator repair takes priority over healing + // Generator repair takes priority if (tryGeneratorInteract(player)) return; + // Computer repair + if (tryComputerInteract(player)) return; // Healing: check if sneaking near an injured/downed survivor if (onHealSneak != null) { @@ -216,6 +226,19 @@ public class PlayerInteractListener implements Listener { return true; } + private boolean tryComputerInteract(Player player) { + if (onComputerInteract == null || computerManager == null) return false; + var session = gameManager.getPlayerSession(player.getUniqueId()); + if (session == null) return false; + BasePlayer bp = session.getPlayer(player.getUniqueId()); + if (bp == null || bp.isKiller()) return false; + var comp = computerManager.getNearestComputer( + player.getLocation(), session.getConfig().getComputerInteractionRange()); + if (comp == null) return false; + onComputerInteract.accept(player, Action.RIGHT_CLICK_AIR); + return true; + } + private boolean isOnCooldown(Player player) { long now = System.currentTimeMillis(); Long last = lastClick.get(player.getUniqueId()); @@ -234,6 +257,11 @@ public class PlayerInteractListener implements Listener { return; } + // Cancel right-click on LECTERN (computer block) to prevent book editing GUI + if (type.equals("LECTERN")) { + event.setCancelled(true); + } + // Chest interaction - cancel vanilla GUI for ALL players (including killer) if (type.contains("CHEST")) { event.setCancelled(true); @@ -257,6 +285,7 @@ public class PlayerInteractListener implements Listener { } if (!bp.isKiller() && tryGeneratorInteract(player)) return; + if (!bp.isKiller() && tryComputerInteract(player)) return; // Killer attack (hook/carry) — fires on both air AND block clicks if (bp.isKiller() && onAttack != null) { diff --git a/src/main/java/dev/loki/listener/SetupBlockBreakListener.java b/src/main/java/dev/loki/listener/SetupBlockBreakListener.java index ae2b868..80597fe 100644 --- a/src/main/java/dev/loki/listener/SetupBlockBreakListener.java +++ b/src/main/java/dev/loki/listener/SetupBlockBreakListener.java @@ -49,6 +49,7 @@ public class SetupBlockBreakListener implements Listener { Material placeMat = switch (mode) { case "chest" -> Material.matchMaterial(cfg.getChestBlock()); case "generator" -> Material.matchMaterial(cfg.getGeneratorBlock()); + case "computer" -> Material.matchMaterial(cfg.getComputerBlock()); case "hook" -> Material.matchMaterial(cfg.getHookBlock()); default -> null; }; diff --git a/src/main/java/dev/loki/mdbd.java b/src/main/java/dev/loki/mdbd.java index d1b2d93..6799ed3 100644 --- a/src/main/java/dev/loki/mdbd.java +++ b/src/main/java/dev/loki/mdbd.java @@ -6,6 +6,7 @@ import dev.loki.command.SetupCommand; import dev.loki.config.ConfigManager; import dev.loki.core.GameManager; import dev.loki.data.GeneratorDataManager; +import dev.loki.data.ComputerDataManager; import dev.loki.listener.PlayerInteractListener; import dev.loki.listener.PlayerJoinListener; import dev.loki.listener.PlayerMoveListener; @@ -25,6 +26,7 @@ public final class mdbd extends JavaPlugin { private MapManager mapManager; private MapBuilder mapBuilder; private GeneratorDataManager generatorData; + private ComputerDataManager computerData; private LobbyRegistry lobbyRegistry; private DBDCommand dbdCommand; private DBDAdminCommand dbdAdminCommand; @@ -53,6 +55,7 @@ public final class mdbd extends JavaPlugin { this.mapBuilder = new MapBuilder(this, templateManager); this.generatorData = new GeneratorDataManager(this); + this.computerData = new ComputerDataManager(this); this.lobbyManager = new LobbyManager(this); this.lobbyManager.loadWorlds(); @@ -103,6 +106,9 @@ public final class mdbd extends JavaPlugin { if (generatorData != null) { generatorData.save(); } + if (computerData != null) { + computerData.save(); + } if (lobbyRegistry != null) { lobbyRegistry.save(); } @@ -137,6 +143,10 @@ public final class mdbd extends JavaPlugin { return generatorData; } + public ComputerDataManager getComputerData() { + return computerData; + } + public DBDAdminCommand getDbdAdminCommand() { return dbdAdminCommand; } @@ -162,6 +172,7 @@ public final class mdbd extends JavaPlugin { dev.loki.item.DBDItems.loadModelData(configManager.getGame().getConfig()); templateManager.loadAllTemplates(); generatorData.load(); + computerData.load(); getLogger().info("All configs reloaded!"); } } diff --git a/src/main/java/dev/loki/objective/Computer.java b/src/main/java/dev/loki/objective/Computer.java new file mode 100644 index 0000000..bf64c19 --- /dev/null +++ b/src/main/java/dev/loki/objective/Computer.java @@ -0,0 +1,146 @@ +package dev.loki.objective; + +import dev.loki.data.ComputerDataManager; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class Computer { + private final int id; + private final Location location; + private final double zoneRadius; + private double progress; + private boolean isCompleted; + private boolean isBlocked; + private UUID currentRepairer; + private int regressionTicks; + private boolean wasKicked; + + public Computer(int id, Location location) { + this(id, location, ComputerDataManager.getZoneRadius()); + } + + public Computer(int id, Location location, double zoneRadius) { + this.id = id; + this.location = location; + this.zoneRadius = zoneRadius; + this.progress = 0.0; + this.isCompleted = false; + this.isBlocked = false; + this.currentRepairer = null; + this.regressionTicks = 0; + this.wasKicked = false; + } + + public int getId() { + return id; + } + + public Location getLocation() { + return location; + } + + public double getZoneRadius() { + return zoneRadius; + } + + public boolean isPlayerInZone(Player player) { + if (!player.getWorld().equals(location.getWorld())) return false; + return player.getLocation().distance(location) <= zoneRadius; + } + + public boolean isLocationInZone(Location loc) { + if (!loc.getWorld().equals(location.getWorld())) return false; + return loc.distance(location) <= zoneRadius; + } + + public double getProgress() { + return progress; + } + + public void setProgress(double progress) { + this.progress = Math.max(0, Math.min(100, progress)); + if (this.progress >= 100) { + this.progress = 100; + this.isCompleted = true; + } + } + + public void addProgress(double amount) { + setProgress(progress + amount); + } + + public void removeProgress(double amount) { + setProgress(progress - amount); + } + + public boolean isCompleted() { + return isCompleted; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + this.isBlocked = blocked; + } + + public UUID getCurrentRepairer() { + return currentRepairer; + } + + public void setCurrentRepairer(UUID repairer) { + this.currentRepairer = repairer; + } + + public boolean isBeingRepaired() { + return currentRepairer != null; + } + + public int getRegressionTicks() { + return regressionTicks; + } + + public void setRegressionTicks(int ticks) { + this.regressionTicks = ticks; + } + + public void decrementRegression() { + if (regressionTicks > 0) { + regressionTicks--; + } + } + + public boolean isUnderRegression() { + return regressionTicks > 0 && !isCompleted && !isBeingRepaired(); + } + + public void setUnderRegression(boolean kicked, int durationTicks) { + this.wasKicked = kicked; + this.regressionTicks = durationTicks; + } + + public boolean wasKicked() { + return wasKicked; + } + + public void setKicked(boolean kicked) { + this.wasKicked = kicked; + } + + public boolean canBeRepaired() { + return !isCompleted && !isBlocked && !isUnderRegression(); + } + + @Deprecated + public Block getBlock() { + return location.getBlock(); + } + + public double getDistance(Location loc) { + return location.distance(loc); + } +} diff --git a/src/main/java/dev/loki/objective/ComputerHologram.java b/src/main/java/dev/loki/objective/ComputerHologram.java new file mode 100644 index 0000000..8dbf7a7 --- /dev/null +++ b/src/main/java/dev/loki/objective/ComputerHologram.java @@ -0,0 +1,134 @@ +package dev.loki.objective; + +import dev.loki.mdbd; +import dev.loki.util.TextUtil; +import org.bukkit.Location; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Display; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.TextDisplay; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.Map; + +public class ComputerHologram { + + private final mdbd plugin; + private final ComputerManager manager; + private final Map displays = new HashMap<>(); + private BukkitTask task; + private boolean useTextDisplay; + + public ComputerHologram(mdbd plugin, ComputerManager manager) { + this.plugin = plugin; + this.manager = manager; + this.useTextDisplay = isTextDisplayAvailable(); + } + + private static boolean isTextDisplayAvailable() { + try { + EntityType.valueOf("TEXT_DISPLAY"); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public void start() { + stop(); + spawnAll(); + task = plugin.getServer().getScheduler().runTaskTimer(plugin, this::refresh, 20L, 20L); + plugin.getLogger().info("[ComputerHologram] Started with " + displays.size() + " holograms (mode=" + (useTextDisplay ? "TextDisplay" : "ArmorStand") + ")"); + } + + public void stop() { + if (task != null) { + task.cancel(); + task = null; + } + for (Object display : displays.values()) { + if (display instanceof TextDisplay td && td.isValid()) { + td.remove(); + } else if (display instanceof ArmorStand as && as.isValid()) { + as.remove(); + } + } + displays.clear(); + } + + private void spawnAll() { + double heightOffset = plugin.getConfigManager().getGame().getComputerHologramHeightOffset(); + for (Computer comp : manager.getAllComputers()) { + Location loc = comp.getLocation(); + if (loc.getWorld() == null) continue; + + Location holoLoc = loc.getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); + try { + if (useTextDisplay) { + TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { + d.setBillboard(Display.Billboard.VERTICAL); + d.setSeeThrough(false); + d.setShadowed(true); + d.setDefaultBackground(false); + d.setPersistent(false); + d.setTextOpacity((byte) -1); + }); + displays.put(comp.getId(), display); + } else { + ArmorStand stand = loc.getWorld().spawn(holoLoc, ArmorStand.class, as -> { + as.setInvisible(true); + as.setInvulnerable(true); + as.setGravity(false); + as.setMarker(true); + as.setCustomNameVisible(true); + as.setPersistent(false); + }); + displays.put(comp.getId(), stand); + } + } catch (Exception e) { + plugin.getLogger().warning("[ComputerHologram] Failed to spawn hologram for computer " + comp.getId() + ": " + e.getMessage()); + } + } + } + + private void refresh() { + double heightOffset = plugin.getConfigManager().getGame().getComputerHologramHeightOffset(); + int total = manager.getTotalCount(); + for (Computer comp : manager.getAllComputers()) { + Object display = displays.get(comp.getId()); + if (display == null) continue; + + boolean valid = (display instanceof TextDisplay td && td.isValid()) || + (display instanceof ArmorStand as && as.isValid()); + if (!valid) continue; + + int pct = (int) comp.getProgress(); + String bar = buildBar(pct); + String text; + if (comp.isCompleted()) { + text = "§b🖥 Компьютер §f#" + (comp.getId() + 1) + "/" + total + "\n§a✔ Починен!"; + } else if (comp.isUnderRegression()) { + text = "§b🖥 Компьютер §f#" + (comp.getId() + 1) + "/" + total + "\n§c↓ " + bar + " §f" + pct + "%"; + } else { + text = "§b🖥 Компьютер §f#" + (comp.getId() + 1) + "/" + total + "\n§e" + bar + " §f" + pct + "%"; + } + + Location base = comp.getLocation().getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); + + if (display instanceof TextDisplay td) { + td.text(TextUtil.color(text)); + td.teleport(base); + } else if (display instanceof ArmorStand as) { + as.setCustomName(text); + as.teleport(base); + } + } + } + + private String buildBar(int pct) { + int filled = pct / 10; + int empty = 10 - filled; + return "§e" + "█".repeat(Math.max(0, filled)) + "§7" + "░".repeat(Math.max(0, empty)); + } +} diff --git a/src/main/java/dev/loki/objective/ComputerManager.java b/src/main/java/dev/loki/objective/ComputerManager.java new file mode 100644 index 0000000..e63eadf --- /dev/null +++ b/src/main/java/dev/loki/objective/ComputerManager.java @@ -0,0 +1,114 @@ +package dev.loki.objective; + +import dev.loki.data.ComputerDataManager; +import dev.loki.config.GameConfig; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ComputerManager { + private final Map computers; + private final int requiredCount; + private final double normalRegressionRate; + private final double kickRegressionRate; + + public ComputerManager(ComputerDataManager dataManager, int requiredCount, GameConfig config) { + this.computers = new HashMap<>(); + loadComputers(dataManager); + this.requiredCount = Math.min(requiredCount, computers.size()); + this.normalRegressionRate = config.getComputerRegressionRate() / 20.0; + this.kickRegressionRate = config.getComputerKickRegressionRate() / 20.0; + } + + private void loadComputers(ComputerDataManager dataManager) { + List locations = dataManager.getComputers(); + for (int i = 0; i < locations.size(); i++) { + computers.put(i, new Computer(i, locations.get(i))); + } + } + + public Computer getComputer(int id) { return computers.get(id); } + + public Computer getComputerAt(Location location) { + for (Computer comp : computers.values()) { + if (comp.isLocationInZone(location)) return comp; + } + return null; + } + + public Computer getNearestComputer(Location location, double maxDistance) { + Computer nearest = null; + double nearestDist = maxDistance; + for (Computer comp : computers.values()) { + if (!comp.getLocation().getWorld().equals(location.getWorld())) continue; + double dist = comp.getDistance(location); + if (dist < nearestDist) { nearestDist = dist; nearest = comp; } + } + return nearest; + } + + public boolean isInAnyZone(Player player) { + return getComputerAt(player.getLocation()) != null; + } + + public List getAllComputers() { return new ArrayList<>(computers.values()); } + + public List getIncompleteComputers() { + List incomplete = new ArrayList<>(); + for (Computer comp : computers.values()) { + if (!comp.isCompleted()) incomplete.add(comp); + } + return incomplete; + } + + public int getTotalCount() { return computers.size(); } + public int getRequiredCount() { return requiredCount; } + + public int getCompletedCount() { + int count = 0; + for (Computer comp : computers.values()) { + if (comp.isCompleted()) count++; + } + return count; + } + + public int getRemainingRequired() { + return Math.max(0, requiredCount - getCompletedCount()); + } + + public void onComputerCompleted(Computer computer) { + } + + public boolean areAllRequiredCompleted() { + return getCompletedCount() >= requiredCount; + } + + public double getAverageProgress() { + if (computers.isEmpty()) return 0; + double total = 0; + for (Computer comp : computers.values()) total += comp.getProgress(); + return total / computers.size(); + } + + public int getActiveRepairCount() { + int count = 0; + for (Computer comp : computers.values()) { + if (comp.isBeingRepaired()) count++; + } + return count; + } + + public void tick() { + for (Computer comp : computers.values()) { + if (comp.isUnderRegression()) { + double rate = comp.wasKicked() ? kickRegressionRate : normalRegressionRate; + comp.removeProgress(rate); + } + comp.decrementRegression(); + } + } +} diff --git a/src/main/java/dev/loki/objective/ComputerRepair.java b/src/main/java/dev/loki/objective/ComputerRepair.java new file mode 100644 index 0000000..bf193fd --- /dev/null +++ b/src/main/java/dev/loki/objective/ComputerRepair.java @@ -0,0 +1,195 @@ +package dev.loki.objective; + +import dev.loki.mdbd; +import dev.loki.minigames.GeneratorMinigame; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ComputerRepair { + private final mdbd plugin; + private final ComputerManager manager; + private final Map sessions; + private final GeneratorMinigame minigame; + + private dev.loki.item.ItemUseListener itemUseListener; + + private static final double REPAIR_PER_TICK = 100.0 / 1600.0; + private static final double REPAIR_BOOST_MULTIPLIER = 2.0; + private static final double REPAIR_RANGE = 3.0; + + public void setItemUseListener(dev.loki.item.ItemUseListener listener) { + this.itemUseListener = listener; + } + + public ComputerRepair(mdbd plugin, ComputerManager manager, GeneratorMinigame minigame) { + this.plugin = plugin; + this.manager = manager; + this.minigame = minigame; + this.sessions = new HashMap<>(); + } + + public void startRepair(Player player, Computer computer) { + UUID uuid = player.getUniqueId(); + + if (sessions.containsKey(uuid)) { + RepairSession existing = sessions.get(uuid); + if (existing.computer.getId() == computer.getId()) { + return; + } else { + stopRepair(player); + } + } + + if (!computer.canBeRepaired()) { + player.sendMessage(plugin.getConfigManager().getMessages().get("computer.repair.cannot")); + return; + } + + if (computer.isBeingRepaired() && !computer.getCurrentRepairer().equals(uuid)) { + player.sendMessage(plugin.getConfigManager().getMessages().get("computer.repair.busy")); + return; + } + + computer.setCurrentRepairer(uuid); + RepairSession session = new RepairSession(player, computer); + sessions.put(uuid, session); + } + + public void stopRepair(Player player) { + UUID uuid = player.getUniqueId(); + RepairSession session = sessions.remove(uuid); + + if (session != null) { + session.computer.setCurrentRepairer(null); + if (!session.computer.isUnderRegression() && !session.computer.isBlocked()) { + session.computer.setBlocked(false); + } + player.sendActionBar(Component.empty()); + } + } + + public void tick() { + for (RepairSession session : new HashMap<>(sessions).values()) { + if (!updateRepair(session)) { + stopRepair(session.player); + } + } + } + + private boolean updateRepair(RepairSession session) { + Player player = session.player; + Computer computer = session.computer; + + if (!player.isOnline()) return false; + if (!player.isSneaking()) return false; + if (computer.getDistance(player.getLocation()) > REPAIR_RANGE) return false; + if (computer.isCompleted()) return false; + + if (minigame.isActive(player.getUniqueId())) return true; + + if (session.skillCheckCooldown > 0) { + session.skillCheckCooldown--; + } else if (shouldTriggerSkillCheck()) { + minigame.start(player, (p, success) -> { + Computer c = session.computer; + if (success) { + double winPercent = plugin.getConfigManager().getGame().getSkillCheckWinPercent(); + c.addProgress(winPercent); + p.sendActionBar(plugin.getConfigManager().getMessages().get("computer.repair.skillcheck-success", (int)winPercent)); + p.playSound(p.getLocation(), org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.2f); + } else { + double lossPercent = plugin.getConfigManager().getGame().getSkillCheckLossPercent(); + c.removeProgress(lossPercent); + p.sendActionBar(plugin.getConfigManager().getMessages().get("computer.repair.skillcheck-fail", (int)lossPercent)); + p.playSound(p.getLocation(), org.bukkit.Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); + } + }); + session.skillCheckCooldown = 100 + (int)(Math.random() * 200); + return true; + } + + double rate = REPAIR_PER_TICK; + if (itemUseListener != null && itemUseListener.hasRepairBoost(player.getUniqueId())) { + rate *= REPAIR_BOOST_MULTIPLIER; + } + computer.addProgress(rate); + showProgressBar(player, session); + + if (computer.isCompleted()) { + onComputerComplete(computer, player); + return false; + } + + return true; + } + + private void showProgressBar(Player player, RepairSession session) { + double progress = session.computer.getProgress(); + String bar = renderProgressBar(progress); + player.sendActionBar(plugin.getConfigManager().getMessages().get("computer.repair.actionbar", bar)); + } + + private String renderProgressBar(double progress) { + int filled = (int) (10 * progress / 100.0); + int empty = 10 - filled; + StringBuilder bar = new StringBuilder(""); + bar.append("█".repeat(Math.max(0, filled))); + bar.append(""); + bar.append("░".repeat(Math.max(0, empty))); + bar.append(" ").append((int)progress).append("%"); + return bar.toString(); + } + + private void onComputerComplete(Computer computer, Player player) { + manager.onComputerCompleted(computer); + Bukkit.broadcast(plugin.getConfigManager().getMessages().get( + "computer.repair.broadcast", manager.getCompletedCount(), manager.getRequiredCount())); + } + + private boolean shouldTriggerSkillCheck() { + double chance = plugin.getConfigManager().getGame().getSkillCheckChance(); + return Math.random() < chance; + } + + public boolean isRepairing(UUID uuid) { + return sessions.containsKey(uuid); + } + + public GeneratorMinigame getMinigame() { + return minigame; + } + + public boolean isMinigameActive(UUID playerId) { + return minigame.isActive(playerId); + } + + public void handleMinigameClick(Player player) { + minigame.handleRightClick(player); + } + + public void handleMinigameSneak(Player player) { + minigame.handleSneak(player); + } + + public void shutdown() { + minigame.cleanup(); + sessions.clear(); + } + + public static class RepairSession { + final Player player; + final Computer computer; + int skillCheckCooldown; + + RepairSession(Player player, Computer computer) { + this.player = player; + this.computer = computer; + this.skillCheckCooldown = 50; + } + } +} diff --git a/src/main/java/dev/loki/phase/EndgamePhase.java b/src/main/java/dev/loki/phase/EndgamePhase.java index c8e76cf..220757d 100644 --- a/src/main/java/dev/loki/phase/EndgamePhase.java +++ b/src/main/java/dev/loki/phase/EndgamePhase.java @@ -104,7 +104,7 @@ public class EndgamePhase implements Phase { for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null && p.isOnline()) { - sidebar.updateGameStats(p, session, session.getGeneratorManager()); + sidebar.updateGameStats(p, session, session.getGeneratorManager(), session.getComputerManager()); } } } diff --git a/src/main/java/dev/loki/phase/GameplayInteraction.java b/src/main/java/dev/loki/phase/GameplayInteraction.java index a077b94..bd2ed50 100644 --- a/src/main/java/dev/loki/phase/GameplayInteraction.java +++ b/src/main/java/dev/loki/phase/GameplayInteraction.java @@ -45,6 +45,7 @@ public class GameplayInteraction { private dev.loki.chest.ChestOpening chestOpening; private GateOpening gateOpening; private dev.loki.objective.GeneratorRepair generatorRepair; + private dev.loki.objective.ComputerRepair computerRepair; private boolean allGensDone; private final Map killerLastSneakMs = new HashMap<>(); private final Map survivorControllers = new HashMap<>(); @@ -92,7 +93,8 @@ public class GameplayInteraction { dev.loki.chest.ChestManager chestManager, dev.loki.chest.ChestOpening chestOpening, GateOpening gateOpening, - dev.loki.objective.GeneratorRepair generatorRepair) { + dev.loki.objective.GeneratorRepair generatorRepair, + dev.loki.objective.ComputerRepair computerRepair) { this.gateManager = gateManager; this.hookManager = hookManager; this.hookRescue = hookRescue; @@ -103,6 +105,7 @@ public class GameplayInteraction { this.chestOpening = chestOpening; this.gateOpening = gateOpening; this.generatorRepair = generatorRepair; + this.computerRepair = computerRepair; } public void setAllGensDone(boolean allGensDone) { @@ -253,6 +256,9 @@ public class GameplayInteraction { if (generatorRepair.isMinigameActive(player.getUniqueId())) { generatorRepair.handleMinigameSneak(player); } + if (computerRepair != null && computerRepair.isMinigameActive(player.getUniqueId())) { + computerRepair.handleMinigameSneak(player); + } }); // Generator repair + right‑click for text skill check @@ -275,6 +281,26 @@ public class GameplayInteraction { } }); + // Computer repair + right‑click for text skill check + interactListener.setOnComputerInteract((player, action) -> { + BasePlayer bp = session.getPlayer(player.getUniqueId()); + if (bp == null || bp.isKiller()) return; + if (computerRepair == null) return; + + if (computerRepair.isMinigameActive(player.getUniqueId())) { + computerRepair.handleMinigameClick(player); + return; + } + + var compManager = session.getComputerManager(); + if (compManager == null) return; + double range = session.getConfig().getComputerInteractionRange(); + var computer = compManager.getNearestComputer(player.getLocation(), range); + if (computer != null) { + computerRepair.startRepair(player, computer); + } + }); + // Chest interactListener.setOnChestInteract((player, ignored) -> { BasePlayer bp = session.getPlayer(player.getUniqueId()); @@ -337,6 +363,7 @@ public class GameplayInteraction { interactListener.setOnCarriedShift(null); interactListener.setOnGeneratorSkillCheckSneak(null); interactListener.setOnGeneratorInteract(null); + interactListener.setOnComputerInteract(null); interactListener.setOnGateInteract(null); } } diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index 21c51a0..a7b8e97 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -19,6 +19,7 @@ import dev.loki.mdbd; import dev.loki.minigames.GeneratorMinigame; import dev.loki.minigames.generator.GuiRouletteMinigame; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; @@ -44,9 +45,11 @@ public class GameplayPhase implements Phase { private enum EndgameState { NONE, PREGAME, POSTGAME } private final GeneratorManager generatorManager; + private final ComputerManager computerManager; private int tickCounter; private boolean shouldEnd; private boolean allGensDone; + private boolean allCompsDone; private EndgameState endgameState = EndgameState.NONE; private int endgameTimer; // секунд до смены состояния @@ -69,12 +72,15 @@ public class GameplayPhase implements Phase { private ChestOpening chestOpening; private GeneratorKick generatorKick; private dev.loki.objective.GeneratorRepair generatorRepair; + private dev.loki.objective.ComputerRepair computerRepair; private dev.loki.objective.GeneratorHologram generatorHologram; + private dev.loki.objective.ComputerHologram computerHologram; private dev.loki.hook.HookHologram hookHologram; private dev.loki.chest.ChestHologram chestHologram; - public GameplayPhase(GeneratorManager generatorManager) { + public GameplayPhase(GeneratorManager generatorManager, ComputerManager computerManager) { this.generatorManager = generatorManager; + this.computerManager = computerManager; } // ========================================================================= @@ -88,6 +94,7 @@ public class GameplayPhase implements Phase { tickCounter = 0; shouldEnd = false; allGensDone = false; + allCompsDone = false; endgameState = EndgameState.NONE; endgameTimer = 0; winConditions = new GameplayWinConditions(); @@ -110,22 +117,36 @@ public class GameplayPhase implements Phase { public void tick(GameSession session) { tickCounter++; generatorManager.tick(); + computerManager.tick(); if (generatorRepair != null) generatorRepair.tick(); + if (computerRepair != null) computerRepair.tick(); if (interaction != null) interaction.tickSurvivors(); hookHandler.tick(); if (ui.tickMatchTimer(session)) { shouldEnd = true; } - // Particle effects around generators (every 10 ticks) - if (tickCounter % 10 == 0 && generatorManager != null) { - for (var gen : generatorManager.getAllGenerators()) { - if (gen.isCompleted()) continue; - Location gloc = gen.getLocation().clone().add(0.5, 0.5, 0.5); - gloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK, - gloc, 2, 0.8, 0.5, 0.8, 0.01); - gloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD, - gloc, 1, 0.6, 0.3, 0.6, 0.01); + // Particle effects around generators and computers (every 10 ticks) + if (tickCounter % 10 == 0) { + if (generatorManager != null) { + for (var gen : generatorManager.getAllGenerators()) { + if (gen.isCompleted()) continue; + Location gloc = gen.getLocation().clone().add(0.5, 0.5, 0.5); + gloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK, + gloc, 2, 0.8, 0.5, 0.8, 0.01); + gloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD, + gloc, 1, 0.6, 0.3, 0.6, 0.01); + } + } + if (computerManager != null) { + for (var comp : computerManager.getAllComputers()) { + if (comp.isCompleted()) continue; + Location cloc = comp.getLocation().clone().add(0.5, 0.5, 0.5); + cloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK, + cloc, 2, 0.8, 0.5, 0.8, 0.01); + cloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD, + cloc, 1, 0.6, 0.3, 0.6, 0.01); + } } } @@ -147,7 +168,7 @@ public class GameplayPhase implements Phase { shouldEnd = true; } } - ui.updateSidebar(session, generatorManager); + ui.updateSidebar(session, generatorManager, computerManager); } // Escape check — каждый тик для мгновенной реакции @@ -225,6 +246,7 @@ public class GameplayPhase implements Phase { for (var h : hookManager.getAllHooks()) h.getLocation().setWorld(gameWorld); for (var c : chestManager.getAllChests()) c.getLocation().setWorld(gameWorld); for (var g : generatorManager.getAllGenerators()) g.getLocation().setWorld(gameWorld); + for (var c : computerManager.getAllComputers()) c.getLocation().setWorld(gameWorld); } spawnGameBlocks(plugin, cfg); @@ -232,11 +254,16 @@ public class GameplayPhase implements Phase { generatorKick = new GeneratorKick(plugin, cfg, generatorManager); generatorRepair = new dev.loki.objective.GeneratorRepair(plugin, generatorManager, genMinigame); + computerRepair = new dev.loki.objective.ComputerRepair(plugin, computerManager, genMinigame); // Голограммы ВСЕГДА включены generatorHologram = new dev.loki.objective.GeneratorHologram(plugin, generatorManager); generatorHologram.start(); plugin.getLogger().info("[GameplayPhase] Generator holograms started"); + computerHologram = new dev.loki.objective.ComputerHologram(plugin, computerManager); + computerHologram.start(); + plugin.getLogger().info("[GameplayPhase] Computer holograms started"); + hookHologram = new dev.loki.hook.HookHologram(plugin, hookManager); hookHologram.start(); plugin.getLogger().info("[GameplayPhase] Hook holograms: " + hookManager.getAllHooks().size()); @@ -253,6 +280,13 @@ public class GameplayPhase implements Phase { damageSystem = new GameplayDamageSystem(); damageSystem.init(session, plugin, cfg); + if (generatorRepair != null) { + generatorRepair.setItemUseListener(damageSystem.getItemUseListener()); + } + if (computerRepair != null) { + computerRepair.setItemUseListener(damageSystem.getItemUseListener()); + } + ui = new GameplayUI(); ui.init(session, plugin, cfg); ui.setWinConditions(winConditions); @@ -262,9 +296,10 @@ public class GameplayPhase implements Phase { interaction.setInteractListener(plugin.getInteractListener()); // ВАЖНО: переустановить generatorManager, поскольку он может быть null после exit() plugin.getInteractListener().setGeneratorManager(generatorManager); + plugin.getInteractListener().setComputerManager(computerManager); interaction.setManagers(gateManager, hookManager, hookRescue, hookSabotage, generatorKick, damageSystem, - chestManager, chestOpening, gateOpening, generatorRepair); + chestManager, chestOpening, gateOpening, generatorRepair, computerRepair); } private void initListeners(GameSession session, mdbd plugin) { @@ -304,14 +339,26 @@ public class GameplayPhase implements Phase { } private void checkGeneratorProgress(GameSession session) { - if (allGensDone) return; - if (generatorManager.areAllRequiredCompleted()) { + if (allGensDone && allCompsDone) return; + + boolean gensDone = generatorManager.areAllRequiredCompleted(); + boolean compsDone = computerManager.areAllRequiredCompleted(); + + if (gensDone && !allGensDone) { allGensDone = true; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done")); + } + if (compsDone && !allCompsDone) { + allCompsDone = true; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("computer.repair.all-done")); + } + + if (gensDone && compsDone) { interaction.setAllGensDone(true); 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-objectives-done")); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null) p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f); @@ -419,6 +466,17 @@ public class GameplayPhase implements Phase { } plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + hookManager.getAllHooks().size() + " hooks"); + // Спавн блоков на местах компьютеров + Material compMat = Material.matchMaterial(cfg.getComputerBlock()); + if (compMat == null) compMat = Material.LECTERN; + for (var comp : computerManager.getAllComputers()) { + Location loc = comp.getLocation(); + if (loc.getWorld() == null) continue; + Block b = loc.getBlock(); + if (b.getType() != compMat) b.setType(compMat, false); + } + plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + computerManager.getTotalCount() + " computers"); + // Спавн структуры ворот (рамка + кнопка + Hopper для взаимодействия) for (ExitGate gate : gateManager.getAllGates()) { gate.spawnGateStructure(); @@ -458,7 +516,9 @@ public class GameplayPhase implements Phase { if (chestOpening != null) chestOpening.cancelAll(); if (chestManager != null) chestManager.resetAllChests(); if (generatorKick != null) generatorKick.shutdown(); + if (computerRepair != null) computerRepair.shutdown(); if (generatorHologram != null) generatorHologram.stop(); + if (computerHologram != null) computerHologram.stop(); if (hookHologram != null) hookHologram.stop(); if (titleUI != null) titleUI.clearSkillChecks(); if (hookStruggle != null) hookStruggle.clear(); @@ -471,5 +531,6 @@ public class GameplayPhase implements Phase { } } session.getPlugin().getInteractListener().setGeneratorManager(null); + session.getPlugin().getInteractListener().setComputerManager(null); } } \ No newline at end of file diff --git a/src/main/java/dev/loki/phase/GameplayUI.java b/src/main/java/dev/loki/phase/GameplayUI.java index ba19952..74cde24 100644 --- a/src/main/java/dev/loki/phase/GameplayUI.java +++ b/src/main/java/dev/loki/phase/GameplayUI.java @@ -4,6 +4,7 @@ import dev.loki.config.GameConfig; import dev.loki.core.GameSession; import dev.loki.mdbd; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.Killer; import dev.loki.player.Survivor; @@ -114,11 +115,11 @@ public class GameplayUI { /** * Update sidebar stats. */ - public void updateSidebar(GameSession session, GeneratorManager generatorManager) { + public void updateSidebar(GameSession session, GeneratorManager generatorManager, ComputerManager computerManager) { if (sidebar == null) return; for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); - if (p != null && p.isOnline()) sidebar.updateGameStats(p, session, generatorManager); + if (p != null && p.isOnline()) sidebar.updateGameStats(p, session, generatorManager, computerManager); } } diff --git a/src/main/java/dev/loki/ui/Sidebar.java b/src/main/java/dev/loki/ui/Sidebar.java index fd3eb62..506155c 100644 --- a/src/main/java/dev/loki/ui/Sidebar.java +++ b/src/main/java/dev/loki/ui/Sidebar.java @@ -3,6 +3,7 @@ package dev.loki.ui; import dev.loki.config.GameConfig; import dev.loki.core.GameSession; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; @@ -138,6 +139,10 @@ public class Sidebar { } public void updateGameStats(Player player, GameSession session, GeneratorManager generatorManager) { + updateGameStats(player, session, generatorManager, null); + } + + public void updateGameStats(Player player, GameSession session, GeneratorManager generatorManager, ComputerManager computerManager) { Scoreboard board = scoreboards.get(player.getUniqueId()); if (board == null) return; @@ -159,13 +164,21 @@ public class Sidebar { setScore(objective, "§8─────────────────", line--); } - // Generator progress — use actual placed count, not config max + // Generator progress int completedGens = generatorManager != null ? generatorManager.getCompletedCount() : 0; int totalGens = generatorManager != null ? generatorManager.getTotalCount() : cfg.getGeneratorRequired(); setScore(objective, "§eГенераторы:", line--); String genBar = buildGenBar(completedGens, totalGens); setScore(objective, " §f" + completedGens + "/" + totalGens + " " + genBar, line--); + + // Computer progress + int completedComps = computerManager != null ? computerManager.getCompletedCount() : 0; + int totalComps = computerManager != null ? computerManager.getTotalCount() : cfg.getComputerRequired(); + + setScore(objective, "§bКомпьютеры:", line--); + String compBar = buildGenBar(completedComps, totalComps); + setScore(objective, " §f" + completedComps + "/" + totalComps + " " + compBar, line--); setScore(objective, " ", line--); // Survivor list with hearts and colors diff --git a/src/main/resources/game.yml b/src/main/resources/game.yml index 540012c..59f58d7 100644 --- a/src/main/resources/game.yml +++ b/src/main/resources/game.yml @@ -35,12 +35,6 @@ generators: # Время починки одного генератора (секунд) [90 сек по ТЗ] repair-seconds: 90 - # Скорость отката без чинильщика (%/сек) - regression-rate: 0.25 - - # Скорость отката после удара (%/сек) - kick-regression-rate: 2.5 - # Потеря прогресса после удара (%) kick-regression-percent: 20.0 @@ -56,6 +50,20 @@ generators: # Расстояние для взаимодействия с генератором (блоков) interaction-range: 3.0 +# Компьютеры (аналог генераторов — нужно чинить и их тоже для победы) +computers: + # Всего компьютеров на карте + total-count: 2 + + # Сколько нужно починить (должно быть = total-count для победы) + required-count: 2 + + # Время починки одного компьютера (секунд) + repair-seconds: 90 + + # Расстояние для взаимодействия с компьютером (блоков) + interaction-range: 3.0 + # Крюки hooks: # Длительность Фазы 1 (секунд) @@ -213,6 +221,8 @@ scoreboard: blocks: # Блок на месте генератора generator: "BLAST_FURNACE" + # Блок на месте компьютера + computer: "LECTERN" # Блок на месте сундука chest: "CHEST" # Блок на месте крюка @@ -221,13 +231,17 @@ blocks: # Лут сундуков loot: medkit: - weight: 55 + weight: 25 min-charges: 12 max-charges: 16 - flashlight: - weight: 45 - min-charges: 6 - max-charges: 8 + bandage: + weight: 25 + full-medkit: + weight: 10 + soda: + weight: 20 + wrench: + weight: 20 # Custom Model Data для ресурспака # Задайте числа соответствующие overrides в вашем ресурспаке. @@ -236,6 +250,10 @@ items: model-data: medkit: 1001 flashlight: 1002 + bandage: 1004 + fullmedkit: 1005 + soda: 1006 + wrench: 1007 # Голограммы holograms: @@ -243,6 +261,9 @@ holograms: generator: enabled: true height-offset: -1.0 + computer: + enabled: true + height-offset: -1.0 hook: enabled: true height-offset: 0.0 diff --git a/src/main/resources/locations.yml b/src/main/resources/locations.yml index feefcc5..0e5bcbf 100644 --- a/src/main/resources/locations.yml +++ b/src/main/resources/locations.yml @@ -15,6 +15,9 @@ lobby: # Генераторы — настраиваются через /dbdadmin create generator # и сохраняются в data/generators.yml +# Компьютеры — настраиваются через /dbdsetup set computer +# и сохраняются в data/computers.yml + # Крюки (обычно 4-6) hooks: [] # Пример: diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index d518b13..bc2f9e1 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -110,6 +110,7 @@ setup: hook-placed: "%setup-prefix%&aКрюк установлен: %s" chest-placed: "%setup-prefix%&aСундук добавлен: %s" generator-placed: "%setup-prefix%&aГенератор добавлен: %s" + computer-placed: "%setup-prefix%&bКомпьютер установлен: %s" killer-spawn-exists: "%setup-prefix%&cСпавн киллера уже установлен! Используй /dbdsetup clear-spawns для сброса." killer-spawn-placed: "&#FF4444Спавн киллера установлен: %s" survivor-spawn-limit: "%setup-prefix%&cДостигнут лимит спавнов выживших (4)! Используй /dbdsetup clear-spawns для сброса." @@ -155,6 +156,21 @@ role: killer: "%prefix%&4Вы - Маньяк! Поймайте всех выживших!" survivor: "%prefix%&bВы - Выживший! Почините генераторы и сбегите!" +# ── Компьютеры ──────────────────────────────── + +computer: + repair: + start: "&bВы начали чинить компьютер..." + stop: "&eВы остановились." + complete: "&a✔ Компьютер активирован!" + broadcast: "&bКомпьютер починен! &7(%d/%d)" + cannot: "&cКомпьютер не может быть починен!" + busy: "&cКто-то уже чинит этот компьютер!" + actionbar: "&7Починка компьютера: %s" + skillcheck-success: "&a✓ Успех! +%d%%" + skillcheck-fail: "&c✗ Промах! -%d%%. Чини дальше." + all-done: "%prefix%&bВсе компьютеры починены!" + # ── Генераторы ───────────────────────────────── generator: @@ -169,7 +185,8 @@ generator: skillcheck-title: "&eПроверка реакции! %s" skillcheck-success: "&a✓ Успех! +%d%%" skillcheck-fail: "&c✗ Промах! -%d%%. Чини дальше." - all-done: "%prefix%&aВсе генераторы починены! Открывайте ворота!" + all-done: "%prefix%&aВсе генераторы починены!" + all-objectives-done: "%prefix%&6Все генераторы и компьютеры починены! Ворота открываются!" kicked: begin: "%prefix%&cДержите Shift для разрушения генератора..." cooldown: "%prefix%&cКулдаун удара: %d сек" @@ -205,6 +222,11 @@ survivor: done: "%prefix%&a✔ Вы вылечились!" cancelled: "%prefix%&eЛечение прервано." depleted: "&cМедкит закончился!" + item: + bandage: "%prefix%&aВы наложили бинт. Здоровье регенерирует!" + fullmedkit: "%prefix%&aАптечка использована. Здоровье полностью восстановлено!" + soda: "%prefix%&eСодовая выпита. Скорость увеличена!" + wrench: "%prefix%&aГаечный ключ использован. Починка ускорена!" # ── Крюки ────────────────────────────────────── @@ -301,6 +323,7 @@ usage: setup-hook: "&6/dbdsetup set hook - Установить крюк (ломай блок)" setup-chest: "&6/dbdsetup set chest - Установить сундук" setup-generator: "&6/dbdsetup set generator - Установить генератор" + setup-computer: "&6/dbdsetup set computer - Установить компьютер" setup-killer-spawn: "&6/dbdsetup set killer-spawn - Спавн киллера (только 1)" setup-survivor-spawn: "&6/dbdsetup set survivor-spawn - Спавн выжившего (до 4)" setup-gate: "&6/dbdsetup set gate A|B - Установить ворота (из точек ванда)"