From 1f9a2c21989c27fabdd0864643f411eeb3664b82 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Wed, 25 Mar 2026 23:34:26 +0100 Subject: [PATCH] Update from Git Manager GUI --- src/main/java/viper/ButtonControl.java | 832 +++++++++++++++++++- src/main/java/viper/ButtonListener.java | 108 ++- src/main/java/viper/ButtonTabCompleter.java | 22 +- src/main/java/viper/ConfigManager.java | 16 + src/main/java/viper/DataManager.java | 98 ++- src/main/java/viper/MySQLStorage.java | 190 ++++- src/main/java/viper/ScheduleGUI.java | 103 ++- src/main/resources/config.yml | 10 + src/main/resources/lang.yml | 8 + src/main/resources/plugin.yml | 4 +- 10 files changed, 1342 insertions(+), 49 deletions(-) diff --git a/src/main/java/viper/ButtonControl.java b/src/main/java/viper/ButtonControl.java index 747f3e4..d7a9199 100644 --- a/src/main/java/viper/ButtonControl.java +++ b/src/main/java/viper/ButtonControl.java @@ -1,11 +1,13 @@ package viper; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; import org.bukkit.block.data.Lightable; import org.bukkit.block.data.type.NoteBlock; import org.bukkit.command.Command; @@ -24,11 +26,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; public class ButtonControl extends JavaPlugin { + private static final String TIMED_CONTAINER_MODE_SIMULTANEOUS = "simultaneous"; + private static final String TIMED_CONTAINER_MODE_SEQUENTIAL = "sequential"; + private ConfigManager configManager; private DataManager dataManager; @@ -42,6 +49,27 @@ public class ButtonControl extends JavaPlugin { // Actionbar-Status pro Spieler für die Namensanzeige private final Map lastControllerActionbar = new HashMap<>(); + // Undo-System: letzte Aktion pro Controller (wird nach 5 Minuten gelöscht) + private final Map lastActions = new HashMap<>(); + + // Secret-Wall Runtime (offene Wände + Originalzustand) + private final Map> openSecretWalls = new HashMap<>(); + + // Knarrherz-Override: aktivierte Herzen werden zyklisch auf "active=true" gehalten + private final Set forcedActiveCreakingHearts = new HashSet<>(); + + // Geöffnete Gitter (AIR) mit Originalmaterial für die Wiederherstellung + private final Map openGrates = new HashMap<>(); + + // Laufende Zeitplan-Shows für Werfer/Spender pro Controller + private final Map timedContainerTasks = new HashMap<>(); + private final Map timedContainerTaskShotDelays = new HashMap<>(); + private final Map timedContainerTaskModes = new HashMap<>(); + private final Map timedContainerNextIndices = new HashMap<>(); + + // Secret-Editor: Spieler -> ausgewählter Controller + private final Map selectedSecretController = new HashMap<>(); + @Override public void onEnable() { configManager = new ConfigManager(this); @@ -89,13 +117,20 @@ public class ButtonControl extends JavaPlugin { getServer().getScheduler().runTaskTimer(this, this::checkDaylightSensors, 0L, 20L * 10); getServer().getScheduler().runTaskTimer(this, this::checkMotionSensors, 0L, 10L); getServer().getScheduler().runTaskTimer(this, this::checkTimedControllers, 0L, 20L * 5); + getServer().getScheduler().runTaskTimer(this, this::enforceCreakingHeartStates, 1L, 1L); getServer().getScheduler().runTaskTimer(this, this::updateControllerNameActionBar, 0L, 5L); + // Undo-Actions nach 5 Minuten aufräumen + getServer().getScheduler().runTaskTimer(this, this::cleanupOldUndoActions, 0L, 20L * 60 * 5); + getLogger().info("ButtonControl v" + getDescription().getVersion() + " wurde erfolgreich aktiviert!"); } @Override public void onDisable() { + stopAllTimedContainerTasks(); + openGrates.clear(); + forcedActiveCreakingHearts.clear(); if (dataManager != null) { dataManager.shutdown(); } @@ -237,12 +272,19 @@ public class ButtonControl extends JavaPlugin { Location tl = parseLocation(ts); if (tl == null) continue; Block tb = tl.getBlock(); - if (tb.getType() == Material.REDSTONE_LAMP) { + if (isLamp(tb.getType()) && tb.getBlockData() instanceof Lightable) { Lightable lamp = (Lightable) tb.getBlockData(); lamp.setLit(!isDay); tb.setBlockData(lamp); } } + + // Secret Wall: bei Tag öffnen, bei Nacht schließen + if (isDay) { + triggerSecretWall(buttonId, false); + } else { + closeSecretWall(buttonId); + } } } @@ -258,13 +300,20 @@ public class ButtonControl extends JavaPlugin { * Anzeige: ticksToTime() wandelt in "HH:MM" um (Tag beginnt um 06:00). */ public void checkTimedControllers() { + Set activeScheduleButtons = new HashSet<>(); + for (String controllerLoc : dataManager.getAllPlacedControllers()) { String buttonId = dataManager.getButtonIdForPlacedController(controllerLoc); if (buttonId == null) continue; long openTime = dataManager.getScheduleOpenTime(buttonId); long closeTime = dataManager.getScheduleCloseTime(buttonId); - if (openTime < 0 || closeTime < 0) continue; + if (openTime < 0 || closeTime < 0) { + stopTimedContainerTask(buttonId); + continue; + } + + activeScheduleButtons.add(buttonId); Location loc = parseLocation(controllerLoc); if (loc == null) continue; @@ -281,14 +330,160 @@ public class ButtonControl extends JavaPlugin { } Boolean lastState = timedControllerLastState.get(controllerLoc); + List connected = dataManager.getConnectedBlocks(buttonId); + if (connected != null && !connected.isEmpty()) { + updateTimedContainerAutomation(buttonId, connected, shouldBeOpen); + } else { + stopTimedContainerTask(buttonId); + } + if (lastState != null && lastState == shouldBeOpen) continue; timedControllerLastState.put(controllerLoc, shouldBeOpen); - List connected = dataManager.getConnectedBlocks(buttonId); if (connected != null && !connected.isEmpty()) { setOpenables(connected, shouldBeOpen); } } + + // Falls Zeitpläne entfernt wurden, zugehörige Show-Tasks sauber stoppen. + List inactiveTaskButtons = new ArrayList<>(timedContainerTasks.keySet()); + for (String buttonId : inactiveTaskButtons) { + if (!activeScheduleButtons.contains(buttonId)) { + stopTimedContainerTask(buttonId); + } + } + } + + private void updateTimedContainerAutomation(String buttonId, List connected, boolean activeWindow) { + if (!containsTimedContainer(connected)) { + stopTimedContainerTask(buttonId); + return; + } + + if (!activeWindow) { + stopTimedContainerTask(buttonId); + return; + } + + int configuredDelay = dataManager.getScheduleShotDelayTicks(buttonId); + int shotDelay = configuredDelay >= 0 + ? Math.max(0, configuredDelay) + : Math.max(0, configManager.getConfig().getInt( + "timed-container-shot-delay-ticks", + configManager.getConfig().getInt("timed-container-interval-ticks", 40))); + String configuredMode = normalizeTimedContainerMode(dataManager.getScheduleTriggerMode(buttonId)); + String taskMode = configuredMode != null + ? configuredMode + : normalizeTimedContainerMode(configManager.getConfig().getString( + "timed-container-trigger-mode", + TIMED_CONTAINER_MODE_SIMULTANEOUS)); + int taskPeriod = Math.max(1, shotDelay); + + Integer existingTaskId = timedContainerTasks.get(buttonId); + if (existingTaskId != null) { + Integer existingDelay = timedContainerTaskShotDelays.get(buttonId); + String existingMode = timedContainerTaskModes.get(buttonId); + if (existingDelay != null && existingDelay == taskPeriod && taskMode.equals(existingMode)) { + return; + } + stopTimedContainerTask(buttonId); + } + + int taskId = getServer().getScheduler().scheduleSyncRepeatingTask(this, () -> { + List latestConnected = dataManager.getConnectedBlocks(buttonId); + List timedContainers = getTimedContainerLocations(latestConnected); + if (timedContainers.isEmpty()) { + stopTimedContainerTask(buttonId); + return; + } + + if (TIMED_CONTAINER_MODE_SEQUENTIAL.equals(taskMode)) { + int nextIndex = timedContainerNextIndices.getOrDefault(buttonId, 0); + if (nextIndex >= timedContainers.size()) nextIndex = 0; + + String locStr = timedContainers.get(nextIndex); + Location l = parseLocation(locStr); + if (l != null) { + Block b = l.getBlock(); + if (b.getType() == Material.DISPENSER) { + triggerContainer(b, "dispense"); + } else if (b.getType() == Material.DROPPER) { + triggerContainer(b, "drop"); + } + } + timedContainerNextIndices.put(buttonId, (nextIndex + 1) % timedContainers.size()); + } else { + for (String locStr : timedContainers) { + Location l = parseLocation(locStr); + if (l == null) continue; + + Block b = l.getBlock(); + if (b.getType() == Material.DISPENSER) { + triggerContainer(b, "dispense"); + } else if (b.getType() == Material.DROPPER) { + triggerContainer(b, "drop"); + } + } + } + }, 0L, taskPeriod); + + timedContainerTasks.put(buttonId, taskId); + timedContainerTaskShotDelays.put(buttonId, taskPeriod); + timedContainerTaskModes.put(buttonId, taskMode); + timedContainerNextIndices.put(buttonId, 0); + } + + private boolean containsTimedContainer(List connected) { + return !getTimedContainerLocations(connected).isEmpty(); + } + + private List getTimedContainerLocations(List connected) { + List timedContainers = new ArrayList<>(); + for (String locStr : connected) { + Location l = parseLocation(locStr); + if (l == null) continue; + Material m = l.getBlock().getType(); + if (m == Material.DISPENSER || m == Material.DROPPER) { + timedContainers.add(locStr); + } + } + return timedContainers; + } + + private String normalizeTimedContainerMode(String mode) { + if (mode == null) return null; + String normalized = mode.trim().toLowerCase(Locale.ROOT); + if (TIMED_CONTAINER_MODE_SEQUENTIAL.equals(normalized)) return TIMED_CONTAINER_MODE_SEQUENTIAL; + return TIMED_CONTAINER_MODE_SIMULTANEOUS; + } + + private void stopTimedContainerTask(String buttonId) { + Integer taskId = timedContainerTasks.remove(buttonId); + if (taskId != null) getServer().getScheduler().cancelTask(taskId); + timedContainerTaskShotDelays.remove(buttonId); + timedContainerTaskModes.remove(buttonId); + timedContainerNextIndices.remove(buttonId); + } + + private void stopAllTimedContainerTasks() { + for (Integer taskId : timedContainerTasks.values()) { + getServer().getScheduler().cancelTask(taskId); + } + timedContainerTasks.clear(); + timedContainerTaskShotDelays.clear(); + timedContainerTaskModes.clear(); + timedContainerNextIndices.clear(); + } + + private boolean triggerContainer(Block block, String methodName) { + try { + Object state = block.getState(); + java.lang.reflect.Method method = state.getClass().getMethod(methodName); + Object result = method.invoke(state); + return !(result instanceof Boolean) || (Boolean) result; + } catch (ReflectiveOperationException ignored) { + return false; + } } // ----------------------------------------------------------------------- @@ -388,18 +583,23 @@ public class ButtonControl extends JavaPlugin { } List connected = dataManager.getConnectedBlocks(buttonId); - if (connected == null || connected.isEmpty()) continue; + boolean hasConnected = connected != null && !connected.isEmpty(); + List secretBlocks = dataManager.getSecretBlocks(buttonId); + boolean hasSecret = secretBlocks != null && !secretBlocks.isEmpty(); + if (!hasConnected && !hasSecret) continue; if (detected) { if (!activeSensors.contains(controllerLoc)) { - setOpenables(connected, true); + if (hasConnected) setOpenables(connected, true); + triggerSecretWall(buttonId, false); activeSensors.add(controllerLoc); } lastMotionDetections.put(controllerLoc, now); } else { Long last = lastMotionDetections.get(controllerLoc); if (last != null && now - last >= delay) { - setOpenables(connected, false); + if (hasConnected) setOpenables(connected, false); + closeSecretWall(buttonId); lastMotionDetections.remove(controllerLoc); activeSensors.remove(controllerLoc); } @@ -416,6 +616,8 @@ public class ButtonControl extends JavaPlugin { org.bukkit.block.data.Openable o = (org.bukkit.block.data.Openable) tb.getBlockData(); o.setOpen(open); tb.setBlockData(o); + } else if (isGrate(tb.getType()) || (tb.getType() == Material.AIR && openGrates.containsKey(locStr))) { + setGrateOpenState(tb, locStr, open); } } } @@ -429,7 +631,7 @@ public class ButtonControl extends JavaPlugin { if (!command.getName().equalsIgnoreCase("bc")) return false; if (args.length == 0) { - sender.sendMessage("§6[BC] §7/bc "); + sender.sendMessage("§6[BC] §7/bc "); return true; } @@ -450,7 +652,10 @@ public class ButtonControl extends JavaPlugin { } configManager.reloadConfig(); dataManager.reloadData(); + stopAllTimedContainerTasks(); timedControllerLastState.clear(); + openGrates.clear(); + forcedActiveCreakingHearts.clear(); clearAllControllerActionBars(); sender.sendMessage(configManager.getMessage("konfiguration-neugeladen")); return true; @@ -477,9 +682,13 @@ public class ButtonControl extends JavaPlugin { } Player player = (Player) sender; + if (sub.equals("secret")) { + return handleSecretCommand(player, args); + } + if (sub.equals("list") || sub.equals("rename") || sub.equals("schedule") || sub.equals("trust") || sub.equals("untrust") - || sub.equals("public") || sub.equals("private")) { + || sub.equals("public") || sub.equals("private") || sub.equals("undo")) { Block target = player.getTargetBlockExact(5); if (!isValidController(target)) { @@ -502,12 +711,18 @@ public class ButtonControl extends JavaPlugin { break; case "rename": - if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; } + if (!isOwner && !isAdmin) { + player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun."); + return true; + } if (args.length < 2) { player.sendMessage("§7/bc rename "); return true; } + String oldRename = dataManager.getControllerName(buttonId); String newName = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); if (newName.length() > 32) { player.sendMessage("§cName zu lang (max. 32 Zeichen)."); return true; } - dataManager.setControllerName(buttonId, newName); - player.sendMessage(String.format(configManager.getMessage("controller-umbenannt"), newName)); + String coloredName = ChatColor.translateAlternateColorCodes('&', newName); + dataManager.setControllerName(buttonId, coloredName); + saveUndoAction(buttonId, new UndoAction(UndoAction.Type.RENAME, oldRename, coloredName)); + player.sendMessage(String.format(configManager.getMessage("controller-umbenannt"), coloredName)); break; case "schedule": @@ -516,30 +731,61 @@ public class ButtonControl extends JavaPlugin { break; case "trust": - if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; } + if (!isOwner && !isAdmin) { + player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun."); + return true; + } if (args.length < 2) { player.sendMessage("§7/bc trust "); return true; } org.bukkit.OfflinePlayer tp = Bukkit.getOfflinePlayer(args[1]); if (!tp.hasPlayedBefore() && !tp.isOnline()) { player.sendMessage(configManager.getMessage("spieler-nicht-gefunden")); return true; } dataManager.addTrustedPlayer(buttonId, tp.getUniqueId()); + saveUndoAction(buttonId, new UndoAction(UndoAction.Type.TRUST_ADD, tp.getUniqueId().toString(), null)); player.sendMessage(String.format(configManager.getMessage("trust-hinzugefuegt"), args[1])); break; case "untrust": - if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; } + if (!isOwner && !isAdmin) { + player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun."); + return true; + } if (args.length < 2) { player.sendMessage("§7/bc untrust "); return true; } - dataManager.removeTrustedPlayer(buttonId, Bukkit.getOfflinePlayer(args[1]).getUniqueId()); + org.bukkit.OfflinePlayer untp = Bukkit.getOfflinePlayer(args[1]); + java.util.UUID uuid = untp.getUniqueId(); + dataManager.removeTrustedPlayer(buttonId, uuid); + saveUndoAction(buttonId, new UndoAction(UndoAction.Type.TRUST_REMOVE, uuid.toString(), null)); player.sendMessage(String.format(configManager.getMessage("trust-entfernt"), args[1])); break; default: // public / private - if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; } + if (!isOwner && !isAdmin) { + player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun."); + return true; + } boolean pub = sub.equals("public"); + boolean oldStatus = dataManager.isPublic(buttonId); dataManager.setPublic(buttonId, pub); + saveUndoAction(buttonId, new UndoAction(pub ? UndoAction.Type.PUBLIC : UndoAction.Type.PRIVATE, + String.valueOf(oldStatus), String.valueOf(pub))); player.sendMessage(String.format(configManager.getMessage("status-geandert"), pub ? "§aÖffentlich" : "§cPrivat")); break; + + case "undo": + if (lastActions.containsKey(buttonId)) { + UndoAction action = lastActions.get(buttonId); + boolean canUndo = dataManager.isOwner(buttonId, player.getUniqueId()) || player.hasPermission("buttoncontrol.admin"); + if (!canUndo) { + player.sendMessage("§c✖ Du kannst nur Aktionen deiner eigenen Controller rückgängig machen."); + return true; + } + undoAction(buttonId, action, player); + } else { + player.sendMessage("§c✖ Keine Aktion zum Rückgängigmachen für diesen Controller."); + } + break; + } } return true; @@ -609,6 +855,113 @@ public class ButtonControl extends JavaPlugin { || m.name().endsWith("_CARPET"); } + private boolean isLamp(Material m) { + return m == Material.REDSTONE_LAMP + || "COPPER_BULB".equals(m.name()) + || m.name().endsWith("_COPPER_BULB"); + } + + public boolean isGrate(Material m) { + return m == Material.IRON_BARS || m.name().endsWith("_GRATE"); + } + + public boolean isManagedOpenGrateLocation(String locStr) { + return openGrates.containsKey(locStr); + } + + public Boolean toggleGrate(Block block) { + if (block == null) return null; + + String locStr = toLoc(block); + Material type = block.getType(); + + if (isGrate(type)) { + openGrates.put(locStr, type); + block.setType(Material.AIR, false); + return true; + } + + if (type == Material.AIR) { + Material original = openGrates.get(locStr); + if (original != null) { + block.setType(original, false); + openGrates.remove(locStr); + return false; + } + } + + return null; + } + + private void setGrateOpenState(Block block, String locStr, boolean open) { + if (open) { + if (isGrate(block.getType())) { + openGrates.put(locStr, block.getType()); + block.setType(Material.AIR, false); + } + return; + } + + Material original = openGrates.get(locStr); + if (original != null) { + block.setType(original, false); + openGrates.remove(locStr); + } + } + + private boolean isCreakingHeart(Material m) { + return "CREAKING_HEART".equals(m.name()); + } + + public Boolean togglePersistentCreakingHeart(Block block) { + if (block == null || !isCreakingHeart(block.getType())) return null; + + String loc = toLoc(block); + boolean targetActive = !forcedActiveCreakingHearts.contains(loc); + if (!applyCreakingHeartActive(block, targetActive)) return null; + + if (targetActive) forcedActiveCreakingHearts.add(loc); + else forcedActiveCreakingHearts.remove(loc); + + return targetActive; + } + + private void enforceCreakingHeartStates() { + if (forcedActiveCreakingHearts.isEmpty()) return; + + java.util.Iterator it = forcedActiveCreakingHearts.iterator(); + while (it.hasNext()) { + String loc = it.next(); + Location l = parseLocation(loc); + if (l == null) { + it.remove(); + continue; + } + + Block block = l.getBlock(); + if (!isCreakingHeart(block.getType())) { + it.remove(); + continue; + } + + if (!applyCreakingHeartActive(block, true)) { + it.remove(); + } + } + } + + private boolean applyCreakingHeartActive(Block block, boolean active) { + org.bukkit.block.data.BlockData data = block.getBlockData(); + try { + java.lang.reflect.Method setActive = data.getClass().getMethod("setActive", boolean.class); + setActive.invoke(data, active); + block.setBlockData(data); + return true; + } catch (ReflectiveOperationException ignored) { + return false; + } + } + public String toLoc(Block b) { return b.getWorld().getName() + "," + b.getX() + "," + b.getY() + "," + b.getZ(); } @@ -650,4 +1003,453 @@ public class ButtonControl extends JavaPlugin { public ConfigManager getConfigManager() { return configManager; } public DataManager getDataManager() { return dataManager; } + + private boolean handleSecretCommand(Player player, String[] args) { + if (args.length < 2) { + player.sendMessage("§7/bc secret "); + return true; + } + + String mode = args[1].toLowerCase(); + String buttonId = resolveSecretController(player); + + if (mode.equals("select")) { + Block target = player.getTargetBlockExact(5); + if (!isValidController(target)) { + player.sendMessage(configManager.getMessage("kein-controller-im-blick")); + return true; + } + String targetLoc = toLoc(target); + String targetButtonId = dataManager.getButtonIdForLocation(targetLoc); + if (targetButtonId == null) { + player.sendMessage(configManager.getMessage("keine-bloecke-verbunden")); + return true; + } + boolean isAdmin = player.hasPermission("buttoncontrol.admin"); + boolean isOwner = dataManager.isOwner(targetButtonId, player.getUniqueId()); + if (!isOwner && !isAdmin) { + player.sendMessage("§c✖ Nur der Besitzer oder Admins können Secret-Türen verwalten."); + return true; + } + selectedSecretController.put(player.getUniqueId(), targetButtonId); + player.sendMessage("§6[Secret] §7Controller ausgewählt. Jetzt auf Wandblock schauen + /bc secret add"); + return true; + } + + if (buttonId == null) { + player.sendMessage("§c✖ Kein Controller ausgewählt. Schau auf den Controller und nutze §7/bc secret select§c."); + return true; + } + + boolean isAdmin = player.hasPermission("buttoncontrol.admin"); + boolean isOwner = dataManager.isOwner(buttonId, player.getUniqueId()); + if (!isOwner && !isAdmin) { + player.sendMessage("§c✖ Nur der Besitzer oder Admins können Secret-Türen verwalten."); + return true; + } + + if (mode.equals("info")) { + List sb = dataManager.getSecretBlocks(buttonId); + long delayMs = dataManager.getSecretRestoreDelayMs(buttonId); + String animation = normalizeSecretAnimation(dataManager.getSecretAnimation(buttonId)); + player.sendMessage("§6[Secret] §7Blöcke: §f" + sb.size() + " §8| §7Delay: §f" + (delayMs / 1000.0) + "s"); + player.sendMessage("§6[Secret] §7Animation: §f" + animation); + return true; + } + + if (mode.equals("clear")) { + dataManager.clearSecret(buttonId); + closeSecretWall(buttonId); + player.sendMessage("§6[Secret] §7Alle Secret-Blöcke entfernt."); + return true; + } + + if (mode.equals("delay")) { + if (args.length < 3) { + player.sendMessage("§7/bc secret delay "); + return true; + } + try { + int sec = Integer.parseInt(args[2]); + if (sec < 1 || sec > 300) { + player.sendMessage("§c✖ Delay muss zwischen 1 und 300 Sekunden liegen."); + return true; + } + dataManager.setSecretRestoreDelayMs(buttonId, sec * 1000L); + player.sendMessage("§6[Secret] §7Wiederherstellung: §f" + sec + "s"); + } catch (NumberFormatException e) { + player.sendMessage("§c✖ Ungültige Zahl."); + } + return true; + } + + if (mode.equals("animation")) { + if (args.length < 3) { + player.sendMessage("§7/bc secret animation "); + return true; + } + String animation = normalizeSecretAnimation(args[2]); + if (!isValidSecretAnimation(animation)) { + player.sendMessage("§c✖ Nutze: instant, wave, reverse oder center"); + return true; + } + dataManager.setSecretAnimation(buttonId, animation); + player.sendMessage("§6[Secret] §7Animation gesetzt: §f" + animation); + return true; + } + + Block wallTarget = player.getTargetBlockExact(6); + if (wallTarget == null || wallTarget.getType() == Material.AIR) { + player.sendMessage("§c✖ Schau auf einen Block innerhalb von 6 Blöcken."); + return true; + } + + String wallLoc = toLoc(wallTarget); + List secretBlocks = new ArrayList<>(dataManager.getSecretBlocks(buttonId)); + + if (mode.equals("add")) { + if (isUnsafeSecretBlock(wallTarget)) { + player.sendMessage("§c✖ Dieser Block trägt/enthält einen Controller oder Schalter. Nicht als Secret-Block erlaubt."); + return true; + } + if (secretBlocks.contains(wallLoc)) { + player.sendMessage("§c✖ Dieser Block ist bereits als Secret-Block gesetzt."); + return true; + } + secretBlocks.add(wallLoc); + dataManager.setSecretBlocks(buttonId, secretBlocks); + player.sendMessage("§6[Secret] §7Block hinzugefügt. §8(" + secretBlocks.size() + ")"); + return true; + } + + if (mode.equals("remove")) { + if (!secretBlocks.remove(wallLoc)) { + player.sendMessage("§c✖ Dieser Block ist kein Secret-Block."); + return true; + } + dataManager.setSecretBlocks(buttonId, secretBlocks); + player.sendMessage("§6[Secret] §7Block entfernt. §8(" + secretBlocks.size() + ")"); + return true; + } + + player.sendMessage("§7/bc secret "); + return true; + } + + private String resolveSecretController(Player player) { + Block target = player.getTargetBlockExact(5); + if (isValidController(target)) { + String id = dataManager.getButtonIdForLocation(toLoc(target)); + if (id != null) { + selectedSecretController.put(player.getUniqueId(), id); + return id; + } + } + return selectedSecretController.get(player.getUniqueId()); + } + + private boolean isUnsafeSecretBlock(Block wallBlock) { + String wallLoc = toLoc(wallBlock); + for (String controllerLoc : dataManager.getAllPlacedControllers()) { + Location l = parseLocation(controllerLoc); + if (l == null) continue; + Block controller = l.getBlock(); + + // Controller selbst nie entfernen + if (controllerLoc.equals(wallLoc)) return true; + + // Trägerblock des Controllers nie entfernen + Block support = getSupportBlock(controller); + if (support != null && toLoc(support).equals(wallLoc)) return true; + } + return false; + } + + private Block getSupportBlock(Block controller) { + Material m = controller.getType(); + + // Aufliegende Controller: Teppich / Tageslichtsensor / Tripwire Hook + if (m.name().endsWith("_CARPET") || m == Material.DAYLIGHT_DETECTOR || m == Material.TRIPWIRE_HOOK) { + return controller.getRelative(BlockFace.DOWN); + } + + // Buttons & wall-attached Blöcke + if (controller.getBlockData() instanceof org.bukkit.block.data.FaceAttachable) { + org.bukkit.block.data.FaceAttachable fa = (org.bukkit.block.data.FaceAttachable) controller.getBlockData(); + switch (fa.getAttachedFace()) { + case FLOOR: + return controller.getRelative(BlockFace.DOWN); + case CEILING: + return controller.getRelative(BlockFace.UP); + case WALL: + if (controller.getBlockData() instanceof org.bukkit.block.data.Directional) { + org.bukkit.block.data.Directional d = (org.bukkit.block.data.Directional) controller.getBlockData(); + return controller.getRelative(d.getFacing().getOppositeFace()); + } + break; + default: + break; + } + } + + // Schilder + if (m.name().endsWith("_SIGN")) { + if (m.name().contains("WALL") && controller.getBlockData() instanceof org.bukkit.block.data.Directional) { + org.bukkit.block.data.Directional d = (org.bukkit.block.data.Directional) controller.getBlockData(); + return controller.getRelative(d.getFacing().getOppositeFace()); + } + return controller.getRelative(BlockFace.DOWN); + } + + return null; + } + + /** Löst eine Secret Wall aus. autoClose=false: kein automatisches Schließen (für Sensoren). */ + public boolean triggerSecretWall(String buttonId) { + return triggerSecretWall(buttonId, true); + } + + public boolean triggerSecretWall(String buttonId, boolean autoClose) { + if (buttonId == null) return false; + List secretBlocks = dataManager.getSecretBlocks(buttonId); + if (secretBlocks == null || secretBlocks.isEmpty()) return false; + if (openSecretWalls.containsKey(buttonId)) return false; + + List snapshots = new ArrayList<>(); + for (String locStr : secretBlocks) { + Location l = parseLocation(locStr); + if (l == null) continue; + Block b = l.getBlock(); + if (b.getType() == Material.AIR) continue; + snapshots.add(new SecretBlockSnapshot(locStr, b.getType().name(), b.getBlockData().getAsString(), l.getBlockX(), l.getBlockY(), l.getBlockZ())); + } + + if (snapshots.isEmpty()) return false; + openSecretWalls.put(buttonId, snapshots); + + String animation = normalizeSecretAnimation(dataManager.getSecretAnimation(buttonId)); + List openOrder = getOrderedSecretSnapshots(snapshots, animation, true); + long openStepTicks = getSecretStepTicks(animation); + long[] openDelays = computeDelays(openOrder, animation, openStepTicks); + scheduleSecretOpen(openOrder, openDelays); + + if (autoClose) { + long delayMs = Math.max(1000L, dataManager.getSecretRestoreDelayMs(buttonId)); + long delayTicks = Math.max(1L, delayMs / 50L); + long openDurationTicks = openDelays.length == 0 ? 0L : openDelays[openDelays.length - 1]; + getServer().getScheduler().runTaskLater(this, + () -> closeSecretWall(buttonId), openDurationTicks + delayTicks); + } + return true; + } + + public void closeSecretWall(String buttonId) { + List snapshots = openSecretWalls.remove(buttonId); + if (snapshots == null || snapshots.isEmpty()) return; + + String animation = normalizeSecretAnimation(dataManager.getSecretAnimation(buttonId)); + List closeOrder = getOrderedSecretSnapshots(snapshots, animation, false); + long closeStepTicks = getSecretStepTicks(animation); + long[] closeDelays = computeDelays(closeOrder, animation, closeStepTicks); + scheduleSecretClose(closeOrder, closeDelays); + } + + private void scheduleSecretOpen(List snapshots, long[] delays) { + for (int i = 0; i < snapshots.size(); i++) { + SecretBlockSnapshot s = snapshots.get(i); + long when = delays[i]; + getServer().getScheduler().runTaskLater(this, () -> { + Location l = parseLocation(s.loc); + if (l == null) return; + Block b = l.getBlock(); + if (b.getType() != Material.AIR) { + b.setType(Material.AIR, false); + } + }, when); + } + } + + private void scheduleSecretClose(List snapshots, long[] delays) { + for (int i = 0; i < snapshots.size(); i++) { + SecretBlockSnapshot s = snapshots.get(i); + long when = delays[i]; + getServer().getScheduler().runTaskLater(this, () -> { + Location l = parseLocation(s.loc); + if (l == null) return; + Block b = l.getBlock(); + Material m = Material.matchMaterial(s.materialName); + if (m == null || m == Material.AIR) return; + b.setType(m, false); + try { + b.setBlockData(Bukkit.createBlockData(s.blockData), false); + } catch (IllegalArgumentException ignored) { + // Fallback: Material wurde bereits gesetzt + } + }, when); + } + } + + private List getOrderedSecretSnapshots(List source, String animation, boolean opening) { + List ordered = new ArrayList<>(source); + switch (animation) { + case "instant": + case "wave": + if (!opening) { + java.util.Collections.reverse(ordered); + } + return ordered; + case "reverse": + if (opening) { + java.util.Collections.reverse(ordered); + } + return ordered; + case "center": + double centerX = 0; + double centerY = 0; + double centerZ = 0; + for (SecretBlockSnapshot s : ordered) { + centerX += s.x; + centerY += s.y; + centerZ += s.z; + } + centerX /= ordered.size(); + centerY /= ordered.size(); + centerZ /= ordered.size(); + final double cx = centerX; + final double cy = centerY; + final double cz = centerZ; + ordered.sort((a, b) -> Double.compare(distanceSquared(a, cx, cy, cz), distanceSquared(b, cx, cy, cz))); + if (!opening) { + java.util.Collections.reverse(ordered); + } + return ordered; + default: + return ordered; + } + } + + private long[] computeDelays(List ordered, String animation, long stepTicks) { + long[] delays = new long[ordered.size()]; + if (!animation.equals("center")) { + for (int i = 0; i < ordered.size(); i++) { + delays[i] = i * stepTicks; + } + return delays; + } + // Für center: Blöcke im gleichen Abstandsring bekommen denselben Tick + double cx = 0, cy = 0, cz = 0; + for (SecretBlockSnapshot s : ordered) { cx += s.x; cy += s.y; cz += s.z; } + cx /= ordered.size(); cy /= ordered.size(); cz /= ordered.size(); + final double fcx = cx, fcy = cy, fcz = cz; + double prev = -1; + int rank = -1; + for (int i = 0; i < ordered.size(); i++) { + double d = distanceSquared(ordered.get(i), fcx, fcy, fcz); + if (i == 0 || Math.abs(d - prev) > 0.01) { + rank++; + prev = d; + } + delays[i] = rank * stepTicks; + } + return delays; + } + + private double distanceSquared(SecretBlockSnapshot s, double centerX, double centerY, double centerZ) { + double dx = s.x - centerX; + double dy = s.y - centerY; + double dz = s.z - centerZ; + return dx * dx + dy * dy + dz * dz; + } + + private long getSecretStepTicks(String animation) { + return animation.equals("instant") ? 0L : 2L; + } + + private boolean isValidSecretAnimation(String animation) { + return animation.equals("instant") || animation.equals("wave") + || animation.equals("reverse") || animation.equals("center"); + } + + private String normalizeSecretAnimation(String animation) { + if (animation == null) return "wave"; + return animation.trim().toLowerCase(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Undo-System + // ───────────────────────────────────────────────────────────────────────── + + private void saveUndoAction(String buttonId, UndoAction action) { + lastActions.put(buttonId, action); + } + + private void undoAction(String buttonId, UndoAction action, Player player) { + switch (action.type) { + case RENAME: + dataManager.setControllerName(buttonId, (String) action.oldValue); + player.sendMessage("§a✔ Rename rückgängig gemacht."); + break; + case TRUST_ADD: + dataManager.removeTrustedPlayer(buttonId, java.util.UUID.fromString((String) action.oldValue)); + player.sendMessage("§a✔ Trust-Hinzufügung rückgängig gemacht."); + break; + case TRUST_REMOVE: + dataManager.addTrustedPlayer(buttonId, java.util.UUID.fromString((String) action.oldValue)); + player.sendMessage("§a✔ Trust-Entfernung rückgängig gemacht."); + break; + case PUBLIC: + case PRIVATE: + boolean wasPublic = Boolean.parseBoolean((String) action.oldValue); + dataManager.setPublic(buttonId, wasPublic); + player.sendMessage("§a✔ Status rückgängig gemacht: " + (wasPublic ? "§aÖffentlich" : "§cPrivat")); + break; + } + lastActions.remove(buttonId); + } + + private void cleanupOldUndoActions() { + long currentTime = System.currentTimeMillis(); + long timeout = 5 * 60 * 1000; // 5 Minuten + lastActions.entrySet().removeIf(entry -> + currentTime - entry.getValue().timestamp > timeout); + } + + // ───────────────────────────────────────────────────────────────────────── + // Undo Action Klasse + // ───────────────────────────────────────────────────────────────────────── + + public static class UndoAction { + public enum Type { RENAME, TRUST_ADD, TRUST_REMOVE, PUBLIC, PRIVATE } + + public Type type; + public Object oldValue; + public Object newValue; + public long timestamp; + + public UndoAction(Type type, Object oldValue, Object newValue) { + this.type = type; + this.oldValue = oldValue; + this.newValue = newValue; + this.timestamp = System.currentTimeMillis(); + } + } + + public static class SecretBlockSnapshot { + public final String loc; + public final String materialName; + public final String blockData; + public final int x; + public final int y; + public final int z; + + public SecretBlockSnapshot(String loc, String materialName, String blockData, int x, int y, int z) { + this.loc = loc; + this.materialName = materialName; + this.blockData = blockData; + this.x = x; + this.y = y; + this.z = z; + } + } + } \ No newline at end of file diff --git a/src/main/java/viper/ButtonListener.java b/src/main/java/viper/ButtonListener.java index fed1cc7..80c1a77 100644 --- a/src/main/java/viper/ButtonListener.java +++ b/src/main/java/viper/ButtonListener.java @@ -83,9 +83,14 @@ public class ButtonListener implements Listener { event.setCancelled(true); List connectedBlocks = dataManager.getConnectedBlocks(buttonId); - if (connectedBlocks != null && !connectedBlocks.isEmpty()) { + boolean hasConnectedBlocks = connectedBlocks != null && !connectedBlocks.isEmpty(); + boolean secretTriggered = plugin.triggerSecretWall(buttonId); + + if (hasConnectedBlocks) { toggleConnectedBlocks(player, playerUUID, connectedBlocks); - } else { + } + + if (!hasConnectedBlocks && !secretTriggered) { player.sendMessage(configManager.getMessage("keine-bloecke-verbunden")); } } @@ -202,8 +207,12 @@ public class ButtonListener implements Listener { boolean anyIronDoorOpened = false, anyIronDoorClosed = false; boolean anyIronTrapOpened = false, anyIronTrapClosed = false; boolean anyLampOn = false, anyLampOff = false; + boolean anyGrateOpened = false, anyGrateClosed = false; + boolean anyCreakingHeartOn = false, anyCreakingHeartOff = false; boolean anyNoteBlockPlayed = false; boolean anyBellPlayed = false; + boolean anyDispenserTriggered = false; + boolean anyDropperTriggered = false; boolean soundsEnabled = configManager.getConfig().getBoolean("sounds.enabled", true); @@ -268,18 +277,28 @@ public class ButtonListener implements Listener { else { if (!wasOpen) anyTrapOpened = true; else anyTrapClosed = true; } } } - // ── Redstone-Lampe ──────────────────────────────────────────── - else if (mat == Material.REDSTONE_LAMP) { - Lightable lamp = (Lightable) targetBlock.getBlockData(); - boolean wasLit = lamp.isLit(); - lamp.setLit(!wasLit); - targetBlock.setBlockData(lamp); - if (soundsEnabled) { - playConfigSound(location, - wasLit ? "sounds.lamp-off" : "sounds.lamp-on", - "BLOCK_LEVER_CLICK"); + // ── Lampen (Redstone + Kupferlampen) ───────────────────────── + else if (isLamp(mat)) { + if (targetBlock.getBlockData() instanceof Lightable) { + Lightable lamp = (Lightable) targetBlock.getBlockData(); + boolean wasLit = lamp.isLit(); + lamp.setLit(!wasLit); + targetBlock.setBlockData(lamp); + if (soundsEnabled) { + playConfigSound(location, + wasLit ? "sounds.lamp-off" : "sounds.lamp-on", + "BLOCK_LEVER_CLICK"); + } + if (!wasLit) anyLampOn = true; else anyLampOff = true; + } + } + // ── Gitter (alle *_GRATE + Eisenstangen) ───────────────────── + else if (plugin.isGrate(mat) || (mat == Material.AIR && plugin.isManagedOpenGrateLocation(locStr))) { + Boolean nowOpen = plugin.toggleGrate(targetBlock); + if (nowOpen != null) { + if (nowOpen) anyGrateOpened = true; + else anyGrateClosed = true; } - if (!wasLit) anyLampOn = true; else anyLampOff = true; } // ── Notenblock ──────────────────────────────────────────────── else if (mat == Material.NOTE_BLOCK) { @@ -294,6 +313,27 @@ public class ButtonListener implements Listener { targetBlock.getWorld().playSound(location, Sound.BLOCK_BELL_USE, 3.0f, 1.0f); anyBellPlayed = true; } + // ── Spender / Werfer ────────────────────────────────────────── + else if (mat == Material.DISPENSER) { + if (triggerContainer(targetBlock, "dispense")) { + targetBlock.getWorld().playSound(location, Sound.BLOCK_DISPENSER_DISPENSE, 1.0f, 1.0f); + anyDispenserTriggered = true; + } + } + else if (mat == Material.DROPPER) { + if (triggerContainer(targetBlock, "drop")) { + targetBlock.getWorld().playSound(location, Sound.BLOCK_DISPENSER_DISPENSE, 1.0f, 1.0f); + anyDropperTriggered = true; + } + } + // ── Creaking Heart ──────────────────────────────────────────── + else if (isCreakingHeart(mat)) { + Boolean nowActive = plugin.togglePersistentCreakingHeart(targetBlock); + if (nowActive != null) { + if (nowActive) anyCreakingHeartOn = true; + else anyCreakingHeartOff = true; + } + } } // Feedback-Nachrichten @@ -309,8 +349,14 @@ public class ButtonListener implements Listener { if (anyTrapClosed) player.sendMessage(configManager.getMessage("fallturen-geschlossen")); if (anyLampOn) player.sendMessage(configManager.getMessage("lampen-eingeschaltet")); if (anyLampOff) player.sendMessage(configManager.getMessage("lampen-ausgeschaltet")); + if (anyGrateOpened) player.sendMessage(configManager.getMessage("gitter-geoeffnet")); + if (anyGrateClosed) player.sendMessage(configManager.getMessage("gitter-geschlossen")); + if (anyCreakingHeartOn) player.sendMessage(configManager.getMessage("creaking-heart-aktiviert")); + if (anyCreakingHeartOff) player.sendMessage(configManager.getMessage("creaking-heart-deaktiviert")); if (anyNoteBlockPlayed) player.sendMessage(configManager.getMessage("notenblock-ausgeloest")); if (anyBellPlayed) player.sendMessage(configManager.getMessage("glocke-gelaeutet")); + if (anyDispenserTriggered) player.sendMessage(configManager.getMessage("spender-ausgeloest")); + if (anyDropperTriggered) player.sendMessage(configManager.getMessage("werfer-ausgeloest")); } /** @@ -330,6 +376,17 @@ public class ButtonListener implements Listener { } } + private boolean triggerContainer(Block block, String methodName) { + try { + Object state = block.getState(); + java.lang.reflect.Method method = state.getClass().getMethod(methodName); + Object result = method.invoke(state); + return !(result instanceof Boolean) || (Boolean) result; + } catch (ReflectiveOperationException ignored) { + return false; + } + } + // ----------------------------------------------------------------------- // Limits // ----------------------------------------------------------------------- @@ -360,8 +417,8 @@ public class ButtonListener implements Listener { >= configManager.getMaxTrapdoors()) { player.sendMessage(configManager.getMessage("max-fallturen-erreicht")); return false; } - } else if (type == Material.REDSTONE_LAMP) { - if (connected.stream().filter(l -> getMaterialAt(l) == Material.REDSTONE_LAMP).count() + } else if (isLamp(type)) { + if (connected.stream().filter(l -> isLamp(getMaterialAt(l))).count() >= configManager.getMaxLamps()) { player.sendMessage(configManager.getMessage("max-lampen-erreicht")); return false; } @@ -375,6 +432,16 @@ public class ButtonListener implements Listener { >= configManager.getMaxBells()) { player.sendMessage(configManager.getMessage("max-glocken-erreicht")); return false; } + } else if (type == Material.DISPENSER) { + if (connected.stream().filter(l -> getMaterialAt(l) == Material.DISPENSER).count() + >= configManager.getMaxDispensers()) { + player.sendMessage(configManager.getMessage("max-spender-erreicht")); return false; + } + } else if (type == Material.DROPPER) { + if (connected.stream().filter(l -> getMaterialAt(l) == Material.DROPPER).count() + >= configManager.getMaxDroppers()) { + player.sendMessage(configManager.getMessage("max-werfer-erreicht")); return false; + } } return true; } @@ -415,11 +482,20 @@ public class ButtonListener implements Listener { private boolean isDoor(Material m) { return m.name().endsWith("_DOOR") && m != Material.IRON_DOOR; } private boolean isGate(Material m) { return m.name().endsWith("_FENCE_GATE"); } private boolean isTrapdoor(Material m) { return m.name().endsWith("_TRAPDOOR") && m != Material.IRON_TRAPDOOR; } + private boolean isLamp(Material m) { + return m == Material.REDSTONE_LAMP + || "COPPER_BULB".equals(m.name()) + || m.name().endsWith("_COPPER_BULB"); + } + private boolean isCreakingHeart(Material m) { return "CREAKING_HEART".equals(m.name()); } private boolean isInteractableTarget(Material m) { return isDoor(m) || isGate(m) || isTrapdoor(m) || m == Material.IRON_DOOR || m == Material.IRON_TRAPDOOR - || m == Material.REDSTONE_LAMP || m == Material.NOTE_BLOCK || m == Material.BELL; + || isLamp(m) || plugin.isGrate(m) + || m == Material.NOTE_BLOCK || m == Material.BELL + || m == Material.DISPENSER || m == Material.DROPPER + || isCreakingHeart(m); } private Material getMaterialAt(String locString) { diff --git a/src/main/java/viper/ButtonTabCompleter.java b/src/main/java/viper/ButtonTabCompleter.java index 650320f..62de730 100644 --- a/src/main/java/viper/ButtonTabCompleter.java +++ b/src/main/java/viper/ButtonTabCompleter.java @@ -15,7 +15,7 @@ public class ButtonTabCompleter implements TabCompleter { private final List commands = Arrays.asList( "info", "reload", "note", "list", "rename", "schedule", - "trust", "untrust", "public", "private" + "trust", "untrust", "public", "private", "undo", "secret" ); private final List instruments = Arrays.asList( @@ -44,6 +44,26 @@ public class ButtonTabCompleter implements TabCompleter { case "rename": completions.add(""); break; + case "secret": + StringUtil.copyPartialMatches(args[1], Arrays.asList("select", "info", "add", "remove", "clear", "delay", "animation"), completions); + break; + } + } else if (args.length == 3 && args[0].equalsIgnoreCase("secret") && args[1].equalsIgnoreCase("delay")) { + completions.add("3"); + completions.add("5"); + completions.add("10"); + completions.add("30"); + completions.add("60"); + } else if (args.length == 3 && args[0].equalsIgnoreCase("secret") && args[1].equalsIgnoreCase("animation")) { + completions.add("instant"); + completions.add("wave"); + completions.add("reverse"); + completions.add("center"); + } + + if (args.length == 2 && args[0].equalsIgnoreCase("secret")) { + if (completions.isEmpty()) { + StringUtil.copyPartialMatches(args[1], Arrays.asList("select", "info", "add", "remove", "clear", "delay", "animation"), completions); } } diff --git a/src/main/java/viper/ConfigManager.java b/src/main/java/viper/ConfigManager.java index 507507d..ece1af1 100644 --- a/src/main/java/viper/ConfigManager.java +++ b/src/main/java/viper/ConfigManager.java @@ -68,12 +68,15 @@ public class ConfigManager { def(config, "max-gates", 20); def(config, "max-trapdoors", 20); def(config, "max-bells", 5); + def(config, "max-dispensers", 20); + def(config, "max-droppers", 20); def(config, "default-note", "PIANO"); def(config, "double-note-enabled", true); def(config, "double-note-delay-ms", 1000); def(config, "motion-detection-radius", 5.0); def(config, "motion-close-delay-ms", 5000); def(config, "motion-trigger-cooldown-ms", 2000); + def(config, "timed-container-interval-ticks", 40); // Optionales MySQL-Backend def(config, "mysql.enabled", false); @@ -122,9 +125,20 @@ public class ConfigManager { def(lang, "lampen-eingeschaltet", "§aLampen wurden eingeschaltet."); def(lang, "lampen-ausgeschaltet", "§cLampen wurden ausgeschaltet."); def(lang, "max-lampen-erreicht", "§cMaximale Anzahl an Lampen erreicht."); + // Creaking Heart + def(lang, "creaking-heart-aktiviert", "§aKnarrherz wurde aktiviert."); + def(lang, "creaking-heart-deaktiviert", "§cKnarrherz wurde deaktiviert."); + // Gitter + def(lang, "gitter-geoeffnet", "§aGitter wurden geöffnet."); + def(lang, "gitter-geschlossen", "§cGitter wurden geschlossen."); // Glocken def(lang, "glocke-gelaeutet", "§aGlocke wurde geläutet."); def(lang, "max-glocken-erreicht", "§cMaximale Anzahl an Glocken erreicht."); + // Spender / Werfer + def(lang, "spender-ausgeloest", "§aSpender wurden ausgelöst."); + def(lang, "werfer-ausgeloest", "§aWerfer wurden ausgelöst."); + def(lang, "max-spender-erreicht", "§cMaximale Anzahl an Spendern erreicht."); + def(lang, "max-werfer-erreicht", "§cMaximale Anzahl an Werfern erreicht."); // Notenblöcke def(lang, "notenblock-ausgeloest", "§aNotenblock-Klingel wurde ausgelöst."); def(lang, "instrument-gesetzt", "§aDein Instrument wurde auf %s gesetzt."); @@ -179,6 +193,8 @@ public class ConfigManager { public int getMaxGates() { return config.getInt("max-gates", 20); } public int getMaxTrapdoors() { return config.getInt("max-trapdoors", 20); } public int getMaxBells() { return config.getInt("max-bells", 5); } + public int getMaxDispensers() { return config.getInt("max-dispensers", 20); } + public int getMaxDroppers() { return config.getInt("max-droppers", 20); } public String getMessage(String key) { return lang.getString(key, "§cNachricht fehlt: " + key); diff --git a/src/main/java/viper/DataManager.java b/src/main/java/viper/DataManager.java index 11ce9d3..7236b8a 100644 --- a/src/main/java/viper/DataManager.java +++ b/src/main/java/viper/DataManager.java @@ -96,24 +96,29 @@ public class DataManager { } // buttonId vor dem Löschen des Location-Eintrags ermitteln String buttonId = getButtonIdForPlacedController(location); - String ownerUUID = null; if (data.getConfigurationSection("players") != null) { for (String uuid : data.getConfigurationSection("players").getKeys(false)) { String path = "players." + uuid + ".placed-controllers." + location; if (data.contains(path)) { data.set(path, null); - ownerUUID = uuid; } } } - // Alle zugehörigen Daten (Name, Status, Trust, Zeitplan, Verbindungen) bereinigen + // Alle zugehörigen Daten (Name, Status, Trust, Zeitplan, Verbindungen, Secret) bereinigen if (buttonId != null) { data.set("names." + buttonId, null); data.set("public-status." + buttonId, null); data.set("trust." + buttonId, null); data.set("schedules." + buttonId, null); - if (ownerUUID != null) { - data.set("players." + ownerUUID + ".buttons." + buttonId, null); + + // Secret-Wall-Daten ebenfalls entfernen + data.set("secret-walls." + buttonId, null); + + // Sicherheitshalber bei ALLEN Spielern den Button-Eintrag löschen + if (data.getConfigurationSection("players") != null) { + for (String uuid : data.getConfigurationSection("players").getKeys(false)) { + data.set("players." + uuid + ".buttons." + buttonId, null); + } } } removeMotionSensorSettings(location); @@ -238,6 +243,34 @@ public class DataManager { return data.getLong("schedules." + buttonId + ".close-time", -1); } + public void setScheduleShotDelayTicks(String buttonId, int ticks) { + if (mySQLStorage != null) { + mySQLStorage.setScheduleShotDelayTicks(buttonId, ticks); + return; + } + data.set("schedules." + buttonId + ".shot-delay-ticks", ticks); + saveData(); + } + + public int getScheduleShotDelayTicks(String buttonId) { + if (mySQLStorage != null) return mySQLStorage.getScheduleShotDelayTicks(buttonId); + return data.getInt("schedules." + buttonId + ".shot-delay-ticks", -1); + } + + public void setScheduleTriggerMode(String buttonId, String mode) { + if (mySQLStorage != null) { + mySQLStorage.setScheduleTriggerMode(buttonId, mode); + return; + } + data.set("schedules." + buttonId + ".trigger-mode", mode); + saveData(); + } + + public String getScheduleTriggerMode(String buttonId) { + if (mySQLStorage != null) return mySQLStorage.getScheduleTriggerMode(buttonId); + return data.getString("schedules." + buttonId + ".trigger-mode"); + } + /** Entfernt den kompletten Zeitplan für einen Controller. */ public void clearSchedule(String buttonId) { if (mySQLStorage != null) { @@ -349,6 +382,61 @@ public class DataManager { saveData(); } + // ----------------------------------------------------------------------- + // Secret-Wall (Geheimwand) + // ----------------------------------------------------------------------- + + public void setSecretBlocks(String buttonId, List blocks) { + if (mySQLStorage != null) { + mySQLStorage.setSecretBlocks(buttonId, blocks); + return; + } + data.set("secret-walls." + buttonId + ".blocks", blocks); + saveData(); + } + + public List getSecretBlocks(String buttonId) { + if (mySQLStorage != null) return mySQLStorage.getSecretBlocks(buttonId); + return data.getStringList("secret-walls." + buttonId + ".blocks"); + } + + public void setSecretRestoreDelayMs(String buttonId, long delayMs) { + if (mySQLStorage != null) { + mySQLStorage.setSecretRestoreDelayMs(buttonId, delayMs); + return; + } + data.set("secret-walls." + buttonId + ".delay-ms", delayMs); + saveData(); + } + + public long getSecretRestoreDelayMs(String buttonId) { + if (mySQLStorage != null) return mySQLStorage.getSecretRestoreDelayMs(buttonId); + return data.getLong("secret-walls." + buttonId + ".delay-ms", 5000L); + } + + public void setSecretAnimation(String buttonId, String animation) { + if (mySQLStorage != null) { + mySQLStorage.setSecretAnimation(buttonId, animation); + return; + } + data.set("secret-walls." + buttonId + ".animation", animation); + saveData(); + } + + public String getSecretAnimation(String buttonId) { + if (mySQLStorage != null) return mySQLStorage.getSecretAnimation(buttonId); + return data.getString("secret-walls." + buttonId + ".animation", "wave"); + } + + public void clearSecret(String buttonId) { + if (mySQLStorage != null) { + mySQLStorage.clearSecret(buttonId); + return; + } + data.set("secret-walls." + buttonId, null); + saveData(); + } + // ----------------------------------------------------------------------- // Speichern – asynchron // ----------------------------------------------------------------------- diff --git a/src/main/java/viper/MySQLStorage.java b/src/main/java/viper/MySQLStorage.java index 1c00ca0..90c196d 100644 --- a/src/main/java/viper/MySQLStorage.java +++ b/src/main/java/viper/MySQLStorage.java @@ -45,7 +45,8 @@ public class MySQLStorage { plugin.getLogger().info("MySQL aktiviert."); return true; } catch (Exception e) { - plugin.getLogger().warning("MySQL konnte nicht initialisiert werden, verwende data.yml: " + e.getMessage()); + plugin.getLogger().warning("MySQL Verbindung fehlgeschlagen - verwende data.yml für Datenspeicherung."); + plugin.getLogger().fine("Fehlerdetails: " + e.getMessage()); return false; } } @@ -93,7 +94,9 @@ public class MySQLStorage { st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_schedules (" + "button_id VARCHAR(64) PRIMARY KEY," + "open_time BIGINT," - + "close_time BIGINT" + + "close_time BIGINT," + + "shot_delay_ticks INT," + + "trigger_mode VARCHAR(16)" + ")"); st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_trust (" @@ -117,6 +120,27 @@ public class MySQLStorage { + "radius DOUBLE," + "delay_ms BIGINT" + ")"); + + st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_secret_walls (" + + "button_id VARCHAR(64) NOT NULL," + + "block_location VARCHAR(128) NOT NULL," + + "PRIMARY KEY (button_id, block_location)" + + ")"); + + st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_secret_settings (" + + "button_id VARCHAR(64) PRIMARY KEY," + + "delay_ms BIGINT NOT NULL," + + "animation VARCHAR(16) NOT NULL DEFAULT 'wave'" + + ")"); + + st.executeUpdate("ALTER TABLE bc_schedules " + + "ADD COLUMN IF NOT EXISTS shot_delay_ticks INT"); + + st.executeUpdate("ALTER TABLE bc_schedules " + + "ADD COLUMN IF NOT EXISTS trigger_mode VARCHAR(16)"); + + st.executeUpdate("ALTER TABLE bc_secret_settings " + + "ADD COLUMN IF NOT EXISTS animation VARCHAR(16) NOT NULL DEFAULT 'wave'"); } } @@ -195,7 +219,9 @@ public class MySQLStorage { "DELETE FROM bc_public_status WHERE button_id = ?", "DELETE FROM bc_trust WHERE button_id = ?", "DELETE FROM bc_schedules WHERE button_id = ?", - "DELETE FROM bc_button_connections WHERE button_id = ?" + "DELETE FROM bc_button_connections WHERE button_id = ?", + "DELETE FROM bc_secret_walls WHERE button_id = ?", + "DELETE FROM bc_secret_settings WHERE button_id = ?" }; for (String q : queries) { try (PreparedStatement ps = getConnection().prepareStatement(q)) { @@ -295,12 +321,11 @@ public class MySQLStorage { } public void setScheduleOpenTime(String buttonId, long ticks) { - String q = "INSERT INTO bc_schedules (button_id, open_time, close_time) VALUES (?, ?, COALESCE((SELECT close_time FROM bc_schedules WHERE button_id = ?), -1))" + String q = "INSERT INTO bc_schedules (button_id, open_time, close_time, shot_delay_ticks, trigger_mode) VALUES (?, ?, -1, -1, 'simultaneous')" + " ON DUPLICATE KEY UPDATE open_time = VALUES(open_time)"; try (PreparedStatement ps = getConnection().prepareStatement(q)) { ps.setString(1, buttonId); ps.setLong(2, ticks); - ps.setString(3, buttonId); ps.executeUpdate(); } catch (SQLException e) { plugin.getLogger().warning("MySQL setScheduleOpenTime Fehler: " + e.getMessage()); @@ -321,12 +346,11 @@ public class MySQLStorage { } public void setScheduleCloseTime(String buttonId, long ticks) { - String q = "INSERT INTO bc_schedules (button_id, open_time, close_time) VALUES (?, COALESCE((SELECT open_time FROM bc_schedules WHERE button_id = ?), -1), ?)" + String q = "INSERT INTO bc_schedules (button_id, open_time, close_time, shot_delay_ticks, trigger_mode) VALUES (?, -1, ?, -1, 'simultaneous')" + " ON DUPLICATE KEY UPDATE close_time = VALUES(close_time)"; try (PreparedStatement ps = getConnection().prepareStatement(q)) { ps.setString(1, buttonId); - ps.setString(2, buttonId); - ps.setLong(3, ticks); + ps.setLong(2, ticks); ps.executeUpdate(); } catch (SQLException e) { plugin.getLogger().warning("MySQL setScheduleCloseTime Fehler: " + e.getMessage()); @@ -346,6 +370,58 @@ public class MySQLStorage { } } + public void setScheduleShotDelayTicks(String buttonId, int ticks) { + String q = "INSERT INTO bc_schedules (button_id, open_time, close_time, shot_delay_ticks, trigger_mode) VALUES (?, -1, -1, ?, 'simultaneous')" + + " ON DUPLICATE KEY UPDATE shot_delay_ticks = VALUES(shot_delay_ticks)"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + ps.setInt(2, ticks); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().warning("MySQL setScheduleShotDelayTicks Fehler: " + e.getMessage()); + } + } + + public int getScheduleShotDelayTicks(String buttonId) { + String q = "SELECT shot_delay_ticks FROM bc_schedules WHERE button_id = ? LIMIT 1"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return -1; + int delay = rs.getInt(1); + return rs.wasNull() ? -1 : delay; + } + } catch (SQLException e) { + plugin.getLogger().warning("MySQL getScheduleShotDelayTicks Fehler: " + e.getMessage()); + return -1; + } + } + + public void setScheduleTriggerMode(String buttonId, String mode) { + String q = "INSERT INTO bc_schedules (button_id, open_time, close_time, shot_delay_ticks, trigger_mode) VALUES (?, -1, -1, -1, ?)" + + " ON DUPLICATE KEY UPDATE trigger_mode = VALUES(trigger_mode)"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + ps.setString(2, mode); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().warning("MySQL setScheduleTriggerMode Fehler: " + e.getMessage()); + } + } + + public String getScheduleTriggerMode(String buttonId) { + String q = "SELECT trigger_mode FROM bc_schedules WHERE button_id = ? LIMIT 1"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getString(1) : null; + } + } catch (SQLException e) { + plugin.getLogger().warning("MySQL getScheduleTriggerMode Fehler: " + e.getMessage()); + return null; + } + } + public void clearSchedule(String buttonId) { String q = "DELETE FROM bc_schedules WHERE button_id = ?"; try (PreparedStatement ps = getConnection().prepareStatement(q)) { @@ -490,6 +566,104 @@ public class MySQLStorage { } } + public void setSecretBlocks(String buttonId, List blocks) { + String del = "DELETE FROM bc_secret_walls WHERE button_id = ?"; + String ins = "INSERT INTO bc_secret_walls (button_id, block_location) VALUES (?, ?)"; + try (PreparedStatement psDel = getConnection().prepareStatement(del); + PreparedStatement psIns = getConnection().prepareStatement(ins)) { + psDel.setString(1, buttonId); + psDel.executeUpdate(); + + for (String block : blocks) { + psIns.setString(1, buttonId); + psIns.setString(2, block); + psIns.addBatch(); + } + psIns.executeBatch(); + } catch (SQLException e) { + plugin.getLogger().warning("MySQL setSecretBlocks Fehler: " + e.getMessage()); + } + } + + public List getSecretBlocks(String buttonId) { + List result = new ArrayList<>(); + String q = "SELECT block_location FROM bc_secret_walls WHERE button_id = ?"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) result.add(rs.getString(1)); + } + } catch (SQLException e) { + plugin.getLogger().warning("MySQL getSecretBlocks Fehler: " + e.getMessage()); + } + return result; + } + + public void setSecretRestoreDelayMs(String buttonId, long delayMs) { + String q = "INSERT INTO bc_secret_settings (button_id, delay_ms) VALUES (?, ?)" + + " ON DUPLICATE KEY UPDATE delay_ms = VALUES(delay_ms)"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + ps.setLong(2, delayMs); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().warning("MySQL setSecretRestoreDelayMs Fehler: " + e.getMessage()); + } + } + + public long getSecretRestoreDelayMs(String buttonId) { + String q = "SELECT delay_ms FROM bc_secret_settings WHERE button_id = ? LIMIT 1"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getLong(1) : 5000L; + } + } catch (SQLException e) { + plugin.getLogger().warning("MySQL getSecretRestoreDelayMs Fehler: " + e.getMessage()); + return 5000L; + } + } + + public void setSecretAnimation(String buttonId, String animation) { + String q = "INSERT INTO bc_secret_settings (button_id, delay_ms, animation) VALUES (?, COALESCE((SELECT delay_ms FROM bc_secret_settings WHERE button_id = ?), 5000), ?)" + + " ON DUPLICATE KEY UPDATE animation = VALUES(animation)"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + ps.setString(2, buttonId); + ps.setString(3, animation); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().warning("MySQL setSecretAnimation Fehler: " + e.getMessage()); + } + } + + public String getSecretAnimation(String buttonId) { + String q = "SELECT animation FROM bc_secret_settings WHERE button_id = ? LIMIT 1"; + try (PreparedStatement ps = getConnection().prepareStatement(q)) { + ps.setString(1, buttonId); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getString(1) : "wave"; + } + } catch (SQLException e) { + plugin.getLogger().warning("MySQL getSecretAnimation Fehler: " + e.getMessage()); + return "wave"; + } + } + + public void clearSecret(String buttonId) { + String q1 = "DELETE FROM bc_secret_walls WHERE button_id = ?"; + String q2 = "DELETE FROM bc_secret_settings WHERE button_id = ?"; + try (PreparedStatement ps1 = getConnection().prepareStatement(q1); + PreparedStatement ps2 = getConnection().prepareStatement(q2)) { + ps1.setString(1, buttonId); + ps1.executeUpdate(); + ps2.setString(1, buttonId); + ps2.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().warning("MySQL clearSecret Fehler: " + e.getMessage()); + } + } + private boolean isTrusted(String buttonId, UUID playerUUID) { String q = "SELECT 1 FROM bc_trust WHERE button_id = ? AND target_uuid = ? LIMIT 1"; try (PreparedStatement ps = getConnection().prepareStatement(q)) { diff --git a/src/main/java/viper/ScheduleGUI.java b/src/main/java/viper/ScheduleGUI.java index 9f44c0c..e98dc6f 100644 --- a/src/main/java/viper/ScheduleGUI.java +++ b/src/main/java/viper/ScheduleGUI.java @@ -16,11 +16,14 @@ import org.bukkit.inventory.meta.ItemMeta; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; /** * GUI zur Konfiguration der zeitgesteuerten Automatisierung eines Controllers. * * Layout (9×3 = 27 Slots): + * Slot 4 – Abschuss-Verzögerung Werfer/Spender (REPEATER) ← Links/Rechts: ±1 Tick | Shift: ±5 + * Slot 6 – Schuss-Modus (COMPARATOR) ← Klick: gleichzeitig / nacheinander * Slot 10 – Öffnungszeit (LIME_DYE / Sonne) ← Links/Rechts: ±1h | Shift: ±15min * Slot 13 – Aktivierung an/aus (LEVER) * Slot 16 – Schließzeit (RED_DYE / Mond) ← Links/Rechts: ±1h | Shift: ±15min @@ -39,6 +42,8 @@ public class ScheduleGUI implements Listener { // Aktuelle Werte während die GUI offen ist private long openTime; private long closeTime; + private int shotDelayTicks; + private String triggerMode; private boolean enabled; public ScheduleGUI(ButtonControl plugin, Player player, String buttonId) { @@ -51,8 +56,16 @@ public class ScheduleGUI implements Listener { // Gespeicherte Werte laden (oder Standardwerte) long savedOpen = dataManager.getScheduleOpenTime(buttonId); long savedClose = dataManager.getScheduleCloseTime(buttonId); + int savedShotDelay = dataManager.getScheduleShotDelayTicks(buttonId); + String savedTriggerMode = dataManager.getScheduleTriggerMode(buttonId); this.openTime = savedOpen >= 0 ? savedOpen : plugin.timeToTicks(7, 0); // 07:00 this.closeTime = savedClose >= 0 ? savedClose : plugin.timeToTicks(19, 0); // 19:00 + this.shotDelayTicks = savedShotDelay >= 0 + ? savedShotDelay + : Math.max(1, plugin.getConfigManager().getConfig().getInt("timed-container-shot-delay-ticks", 2)); + this.triggerMode = normalizeTriggerMode(savedTriggerMode != null + ? savedTriggerMode + : plugin.getConfigManager().getConfig().getString("timed-container-trigger-mode", "simultaneous")); this.enabled = savedOpen >= 0; plugin.getServer().getPluginManager().registerEvents(this, plugin); @@ -72,6 +85,12 @@ public class ScheduleGUI implements Listener { ItemStack filler = makeItem(Material.GRAY_STAINED_GLASS_PANE, ChatColor.RESET + ""); for (int i = 0; i < 27; i++) inv.setItem(i, filler); + // Slot 4 – Abschuss-Verzögerung + inv.setItem(4, makeDelayItem()); + + // Slot 6 – Modus + inv.setItem(6, makeModeItem()); + // Slot 10 – Öffnungszeit inv.setItem(10, makeTimeItem( Material.LIME_DYE, @@ -106,6 +125,65 @@ public class ScheduleGUI implements Listener { "§7Speichert den aktuellen Zeitplan.")); } + private ItemStack makeDelayItem() { + List lore = new ArrayList<>(); + lore.add("§e§l" + shotDelayTicks + " Ticks §7(" + formatShotDelaySeconds() + "s§7)"); + if (isSequentialMode()) { + lore.add("§7Aktuell: §f" + shotDelayTicks + " Ticks zwischen einzelnen Geräten"); + } else if (shotDelayTicks <= 1) { + lore.add("§7Aktuell: §falle verbundenen Werfer schießen jeden Tick"); + } else { + lore.add("§7Aktuell: §f" + shotDelayTicks + " Ticks zwischen gemeinsamen Schüssen"); + } + lore.add(""); + lore.add("§7Linksklick: §f+1 Tick"); + lore.add("§7Rechtsklick: §f−1 Tick"); + lore.add("§7Shift+Links: §f+5 Ticks"); + lore.add("§7Shift+Rechts: §f−5 Ticks"); + lore.add("§8(1 Tick = schnellstmöglich)"); + + ItemStack item = new ItemStack(Material.REPEATER); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName("§b§lAbschuss-Verzögerung"); + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack makeModeItem() { + List lore = new ArrayList<>(); + lore.add(isSequentialMode() + ? "§e§lNacheinander" + : "§e§lGleichzeitig"); + lore.add(""); + lore.add("§7Klick: §fModus wechseln"); + lore.add("§8Gleichzeitig = alle Werfer zusammen"); + lore.add("§8Nacheinander = Geräte rotieren der Reihe nach"); + + ItemStack item = new ItemStack(Material.COMPARATOR); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName("§d§lSchuss-Modus"); + meta.setLore(lore); + item.setItemMeta(meta); + } + return item; + } + + private String formatShotDelaySeconds() { + return String.format(Locale.US, "%.2f", shotDelayTicks / 20.0); + } + + private boolean isSequentialMode() { + return "sequential".equals(triggerMode); + } + + private String normalizeTriggerMode(String mode) { + return "sequential".equalsIgnoreCase(mode) ? "sequential" : "simultaneous"; + } + private ItemStack makeTimeItem(Material mat, String name, long ticks, String... loreLines) { String timeStr = plugin.ticksToTime(ticks); List lore = new ArrayList<>(); @@ -153,7 +231,20 @@ public class ScheduleGUI implements Listener { // Schrittgröße: Shift = 15 Min (250 Ticks), sonst 1 Std (1000 Ticks) long step = event.isShiftClick() ? 250L : 1000L; - if (slot == 10) { + if (slot == 4) { + int delayStep = event.isShiftClick() ? 5 : 1; + if (event.isLeftClick()) shotDelayTicks += delayStep; + if (event.isRightClick()) shotDelayTicks -= delayStep; + if (shotDelayTicks < 1) shotDelayTicks = 1; + if (shotDelayTicks > 200) shotDelayTicks = 200; + inv.setItem(4, makeDelayItem()); + + } else if (slot == 6) { + triggerMode = isSequentialMode() ? "simultaneous" : "sequential"; + inv.setItem(4, makeDelayItem()); + inv.setItem(6, makeModeItem()); + + } else if (slot == 10) { // Öffnungszeit anpassen if (event.isLeftClick()) openTime = (openTime + step + 24000) % 24000; if (event.isRightClick()) openTime = (openTime - step + 24000) % 24000; @@ -203,10 +294,18 @@ public class ScheduleGUI implements Listener { if (enabled) { dataManager.setScheduleOpenTime(buttonId, openTime); dataManager.setScheduleCloseTime(buttonId, closeTime); + dataManager.setScheduleShotDelayTicks(buttonId, shotDelayTicks); + dataManager.setScheduleTriggerMode(buttonId, triggerMode); player.sendMessage("§a[BC] §7Zeitplan gespeichert: §aÖffnet §7um §e" + plugin.ticksToTime(openTime) + " §7· §cSchließt §7um §e" - + plugin.ticksToTime(closeTime)); + + plugin.ticksToTime(closeTime) + + " §7· §bDelay §e" + + shotDelayTicks + + "§7 Ticks §8(" + + formatShotDelaySeconds() + + "s§8) §7· §dModus §e" + + (isSequentialMode() ? "nacheinander" : "gleichzeitig")); } else { dataManager.clearSchedule(buttonId); player.sendMessage("§7[BC] Zeitplan deaktiviert."); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f0e1bfe..2252d14 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -7,6 +7,8 @@ max-noteblocks: 10 max-gates: 20 max-trapdoors: 20 max-bells: 5 +max-dispensers: 20 +max-droppers: 20 # ── Notenblöcke ───────────────────────────────────────────────────────────── default-note: "PIANO" @@ -19,6 +21,14 @@ motion-detection-radius: 5.0 motion-close-delay-ms: 5000 # Cooldown: wie lange nach dem Schließen der Sensor nicht erneut auslöst motion-trigger-cooldown-ms: 2000 +# Legacy-Fallback für ältere Zeitpläne ohne eigenen GUI-Delay-Wert +# (20 Ticks = 1 Sekunde) +timed-container-interval-ticks: 40 +# Standardwert für die Zeit zwischen einzelnen Abschüssen im Zeitplan +# Wird verwendet, bis ein Controller in der GUI einen eigenen Wert speichert +timed-container-shot-delay-ticks: 2 +# Standardmodus für Zeitplan-Werfer/Spender: simultaneous oder sequential +timed-container-trigger-mode: simultaneous # ── Optionales MySQL-Backend ──────────────────────────────────────────────── mysql: diff --git a/src/main/resources/lang.yml b/src/main/resources/lang.yml index dc0eb28..0a86e1f 100644 --- a/src/main/resources/lang.yml +++ b/src/main/resources/lang.yml @@ -25,8 +25,16 @@ max-fallturen-erreicht: "§cMaximale Anzahl an Falltüren erreicht." lampen-eingeschaltet: "§aLampen wurden eingeschaltet." lampen-ausgeschaltet: "§cLampen wurden ausgeschaltet." max-lampen-erreicht: "§cMaximale Anzahl an Lampen erreicht." +creaking-heart-aktiviert: "§aKnarrherz wurde aktiviert." +creaking-heart-deaktiviert: "§cKnarrherz wurde deaktiviert." +gitter-geoeffnet: "§aGitter wurden geöffnet." +gitter-geschlossen: "§cGitter wurden geschlossen." glocke-gelaeutet: "§aGlocke wurde geläutet." max-glocken-erreicht: "§cMaximale Anzahl an Glocken erreicht." +spender-ausgeloest: "§aSpender wurden ausgelöst." +werfer-ausgeloest: "§aWerfer wurden ausgelöst." +max-spender-erreicht: "§cMaximale Anzahl an Spendern erreicht." +max-werfer-erreicht: "§cMaximale Anzahl an Werfern erreicht." # ── Notenblöcke ────────────────────────────────────────────────────────────── notenblock-ausgeloest: "§aNotenblock-Klingel wurde ausgelöst." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 20af5e2..d31ed04 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ButtonControl -version: 1.7 +version: 1.8 main: viper.ButtonControl api-version: 1.21 author: M_Viper @@ -11,7 +11,7 @@ description: > commands: bc: description: Hauptbefehl für ButtonControl - usage: /bc + usage: "/bc (Secret: select|info|add|remove|clear|delay|animation)" aliases: [buttoncontrol] permissions: