diff --git a/src/main/java/dev/viper/eventengine/command/EventCommand.java b/src/main/java/dev/viper/eventengine/command/EventCommand.java index ff3b7d6..97662b7 100644 --- a/src/main/java/dev/viper/eventengine/command/EventCommand.java +++ b/src/main/java/dev/viper/eventengine/command/EventCommand.java @@ -1,9 +1,10 @@ package dev.viper.eventengine.command; import dev.viper.eventengine.EventEngine; -import dev.viper.eventengine.command.RegionSetupManager; +import dev.viper.eventengine.events.builtin.ElytraRaceHandler; import dev.viper.eventengine.model.ActiveEvent; import dev.viper.eventengine.model.EventDefinition; +import dev.viper.eventengine.model.EventRegion; import dev.viper.eventengine.model.ScheduleEntry; import dev.viper.eventengine.manager.EventManager; import org.bukkit.Bukkit; @@ -44,14 +45,14 @@ public class EventCommand implements CommandExecutor, TabCompleter { requirePlayer(sender, () -> plugin.getEventGUI().openMain((Player) sender)); } - // ─── ITEM-REGEN GUI ────────────────────────────────────────── + // ─── ITEM-REGEN GUI ─────────────────────────────────────────── case "itemrain" -> { requirePerm(sender, () -> { requirePlayer(sender, () -> plugin.getItemRainGUI().open((Player) sender)); }); } - // ─── START ───────────────────────────────────────────────────── + // ─── START ──────────────────────────────────────────────────── case "start" -> { requirePerm(sender, () -> { if (args.length < 2) { sender.sendMessage(p + "§cUsage: /event start "); return; } @@ -68,7 +69,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── STOP ────────────────────────────────────────────────────── + // ─── STOP ───────────────────────────────────────────────────── case "stop" -> { requirePerm(sender, () -> { String reason = args.length > 1 ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) : "Admin"; @@ -78,14 +79,13 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── JOIN/LEAVE ──────────────────────────────────────────────── + // ─── JOIN/LEAVE ─────────────────────────────────────────────── case "join" -> requirePlayer(sender, () -> plugin.getEventManager().join((Player) sender)); case "leave" -> requirePlayer(sender, () -> plugin.getEventManager().leave((Player) sender)); - // ─── INFO ────────────────────────────────────────────────────── + // ─── INFO ───────────────────────────────────────────────────── case "info" -> { if (args.length >= 2) { - // Event-Info plugin.getEventRegistry().get(args[1]).ifPresentOrElse(def -> { sender.sendMessage("§8§m══════════════════════════════"); sender.sendMessage("§6 " + def.getDisplayName() + " §8[" + def.getId() + "]"); @@ -94,15 +94,19 @@ public class EventCommand implements CommandExecutor, TabCompleter { sender.sendMessage("§7 Dauer: §f" + EventManager.formatTime(def.getDurationSeconds())); sender.sendMessage("§7 Spieler: §f" + def.getMinPlayers() + "–" + def.getMaxPlayers()); sender.sendMessage("§7 Custom: §f" + def.isCustom()); + sender.sendMessage("§7 Renn-Modus: §f" + def.getRaceMode().name() + + (def.isCircuit() ? " §8(§e" + def.getLaps() + " Runden§8)" : "")); sender.sendMessage("§7 Start-Befehle: §f" + def.getStartCommands().size()); sender.sendMessage("§7 Belohnungen: §f" + def.getRewards().size()); sender.sendMessage("§7 Winner-Belohnungen: §f" + def.getWinnerRewards().size()); sender.sendMessage("§7 Startbereich: §f" + (def.hasStartRegion() ? "gesetzt" : "-")); sender.sendMessage("§7 Zielbereich: §f" + (def.hasGoalRegion() ? "gesetzt" : "-")); + Object cpCount = def.getCustomSettings().get("checkpoint_count"); + if (cpCount != null) + sender.sendMessage("§7 Elytra-Checkpoints: §f" + cpCount); sender.sendMessage("§8§m══════════════════════════════"); }, () -> sender.sendMessage(p + "§cEvent nicht gefunden.")); } else { - // Aktuelles Event ActiveEvent current = plugin.getEventManager().getCurrentEvent(); if (current == null) { sender.sendMessage(p + "§cKein Event aktiv."); @@ -117,7 +121,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { } } - // ─── LIST ────────────────────────────────────────────────────── + // ─── LIST ───────────────────────────────────────────────────── case "list" -> { int page = args.length >= 2 ? parseIntSafe(args[1], 1) - 1 : 0; List all = new ArrayList<>(plugin.getEventRegistry().getAll()); @@ -134,7 +138,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { if (totalPages > 1) sender.sendMessage("§7Nächste Seite: §f/event list " + (page + 2)); } - // ─── SCHEDULE ────────────────────────────────────────────────── + // ─── SCHEDULE ───────────────────────────────────────────────── case "schedule" -> { List schedule = plugin.getConfigManager().getSchedule(); sender.sendMessage("§8§m══════════════════════════════"); @@ -148,7 +152,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { sender.sendMessage("§8§m══════════════════════════════"); } - // ─── CREATE ──────────────────────────────────────────────────── + // ─── CREATE ─────────────────────────────────────────────────── case "create" -> { requirePerm(sender, () -> { requirePlayer(sender, () -> { @@ -167,7 +171,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── DELETE ──────────────────────────────────────────────────── + // ─── DELETE ─────────────────────────────────────────────────── case "delete" -> { requirePerm(sender, () -> { if (args.length < 2) { sender.sendMessage(p + "§cUsage: /event delete "); return; } @@ -180,7 +184,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── SCORE ───────────────────────────────────────────────────── + // ─── SCORE ──────────────────────────────────────────────────── case "score" -> { requirePerm(sender, () -> { if (args.length < 3) { sender.sendMessage(p + "§cUsage: /event score "); return; } @@ -194,7 +198,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── ROTATION ────────────────────────────────────────────────── + // ─── ROTATION ───────────────────────────────────────────────── case "rotation" -> { requirePerm(sender, () -> { if (args.length < 3) { sender.sendMessage(p + "§cUsage: /event rotation "); return; } @@ -209,7 +213,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── RELOAD ──────────────────────────────────────────────────── + // ─── RELOAD ─────────────────────────────────────────────────── case "reload" -> { requirePerm(sender, () -> { plugin.getConfigManager().load(); @@ -219,7 +223,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── REGION ──────────────────────────────────────────────── + // ─── REGION ─────────────────────────────────────────────────── case "region" -> { requirePerm(sender, () -> { requirePlayer(sender, () -> { @@ -241,7 +245,7 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } - // ─── RACE START/GOAL ─────────────────────────────────────── + // ─── RACE START/GOAL ────────────────────────────────────────── case "race" -> { requirePerm(sender, () -> { requirePlayer(sender, () -> { @@ -278,23 +282,94 @@ public class EventCommand implements CommandExecutor, TabCompleter { }); } + // ─── ELYTRA CHECKPOINT ──────────────────────────────────────── + case "elytra" -> { + requirePerm(sender, () -> { + requirePlayer(sender, () -> { + if (args.length < 4 || !args[1].equalsIgnoreCase("checkpoint")) { + sender.sendMessage(p + "§cUsage: /event elytra checkpoint "); + return; + } + String sub = args[2].toLowerCase(); + String id = args[3].toLowerCase(); + EventDefinition def = plugin.getEventRegistry().get(id).orElse(null); + if (def == null) { sender.sendMessage(p + "§cEvent nicht gefunden: §f" + id); return; } + Player pl = (Player) sender; + RegionSetupManager rsm = plugin.getRegionSetupManager(); + switch (sub) { + case "add" -> rsm.handleCheckpointPos1(pl, id); + case "confirm" -> rsm.handleCheckpointPos2(pl, id); + case "clear" -> { + ElytraRaceHandler.clearCheckpoints(def); + if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); + sender.sendMessage(p + "§7Alle Checkpoints für §e" + def.getDisplayName() + " §7gelöscht."); + } + case "list" -> { + Object countObj = def.getCustomSettings().get("checkpoint_count"); + int count = countObj instanceof Number n ? n.intValue() : 0; + sender.sendMessage("§8§m══════════════════════════════"); + sender.sendMessage("§6 Elytra Checkpoints: §e" + def.getDisplayName() + " §8(" + count + ")"); + for (int i = 0; i < count; i++) { + String pf = "checkpoint_" + i + "_"; + Map s = def.getCustomSettings(); + sender.sendMessage(" §7" + (i + 1) + ". §f" + s.get(pf + "world") + + " §8[§f" + s.get(pf + "min-x") + "," + s.get(pf + "min-y") + "," + s.get(pf + "min-z") + + "§8] → [§f" + s.get(pf + "max-x") + "," + s.get(pf + "max-y") + "," + s.get(pf + "max-z") + "§8]"); + } + sender.sendMessage("§8§m══════════════════════════════"); + } + default -> sender.sendMessage(p + "§cUnbekannte Option. add | confirm | clear | list"); + } + }); + }); + } + + // ─── RACEMODE ───────────────────────────────────────────────── + case "racemode" -> { + requirePerm(sender, () -> { + if (args.length < 3) { + sender.sendMessage(p + "§cUsage: /event racemode [laps]"); + return; + } + String id = args[1].toLowerCase(); + String mode = args[2].toLowerCase(); + int laps = args.length >= 4 ? parseIntSafe(args[3], 1) : 1; + + EventDefinition def = plugin.getEventRegistry().get(id).orElse(null); + if (def == null) { sender.sendMessage(p + "§cEvent nicht gefunden: §f" + id); return; } + + if (mode.equals("circuit")) { + def.setRaceMode(EventDefinition.RaceMode.CIRCUIT); + def.setLaps(Math.max(1, laps)); + sender.sendMessage(p + "§a✔ §e" + def.getDisplayName() + + " §7→ Modus: §6Circuit §8(§e" + def.getLaps() + " Runden§8)"); + } else { + def.setRaceMode(EventDefinition.RaceMode.SPRINT); + sender.sendMessage(p + "§a✔ §e" + def.getDisplayName() + + " §7→ Modus: §6Sprint §8(A→B)"); + } + if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); + }); + } + default -> sendHelp(sender); } return true; } - // ─── Tab-Completer ───────────────────────────────────────────────────── + // ─── Tab-Completer ──────────────────────────────────────────────────── @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { if (args.length == 1) { return filter(args[0], "gui", "start", "stop", "join", "leave", "info", "list", - "schedule", "create", "delete", "score", "rotation", "region", "race", "itemrain", "reload"); + "schedule", "create", "delete", "score", "rotation", "region", "race", + "elytra", "racemode", "itemrain", "reload"); } if (args.length == 2) { return switch (args[0].toLowerCase()) { - case "start", "info", "delete" -> { + case "start", "info", "delete", "racemode" -> { List ids = plugin.getEventRegistry().getAll().stream() .map(EventDefinition::getId) .collect(Collectors.toList()); @@ -302,31 +377,36 @@ public class EventCommand implements CommandExecutor, TabCompleter { yield filter(args[1], ids.toArray(new String[0])); } case "rotation" -> filter(args[1], "add", "remove"); - case "score" -> null; // Spielernamen - case "region" -> filter(args[1], "pos1", "pos2", "clear", "info"); - case "race" -> filter(args[1], "start", "goal"); + case "region" -> filter(args[1], "pos1", "pos2", "clear", "info"); + case "race" -> filter(args[1], "start", "goal"); + case "elytra" -> filter(args[1], "checkpoint"); default -> null; }; } - if (args.length == 3 && args[0].equalsIgnoreCase("race")) { - return filter(args[2], "pos1", "pos2", "clear", "info"); + if (args.length == 3) { + return switch (args[0].toLowerCase()) { + case "race" -> filter(args[2], "pos1", "pos2", "clear", "info"); + case "elytra" -> filter(args[2], "add", "confirm", "clear", "list"); + case "racemode" -> filter(args[2], "sprint", "circuit"); + case "rotation" -> plugin.getEventRegistry().getAll().stream() + .map(EventDefinition::getId) + .filter(id -> id.startsWith(args[2].toLowerCase())) + .collect(Collectors.toList()); + default -> null; + }; } - if (args.length == 3 && args[0].equalsIgnoreCase("rotation")) { - return plugin.getEventRegistry().getAll().stream() - .map(EventDefinition::getId) - .filter(id -> id.startsWith(args[2].toLowerCase())) - .collect(Collectors.toList()); - } - if (args.length == 4 && args[0].equalsIgnoreCase("race")) { - return plugin.getEventRegistry().getAll().stream() - .map(EventDefinition::getId) - .filter(id -> id.startsWith(args[3].toLowerCase())) - .collect(Collectors.toList()); + if (args.length == 4) { + if (args[0].equalsIgnoreCase("race") || args[0].equalsIgnoreCase("elytra")) { + return plugin.getEventRegistry().getAll().stream() + .map(EventDefinition::getId) + .filter(id -> id.startsWith(args[3].toLowerCase())) + .collect(Collectors.toList()); + } } return null; } - // ─── Hilfsmethoden ───────────────────────────────────────────────────── + // ─── Hilfsmethoden ─────────────────────────────────────────────────── private void sendHelp(CommandSender sender) { sender.sendMessage("§8§m══════════════════════════════"); @@ -345,6 +425,8 @@ public class EventCommand implements CommandExecutor, TabCompleter { sender.sendMessage("§e /event reload §7- Config neu laden"); sender.sendMessage("§e /event region §7- Event-Arena setzen"); sender.sendMessage("§e /event race §7- Start/Ziel setzen"); + sender.sendMessage("§e /event racemode [laps] §7- Renn-Modus setzen"); + sender.sendMessage("§e /event elytra checkpoint §7- Elytra-Checkpoints"); sender.sendMessage("§e /event itemrain §7- Item-Regen GUI"); sender.sendMessage("§8§m══════════════════════════════"); } @@ -374,4 +456,4 @@ public class EventCommand implements CommandExecutor, TabCompleter { private int parseIntSafe(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } } -} +} \ No newline at end of file diff --git a/src/main/java/dev/viper/eventengine/command/RegionSetupManager.java b/src/main/java/dev/viper/eventengine/command/RegionSetupManager.java index 628d061..4bec3d1 100644 --- a/src/main/java/dev/viper/eventengine/command/RegionSetupManager.java +++ b/src/main/java/dev/viper/eventengine/command/RegionSetupManager.java @@ -1,6 +1,7 @@ package dev.viper.eventengine.command; import dev.viper.eventengine.EventEngine; +import dev.viper.eventengine.events.builtin.ElytraRaceHandler; import dev.viper.eventengine.model.EventDefinition; import dev.viper.eventengine.model.EventRegion; import org.bukkit.entity.Player; @@ -11,125 +12,113 @@ import java.util.UUID; /** * Stellt Pos1/Pos2-Auswahl für Event-Regionen bereit. - * Eingebaut in EventCommand via sub-commands: * - * /event region pos1 → Pos1 auf Spieler-Position setzen - * /event region pos2 → Pos2 setzen + Region speichern - * /event region clear → Region entfernen - * /event region info → Region anzeigen + * Befehle: + * /event region pos1/pos2/clear/info + * /event race start pos1/pos2/clear/info + * /event race goal pos1/pos2/clear/info + * /event elytra checkpoint add/confirm/clear/list */ public class RegionSetupManager { private final EventEngine plugin; - // Temporäre Pos1-Speicher pro Spieler: UUID → [eventId, Location] - private final Map pos1Selection = new HashMap<>(); - private final Map startPos1Selection = new HashMap<>(); - private final Map goalPos1Selection = new HashMap<>(); + + private final Map pos1Selection = new HashMap<>(); + private final Map startPos1Selection = new HashMap<>(); + private final Map goalPos1Selection = new HashMap<>(); + private final Map checkpointPos1Selection = new HashMap<>(); public RegionSetupManager(EventEngine plugin) { this.plugin = plugin; } - public void handlePos1(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + // ─── Haupt-Region ──────────────────────────────────────────────────── + public void handlePos1(Player player, String eventId) { + EventDefinition def = resolve(player, eventId); if (def == null) return; pos1Selection.put(player.getUniqueId(), new Object[]{eventId, player.getLocation().clone()}); player.sendMessage(plugin.prefix() + "§aPos1 gesetzt: §f" + formatLoc(player)); player.sendMessage(plugin.prefix() + "§7Jetzt: §f/event region pos2 " + eventId); } public void handlePos2(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } - + EventDefinition def = resolve(player, eventId); if (def == null) return; Object[] sel = pos1Selection.get(player.getUniqueId()); if (sel == null || !sel[0].equals(eventId)) { - player.sendMessage(plugin.prefix() + "§cZuerst Pos1 setzen: §f/event region pos1 " + eventId); - return; + player.sendMessage(plugin.prefix() + "§cZuerst Pos1 setzen: §f/event region pos1 " + eventId); return; } - org.bukkit.Location pos1 = (org.bukkit.Location) sel[1]; org.bukkit.Location pos2 = player.getLocation().clone(); - if (!pos1.getWorld().equals(pos2.getWorld())) { - player.sendMessage(plugin.prefix() + "§cBeide Positionen müssen in der gleichen Welt sein!"); - return; + player.sendMessage(plugin.prefix() + "§cBeide Positionen müssen in der gleichen Welt sein!"); return; } - EventRegion region = new EventRegion(pos1, pos2); def.setRegion(region); - - // Custom Events direkt speichern; builtin: in memory (gilt bis Reload) if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); - pos1Selection.remove(player.getUniqueId()); - player.sendMessage(plugin.prefix() + "§a✔ Region gesetzt für §e" + def.getDisplayName() + "§a:"); - player.sendMessage("§7 Welt: §f" + region.getWorldName()); - player.sendMessage("§7 Von: §f" + region.getMinX() + ", " + region.getMinY() + ", " + region.getMinZ()); - player.sendMessage("§7 Bis: §f" + region.getMaxX() + ", " + region.getMaxY() + ", " + region.getMaxZ()); - player.sendMessage("§7 Größe: §f" + region.getSizeX() + "x" + region.getSizeY() + "x" + region.getSizeZ()); + sendRegionInfo(player, region); } public void handleClear(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + EventDefinition def = resolve(player, eventId); if (def == null) return; def.setRegion(null); if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); player.sendMessage(plugin.prefix() + "§7Region für §e" + def.getDisplayName() + " §7entfernt (gesamte Welt)."); } - public void handleStartPos1(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + public void handleInfo(Player player, String eventId) { + EventDefinition def = resolve(player, eventId); if (def == null) return; + if (!def.hasRegion()) { + player.sendMessage(plugin.prefix() + "§e" + def.getDisplayName() + " §7hat keine Region (gesamte Welt)."); return; + } + EventRegion r = def.getRegion(); + player.sendMessage("§8§m══════════════════════════════"); + player.sendMessage("§6 Region: §e" + def.getDisplayName()); + sendRegionInfo(player, r); + player.sendMessage("§7 Größe: §f" + r.getSizeX() + "×" + r.getSizeY() + "×" + r.getSizeZ() + " Blöcke"); + player.sendMessage("§8§m══════════════════════════════"); + } + // ─── Start-Region ──────────────────────────────────────────────────── + + public void handleStartPos1(Player player, String eventId) { + EventDefinition def = resolve(player, eventId); if (def == null) return; startPos1Selection.put(player.getUniqueId(), new Object[]{eventId, player.getLocation().clone()}); player.sendMessage(plugin.prefix() + "§aStart-Pos1 gesetzt: §f" + formatLoc(player)); player.sendMessage(plugin.prefix() + "§7Jetzt: §f/event race start pos2 " + eventId); } public void handleStartPos2(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } - + EventDefinition def = resolve(player, eventId); if (def == null) return; Object[] sel = startPos1Selection.get(player.getUniqueId()); if (sel == null || !sel[0].equals(eventId)) { - player.sendMessage(plugin.prefix() + "§cZuerst Start-Pos1 setzen: §f/event race start pos1 " + eventId); - return; + player.sendMessage(plugin.prefix() + "§cZuerst Start-Pos1 setzen: §f/event race start pos1 " + eventId); return; } - org.bukkit.Location pos1 = (org.bukkit.Location) sel[1]; org.bukkit.Location pos2 = player.getLocation().clone(); - if (!pos1.getWorld().equals(pos2.getWorld())) { - player.sendMessage(plugin.prefix() + "§cBeide Positionen müssen in der gleichen Welt sein!"); - return; + player.sendMessage(plugin.prefix() + "§cBeide Positionen müssen in der gleichen Welt sein!"); return; } - EventRegion region = new EventRegion(pos1, pos2); def.setStartRegion(region); if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); startPos1Selection.remove(player.getUniqueId()); - player.sendMessage(plugin.prefix() + "§a✔ Startbereich gesetzt für §e" + def.getDisplayName() + "§a:"); sendRegionInfo(player, region); } public void handleStartClear(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + EventDefinition def = resolve(player, eventId); if (def == null) return; def.setStartRegion(null); if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); player.sendMessage(plugin.prefix() + "§7Startbereich für §e" + def.getDisplayName() + " §7entfernt."); } public void handleStartInfo(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + EventDefinition def = resolve(player, eventId); if (def == null) return; if (!def.hasStartRegion()) { - player.sendMessage(plugin.prefix() + "§e" + def.getDisplayName() + " §7hat keinen Startbereich."); - return; + player.sendMessage(plugin.prefix() + "§e" + def.getDisplayName() + " §7hat keinen Startbereich."); return; } player.sendMessage("§8§m══════════════════════════════"); player.sendMessage("§6 Startbereich: §e" + def.getDisplayName()); @@ -137,56 +126,45 @@ public class RegionSetupManager { player.sendMessage("§8§m══════════════════════════════"); } - public void handleGoalPos1(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + // ─── Ziel-Region ───────────────────────────────────────────────────── + public void handleGoalPos1(Player player, String eventId) { + EventDefinition def = resolve(player, eventId); if (def == null) return; goalPos1Selection.put(player.getUniqueId(), new Object[]{eventId, player.getLocation().clone()}); player.sendMessage(plugin.prefix() + "§aZiel-Pos1 gesetzt: §f" + formatLoc(player)); player.sendMessage(plugin.prefix() + "§7Jetzt: §f/event race goal pos2 " + eventId); } public void handleGoalPos2(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } - + EventDefinition def = resolve(player, eventId); if (def == null) return; Object[] sel = goalPos1Selection.get(player.getUniqueId()); if (sel == null || !sel[0].equals(eventId)) { - player.sendMessage(plugin.prefix() + "§cZuerst Ziel-Pos1 setzen: §f/event race goal pos1 " + eventId); - return; + player.sendMessage(plugin.prefix() + "§cZuerst Ziel-Pos1 setzen: §f/event race goal pos1 " + eventId); return; } - org.bukkit.Location pos1 = (org.bukkit.Location) sel[1]; org.bukkit.Location pos2 = player.getLocation().clone(); - if (!pos1.getWorld().equals(pos2.getWorld())) { - player.sendMessage(plugin.prefix() + "§cBeide Positionen müssen in der gleichen Welt sein!"); - return; + player.sendMessage(plugin.prefix() + "§cBeide Positionen müssen in der gleichen Welt sein!"); return; } - EventRegion region = new EventRegion(pos1, pos2); def.setGoalRegion(region); if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); goalPos1Selection.remove(player.getUniqueId()); - player.sendMessage(plugin.prefix() + "§a✔ Zielbereich gesetzt für §e" + def.getDisplayName() + "§a:"); sendRegionInfo(player, region); } public void handleGoalClear(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + EventDefinition def = resolve(player, eventId); if (def == null) return; def.setGoalRegion(null); if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); player.sendMessage(plugin.prefix() + "§7Zielbereich für §e" + def.getDisplayName() + " §7entfernt."); } public void handleGoalInfo(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } + EventDefinition def = resolve(player, eventId); if (def == null) return; if (!def.hasGoalRegion()) { - player.sendMessage(plugin.prefix() + "§e" + def.getDisplayName() + " §7hat keinen Zielbereich."); - return; + player.sendMessage(plugin.prefix() + "§e" + def.getDisplayName() + " §7hat keinen Zielbereich."); return; } player.sendMessage("§8§m══════════════════════════════"); player.sendMessage("§6 Zielbereich: §e" + def.getDisplayName()); @@ -194,21 +172,46 @@ public class RegionSetupManager { player.sendMessage("§8§m══════════════════════════════"); } - public void handleInfo(Player player, String eventId) { - EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); - if (def == null) { player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); return; } - if (!def.hasRegion()) { - player.sendMessage(plugin.prefix() + "§e" + def.getDisplayName() + " §7hat keine Region (gesamte Welt)."); - return; + // ─── Elytra Checkpoints ─────────────────────────────────────────────── + + public void handleCheckpointPos1(Player player, String eventId) { + EventDefinition def = resolve(player, eventId); if (def == null) return; + checkpointPos1Selection.put(player.getUniqueId(), new Object[]{eventId, player.getLocation().clone()}); + Object countObj = def.getCustomSettings().get("checkpoint_count"); + int nextIdx = countObj instanceof Number n ? n.intValue() : 0; + player.sendMessage(plugin.prefix() + "§aCheckpoint-Pos1 §8(#" + (nextIdx + 1) + ")§a gesetzt: §f" + formatLoc(player)); + player.sendMessage(plugin.prefix() + "§7Jetzt Pos2: §f/event elytra checkpoint confirm " + eventId); + } + + public void handleCheckpointPos2(Player player, String eventId) { + EventDefinition def = resolve(player, eventId); if (def == null) return; + Object[] sel = checkpointPos1Selection.get(player.getUniqueId()); + if (sel == null || !sel[0].equals(eventId)) { + player.sendMessage(plugin.prefix() + "§cZuerst Pos1 setzen: §f/event elytra checkpoint add " + eventId); return; } - EventRegion r = def.getRegion(); - player.sendMessage("§8§m══════════════════════════════"); - player.sendMessage("§6 Region: §e" + def.getDisplayName()); - player.sendMessage("§7 Welt: §f" + r.getWorldName()); - player.sendMessage("§7 Von: §f" + r.getMinX() + ", " + r.getMinY() + ", " + r.getMinZ()); - player.sendMessage("§7 Bis: §f" + r.getMaxX() + ", " + r.getMaxY() + ", " + r.getMaxZ()); - player.sendMessage("§7 Größe: §f" + r.getSizeX() + "×" + r.getSizeY() + "×" + r.getSizeZ() + " Blöcke"); - player.sendMessage("§8§m══════════════════════════════"); + org.bukkit.Location pos1 = (org.bukkit.Location) sel[1]; + org.bukkit.Location pos2 = player.getLocation().clone(); + if (!pos1.getWorld().equals(pos2.getWorld())) { + player.sendMessage(plugin.prefix() + "§cBeide Positionen müssen in der gleichen Welt sein!"); return; + } + EventRegion region = new EventRegion(pos1, pos2); + ElytraRaceHandler.addCheckpoint(def, region); + if (def.isCustom()) plugin.getConfigManager().saveCustomEvent(def); + checkpointPos1Selection.remove(player.getUniqueId()); + + Object countObj = def.getCustomSettings().get("checkpoint_count"); + int total = countObj instanceof Number n ? n.intValue() : 0; + player.sendMessage(plugin.prefix() + "§a✔ Checkpoint §e" + total + " §agespeichert für §f" + def.getDisplayName()); + sendRegionInfo(player, region); + player.sendMessage(plugin.prefix() + "§7Weiterer Checkpoint: §f/event elytra checkpoint add " + eventId); + } + + // ─── Hilfsmethoden ─────────────────────────────────────────────────── + + private EventDefinition resolve(Player player, String eventId) { + EventDefinition def = plugin.getEventRegistry().get(eventId).orElse(null); + if (def == null) player.sendMessage(plugin.prefix() + "§cEvent nicht gefunden: §f" + eventId); + return def; } private String formatLoc(Player p) { @@ -224,4 +227,4 @@ public class RegionSetupManager { player.sendMessage("§7 Bis: §f" + region.getMaxX() + ", " + region.getMaxY() + ", " + region.getMaxZ()); player.sendMessage("§7 Größe: §f" + region.getSizeX() + "x" + region.getSizeY() + "x" + region.getSizeZ()); } -} +} \ No newline at end of file diff --git a/src/main/java/dev/viper/eventengine/config/ConfigManager.java b/src/main/java/dev/viper/eventengine/config/ConfigManager.java index 1c3a469..1474283 100644 --- a/src/main/java/dev/viper/eventengine/config/ConfigManager.java +++ b/src/main/java/dev/viper/eventengine/config/ConfigManager.java @@ -38,27 +38,23 @@ public class ConfigManager { private File customEventsFile; private FileConfiguration customEventsConfig; - // Geladene Zeitplan-Einträge private final List schedule = new ArrayList<>(); - // Intervall-Fallback (Minuten), wenn kein Zeitplan aktiv private int intervalMinutes; private boolean useInterval; private boolean randomOnInterval; private String defaultEventId; - - // Ankündigungs-Vorlauf in Sekunden private int announceBefore; - // Ob Events im Chat-Log erscheinen private boolean logEvents; - // Prefix für alle Nachrichten private String prefix; - // Schutz-Einstellungen private boolean enforceRegionBoundary; private boolean noExplosionBlockDamage; private boolean restrictBlockInteraction; private List itemRainItems; + /** Sekunden für den 3-2-1-Go Countdown vor jedem Event-Start (0 = deaktiviert) */ + private int countdownSeconds; + public ConfigManager(EventEngine plugin) { this.plugin = plugin; this.log = plugin.getLogger(); @@ -83,6 +79,7 @@ public class ConfigManager { defaultEventId = mainConfig.getString("settings.default-event", "RANDOM"); announceBefore = mainConfig.getInt("settings.announce-before-seconds", 30); logEvents = mainConfig.getBoolean("settings.log-events", true); + countdownSeconds = mainConfig.getInt("settings.countdown-seconds", 3); enforceRegionBoundary = mainConfig.getBoolean("protection.enforce-region-boundary", true); noExplosionBlockDamage = mainConfig.getBoolean("protection.no-explosion-block-damage", true); restrictBlockInteraction = mainConfig.getBoolean("protection.restrict-block-interaction", true); @@ -192,6 +189,9 @@ public class ConfigManager { public FileConfiguration getMainConfig() { return mainConfig; } public List getItemRainItems() { return Collections.unmodifiableList(itemRainItems); } + /** Countdown-Sekunden vor dem eigentlichen Event-Start (0 = kein Countdown) */ + public int getCountdownSeconds() { return countdownSeconds; } + public void setItemRainItems(List items) { itemRainItems = new ArrayList<>(items); if (itemRainItems.isEmpty()) itemRainItems.addAll(DEFAULT_ITEM_RAIN_ITEMS); @@ -203,4 +203,4 @@ public class ConfigManager { public void resetItemRainItems() { setItemRainItems(DEFAULT_ITEM_RAIN_ITEMS); } -} +} \ No newline at end of file diff --git a/src/main/java/dev/viper/eventengine/events/builtin/ElytraRaceHandler.java b/src/main/java/dev/viper/eventengine/events/builtin/ElytraRaceHandler.java new file mode 100644 index 0000000..5755bd3 --- /dev/null +++ b/src/main/java/dev/viper/eventengine/events/builtin/ElytraRaceHandler.java @@ -0,0 +1,357 @@ +package dev.viper.eventengine.events.builtin; + +import dev.viper.eventengine.EventEngine; +import dev.viper.eventengine.model.ActiveEvent; +import dev.viper.eventengine.model.EventDefinition; +import dev.viper.eventengine.model.EventRegion; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.*; + +/** + * Elytra-Rennen Handler mit Checkpoint-System. + * + * Setup: + * - Start-Region: /event race start pos1/pos2 + * - Ziel-Region: /event race goal pos1/pos2 + * - Checkpoints: in config.yml / custom_events.yml unter settings.checkpoints: + * settings: + * checkpoints: + * - world: "world" + * min-x: 100 min-y: 60 min-z: 100 + * max-x: 110 max-y: 70 max-z: 110 + * - world: "world" + * ... + * + * Ablauf: + * 1. Spieler erhalten Elytra + Raketen. + * 2. Checkpoints müssen der Reihe nach durchflogen werden. + * 3. Erst nach dem letzten Checkpoint gilt das Ziel. + * 4. Verpasste Checkpoints → Spieler wird mit Nachricht erinnert. + * 5. Im CIRCUIT-Modus: Runden werden gezählt (alle Checkpoints + Ziel = 1 Runde). + */ +public class ElytraRaceHandler implements IEventHandler, Listener { + + private final EventEngine plugin; + private ActiveEvent currentEvent; + + /** Checkpoint-Regionen in der Reihenfolge die geflogen werden muss */ + private final List checkpoints = new ArrayList<>(); + + /** + * Aktueller Checkpoint-Index pro Spieler. + * 0 = muss Checkpoint 0 als nächstes passieren. + * checkpoints.size() = alle Checkpoints durch → Ziel gilt. + */ + private final Map playerCheckpoint = new HashMap<>(); + + /** Runden-Zähler (CIRCUIT-Modus) */ + private final Map lapCount = new HashMap<>(); + + /** Abschlussposition (CIRCUIT) */ + private int finishPosition = 0; + + /** Cooldown gegen Mehrfach-Trigger (ms) */ + private final Map lastTrigger = new HashMap<>(); + private static final long TRIGGER_COOLDOWN_MS = 1500; + + private final Random rng = new Random(); + + public ElytraRaceHandler(EventEngine plugin) { + this.plugin = plugin; + } + + @Override + public void onStart(ActiveEvent event) { + this.currentEvent = event; + this.finishPosition = 0; + playerCheckpoint.clear(); + lapCount.clear(); + lastTrigger.clear(); + checkpoints.clear(); + + loadCheckpoints(event.getDefinition()); + + Bukkit.getPluginManager().registerEvents(this, plugin); + + // Elytra + Raketen verteilen + for (UUID uuid : event.getParticipants()) { + Player p = Bukkit.getPlayer(uuid); + if (p == null) continue; + equipPlayer(p, event.getDefinition().getDurationSeconds()); + playerCheckpoint.put(uuid, 0); + lapCount.put(uuid, 0); + } + + // Start-Teleport + teleportToStart(event); + + // Ankündigung mit Checkpoint-Info + String cpInfo = checkpoints.isEmpty() + ? "§c⚠ Keine Checkpoints konfiguriert — nur Start/Ziel aktiv." + : "§7" + checkpoints.size() + " Checkpoint" + (checkpoints.size() == 1 ? "" : "s") + " auf der Strecke."; + + boolean isCircuit = event.getDefinition().isCircuit(); + String modeInfo = isCircuit + ? "§e" + event.getDefinition().getLaps() + " Runden §7im Rundkurs-Modus." + : "§7Sprint von Start zu Ziel."; + + Bukkit.broadcastMessage(plugin.prefix() + "§b🪂 §eElytra-Rennen! " + modeInfo); + Bukkit.broadcastMessage(plugin.prefix() + cpInfo); + + if (!event.getDefinition().hasStartRegion()) + Bukkit.broadcastMessage(plugin.prefix() + "§c⚠ Kein Startbereich gesetzt. /event race start pos1/pos2 " + event.getDefinition().getId()); + if (!event.getDefinition().hasGoalRegion()) + Bukkit.broadcastMessage(plugin.prefix() + "§c⚠ Kein Zielbereich gesetzt. /event race goal pos1/pos2 " + event.getDefinition().getId()); + } + + @Override + public void onEnd(ActiveEvent event) { + HandlerList.unregisterAll(this); + playerCheckpoint.clear(); + lapCount.clear(); + lastTrigger.clear(); + this.currentEvent = null; + } + + @Override + public void onPlayerJoin(ActiveEvent event, Player player) { + equipPlayer(player, event.getDefinition().getDurationSeconds()); + playerCheckpoint.put(player.getUniqueId(), 0); + lapCount.put(player.getUniqueId(), 0); + } + + @Override + public void onPlayerLeave(ActiveEvent event, Player player) { + playerCheckpoint.remove(player.getUniqueId()); + lapCount.remove(player.getUniqueId()); + lastTrigger.remove(player.getUniqueId()); + } + + // ─── Bewegungs-Listener ─────────────────────────────────────────────── + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onMove(PlayerMoveEvent e) { + if (currentEvent == null || currentEvent.getState() != ActiveEvent.State.RUNNING) return; + Player player = e.getPlayer(); + if (!currentEvent.isParticipant(player)) return; + + Location to = e.getTo(); + if (to == null) return; + // Nur bei echtem Positions-Wechsel + if (e.getFrom().getBlockX() == to.getBlockX() + && e.getFrom().getBlockY() == to.getBlockY() + && e.getFrom().getBlockZ() == to.getBlockZ()) return; + + UUID uuid = player.getUniqueId(); + long now = System.currentTimeMillis(); + Long last = lastTrigger.get(uuid); + if (last != null && now - last < TRIGGER_COOLDOWN_MS) return; + + EventDefinition def = currentEvent.getDefinition(); + int cpIndex = playerCheckpoint.getOrDefault(uuid, 0); + + // Checkpoint-Check (in Reihenfolge) + if (cpIndex < checkpoints.size()) { + EventRegion cp = checkpoints.get(cpIndex); + if (cp.contains(to)) { + lastTrigger.put(uuid, now); + playerCheckpoint.put(uuid, cpIndex + 1); + int nextCp = cpIndex + 1; + String msg = nextCp < checkpoints.size() + ? "§a✔ Checkpoint §f" + nextCp + "§a/§f" + checkpoints.size() + " §apAssiert!" + : "§a✔ Letzter Checkpoint! §eFliege zum Ziel!"; + player.sendActionBar(msg); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1.8f); + } + return; // Ziel gilt erst nach allen Checkpoints + } + + // Ziel-Check (alle Checkpoints durch) + EventRegion goal = def.getGoalRegion(); + if (goal == null || !goal.contains(to)) return; + + lastTrigger.put(uuid, now); + + if (def.isCircuit()) { + handleCircuitGoal(player, def, now); + } else { + handleSprintGoal(player); + } + } + + // ─── Sprint: erster gewinnt ──────────────────────────────────────────── + + private void handleSprintGoal(Player player) { + if (currentEvent.hasWinner()) return; + plugin.getEventManager().endEventWithWinner(player, "Ziel erreicht"); + } + + // ─── Circuit: Runden zählen ─────────────────────────────────────────── + + private void handleCircuitGoal(Player player, EventDefinition def, long now) { + UUID uuid = player.getUniqueId(); + int done = lapCount.merge(uuid, 1, Integer::sum); + int total = def.getLaps(); + long elapsed = currentEvent.getElapsedSeconds(); + + // Checkpoint-Zähler für nächste Runde zurücksetzen + playerCheckpoint.put(uuid, 0); + + if (done < total) { + String msg = "§e🏁 Runde §f" + done + "§e/§f" + total + + " §8(§f" + formatTime((int) elapsed) + "§8)"; + player.sendActionBar(msg); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1.5f); + Bukkit.broadcastMessage(plugin.prefix() + "§f" + player.getName() + + " §7— Runde §e" + done + "§7/§e" + total + + " §8(§f" + formatTime((int) elapsed) + "§8)"); + } else { + finishPosition++; + int pos = finishPosition; + currentEvent.addScore(uuid, Math.max(0, 100 - (pos - 1) * 20)); + + String posStr = switch (pos) { + case 1 -> "§6§l1. Platz 🥇"; + case 2 -> "§7§l2. Platz 🥈"; + case 3 -> "§c§l3. Platz 🥉"; + default -> "§7" + pos + ". Platz"; + }; + + player.sendTitle(posStr, "§fZeit: §e" + formatTime((int) elapsed), 0, 60, 20); + player.playSound(player.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, pos == 1 ? 1.2f : 0.8f); + Bukkit.broadcastMessage(plugin.prefix() + "§b🪂 §f" + player.getName() + + " §ehat das Rennen beendet! §8[" + posStr + "§8] §8(§f" + + formatTime((int) elapsed) + "§8)"); + + currentEvent.removeParticipant(player); + + if (pos == 1) { + currentEvent.setWinner(uuid); + plugin.getEventManager().distributeWinnerRewards(def, player); + } + if (currentEvent.getParticipantCount() == 0) { + plugin.getEventManager().endEvent("Rennen beendet — alle Spieler im Ziel"); + } + } + } + + // ─── Hilfsmethoden ──────────────────────────────────────────────────── + + private void equipPlayer(Player p, int durationSec) { + p.getEquipment().setChestplate(new ItemStack(Material.ELYTRA)); + // Raketen für die gesamte Event-Dauer (mind. 64) + int rockets = Math.max(64, (durationSec / 5)); + p.getInventory().addItem(new ItemStack(Material.FIREWORK_ROCKET, Math.min(rockets, 64))); + } + + private void teleportToStart(ActiveEvent event) { + EventRegion start = event.getDefinition().getStartRegion(); + if (start == null) return; + for (UUID uuid : event.getParticipants()) { + Player p = Bukkit.getPlayer(uuid); + if (p == null) continue; + Location loc = start.randomPoint(rng); + if (loc != null) p.teleport(loc); + } + } + + /** + * Lädt Checkpoint-Regionen aus den Custom-Settings der EventDefinition. + * + * Erwartet in custom_events.yml oder event-overrides unter settings: + * settings: + * checkpoint_count: 2 + * checkpoint_0_world: "world" + * checkpoint_0_min-x: 100 ... checkpoint_0_max-z: 110 + * checkpoint_1_world: "world" + * ... + * + * Alternativ Direktformat mit Liste wenn durch den Builder gesetzt. + */ + private void loadCheckpoints(EventDefinition def) { + Map settings = def.getCustomSettings(); + if (settings == null || settings.isEmpty()) return; + + Object countObj = settings.get("checkpoint_count"); + if (!(countObj instanceof Number)) return; + int count = ((Number) countObj).intValue(); + + for (int i = 0; i < count; i++) { + String prefix = "checkpoint_" + i + "_"; + try { + String world = (String) settings.get(prefix + "world"); + int minX = toInt(settings.get(prefix + "min-x")); + int minY = toInt(settings.get(prefix + "min-y")); + int minZ = toInt(settings.get(prefix + "min-z")); + int maxX = toInt(settings.get(prefix + "max-x")); + int maxY = toInt(settings.get(prefix + "max-y")); + int maxZ = toInt(settings.get(prefix + "max-z")); + checkpoints.add(new EventRegion(world, minX, minY, minZ, maxX, maxY, maxZ)); + plugin.getLogger().info("ElytraRace: Checkpoint " + i + " geladen (" + world + ")"); + } catch (Exception ex) { + plugin.getLogger().warning("ElytraRace: Checkpoint " + i + " fehlerhaft — " + ex.getMessage()); + } + } + plugin.getLogger().info("ElytraRace: " + checkpoints.size() + " Checkpoint(s) geladen."); + } + + private int toInt(Object o) { + if (o instanceof Number n) return n.intValue(); + return Integer.parseInt(String.valueOf(o)); + } + + private String formatTime(int seconds) { + int m = seconds / 60, s = seconds % 60; + if (m == 0) return s + "s"; + return m + "m " + s + "s"; + } + + // ─── Checkpoint-Setup per Command-Hilfsklasse ────────────────────────── + + /** + * Speichert einen neuen Checkpoint in den Custom-Settings der EventDefinition. + * Aufruf z.B. aus EventCommand: /event elytra checkpoint add pos1/pos2 + * + * @param def Die EventDefinition des Elytra-Rennens + * @param region Die fertige Checkpoint-Region + */ + public static void addCheckpoint(EventDefinition def, EventRegion region) { + Map s = def.getCustomSettings(); + Object countObj = s.get("checkpoint_count"); + int idx = countObj instanceof Number n ? n.intValue() : 0; + + String pf = "checkpoint_" + idx + "_"; + s.put(pf + "world", region.getWorldName()); + s.put(pf + "min-x", region.getMinX()); s.put(pf + "min-y", region.getMinY()); s.put(pf + "min-z", region.getMinZ()); + s.put(pf + "max-x", region.getMaxX()); s.put(pf + "max-y", region.getMaxY()); s.put(pf + "max-z", region.getMaxZ()); + s.put("checkpoint_count", idx + 1); + } + + /** + * Entfernt alle Checkpoints aus den Custom-Settings. + */ + public static void clearCheckpoints(EventDefinition def) { + Map s = def.getCustomSettings(); + Object countObj = s.get("checkpoint_count"); + int count = countObj instanceof Number n ? n.intValue() : 0; + for (int i = 0; i < count; i++) { + String pf = "checkpoint_" + i + "_"; + s.remove(pf + "world"); + s.remove(pf + "min-x"); s.remove(pf + "min-y"); s.remove(pf + "min-z"); + s.remove(pf + "max-x"); s.remove(pf + "max-y"); s.remove(pf + "max-z"); + } + s.remove("checkpoint_count"); + } +} \ No newline at end of file diff --git a/src/main/java/dev/viper/eventengine/events/builtin/MiscHandlers.java b/src/main/java/dev/viper/eventengine/events/builtin/MiscHandlers.java index 0244414..a120c7b 100644 --- a/src/main/java/dev/viper/eventengine/events/builtin/MiscHandlers.java +++ b/src/main/java/dev/viper/eventengine/events/builtin/MiscHandlers.java @@ -17,13 +17,16 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.GameMode; + import java.util.*; // ══════════════════════════════════════════════════════════ -// RENNEN — alle 10 +// RENNEN — Basis + alle außer ElytraRaceHandler +// ElytraRaceHandler ist in ElytraRaceHandler.java (eigene Datei) // ══════════════════════════════════════════════════════════ -/** Basis für alle Renn-Events: Punkte pro Sekunde, wer am Ende mehr hat gewinnt */ +/** Basis für alle Renn-Events */ class BaseRaceHandler implements IEventHandler { protected final EventEngine plugin; protected final String icon; @@ -67,14 +70,7 @@ class ParkourRaceHandler extends BaseRaceHandler { ParkourRaceHandler(EventEngine p){super(p,"🏃","Parkour Race");} @Override public void onStart(ActiveEvent e){ super.onStart(e); Bukkit.broadcastMessage(plugin.prefix()+"§a🏃 §eParkour Race! §7Lauft durch den Kurs — wer zuerst am Ziel ist, gewinnt!"); } } -class ElytraRaceHandler extends BaseRaceHandler { - ElytraRaceHandler(EventEngine p){super(p,"🪂","Elytra-Rennen");} - @Override public void onStart(ActiveEvent e){ - super.onStart(e); - for(UUID u:e.getParticipants()){Player p=Bukkit.getPlayer(u);if(p==null)continue;p.getEquipment().setChestplate(new ItemStack(Material.ELYTRA));p.getInventory().addItem(new ItemStack(Material.FIREWORK_ROCKET,64));} - Bukkit.broadcastMessage(plugin.prefix()+"§b🪂 §eElytra-Rennen! §7Fliegt mit Elytren zum Ziel!"); - } -} +// ElytraRaceHandler → eigene Datei: ElytraRaceHandler.java class BoatRaceHandler extends BaseRaceHandler { BoatRaceHandler(EventEngine p){super(p,"⛵","Boot-Rennen");} @Override public void onStart(ActiveEvent e){ super.onStart(e); for(UUID u:e.getParticipants()){Player p=Bukkit.getPlayer(u);if(p==null)continue;p.getInventory().addItem(new ItemStack(Material.OAK_BOAT));} Bukkit.broadcastMessage(plugin.prefix()+"§b⛵ §eBoot-Rennen! §7Klettert in euer Boot und rast zum Ziel!"); } @@ -333,7 +329,7 @@ class CapturePointsHandler implements IEventHandler { CapturePointsHandler(EventEngine p){this.plugin=p;} @Override public void onStart(ActiveEvent e){ Bukkit.broadcastMessage(plugin.prefix()+"§e🚩 §ePunkte-Capture! §7Steht auf den markierten Punkten um Punkte zu sammeln. Mehr Spieler auf einem Punkt = schnellere Einnahme!"); - ticker=new BukkitRunnable(){ @Override public void run(){ if(e.getState()==ActiveEvent.State.ENDED){cancel();return;} /* Punkt-Check hier — benötigt vorbereitete Punkte-Positionen in den Settings */ for(UUID u:e.getParticipants()){Player p=Bukkit.getPlayer(u);if(p==null)continue;/* Placeholder: jeder Spieler auf einem Kontrollpunkt bekommt Punkte */} }}; + ticker=new BukkitRunnable(){ @Override public void run(){ if(e.getState()==ActiveEvent.State.ENDED){cancel();return;} }}; ticker.runTaskTimer(plugin,20L,20L); } @Override public void onEnd(ActiveEvent e){ if(ticker!=null)ticker.cancel(); } @@ -374,15 +370,12 @@ class ColorWarHandler implements IEventHandler, Listener { } @Override public void onEnd(ActiveEvent e){ HandlerList.unregisterAll(this); - // Zähle Blöcke pro Farbe in der Region if(e.getDefinition().hasRegion()){ org.bukkit.World w=e.getDefinition().getRegion().getWorld(); if(w!=null){Mapcounts=new EnumMap<>(Material.class);dev.viper.eventengine.model.EventRegion r=e.getDefinition().getRegion();for(int x=r.getMinX();x<=r.getMaxX();x++)for(int z=r.getMinZ();z<=r.getMaxZ();z++){Material t=w.getBlockAt(x,r.getMinY(),z).getType();if(Arrays.asList(COLORS).contains(t))counts.merge(t,1,Integer::sum);}Bukkit.broadcastMessage(plugin.prefix()+"§e🎨 §7Farben-Ergebnis:");counts.forEach((m,cnt)->Bukkit.broadcastMessage(" §7"+m.name()+": §e"+cnt+" §7Blöcke"));} } current=null;teamColors.clear(); } - // Damit ist der Import nötig - private static final Material[] COLORS_REF = COLORS; } // ══════════════════════════════════════════════════════════ @@ -395,11 +388,8 @@ class TinyPlayersHandler implements IEventHandler { TinyPlayersHandler(EventEngine pp){this.p=pp;} @Override public void onStart(ActiveEvent e){ List players = new ArrayList<>(); - if (e.getParticipants().isEmpty()) { - players.addAll(Bukkit.getOnlinePlayers()); - } else { - for(UUID u:e.getParticipants()){Player pl=Bukkit.getPlayer(u);if(pl!=null)players.add(pl);} - } + if (e.getParticipants().isEmpty()) { players.addAll(Bukkit.getOnlinePlayers()); } + else { for(UUID u:e.getParticipants()){Player pl=Bukkit.getPlayer(u);if(pl!=null)players.add(pl);} } for (Player pl : players) { applyScale(pl, 0.6); pl.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS,e.getDefinition().getDurationSeconds()*20,0,false,false)); @@ -408,138 +398,53 @@ class TinyPlayersHandler implements IEventHandler { Bukkit.broadcastMessage(p.prefix()+"§e🐭 §eMini-Spieler! §7Alle sind jetzt winzig klein!"); } @Override public void onEnd(ActiveEvent e){ - if (e.getParticipants().isEmpty()) { - for (Player pl : Bukkit.getOnlinePlayers()) { - restoreScale(pl); - pl.removePotionEffect(PotionEffectType.SLOWNESS); - pl.removePotionEffect(PotionEffectType.JUMP_BOOST); - } - } else { - for(UUID u:e.getParticipants()){ - Player pl=Bukkit.getPlayer(u); - if(pl!=null){ - restoreScale(pl); - pl.removePotionEffect(PotionEffectType.SLOWNESS); - pl.removePotionEffect(PotionEffectType.JUMP_BOOST); - } - } - } + Iterable targets = e.getParticipants().isEmpty() ? uuidsOf(Bukkit.getOnlinePlayers()) : e.getParticipants(); + for(UUID u:targets){Player pl=Bukkit.getPlayer(u);if(pl!=null){restoreScale(pl);pl.removePotionEffect(PotionEffectType.SLOWNESS);pl.removePotionEffect(PotionEffectType.JUMP_BOOST);}} prevScale.clear(); } - - private void applyScale(Player pl, double scale) { - org.bukkit.attribute.Attribute attrType = getScaleAttribute(); - if (attrType == null) return; - org.bukkit.attribute.AttributeInstance attr = pl.getAttribute(attrType); - if (attr == null) return; - prevScale.putIfAbsent(pl.getUniqueId(), attr.getBaseValue()); - attr.setBaseValue(scale); - } - - private void restoreScale(Player pl) { - org.bukkit.attribute.Attribute attrType = getScaleAttribute(); - if (attrType == null) return; - org.bukkit.attribute.AttributeInstance attr = pl.getAttribute(attrType); - if (attr == null) return; - Double prev = prevScale.remove(pl.getUniqueId()); - if (prev != null) attr.setBaseValue(prev); - } - - private org.bukkit.attribute.Attribute getScaleAttribute() { - try { - return org.bukkit.attribute.Attribute.valueOf("GENERIC_SCALE"); - } catch (IllegalArgumentException ex) { - return null; - } - } + private void applyScale(Player pl, double scale){org.bukkit.attribute.Attribute a=scaleAttr();if(a==null)return;org.bukkit.attribute.AttributeInstance ai=pl.getAttribute(a);if(ai==null)return;prevScale.putIfAbsent(pl.getUniqueId(),ai.getBaseValue());ai.setBaseValue(scale);} + private void restoreScale(Player pl){org.bukkit.attribute.Attribute a=scaleAttr();if(a==null)return;org.bukkit.attribute.AttributeInstance ai=pl.getAttribute(a);if(ai==null)return;Double prev=prevScale.remove(pl.getUniqueId());if(prev!=null)ai.setBaseValue(prev);} + private org.bukkit.attribute.Attribute scaleAttr(){try{return org.bukkit.attribute.Attribute.valueOf("GENERIC_SCALE");}catch(IllegalArgumentException ex){return null;}} + private List uuidsOf(Collection ps){List r=new ArrayList<>();ps.forEach(p->r.add(p.getUniqueId()));return r;} } + class GiantPlayersHandler implements IEventHandler { private final EventEngine p; private final Map prevScale = new HashMap<>(); GiantPlayersHandler(EventEngine pp){this.p=pp;} @Override public void onStart(ActiveEvent e){ List players = new ArrayList<>(); - if (e.getParticipants().isEmpty()) { - players.addAll(Bukkit.getOnlinePlayers()); - } else { - for(UUID u:e.getParticipants()){Player pl=Bukkit.getPlayer(u);if(pl!=null)players.add(pl);} - } - for(Player pl:players){ - applyScale(pl, 1.6); - pl.addPotionEffect(new PotionEffect(PotionEffectType.SPEED,e.getDefinition().getDurationSeconds()*20,1,false,false)); - pl.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH,e.getDefinition().getDurationSeconds()*20,1,false,false)); - } + if (e.getParticipants().isEmpty()) { players.addAll(Bukkit.getOnlinePlayers()); } + else { for(UUID u:e.getParticipants()){Player pl=Bukkit.getPlayer(u);if(pl!=null)players.add(pl);} } + for(Player pl:players){applyScale(pl,1.6);pl.addPotionEffect(new PotionEffect(PotionEffectType.SPEED,e.getDefinition().getDurationSeconds()*20,1,false,false));pl.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH,e.getDefinition().getDurationSeconds()*20,1,false,false));} Bukkit.broadcastMessage(p.prefix()+"§6🦣 §eRiesen-Spieler! §7Alle sind jetzt riesig! Stärke und Geschwindigkeit erhöht!"); } @Override public void onEnd(ActiveEvent e){ - if (e.getParticipants().isEmpty()) { - for (Player pl : Bukkit.getOnlinePlayers()) { - restoreScale(pl); - pl.removePotionEffect(PotionEffectType.SPEED); - pl.removePotionEffect(PotionEffectType.STRENGTH); - } - } else { - for(UUID u:e.getParticipants()){ - Player pl=Bukkit.getPlayer(u); - if(pl!=null){ - restoreScale(pl); - pl.removePotionEffect(PotionEffectType.SPEED); - pl.removePotionEffect(PotionEffectType.STRENGTH); - } - } - } + Iterable targets = e.getParticipants().isEmpty() ? uuidsOf(Bukkit.getOnlinePlayers()) : e.getParticipants(); + for(UUID u:targets){Player pl=Bukkit.getPlayer(u);if(pl!=null){restoreScale(pl);pl.removePotionEffect(PotionEffectType.SPEED);pl.removePotionEffect(PotionEffectType.STRENGTH);}} prevScale.clear(); } - - private void applyScale(Player pl, double scale) { - org.bukkit.attribute.Attribute attrType = getScaleAttribute(); - if (attrType == null) return; - org.bukkit.attribute.AttributeInstance attr = pl.getAttribute(attrType); - if (attr == null) return; - prevScale.putIfAbsent(pl.getUniqueId(), attr.getBaseValue()); - attr.setBaseValue(scale); - } - - private void restoreScale(Player pl) { - org.bukkit.attribute.Attribute attrType = getScaleAttribute(); - if (attrType == null) return; - org.bukkit.attribute.AttributeInstance attr = pl.getAttribute(attrType); - if (attr == null) return; - Double prev = prevScale.remove(pl.getUniqueId()); - if (prev != null) attr.setBaseValue(prev); - } - - private org.bukkit.attribute.Attribute getScaleAttribute() { - try { - return org.bukkit.attribute.Attribute.valueOf("GENERIC_SCALE"); - } catch (IllegalArgumentException ex) { - return null; - } - } + private void applyScale(Player pl, double scale){org.bukkit.attribute.Attribute a=scaleAttr();if(a==null)return;org.bukkit.attribute.AttributeInstance ai=pl.getAttribute(a);if(ai==null)return;prevScale.putIfAbsent(pl.getUniqueId(),ai.getBaseValue());ai.setBaseValue(scale);} + private void restoreScale(Player pl){org.bukkit.attribute.Attribute a=scaleAttr();if(a==null)return;org.bukkit.attribute.AttributeInstance ai=pl.getAttribute(a);if(ai==null)return;Double prev=prevScale.remove(pl.getUniqueId());if(prev!=null)ai.setBaseValue(prev);} + private org.bukkit.attribute.Attribute scaleAttr(){try{return org.bukkit.attribute.Attribute.valueOf("GENERIC_SCALE");}catch(IllegalArgumentException ex){return null;}} + private List uuidsOf(Collection ps){List r=new ArrayList<>();ps.forEach(p->r.add(p.getUniqueId()));return r;} } + class InvisiblePlayersHandler implements IEventHandler { private final EventEngine p; InvisiblePlayersHandler(EventEngine pp){this.p=pp;} @Override public void onStart(ActiveEvent e){ - if (e.getParticipants().isEmpty()) { - for (Player pl : Bukkit.getOnlinePlayers()) { - pl.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY,e.getDefinition().getDurationSeconds()*20,0,false,false)); - } - } else { - for(UUID u:e.getParticipants()){Player pl=Bukkit.getPlayer(u);if(pl==null)continue;pl.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY,e.getDefinition().getDurationSeconds()*20,0,false,false));} - } + Iterable targets = e.getParticipants().isEmpty() ? uuidsOf(Bukkit.getOnlinePlayers()) : e.getParticipants(); + for(UUID u:targets){Player pl=Bukkit.getPlayer(u);if(pl==null)continue;pl.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY,e.getDefinition().getDurationSeconds()*20,0,false,false));} Bukkit.broadcastMessage(p.prefix()+"§7👻 §eUnsichtbarkeit! §7Alle Spieler sind unsichtbar!"); } @Override public void onEnd(ActiveEvent e){ - if (e.getParticipants().isEmpty()) { - for (Player pl : Bukkit.getOnlinePlayers()) { - pl.removePotionEffect(PotionEffectType.INVISIBILITY); - } - } else { - for(UUID u:e.getParticipants()){Player pl=Bukkit.getPlayer(u);if(pl!=null)pl.removePotionEffect(PotionEffectType.INVISIBILITY);} - } + Iterable targets = e.getParticipants().isEmpty() ? uuidsOf(Bukkit.getOnlinePlayers()) : e.getParticipants(); + for(UUID u:targets){Player pl=Bukkit.getPlayer(u);if(pl!=null)pl.removePotionEffect(PotionEffectType.INVISIBILITY);} } + private List uuidsOf(Collection ps){List r=new ArrayList<>();ps.forEach(p->r.add(p.getUniqueId()));return r;} } + class RandomTeleportHandler implements IEventHandler { private final EventEngine plugin; private BukkitRunnable tpRunner; RandomTeleportHandler(EventEngine p){this.plugin=p;} @@ -548,40 +453,22 @@ class RandomTeleportHandler implements IEventHandler { final Random rng=new Random(); tpRunner=new BukkitRunnable(){@Override public void run(){ if(e.getState()==ActiveEvent.State.ENDED){cancel();return;} - if (e.getParticipants().isEmpty()) { - for (Player p : Bukkit.getOnlinePlayers()) { - if (rng.nextInt(3) != 0) continue; - Location loc=e.getDefinition().hasRegion()?e.getDefinition().getRegion().randomLocation(rng):p.getLocation().add((rng.nextDouble()-0.5)*30,0,(rng.nextDouble()-0.5)*30); - if(loc!=null)p.teleport(loc); - p.sendActionBar("§d⚡ ZUFALLS-TP!"); - } - return; - } - for(UUID u:e.getParticipants()){ - Player p=Bukkit.getPlayer(u); - if(p==null||rng.nextInt(3)!=0)continue; - Location loc=e.getDefinition().hasRegion()?e.getDefinition().getRegion().randomLocation(rng):p.getLocation().add((rng.nextDouble()-0.5)*30,0,(rng.nextDouble()-0.5)*30); - if(loc!=null)p.teleport(loc); - p.sendActionBar("§d⚡ ZUFALLS-TP!"); - } - }}; + Iterable targets = e.getParticipants().isEmpty() ? uuidsOf(Bukkit.getOnlinePlayers()) : e.getParticipants(); + for(UUID u:targets){Player p=Bukkit.getPlayer(u);if(p==null||rng.nextInt(3)!=0)continue;Location loc=e.getDefinition().hasRegion()?e.getDefinition().getRegion().randomLocation(rng):p.getLocation().add((rng.nextDouble()-0.5)*30,0,(rng.nextDouble()-0.5)*30);if(loc!=null)p.teleport(loc);p.sendActionBar("§d⚡ ZUFALLS-TP!");} + }}; tpRunner.runTaskTimer(plugin,20L,100L); } @Override public void onEnd(ActiveEvent e){ if(tpRunner!=null)tpRunner.cancel(); } + private List uuidsOf(Collection ps){List r=new ArrayList<>();ps.forEach(p->r.add(p.getUniqueId()));return r;} } + class SwapInventoriesHandler implements IEventHandler { private final EventEngine p; SwapInventoriesHandler(EventEngine pp){this.p=pp;} @Override public void onStart(ActiveEvent e){ - List players = new ArrayList<>(); - if (e.getParticipants().isEmpty()) { - players.addAll(Bukkit.getOnlinePlayers()); - } else { - for (UUID u : e.getParticipants()) { - Player pl = Bukkit.getPlayer(u); - if (pl != null) players.add(pl); - } - } + List players=new ArrayList<>(); + if(e.getParticipants().isEmpty()){players.addAll(Bukkit.getOnlinePlayers());} + else{for(UUID u:e.getParticipants()){Player pl=Bukkit.getPlayer(u);if(pl!=null)players.add(pl);}} if(players.size()<2){Bukkit.broadcastMessage(p.prefix()+"§cMindestens 2 Spieler benötigt!");return;} Collections.shuffle(players); Listinventories=new ArrayList<>(); @@ -591,6 +478,7 @@ class SwapInventoriesHandler implements IEventHandler { } @Override public void onEnd(ActiveEvent e){} } + class ReverseGravityHandler implements IEventHandler { private final EventEngine plugin; private BukkitRunnable ticker; ReverseGravityHandler(EventEngine p){this.plugin=p;} @@ -599,22 +487,15 @@ class ReverseGravityHandler implements IEventHandler { final Random rng=new Random(); ticker=new BukkitRunnable(){@Override public void run(){ if(e.getState()==ActiveEvent.State.ENDED){cancel();return;} - if (e.getParticipants().isEmpty()) { - for (Player p : Bukkit.getOnlinePlayers()) { - p.setVelocity(p.getVelocity().setY(0.5 + rng.nextDouble() * 0.5)); - } - return; - } - for(UUID u:e.getParticipants()){ - Player p=Bukkit.getPlayer(u); - if(p==null)continue; - p.setVelocity(p.getVelocity().setY(0.5+rng.nextDouble()*0.5)); - } - }}; + Iterable targets = e.getParticipants().isEmpty() ? uuidsOf(Bukkit.getOnlinePlayers()) : e.getParticipants(); + for(UUID u:targets){Player p=Bukkit.getPlayer(u);if(p==null)continue;p.setVelocity(p.getVelocity().setY(0.5+rng.nextDouble()*0.5));} + }}; ticker.runTaskTimer(plugin,20L,15L); } @Override public void onEnd(ActiveEvent e){ if(ticker!=null)ticker.cancel(); } + private List uuidsOf(Collection ps){List r=new ArrayList<>();ps.forEach(p->r.add(p.getUniqueId()));return r;} } + class BouncyBlocksHandler implements IEventHandler, Listener { private final EventEngine plugin; private ActiveEvent current; BouncyBlocksHandler(EventEngine p){this.plugin=p;} diff --git a/src/main/java/dev/viper/eventengine/listener/RaceGoalListener.java b/src/main/java/dev/viper/eventengine/listener/RaceGoalListener.java index 420f01a..bb3152f 100644 --- a/src/main/java/dev/viper/eventengine/listener/RaceGoalListener.java +++ b/src/main/java/dev/viper/eventengine/listener/RaceGoalListener.java @@ -6,17 +6,53 @@ import dev.viper.eventengine.model.EventCategory; import dev.viper.eventengine.model.EventDefinition; import dev.viper.eventengine.model.EventRegion; import dev.viper.eventengine.model.EventType; +import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerMoveEvent; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Verarbeitet Ziel-Einläufe für Renn-Events. + * + * SPRINT-Modus (Standard): + * Wer als erstes die Zielregion betritt, gewinnt → endEventWithWinner(). + * + * CIRCUIT-Modus (Rundkurs): + * Jedes Mal wenn ein Spieler die Zielregion passiert, zählt eine Runde. + * Nach def.getLaps() Runden → Spieler gewinnt. + * Zwischenzeiten werden per ActionBar gemeldet. + * Alle anderen können weiterlaufen — Platz 2/3 werden ebenfalls ausgewertet. + */ public class RaceGoalListener implements Listener { private final EventEngine plugin; + /** + * Rundenstand pro Spieler im CIRCUIT-Modus. + * Key = UUID, Value = absolvierte Runden + */ + private final Map lapCount = new HashMap<>(); + + /** + * Verhindert Mehrfach-Trigger wenn der Spieler mehrere Ticks in der Zielregion bleibt. + * Key = UUID, Value = System.currentTimeMillis() des letzten Triggers. + */ + private final Map lastGoalTime = new HashMap<>(); + + /** Mindestabstand in ms zwischen zwei aufeinanderfolgenden Zieltriggern (3s) */ + private static final long GOAL_COOLDOWN_MS = 3000; + + /** Abschlusspositionen für das Leaderboard im CIRCUIT-Modus */ + private int finishPosition = 0; + public RaceGoalListener(EventEngine plugin) { this.plugin = plugin; } @@ -38,18 +74,94 @@ public class RaceGoalListener implements Listener { Location to = e.getTo(); if (to == null) return; + // Nur bei echtem Block-Wechsel prüfen (Performance) if (e.getFrom().getBlockX() == to.getBlockX() && e.getFrom().getBlockY() == to.getBlockY() && e.getFrom().getBlockZ() == to.getBlockZ()) return; if (!goal.contains(to)) return; - if (active.hasWinner()) return; + // Cooldown verhindern + long now = System.currentTimeMillis(); + Long last = lastGoalTime.get(player.getUniqueId()); + if (last != null && now - last < GOAL_COOLDOWN_MS) return; + lastGoalTime.put(player.getUniqueId(), now); + + if (def.isCircuit()) { + handleCircuitGoal(active, def, player); + } else { + handleSprintGoal(active, player); + } + } + + // ─── Sprint: erster gewinnt sofort ──────────────────────────────────── + + private void handleSprintGoal(ActiveEvent active, Player player) { + if (active.hasWinner()) return; // bereits entschieden plugin.getEventManager().endEventWithWinner(player, "Ziel erreicht"); } + // ─── Circuit: Runden zählen ─────────────────────────────────────────── + + private void handleCircuitGoal(ActiveEvent active, EventDefinition def, Player player) { + UUID uuid = player.getUniqueId(); + int done = lapCount.merge(uuid, 1, Integer::sum); + int total = def.getLaps(); + long elapsed = active.getElapsedSeconds(); + + if (done < total) { + // Zwischenrunde + String msg = "§e🏁 Runde §f" + done + "§e/§f" + total + + " §8(Zeit: §f" + formatTime((int) elapsed) + "§8)"; + player.sendActionBar(msg); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1.5f); + Bukkit.broadcastMessage(plugin.prefix() + "§f" + player.getName() + + " §7— Runde §e" + done + "§7/§e" + total + + " §8(§f" + formatTime((int) elapsed) + "§8)"); + } else { + // Ziel erreicht (alle Runden voll) + finishPosition++; + int pos = finishPosition; + active.addScore(uuid, Math.max(0, 100 - (pos - 1) * 20)); // Punkte: 1. = 100, 2. = 80, … + + String posStr = switch (pos) { + case 1 -> "§6§l1. Platz 🥇"; + case 2 -> "§7§l2. Platz 🥈"; + case 3 -> "§c§l3. Platz 🥉"; + default -> "§7" + pos + ". Platz"; + }; + + player.sendTitle(posStr, "§fZeit: §e" + formatTime((int) elapsed), 0, 60, 20); + player.playSound(player.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, pos == 1 ? 1.2f : 0.8f); + + Bukkit.broadcastMessage(plugin.prefix() + "§6🏁 §f" + player.getName() + + " §ehat das Rennen beendet! §8[" + posStr + "§8] §8(Zeit: §f" + + formatTime((int) elapsed) + "§8)"); + + // Spieler aus aktivem Rennen entfernen (kann zuschauen) + active.removeParticipant(player); + + // Wenn alle Spieler fertig (oder nur noch einer übrig) → Event beenden + int remaining = active.getParticipantCount(); + if (pos == 1) { + // Gewinner setzen + active.setWinner(uuid); + plugin.getEventManager().distributeWinnerRewards(def, player); + } + if (remaining == 0 || (remaining == 1 && active.getParticipants().size() == 1)) { + plugin.getEventManager().endEvent("Rennen beendet — alle Spieler im Ziel"); + } + } + } + private boolean isRaceEvent(EventDefinition def) { if (def.getCategory() == EventCategory.RACING) return true; return def.getType() == EventType.TEAM_RELAY_RACE; } -} + + private String formatTime(int seconds) { + int m = seconds / 60, s = seconds % 60; + if (m == 0) return s + "s"; + return m + "m " + s + "s"; + } +} \ No newline at end of file diff --git a/src/main/java/dev/viper/eventengine/manager/EventManager.java b/src/main/java/dev/viper/eventengine/manager/EventManager.java index 9e2e451..fc08400 100644 --- a/src/main/java/dev/viper/eventengine/manager/EventManager.java +++ b/src/main/java/dev/viper/eventengine/manager/EventManager.java @@ -6,6 +6,7 @@ import dev.viper.eventengine.events.builtin.IEventHandler; import dev.viper.eventengine.model.ActiveEvent; import dev.viper.eventengine.model.EventDefinition; import org.bukkit.Bukkit; +import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; @@ -14,7 +15,7 @@ import java.util.logging.Logger; /** * Steuert den Lebenszyklus von Events: - * start → announce → run → end → reward + * start → announce → countdown → run → end → reward */ public class EventManager { @@ -39,9 +40,7 @@ public class EventManager { return false; } - currentEvent = new ActiveEvent(def); - currentEvent.setState(ActiveEvent.State.RUNNING); - + // Ankündigung broadcast(def.getAnnouncement()); if (def.getDurationSeconds() > 0) broadcast(plugin.prefix() + "§7Dauer: §e" + formatTime(def.getDurationSeconds())); @@ -51,12 +50,65 @@ public class EventManager { log.info("Event gestartet von " + by + ": " + def.getDisplayName()); } + // 3..2..1..Go Countdown, danach eigentlicher Start + int countdown = plugin.getConfigManager().getCountdownSeconds(); + if (countdown > 0) { + currentEvent = new ActiveEvent(def); + currentEvent.setState(ActiveEvent.State.WAITING); + runCountdown(def, initiator, countdown); + } else { + launchEvent(def, initiator); + } + + return true; + } + + /** Countdown-Loop: 3..2..1..Go */ + private void runCountdown(EventDefinition def, Player initiator, int seconds) { + final ActiveEvent snap = currentEvent; + new BukkitRunnable() { + int remaining = seconds; + + @Override + public void run() { + if (snap != currentEvent) { cancel(); return; } // abgebrochen + + if (remaining > 0) { + // Titel + Sound für alle Online-Spieler + String color = remaining <= 3 ? "§c" : "§e"; + for (Player p : Bukkit.getOnlinePlayers()) { + p.sendTitle(color + remaining, "§7" + def.getDisplayName() + " §7startet gleich!", 0, 25, 5); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 1f, 1f); + } + broadcast(plugin.prefix() + "§e⏳ " + def.getDisplayName() + " §7startet in §c" + remaining + " §7Sekunde" + (remaining == 1 ? "" : "n") + "..."); + remaining--; + } else { + cancel(); + for (Player p : Bukkit.getOnlinePlayers()) { + p.sendTitle("§a§lGO!", "§7" + def.getDisplayName(), 0, 30, 10); + p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_LARGE_BLAST, 1f, 1.2f); + } + broadcast(plugin.prefix() + "§a§l🚀 GO! §r§e" + def.getDisplayName() + " §7hat begonnen!"); + launchEvent(def, initiator); + } + } + }.runTaskTimer(plugin, 0L, 20L); + } + + /** Eigentlicher Event-Start nach dem Countdown */ + private void launchEvent(EventDefinition def, Player initiator) { + if (currentEvent == null || currentEvent.getState() == ActiveEvent.State.ENDED) { + currentEvent = new ActiveEvent(def); + } + currentEvent.setState(ActiveEvent.State.RUNNING); + runCommands(def.getStartCommands(), null); handlerRegistry.get(def.getType()).ifPresent(h -> h.onStart(currentEvent)); plugin.getScoreboardManager().start(currentEvent); if (def.getDurationSeconds() > 0) { final ActiveEvent snap = currentEvent; + if (def.getDurationSeconds() > 70) new BukkitRunnable() { @Override public void run() { if (currentEvent == snap && currentEvent.getState() == ActiveEvent.State.RUNNING) @@ -75,7 +127,6 @@ public class EventManager { }}.runTaskLater(plugin, def.getDurationSeconds() * 20L).getTaskId(); currentEvent.setTaskId(taskId); } - return true; } public boolean endEvent(String reason) { @@ -112,7 +163,24 @@ public class EventManager { if (currentEvent == null || currentEvent.getState() == ActiveEvent.State.ENDED) return false; if (winner != null) { currentEvent.setWinner(winner.getUniqueId()); - broadcast(plugin.prefix() + "§6🏁 " + winner.getName() + " §ehat das Ziel erreicht und gewinnt!"); + + // Ziel-Nachricht mit Zeit + long elapsed = currentEvent.getElapsedSeconds(); + broadcast(plugin.prefix() + "§6🏁 §f" + winner.getName() + + " §ehat das Ziel erreicht und gewinnt! §8(Zeit: §f" + formatTime((int) elapsed) + "§8)"); + + // Title für Gewinner + winner.sendTitle("§6§l🏁 ZIEL!", "§e" + currentEvent.getDefinition().getDisplayName(), 0, 60, 20); + winner.playSound(winner.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f); + + // Title für alle anderen + for (UUID uuid : currentEvent.getParticipants()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null && !p.equals(winner)) { + p.sendTitle("§c§lZU SPÄT!", "§7" + winner.getName() + " §7war schneller.", 0, 40, 15); + } + } + distributeWinnerRewards(currentEvent.getDefinition(), winner); } return endEvent(reason); @@ -175,7 +243,7 @@ public class EventManager { } } - private void distributeWinnerRewards(EventDefinition def, Player winner) { + public void distributeWinnerRewards(EventDefinition def, Player winner) { if (def.getWinnerRewards().isEmpty() || winner == null) return; runCommands(def.getWinnerRewards(), winner); } @@ -204,4 +272,4 @@ public class EventManager { public ActiveEvent getCurrentEvent() { return currentEvent; } public boolean isRunning() { return currentEvent != null && currentEvent.getState() == ActiveEvent.State.RUNNING; } public EventHandlerRegistry getHandlerRegistry() { return handlerRegistry; } -} +} \ No newline at end of file diff --git a/src/main/java/dev/viper/eventengine/model/EventDefinition.java b/src/main/java/dev/viper/eventengine/model/EventDefinition.java index b0e44c4..dd79396 100644 --- a/src/main/java/dev/viper/eventengine/model/EventDefinition.java +++ b/src/main/java/dev/viper/eventengine/model/EventDefinition.java @@ -1,52 +1,47 @@ package dev.viper.eventengine.model; import dev.viper.eventengine.util.ColorUtil; -import dev.viper.eventengine.model.EventRegion; import org.bukkit.configuration.ConfigurationSection; import java.util.*; /** * Repräsentiert ein einzelnes Event — sowohl eingebaute als auch Custom-Events. - * Custom-Events werden in custom_events.yml gespeichert. */ public class EventDefinition { + /** Sprint = einmaliger Punkt-zu-Punkt Lauf; CIRCUIT = Rundkurs (laps Runden) */ + public enum RaceMode { SPRINT, CIRCUIT } + private final String id; private String displayName; private String description; private EventType type; private EventCategory category; - // Zeitlimit in Sekunden (0 = unbegrenzt) private int durationSeconds; - // Mindest-/Höchst-Spielerzahl private int minPlayers; private int maxPlayers; - // Befehle die beim Start/Ende ausgeführt werden private List startCommands; private List endCommands; - // Belohnungen private List rewards; - // Ankündigungs-Text private String announcement; - // Ob das Event in der Rotation ist private boolean inRotation; - // Gewichtung in der Rotation private int weight; - // Optionaler Start-/Zielbereich (Rennen) private EventRegion startRegion; private EventRegion goalRegion; - // Custom-Einstellungen (Key-Value) private Map customSettings; - // Ist es ein benutzerdefiniertes Event? private boolean isCustom; - // Optionale Event-Region (null = gesamte Welt) private EventRegion region; - // Winner-Belohnungen (nur Sieger) private List winnerRewards; - // ─── Konstruktor für builtin Events ──────────────────────────────────── + // ── Renn-Einstellungen ──────────────────────────────────────────────── + /** Sprint (A→B) oder Circuit (Rundkurs) */ + private RaceMode raceMode = RaceMode.SPRINT; + /** Anzahl der Runden bei CIRCUIT-Modus */ + private int laps = 1; + + // ─── Konstruktor für builtin Events ─────────────────────────────────── public EventDefinition(EventType type) { this.id = type.name().toLowerCase(); this.type = type; @@ -62,14 +57,12 @@ public class EventDefinition { this.announcement = ColorUtil.color("&6✦ &e" + displayName + " &6startet jetzt!"); this.inRotation = true; this.weight = 1; - this.startRegion = null; - this.goalRegion = null; this.customSettings = new HashMap<>(); this.isCustom = false; this.winnerRewards = new ArrayList<>(); } - // ─── Konstruktor für Custom Events ───────────────────────────────────── + // ─── Konstruktor für Custom Events ──────────────────────────────────── public EventDefinition(String id) { this.id = id; this.type = EventType.CUSTOM; @@ -85,14 +78,12 @@ public class EventDefinition { this.announcement = ColorUtil.color("&6✦ &e" + id + " &6startet jetzt!"); this.inRotation = true; this.weight = 1; - this.startRegion = null; - this.goalRegion = null; this.customSettings = new HashMap<>(); this.isCustom = true; this.winnerRewards = new ArrayList<>(); } - // ─── Laden aus ConfigurationSection ──────────────────────────────────── + // ─── Laden aus ConfigurationSection ─────────────────────────────────── public static EventDefinition fromConfig(String id, ConfigurationSection sec) { EventDefinition def = new EventDefinition(id); def.displayName = sec.getString("display-name", id); @@ -108,6 +99,13 @@ public class EventDefinition { def.inRotation = sec.getBoolean("in-rotation", true); def.weight = sec.getInt("weight", 1); def.winnerRewards = sec.getStringList("winner-rewards"); + + // Renn-Einstellungen + String raceModeStr = sec.getString("race-mode", "SPRINT").toUpperCase(); + try { def.raceMode = RaceMode.valueOf(raceModeStr); } + catch (Exception e) { def.raceMode = RaceMode.SPRINT; } + def.laps = Math.max(1, sec.getInt("laps", 1)); + String catStr = sec.getString("category", "CUSTOM"); try { def.category = EventCategory.valueOf(catStr); } catch (Exception e) { def.category = EventCategory.CUSTOM; } @@ -119,49 +117,37 @@ public class EventDefinition { } } - // Region laden + // Region if (sec.contains("region.world")) { try { - String w = sec.getString("region.world"); - int minX = sec.getInt("region.min-x"); - int minY = sec.getInt("region.min-y"); - int minZ = sec.getInt("region.min-z"); - int maxX = sec.getInt("region.max-x"); - int maxY = sec.getInt("region.max-y"); - int maxZ = sec.getInt("region.max-z"); - def.region = new EventRegion(w, minX, minY, minZ, maxX, maxY, maxZ); + def.region = new EventRegion( + sec.getString("region.world"), + sec.getInt("region.min-x"), sec.getInt("region.min-y"), sec.getInt("region.min-z"), + sec.getInt("region.max-x"), sec.getInt("region.max-y"), sec.getInt("region.max-z")); } catch (Exception ignored) {} } - - // Start-/Zielregion laden + // Start-Region if (sec.contains("start-region.world")) { try { - String w = sec.getString("start-region.world"); - int minX = sec.getInt("start-region.min-x"); - int minY = sec.getInt("start-region.min-y"); - int minZ = sec.getInt("start-region.min-z"); - int maxX = sec.getInt("start-region.max-x"); - int maxY = sec.getInt("start-region.max-y"); - int maxZ = sec.getInt("start-region.max-z"); - def.startRegion = new EventRegion(w, minX, minY, minZ, maxX, maxY, maxZ); + def.startRegion = new EventRegion( + sec.getString("start-region.world"), + sec.getInt("start-region.min-x"), sec.getInt("start-region.min-y"), sec.getInt("start-region.min-z"), + sec.getInt("start-region.max-x"), sec.getInt("start-region.max-y"), sec.getInt("start-region.max-z")); } catch (Exception ignored) {} } + // Ziel-Region if (sec.contains("goal-region.world")) { try { - String w = sec.getString("goal-region.world"); - int minX = sec.getInt("goal-region.min-x"); - int minY = sec.getInt("goal-region.min-y"); - int minZ = sec.getInt("goal-region.min-z"); - int maxX = sec.getInt("goal-region.max-x"); - int maxY = sec.getInt("goal-region.max-y"); - int maxZ = sec.getInt("goal-region.max-z"); - def.goalRegion = new EventRegion(w, minX, minY, minZ, maxX, maxY, maxZ); + def.goalRegion = new EventRegion( + sec.getString("goal-region.world"), + sec.getInt("goal-region.min-x"), sec.getInt("goal-region.min-y"), sec.getInt("goal-region.min-z"), + sec.getInt("goal-region.max-x"), sec.getInt("goal-region.max-y"), sec.getInt("goal-region.max-z")); } catch (Exception ignored) {} } return def; } - // ─── Speichern in ConfigurationSection ───────────────────────────────── + // ─── Speichern in ConfigurationSection ──────────────────────────────── public void saveToConfig(ConfigurationSection sec) { sec.set("display-name", displayName); sec.set("description", description); @@ -176,32 +162,23 @@ public class EventDefinition { sec.set("weight", weight); sec.set("winner-rewards", winnerRewards); sec.set("category", category.name()); + sec.set("race-mode", raceMode.name()); + sec.set("laps", laps); + if (region != null) { sec.set("region.world", region.getWorldName()); - sec.set("region.min-x", region.getMinX()); - sec.set("region.min-y", region.getMinY()); - sec.set("region.min-z", region.getMinZ()); - sec.set("region.max-x", region.getMaxX()); - sec.set("region.max-y", region.getMaxY()); - sec.set("region.max-z", region.getMaxZ()); + sec.set("region.min-x", region.getMinX()); sec.set("region.min-y", region.getMinY()); sec.set("region.min-z", region.getMinZ()); + sec.set("region.max-x", region.getMaxX()); sec.set("region.max-y", region.getMaxY()); sec.set("region.max-z", region.getMaxZ()); } if (startRegion != null) { sec.set("start-region.world", startRegion.getWorldName()); - sec.set("start-region.min-x", startRegion.getMinX()); - sec.set("start-region.min-y", startRegion.getMinY()); - sec.set("start-region.min-z", startRegion.getMinZ()); - sec.set("start-region.max-x", startRegion.getMaxX()); - sec.set("start-region.max-y", startRegion.getMaxY()); - sec.set("start-region.max-z", startRegion.getMaxZ()); + sec.set("start-region.min-x", startRegion.getMinX()); sec.set("start-region.min-y", startRegion.getMinY()); sec.set("start-region.min-z", startRegion.getMinZ()); + sec.set("start-region.max-x", startRegion.getMaxX()); sec.set("start-region.max-y", startRegion.getMaxY()); sec.set("start-region.max-z", startRegion.getMaxZ()); } if (goalRegion != null) { sec.set("goal-region.world", goalRegion.getWorldName()); - sec.set("goal-region.min-x", goalRegion.getMinX()); - sec.set("goal-region.min-y", goalRegion.getMinY()); - sec.set("goal-region.min-z", goalRegion.getMinZ()); - sec.set("goal-region.max-x", goalRegion.getMaxX()); - sec.set("goal-region.max-y", goalRegion.getMaxY()); - sec.set("goal-region.max-z", goalRegion.getMaxZ()); + sec.set("goal-region.min-x", goalRegion.getMinX()); sec.set("goal-region.min-y", goalRegion.getMinY()); sec.set("goal-region.min-z", goalRegion.getMinZ()); + sec.set("goal-region.max-x", goalRegion.getMaxX()); sec.set("goal-region.max-y", goalRegion.getMaxY()); sec.set("goal-region.max-z", goalRegion.getMaxZ()); } if (!customSettings.isEmpty()) { for (Map.Entry entry : customSettings.entrySet()) { @@ -210,7 +187,7 @@ public class EventDefinition { } } - // ─── Getter/Setter ────────────────────────────────────────────────────── + // ─── Getter/Setter ───────────────────────────────────────────────────── public String getId() { return id; } public String getDisplayName() { return displayName; } public void setDisplayName(String n) { this.displayName = n; } @@ -252,5 +229,11 @@ public class EventDefinition { public boolean hasGoalRegion() { return goalRegion != null; } public Object getSetting(String key) { return customSettings.get(key); } public void setSetting(String key, Object val) { customSettings.put(key, val); } -} -// Wird unten in der Klasse ergänzt — separate Patch-Datei + + // Renn-Getter/Setter + public RaceMode getRaceMode() { return raceMode; } + public void setRaceMode(RaceMode m) { this.raceMode = m; } + public int getLaps() { return laps; } + public void setLaps(int l) { this.laps = Math.max(1, l); } + public boolean isCircuit() { return raceMode == RaceMode.CIRCUIT; } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index c553773..29bdab4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -14,8 +14,10 @@ settings: # Sekunden VOR dem Event-Start, wann die Ankündigung erfolgt announce-before-seconds: 30 + # 3..2..1..Go Countdown vor jedem Event-Start (Sekunden; 0 = deaktiviert) + countdown-seconds: 3 + # Intervall-Modus: Startet automatisch alle X Minuten ein Event - # Nützlich wenn kein fester Zeitplan gewünscht ist use-interval: false interval-minutes: 60 @@ -31,98 +33,66 @@ settings: # Leer lassen oder "DAILY" für jeden Tag # time: Uhrzeit im Format HH:MM (24h) # event: Event-ID (aus /event list) oder "RANDOM" -# -# ─── Beispiel-Zeitplan ─────────────────────────────────────────── schedule: - # Täglich 12 Uhr: Zufälliges Event - days: [] time: "12:00" event: "RANDOM" - # Täglich 18 Uhr: PvP Deathmatch - days: [] time: "18:00" event: "pvp_deathmatch" - # Freitag & Samstag Abend: Hunger Games - days: [FRIDAY, SATURDAY] time: "20:00" event: "pvp_hunger_games" - # Samstag Nachmittag: Build Battle - days: [SATURDAY] time: "15:00" event: "build_battle" - # Sonntag Mittag: Drop Party - days: [SUNDAY] time: "14:00" event: "fun_drop_party" - # Montag: Lotterie - days: [MONDAY] time: "19:00" event: "economy_lottery" - # Mittwoch: Team Survival - days: [WEDNESDAY] time: "18:00" event: "team_survival" # ── Event-Overrides ─────────────────────────────────────────────── -# Hier können einzelne eingebaute Events angepasst werden. -# OHNE diese Sektion gelten die Plugin-Defaults. -# Custom Events werden in custom_events.yml gespeichert. +# Einzelne eingebaute Events anpassen. +# Renn-Modus Beispiel: # -# Beispiel: # event-overrides: -# pvp_deathmatch: +# race_elytra: +# race-mode: CIRCUIT # SPRINT (A→B) oder CIRCUIT (Rundkurs) +# laps: 3 # Runden im Circuit-Modus # duration-seconds: 600 -# min-players: 4 # rewards: -# - "eco give %player% 500" -# - "give %player% diamond 3" -# fun_drop_party: -# duration-seconds: 120 -# announcement: "&6✦ &eDROP PARTY &7startet! Kommt alle auf den Spawn!" +# - "eco give %player% 1000" +# +# race_parkour: +# race-mode: SPRINT +# +# Elytra Checkpoint-Setup (in-game einfacher per Command): +# /event elytra checkpoint add → Pos1 +# /event elytra checkpoint confirm → Pos2 → Checkpoint wird gespeichert +# /event elytra checkpoint list → Übersicht +# /event elytra checkpoint clear → Alle löschen +# /event racemode [laps] event-overrides: {} -# ── Event-Regionen ──────────────────────────────────────────────── -# Regionen können direkt hier oder ingame per Command gesetzt werden: -# /event region pos1 → Erste Ecke (stehe an der Position) -# /event region pos2 → Zweite Ecke → Region wird gespeichert -# /event region clear → Region entfernen (gesamte Welt) -# /event region info → Region anzeigen -# -# Für Custom Events wird die Region in custom_events.yml gespeichert. -# Für builtin Events hier in event-overrides eintragen: -# -# event-overrides: -# pvp_deathmatch: -# region: -# world: world -# min-x: -50 -# min-y: 60 -# min-z: -50 -# max-x: 50 -# max-y: 120 -# max-z: 50 -# # ── Block-Schutz während Events ────────────────────────────────── -# TNT und Explosionen zerstören KEINE Blöcke solange ein Event läuft. -# Das gilt immer — unabhängig ob eine Region definiert ist oder nicht. -# Spieler (PvP-Schaden durch Explosionen) bleibt aktiv. protection: - # Spieler können die Region während eines Events nicht verlassen enforce-region-boundary: true - # TNT/Explosionen zerstören keine Blöcke während Events no-explosion-block-damage: true - # Teilnehmer können nur innerhalb der Region Blöcke abbauen/setzen restrict-block-interaction: true # ── Item-Regen ────────────────────────────────────────────────── item-rain: - # Items, die vom Himmel fallen (Material-Namen) items: - DIAMOND - GOLD_INGOT @@ -132,4 +102,4 @@ item-rain: - BREAD - ARROW - GOLDEN_APPLE - - EXPERIENCE_BOTTLE + - EXPERIENCE_BOTTLE \ No newline at end of file