From 498c912d5f4f00aa2595eca4a54807c7f7f4ab2b Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Sun, 21 Jun 2026 20:43:31 +0200 Subject: [PATCH] Upload folder via GUI - src --- src/main/java/de/lasertag/LasertagPlugin.java | 77 ++ src/main/java/de/lasertag/arena/Arena.java | 153 ++++ .../java/de/lasertag/arena/ArenaManager.java | 163 ++++ .../de/lasertag/camp/AntiCampManager.java | 149 ++++ .../de/lasertag/command/LasertagCommand.java | 126 +++ .../de/lasertag/command/LtAdminCommand.java | 220 ++++++ .../de/lasertag/config/ConfigManager.java | 178 +++++ src/main/java/de/lasertag/game/Game.java | 736 ++++++++++++++++++ .../java/de/lasertag/game/GameManager.java | 93 +++ src/main/java/de/lasertag/game/GameState.java | 8 + src/main/java/de/lasertag/game/Team.java | 38 + .../de/lasertag/listener/BlockListener.java | 44 ++ .../de/lasertag/listener/PlayerListener.java | 70 ++ .../de/lasertag/listener/SignListener.java | 246 ++++++ .../de/lasertag/listener/WeaponListener.java | 71 ++ .../java/de/lasertag/player/LaserPlayer.java | 125 +++ .../de/lasertag/player/PlayerDataManager.java | 73 ++ .../java/de/lasertag/player/PlayerStats.java | 48 ++ .../protection/ModProtectionManager.java | 215 +++++ .../scoreboard/ScoreboardManager.java | 113 +++ .../java/de/lasertag/weapon/WeaponType.java | 34 + .../java/de/lasertag/weapon/WeaponUtil.java | 100 +++ src/main/resources/config.yml | 206 +++++ src/main/resources/plugin.yml | 24 + 24 files changed, 3310 insertions(+) create mode 100644 src/main/java/de/lasertag/LasertagPlugin.java create mode 100644 src/main/java/de/lasertag/arena/Arena.java create mode 100644 src/main/java/de/lasertag/arena/ArenaManager.java create mode 100644 src/main/java/de/lasertag/camp/AntiCampManager.java create mode 100644 src/main/java/de/lasertag/command/LasertagCommand.java create mode 100644 src/main/java/de/lasertag/command/LtAdminCommand.java create mode 100644 src/main/java/de/lasertag/config/ConfigManager.java create mode 100644 src/main/java/de/lasertag/game/Game.java create mode 100644 src/main/java/de/lasertag/game/GameManager.java create mode 100644 src/main/java/de/lasertag/game/GameState.java create mode 100644 src/main/java/de/lasertag/game/Team.java create mode 100644 src/main/java/de/lasertag/listener/BlockListener.java create mode 100644 src/main/java/de/lasertag/listener/PlayerListener.java create mode 100644 src/main/java/de/lasertag/listener/SignListener.java create mode 100644 src/main/java/de/lasertag/listener/WeaponListener.java create mode 100644 src/main/java/de/lasertag/player/LaserPlayer.java create mode 100644 src/main/java/de/lasertag/player/PlayerDataManager.java create mode 100644 src/main/java/de/lasertag/player/PlayerStats.java create mode 100644 src/main/java/de/lasertag/protection/ModProtectionManager.java create mode 100644 src/main/java/de/lasertag/scoreboard/ScoreboardManager.java create mode 100644 src/main/java/de/lasertag/weapon/WeaponType.java create mode 100644 src/main/java/de/lasertag/weapon/WeaponUtil.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/src/main/java/de/lasertag/LasertagPlugin.java b/src/main/java/de/lasertag/LasertagPlugin.java new file mode 100644 index 0000000..f90e4b7 --- /dev/null +++ b/src/main/java/de/lasertag/LasertagPlugin.java @@ -0,0 +1,77 @@ +package de.lasertag; + +import de.lasertag.arena.ArenaManager; +import de.lasertag.command.LasertagCommand; +import de.lasertag.command.LtAdminCommand; +import de.lasertag.config.ConfigManager; +import de.lasertag.game.GameManager; +import de.lasertag.listener.BlockListener; +import de.lasertag.listener.PlayerListener; +import de.lasertag.listener.SignListener; +import de.lasertag.listener.WeaponListener; +import de.lasertag.player.PlayerDataManager; +import de.lasertag.scoreboard.ScoreboardManager; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; + +public class LasertagPlugin extends JavaPlugin { + + private static LasertagPlugin instance; + + private ConfigManager configManager; + private ArenaManager arenaManager; + private GameManager gameManager; + private PlayerDataManager playerDataManager; + private ScoreboardManager scoreboardManager; + private SignListener signListener; + + @Override + public void onEnable() { + instance = this; + banner(); + + configManager = new ConfigManager(this); + playerDataManager = new PlayerDataManager(this); + arenaManager = new ArenaManager(this); + scoreboardManager = new ScoreboardManager(this); + gameManager = new GameManager(this); + signListener = new SignListener(this); + + getCommand("lasertag").setExecutor(new LasertagCommand(this)); + getCommand("ltadmin").setExecutor(new LtAdminCommand(this)); + + var pm = Bukkit.getPluginManager(); + pm.registerEvents(new PlayerListener(this), this); + pm.registerEvents(new WeaponListener(this), this); + pm.registerEvents(new BlockListener(this), this); + pm.registerEvents(signListener, this); + + getLogger().info("Lasertag v" + getDescription().getVersion() + " aktiviert! (" + + signListener.getSignCount() + " Schilder geladen)"); + } + + @Override + public void onDisable() { + if (signListener != null) signListener.stopUpdateTask(); + if (gameManager != null) gameManager.stopAllGames(); + if (playerDataManager != null) playerDataManager.saveAll(); + getLogger().info("Lasertag deaktiviert."); + } + + private void banner() { + getLogger().info("§b ██╗ █████╗ ███████╗███████╗██████╗ ████████╗███████╗ ██████╗"); + getLogger().info("§b ██║ ██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔════╝██╔════╝"); + getLogger().info("§b ██║ ███████║███████╗█████╗ ██████╔╝ ██║ █████╗ ██║ "); + getLogger().info("§b ██║ ██╔══██║╚════██║██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██║ "); + getLogger().info("§b ███████╗██║ ██║███████║███████╗██║ ██║ ██║ ███████╗╚██████╗"); + getLogger().info("§7 v" + getDescription().getVersion() + " | Multi-Arena LaserTag | Anti-Camp | Mod-Protect"); + } + + public static LasertagPlugin getInstance() { return instance; } + public ConfigManager getConfigManager() { return configManager; } + public ArenaManager getArenaManager() { return arenaManager; } + public GameManager getGameManager() { return gameManager; } + public PlayerDataManager getPlayerDataManager() { return playerDataManager; } + public ScoreboardManager getScoreboardManager() { return scoreboardManager; } + public SignListener getSignListener() { return signListener; } +} diff --git a/src/main/java/de/lasertag/arena/Arena.java b/src/main/java/de/lasertag/arena/Arena.java new file mode 100644 index 0000000..1a448cb --- /dev/null +++ b/src/main/java/de/lasertag/arena/Arena.java @@ -0,0 +1,153 @@ +package de.lasertag.arena; + +import de.lasertag.game.Team; +import org.bukkit.Location; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Repräsentiert eine konfigurierte Lasertag-Arena. + * + * Jedes Team braucht: + * - mind. 1 Spawn-Punkt + * - 1 Basispunkt (Regenerationspunkt + angreifbarer Block) + */ +public class Arena { + + private final String name; + private String displayName; + private boolean enabled; + + // Team-Spawns: team -> Liste von Spawn-Locations + private final Map> teamSpawns = new HashMap<>(); + + // Team-Basis: team -> Location des Basis-Blocks + private final Map teamBases = new HashMap<>(); + + // Team-Basis-Gesundheit (wie oft kann Basis angegriffen werden) + private final Map baseHealth = new HashMap<>(); + + // Lobby dieser Arena (optionaler Wartebereich) + private Location lobbyLocation; + + // Bounds (optional, für Block-Schutz innerhalb der Arena) + private Location corner1; + private Location corner2; + + public Arena(String name) { + this.name = name; + this.displayName = name; + this.enabled = false; + for (Team t : Team.values()) { + teamSpawns.put(t, new ArrayList<>()); + } + } + + // ─── Validation ────────────────────────────────────────────────────────── + + /** + * Arena ist bereit wenn jedes Team mindestens 1 Spawn und 1 Basis hat. + */ + public boolean isReady() { + for (Team t : Team.values()) { + if (teamSpawns.get(t).isEmpty()) return false; + if (!teamBases.containsKey(t)) return false; + } + return true; + } + + public String getMissingSetup() { + StringBuilder sb = new StringBuilder(); + for (Team t : Team.values()) { + if (teamSpawns.get(t).isEmpty()) + sb.append("§c- Kein Spawn für Team ").append(t.colored()).append("§c!\n"); + if (!teamBases.containsKey(t)) + sb.append("§c- Keine Basis für Team ").append(t.colored()).append("§c!\n"); + } + return sb.length() == 0 ? "§aArena bereit!" : sb.toString().trim(); + } + + // ─── Spawns ────────────────────────────────────────────────────────────── + + public void addSpawn(Team team, Location loc) { + teamSpawns.get(team).add(loc.clone()); + } + + public List getSpawns(Team team) { + return teamSpawns.get(team); + } + + public Location getRandomSpawn(Team team) { + List spawns = teamSpawns.get(team); + if (spawns.isEmpty()) return null; + return spawns.get((int)(Math.random() * spawns.size())).clone(); + } + + // ─── Bases ─────────────────────────────────────────────────────────────── + + public void setBase(Team team, Location loc) { + teamBases.put(team, loc.clone()); + } + + public Location getBase(Team team) { + return teamBases.get(team); + } + + public boolean hasBase(Team team) { + return teamBases.containsKey(team); + } + + /** Liefert das Team dessen Basis an der gegebenen Location steht, oder null. */ + public Team getBaseTeam(Location loc) { + for (Map.Entry e : teamBases.entrySet()) { + Location base = e.getValue(); + if (base.getWorld().equals(loc.getWorld()) + && base.getBlockX() == loc.getBlockX() + && base.getBlockY() == loc.getBlockY() + && base.getBlockZ() == loc.getBlockZ()) { + return e.getKey(); + } + } + return null; + } + + // ─── Base Health ───────────────────────────────────────────────────────── + + public void initBaseHealth(int maxHealth) { + for (Team t : Team.values()) baseHealth.put(t, maxHealth); + } + + public int getBaseHealth(Team team) { + return baseHealth.getOrDefault(team, 0); + } + + public boolean damageBase(Team team) { + int hp = baseHealth.getOrDefault(team, 0); + if (hp <= 0) return false; + baseHealth.put(team, hp - 1); + return true; + } + + public boolean isBaseDestroyed(Team team) { + return baseHealth.getOrDefault(team, 0) <= 0; + } + + // ─── Getters / Setters ─────────────────────────────────────────────────── + + public String getName() { return name; } + public String getDisplayName() { return displayName; } + public void setDisplayName(String n) { this.displayName = n; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean e) { this.enabled = e; } + public Location getLobbyLocation() { return lobbyLocation; } + public void setLobbyLocation(Location l) { this.lobbyLocation = l != null ? l.clone() : null; } + public Location getCorner1() { return corner1; } + public void setCorner1(Location l) { this.corner1 = l != null ? l.clone() : null; } + public Location getCorner2() { return corner2; } + public void setCorner2(Location l) { this.corner2 = l != null ? l.clone() : null; } + public Map> getAllSpawns() { return teamSpawns; } + public Map getAllBases() { return teamBases; } +} diff --git a/src/main/java/de/lasertag/arena/ArenaManager.java b/src/main/java/de/lasertag/arena/ArenaManager.java new file mode 100644 index 0000000..630db7b --- /dev/null +++ b/src/main/java/de/lasertag/arena/ArenaManager.java @@ -0,0 +1,163 @@ +package de.lasertag.arena; + +import de.lasertag.LasertagPlugin; +import de.lasertag.game.Team; +import org.bukkit.Location; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class ArenaManager { + + private final LasertagPlugin plugin; + private final Map arenas = new LinkedHashMap<>(); + private File file; + private FileConfiguration cfg; + + public ArenaManager(LasertagPlugin plugin) { + this.plugin = plugin; + load(); + } + + // ─── CRUD ──────────────────────────────────────────────────────────────── + + public Arena createArena(String name) { + String key = name.toLowerCase(); + if (arenas.containsKey(key)) return null; + Arena arena = new Arena(name); + arenas.put(key, arena); + save(); + return arena; + } + + public Arena getArena(String name) { + return arenas.get(name.toLowerCase()); + } + + public boolean deleteArena(String name) { + if (arenas.remove(name.toLowerCase()) == null) return false; + save(); + return true; + } + + public Collection getAll() { return Collections.unmodifiableCollection(arenas.values()); } + + public List getReady() { + List list = new ArrayList<>(); + for (Arena a : arenas.values()) + if (a.isReady() && a.isEnabled()) list.add(a); + return list; + } + + // ─── Persistence ───────────────────────────────────────────────────────── + + private void load() { + file = new File(plugin.getDataFolder(), "arenas.yml"); + if (!file.exists()) { + try { plugin.getDataFolder().mkdirs(); file.createNewFile(); } + catch (IOException ex) { ex.printStackTrace(); return; } + } + cfg = YamlConfiguration.loadConfiguration(file); + + ConfigurationSection root = cfg.getConfigurationSection("arenas"); + if (root == null) return; + + for (String key : root.getKeys(false)) { + ConfigurationSection sec = root.getConfigurationSection(key); + if (sec == null) continue; + + Arena arena = new Arena(sec.getString("display-name", key)); + arena.setEnabled(sec.getBoolean("enabled", false)); + + // Lobby + if (sec.contains("lobby")) + arena.setLobbyLocation(sec.getLocation("lobby")); + + // Corners + if (sec.contains("corner1")) arena.setCorner1(sec.getLocation("corner1")); + if (sec.contains("corner2")) arena.setCorner2(sec.getLocation("corner2")); + + // Team spawns + ConfigurationSection spawnsSec = sec.getConfigurationSection("spawns"); + if (spawnsSec != null) { + for (Team t : Team.values()) { + List> locs = spawnsSec.getMapList(t.name()); + for (Map m : locs) { + Location loc = deserializeLoc(m); + if (loc != null) arena.addSpawn(t, loc); + } + } + } + + // Team bases + ConfigurationSection basesSec = sec.getConfigurationSection("bases"); + if (basesSec != null) { + for (Team t : Team.values()) { + if (basesSec.contains(t.name())) { + Location loc = basesSec.getLocation(t.name()); + if (loc != null) arena.setBase(t, loc); + } + } + } + + arenas.put(key.toLowerCase(), arena); + plugin.getLogger().info("Arena geladen: §e" + key + (arena.isReady() ? " §a[OK]" : " §c[Setup unvollständig]")); + } + } + + public void save() { + cfg.set("arenas", null); + for (Map.Entry entry : arenas.entrySet()) { + Arena a = entry.getValue(); + String path = "arenas." + entry.getKey() + "."; + + cfg.set(path + "display-name", a.getDisplayName()); + cfg.set(path + "enabled", a.isEnabled()); + + if (a.getLobbyLocation() != null) + cfg.set(path + "lobby", a.getLobbyLocation()); + if (a.getCorner1() != null) cfg.set(path + "corner1", a.getCorner1()); + if (a.getCorner2() != null) cfg.set(path + "corner2", a.getCorner2()); + + // Spawns + for (Team t : Team.values()) { + List> list = new ArrayList<>(); + for (Location loc : a.getSpawns(t)) list.add(serializeLoc(loc)); + cfg.set(path + "spawns." + t.name(), list); + } + + // Bases + for (Map.Entry be : a.getAllBases().entrySet()) { + cfg.set(path + "bases." + be.getKey().name(), be.getValue()); + } + } + try { cfg.save(file); } catch (IOException ex) { ex.printStackTrace(); } + } + + // ─── Location serialization (compatible without external libs) ─────────── + + private Map serializeLoc(Location loc) { + Map m = new LinkedHashMap<>(); + m.put("world", loc.getWorld().getName()); + m.put("x", loc.getX()); m.put("y", loc.getY()); m.put("z", loc.getZ()); + m.put("yaw", (double) loc.getYaw()); + m.put("pitch", (double) loc.getPitch()); + return m; + } + + private Location deserializeLoc(Map m) { + try { + String world = (String) m.get("world"); + double x = ((Number) m.get("x")).doubleValue(); + double y = ((Number) m.get("y")).doubleValue(); + double z = ((Number) m.get("z")).doubleValue(); + float yaw = m.containsKey("yaw") ? ((Number)m.get("yaw")).floatValue() : 0f; + float pitch = m.containsKey("pitch") ? ((Number)m.get("pitch")).floatValue() : 0f; + return new Location(plugin.getServer().getWorld(world), x, y, z, yaw, pitch); + } catch (Exception ex) { return null; } + } +} diff --git a/src/main/java/de/lasertag/camp/AntiCampManager.java b/src/main/java/de/lasertag/camp/AntiCampManager.java new file mode 100644 index 0000000..5d32bb9 --- /dev/null +++ b/src/main/java/de/lasertag/camp/AntiCampManager.java @@ -0,0 +1,149 @@ +package de.lasertag.camp; + +import de.lasertag.LasertagPlugin; +import de.lasertag.game.Game; +import de.lasertag.player.LaserPlayer; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Anti-Camp System. + * + * Erkennt Spieler die sich zu lange nicht bewegen und bestraft sie. + * + * Ablauf: + * 1. Position wird jede Sekunde geprüft + * 2. Hat sich der Spieler < camp-idle-radius Blöcke bewegt → idle-Zeit erhöhen + * 3. Nach warn-duration Sekunden → Warnung + * 4. Nach max-idle-seconds Sekunden → Strafe (Punkte-Abzug) + * + * Ausnahmen: + * - Spieler nahe der eigenen Basis (heilen) + * - Spieler die getroffen wurden (müssen zur Basis) + */ +public class AntiCampManager { + + private final LasertagPlugin plugin; + + /** UUID → letzte bekannte Position */ + private final Map lastPosition = new HashMap<>(); + /** UUID → Sekunden an gleicher Stelle */ + private final Map idleSeconds = new HashMap<>(); + /** UUID → wurde bereits gewarnt (true = Warnung lief, false = noch nicht) */ + private final Map warned = new HashMap<>(); + + public AntiCampManager(LasertagPlugin plugin) { + this.plugin = plugin; + } + + /** + * Wird vom Game-Task jede Sekunde aufgerufen. + * Prüft alle Spieler des Spiels auf Camping-Verhalten. + */ + public void tick(Game game) { + if (!plugin.getConfigManager().isAntiCampEnabled()) return; + + double idleRadius = plugin.getConfigManager().getCampIdleRadius(); + double excludeRadius = plugin.getConfigManager().getCampExcludeRadius(); + int maxIdle = plugin.getConfigManager().getCampMaxIdleSecs(); + int warnDuration = plugin.getConfigManager().getCampWarnDuration(); + String action = plugin.getConfigManager().getCampAction(); + int penalty = plugin.getConfigManager().getCampScorePenalty(); + + for (Player player : game.getOnline()) { + UUID uid = player.getUniqueId(); + LaserPlayer lp = game.getLP(uid); + if (lp == null) continue; + + // Getroffene Spieler vom Camp-Check ausnehmen + if (lp.isHit()) { + reset(uid); + continue; + } + + // Spieler nahe eigener Basis: ausschließen (Heilzone) + Location base = game.getArena().getBase(lp.getTeam()); + if (base != null && player.getLocation().distanceSquared(base) <= excludeRadius * excludeRadius) { + reset(uid); + continue; + } + + Location prev = lastPosition.get(uid); + Location curr = player.getLocation(); + + if (prev == null) { + lastPosition.put(uid, curr.clone()); + idleSeconds.put(uid, 0); + warned.put(uid, false); + continue; + } + + // Hat sich der Spieler bewegt? + double moved = prev.distanceSquared(curr); + if (moved > idleRadius * idleRadius) { + // Bewegt → Reset + lastPosition.put(uid, curr.clone()); + idleSeconds.put(uid, 0); + warned.put(uid, false); + continue; + } + + // Still gestanden + int idle = idleSeconds.merge(uid, 1, Integer::sum); + + // Warnphase + if (action.contains("WARN") && idle == warnDuration && !warned.getOrDefault(uid, false)) { + warned.put(uid, true); + player.sendMessage(plugin.getConfigManager().getPrefix() + + plugin.getConfigManager().getCampWarnMsg()); + player.playSound(player.getLocation(), + plugin.getConfigManager().getCampWarnSound(), 1f, + plugin.getConfigManager().getCampWarnSoundPitch()); + } + + // Strafphase + if (action.contains("PUNISH") && idle >= maxIdle) { + lp.addScore(-penalty); + // Score darf nicht unter 0 fallen + if (lp.getScore() < 0) lp.addScore(-lp.getScore()); + + player.sendMessage(plugin.getConfigManager().getPrefix() + + "§c⛔ Camp-Strafe: §7-" + penalty + " Punkte (Bewege dich!)"); + player.playSound(player.getLocation(), + plugin.getConfigManager().getCampWarnSound(), 1f, + plugin.getConfigManager().getCampWarnSoundPitch()); + + // Idle-Zähler auf maxIdle halten (weiter bestrafen jede Sekunde) + idleSeconds.put(uid, maxIdle); + } + } + } + + /** Spieler aus dem Camp-Tracking entfernen (bei Leave/End). */ + public void remove(UUID uuid) { + lastPosition.remove(uuid); + idleSeconds.remove(uuid); + warned.remove(uuid); + } + + /** Alle Daten zurücksetzen (bei Spiel-Reset). */ + public void reset() { + lastPosition.clear(); + idleSeconds.clear(); + warned.clear(); + } + + private void reset(UUID uid) { + lastPosition.remove(uid); + idleSeconds.put(uid, 0); + warned.put(uid, false); + } + + public int getIdleSeconds(UUID uuid) { + return idleSeconds.getOrDefault(uuid, 0); + } +} diff --git a/src/main/java/de/lasertag/command/LasertagCommand.java b/src/main/java/de/lasertag/command/LasertagCommand.java new file mode 100644 index 0000000..ae1f72c --- /dev/null +++ b/src/main/java/de/lasertag/command/LasertagCommand.java @@ -0,0 +1,126 @@ +package de.lasertag.command; + +import de.lasertag.LasertagPlugin; +import de.lasertag.game.Game; +import de.lasertag.game.Team; +import de.lasertag.player.LaserPlayer; +import de.lasertag.player.PlayerStats; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Map; + +public class LasertagCommand implements CommandExecutor { + + private final LasertagPlugin plugin; + + public LasertagCommand(LasertagPlugin plugin) { this.plugin = plugin; } + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { + String pre = plugin.getConfigManager().getPrefix(); + + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + help(sender); + return true; + } + + switch (args[0].toLowerCase()) { + + // ── join ────────────────────────────────────────────────────────── + case "join", "j" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("§cNur für Spieler!"); return true; } + if (!player.hasPermission("lasertag.play")) { + player.sendMessage(pre + "§cKeine Berechtigung (lasertag.play)"); + return true; + } + if (args.length >= 2) plugin.getGameManager().joinGame(player, args[1]); + else plugin.getGameManager().joinBest(player); + } + + // ── leave ───────────────────────────────────────────────────────── + case "leave", "l" -> { + if (!(sender instanceof Player player)) return true; + Game g = plugin.getGameManager().getGameOf(player); + if (g == null) player.sendMessage(pre + "§cDu bist in keinem Spiel!"); + else plugin.getGameManager().leaveGame(player); + } + + // ── list ────────────────────────────────────────────────────────── + case "list" -> { + sender.sendMessage("§8§l═══════════════════════════════════"); + sender.sendMessage("§b§l ⚡ LASERTAG – ARENEN"); + sender.sendMessage("§8§l═══════════════════════════════════"); + var games = plugin.getGameManager().getAllGames(); + if (games.isEmpty()) { sender.sendMessage("§c Keine Arenen konfiguriert."); return true; } + for (Game g : games) { + String status = switch (g.getState()) { + case WAITING -> "§aWartend"; + case STARTING -> "§eStartet"; + case RUNNING -> "§6Läuft"; + case ENDING -> "§7Beendet"; + }; + String ready = g.getArena().isReady() ? "§a✔" : "§c✘"; + sender.sendMessage(" " + ready + " §b" + g.getArena().getName() + + " §8[" + status + "§8] §7Spieler: §e" + g.getPlayerCount() + + "§7/§e" + g.getMaxPlayers()); + } + sender.sendMessage("§8§l═══════════════════════════════════"); + } + + // ── stats ───────────────────────────────────────────────────────── + case "stats" -> { + if (!(sender instanceof Player player)) return true; + PlayerStats s = plugin.getPlayerDataManager().getStats(player.getUniqueId()); + sender.sendMessage("§8§l══════════════════════════════"); + sender.sendMessage("§b§l ⚡ DEINE STATISTIKEN"); + sender.sendMessage("§8§l══════════════════════════════"); + sender.sendMessage("§7Spiele: §e" + s.getGamesPlayed()); + sender.sendMessage("§7Kills gesamt: §a" + s.getTotalKills()); + sender.sendMessage("§7Tode gesamt: §c" + s.getTotalDeaths()); + sender.sendMessage("§7K/D-Ratio: §6" + s.getKDR()); + sender.sendMessage("§7Punkte gesamt: §6" + s.getTotalScore()); + sender.sendMessage("§7Beste Kill-Serie:§b " + s.getBestStreak()); + sender.sendMessage("§7Basis-Angriffe: §d" + s.getTotalBaseAttacks()); + sender.sendMessage("§8§l══════════════════════════════"); + } + + // ── top ─────────────────────────────────────────────────────────── + case "top" -> { + List top = plugin.getPlayerDataManager().getTopByScore(10); + sender.sendMessage("§8§l═══════════════════════════════"); + sender.sendMessage("§6§l 🏆 BESTENLISTE"); + sender.sendMessage("§8§l═══════════════════════════════"); + for (int i = 0; i < top.size(); i++) { + PlayerStats s = top.get(i); + String medal = i == 0 ? "§6§l🥇" : i == 1 ? "§7§l🥈" : i == 2 ? "§c§l🥉" : "§8#" + (i+1); + sender.sendMessage(" " + medal + " §e" + s.getName() + + " §7│ §6" + s.getTotalScore() + " Pkt §7│ §aK:" + s.getTotalKills() + + " §7│ §bKDR:" + s.getKDR()); + } + if (top.isEmpty()) sender.sendMessage("§7Noch keine Statistiken vorhanden."); + sender.sendMessage("§8§l═══════════════════════════════"); + } + + default -> help(sender); + } + return true; + } + + private void help(CommandSender s) { + s.sendMessage("§8§l═══════════════════════════════════"); + s.sendMessage("§b§l ⚡ LASERTAG HILFE"); + s.sendMessage("§8§l═══════════════════════════════════"); + s.sendMessage("§b/lt join §8[Arena] §7– Einem Spiel beitreten"); + s.sendMessage("§b/lt leave §7– Spiel verlassen"); + s.sendMessage("§b/lt list §7– Arenen anzeigen"); + s.sendMessage("§b/lt stats §7– Deine Statistiken"); + s.sendMessage("§b/lt top §7– Bestenliste (Top 10)"); + if (s.hasPermission("lasertag.admin")) + s.sendMessage("§c/ltadmin help §7– Admin-Befehle"); + s.sendMessage("§8§l═══════════════════════════════════"); + } +} diff --git a/src/main/java/de/lasertag/command/LtAdminCommand.java b/src/main/java/de/lasertag/command/LtAdminCommand.java new file mode 100644 index 0000000..46859b5 --- /dev/null +++ b/src/main/java/de/lasertag/command/LtAdminCommand.java @@ -0,0 +1,220 @@ +package de.lasertag.command; + +import de.lasertag.LasertagPlugin; +import de.lasertag.arena.Arena; +import de.lasertag.game.Game; +import de.lasertag.game.Team; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class LtAdminCommand implements CommandExecutor { + + private final LasertagPlugin plugin; + + public LtAdminCommand(LasertagPlugin plugin) { this.plugin = plugin; } + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { + if (!sender.hasPermission("lasertag.admin")) { + sender.sendMessage("§cKeine Berechtigung!"); + return true; + } + String pre = plugin.getConfigManager().getPrefix(); + + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + help(sender); + return true; + } + + switch (args[0].toLowerCase()) { + + // ── Arena erstellen ─────────────────────────────────────────────── + case "create", "createarena" -> { + if (args.length < 2) { sender.sendMessage(pre + "§c/ltadmin create "); return true; } + Arena arena = plugin.getArenaManager().createArena(args[1]); + if (arena == null) { sender.sendMessage(pre + "§cArena existiert bereits!"); return true; } + plugin.getGameManager().createGame(arena); + sender.sendMessage(pre + "§aArena §e" + args[1] + " §aerstellt."); + sender.sendMessage(pre + "§7Nächste Schritte:"); + sender.sendMessage("§7 /ltadmin setspawn §e" + args[1] + " red §7(stehe am Spawn)"); + sender.sendMessage("§7 /ltadmin setbase §e" + args[1] + " red §7(stehe an der Basis)"); + sender.sendMessage("§7 → Für alle 4 Teams wiederholen"); + sender.sendMessage("§7 /ltadmin enable §e" + args[1]); + } + + // ── Arena löschen ───────────────────────────────────────────────── + case "delete", "deletearena" -> { + if (args.length < 2) { sender.sendMessage(pre + "§c/ltadmin delete "); return true; } + if (plugin.getArenaManager().deleteArena(args[1])) + sender.sendMessage(pre + "§aArena §e" + args[1] + " §agelöscht."); + else sender.sendMessage(pre + "§cArena nicht gefunden!"); + } + + // ── Spawn setzen ────────────────────────────────────────────────── + // /ltadmin setspawn + case "setspawn" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("§cNur für Spieler!"); return true; } + if (args.length < 3) { sender.sendMessage(pre + "§c/ltadmin setspawn "); return true; } + Arena arena = requireArena(sender, args[1]); if (arena == null) return true; + Team team = requireTeam(sender, args[2]); if (team == null) return true; + arena.addSpawn(team, player.getLocation()); + plugin.getArenaManager().save(); + sender.sendMessage(pre + "§aSpawn für Team " + team.colored() + " §ain Arena §e" + + arena.getName() + " §ahinzugefügt. §8(Gesamt: " + arena.getSpawns(team).size() + ")"); + } + + // ── Basis setzen ────────────────────────────────────────────────── + // /ltadmin setbase + case "setbase" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("§cNur für Spieler!"); return true; } + if (args.length < 3) { sender.sendMessage(pre + "§c/ltadmin setbase "); return true; } + Arena arena = requireArena(sender, args[1]); if (arena == null) return true; + Team team = requireTeam(sender, args[2]); if (team == null) return true; + arena.setBase(team, player.getLocation()); + plugin.getArenaManager().save(); + sender.sendMessage(pre + "§aBasis für Team " + team.colored() + " §aauf §e" + + blockStr(player) + " §agesetzt."); + sender.sendMessage(pre + "§7Beim Spielstart wird hier ein §b" + + team.getGlassMat().name() + " §7platziert."); + } + + // ── Lobby setzen ────────────────────────────────────────────────── + case "setlobby" -> { + if (!(sender instanceof Player player)) { sender.sendMessage("§cNur für Spieler!"); return true; } + plugin.getConfigManager().setLobbyLocation(player.getLocation()); + sender.sendMessage(pre + "§aLobby-Spawn gesetzt!"); + } + + // ── Arena aktivieren/deaktivieren ───────────────────────────────── + case "enable" -> { + if (args.length < 2) { sender.sendMessage(pre + "§c/ltadmin enable "); return true; } + Arena arena = requireArena(sender, args[1]); if (arena == null) return true; + if (!arena.isReady()) { + sender.sendMessage(pre + "§cArena nicht bereit!\n" + arena.getMissingSetup()); + return true; + } + arena.setEnabled(true); + plugin.getArenaManager().save(); + sender.sendMessage(pre + "§aArena §e" + arena.getName() + " §aaktiviert!"); + } + + case "disable" -> { + if (args.length < 2) { sender.sendMessage(pre + "§c/ltadmin disable "); return true; } + Arena arena = requireArena(sender, args[1]); if (arena == null) return true; + arena.setEnabled(false); + plugin.getArenaManager().save(); + sender.sendMessage(pre + "§cArena §e" + arena.getName() + " §cdeaktiviert."); + } + + // ── Arena-Info ──────────────────────────────────────────────────── + case "info" -> { + if (args.length < 2) { sender.sendMessage(pre + "§c/ltadmin info "); return true; } + Arena arena = requireArena(sender, args[1]); if (arena == null) return true; + sender.sendMessage("§8§l══════════════════════════════"); + sender.sendMessage("§b§l Arena: §e" + arena.getName()); + sender.sendMessage("§8§l══════════════════════════════"); + sender.sendMessage("§7Aktiviert: " + (arena.isEnabled() ? "§aJa" : "§cNein")); + sender.sendMessage("§7Bereit: " + (arena.isReady() ? "§aJa" : "§cNein")); + for (Team t : Team.values()) { + int spawns = arena.getSpawns(t).size(); + boolean hasBase = arena.hasBase(t); + sender.sendMessage(" " + t.colored() + " §7– Spawns: §e" + spawns + + " §7Basis: " + (hasBase ? "§a✔" : "§c✘")); + } + if (!arena.isReady()) { + sender.sendMessage("§c§lFehlend:"); + sender.sendMessage(arena.getMissingSetup()); + } + sender.sendMessage("§8§l══════════════════════════════"); + + // Spiel-Status + Game g = plugin.getGameManager().getGame(arena.getName()); + if (g != null) { + sender.sendMessage("§7Spiel-Status: " + g.getState() + + " §7Spieler: §e" + g.getPlayerCount()); + } + } + + // ── Spiel erzwingen ─────────────────────────────────────────────── + case "forcestart" -> { + if (args.length < 2) { sender.sendMessage(pre + "§c/ltadmin forcestart "); return true; } + Game g = plugin.getGameManager().getGame(args[1]); + if (g == null) { sender.sendMessage(pre + "§cArena/Spiel nicht gefunden!"); return true; } + switch (g.getState()) { + case WAITING -> { g.startCountdown(); sender.sendMessage(pre + "§aCountdown gestartet!"); } + case STARTING -> { g.startGame(); sender.sendMessage(pre + "§aSpiel gestartet!"); } + default -> sender.sendMessage(pre + "§cSpiel läuft bereits!"); + } + } + + case "forcestop" -> { + if (args.length < 2) { sender.sendMessage(pre + "§c/ltadmin forcestop "); return true; } + Game g = plugin.getGameManager().getGame(args[1]); + if (g == null) { sender.sendMessage(pre + "§cArena/Spiel nicht gefunden!"); return true; } + g.forceEnd(); + sender.sendMessage(pre + "§aSpiel beendet!"); + } + + // ── Reload ──────────────────────────────────────────────────────── + case "reload" -> { + plugin.getConfigManager().reload(); + sender.sendMessage(pre + "§aConfig neu geladen!"); + } + + case "modinfo" -> { + new de.lasertag.protection.ModProtectionManager(plugin).sendProtectionInfo(sender); + } + case "signs" -> { + sender.sendMessage(pre + "§7Registrierte Join-Schilder: §b" + plugin.getSignListener().getSignCount()); + sender.sendMessage(pre + "§7Erstelle ein Schild: Zeile 1 = §e[Lasertag] §7| Zeile 2 = Arenaname"); + } + default -> help(sender); + } + return true; + } + + // ─── Helper ────────────────────────────────────────────────────────────── + + private Arena requireArena(CommandSender s, String name) { + Arena a = plugin.getArenaManager().getArena(name); + if (a == null) s.sendMessage(plugin.getConfigManager().getPrefix() + "§cArena '§e" + name + "§c' nicht gefunden!"); + return a; + } + + private Team requireTeam(CommandSender s, String name) { + try { return Team.valueOf(name.toUpperCase()); } + catch (IllegalArgumentException ex) { + s.sendMessage(plugin.getConfigManager().getPrefix() + + "§cUngültiges Team '§e" + name + "§c'! Benutze: red, blue, green, yellow"); + return null; + } + } + + private String blockStr(Player p) { + return p.getLocation().getBlockX() + "," + p.getLocation().getBlockY() + "," + p.getLocation().getBlockZ(); + } + + private void help(CommandSender s) { + s.sendMessage("§8§l══════════════════════════════════════════"); + s.sendMessage("§c§l ⚡ LASERTAG ADMIN-BEFEHLE"); + s.sendMessage("§8§l══════════════════════════════════════════"); + s.sendMessage("§c/ltadmin create §8 §7– Arena erstellen"); + s.sendMessage("§c/ltadmin delete §8 §7– Arena löschen"); + s.sendMessage("§c/ltadmin setspawn §8 §7– Spawn setzen (stehe am Punkt)"); + s.sendMessage("§c/ltadmin setbase §8 §7– Basis-Block setzen"); + s.sendMessage("§c/ltadmin setlobby §7– Lobby-Spawn setzen"); + s.sendMessage("§c/ltadmin enable §8 §7– Arena aktivieren"); + s.sendMessage("§c/ltadmin disable §8 §7– Arena deaktivieren"); + s.sendMessage("§c/ltadmin info §8 §7– Arena-Infos anzeigen"); + s.sendMessage("§c/ltadmin forcestart §8 §7– Countdown/Spiel starten"); + s.sendMessage("§c/ltadmin forcestop §8 §7– Spiel beenden"); + s.sendMessage("§c/ltadmin reload §7– Config neu laden"); + s.sendMessage("§8§l══════════════════════════════════════════"); + s.sendMessage("§c/ltadmin modinfo §7– Mod-Schutz Status anzeigen"); + s.sendMessage("§c/ltadmin signs §7– Schild-Übersicht"); + s.sendMessage("§8§l══════════════════════════════════════════"); + s.sendMessage("§7Teams: §cred §9blue §agreen §eyelow"); + } +} diff --git a/src/main/java/de/lasertag/config/ConfigManager.java b/src/main/java/de/lasertag/config/ConfigManager.java new file mode 100644 index 0000000..c45c5f6 --- /dev/null +++ b/src/main/java/de/lasertag/config/ConfigManager.java @@ -0,0 +1,178 @@ +package de.lasertag.config; + +import de.lasertag.LasertagPlugin; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; + +/** + * Zentraler Zugriffspunkt für alle config.yml-Werte. + * Alle Getter lesen live aus dem Config-Objekt → /ltadmin reload wirkt sofort. + */ +public class ConfigManager { + + private final LasertagPlugin plugin; + + public ConfigManager(LasertagPlugin plugin) { + this.plugin = plugin; + plugin.saveDefaultConfig(); + } + + public void reload() { + plugin.reloadConfig(); + } + + // ── Allgemein ───────────────────────────────────────────────────────────── + + public String getPrefix() { + return c().getString("messages.prefix", "§8[§b§lLASERTAG§8] §r"); + } + + public Location getLobbyLocation() { + return (Location) c().get("lobby.location"); + } + + public void setLobbyLocation(Location loc) { + c().set("lobby.location", loc); + plugin.saveConfig(); + } + + // ── Spiel-Grundeinstellungen ────────────────────────────────────────────── + + public int getGameDuration() { return c().getInt("game.game-duration", 300); } + public int getCountdown() { return c().getInt("game.countdown", 10); } + public int getMinPlayers() { return c().getInt("game.min-players", 2); } + public int getMaxPlayersPerTeam() { return c().getInt("game.max-players-per-team", 4); } + public int getEndDisplayTime() { return c().getInt("game.end-display-time", 8); } + + // ── Heal / Hit-Mechanik ─────────────────────────────────────────────────── + + public int getBaseHealTime() { return c().getInt("heal.base-heal-time", 2); } + public double getBaseRadius() { return c().getDouble("heal.base-radius", 4.0); } + public boolean isInvincibleAfterHeal() { return c().getBoolean("heal.invincible-after-heal", true); } + public int getInvincibleDuration() { return c().getInt("heal.invincible-duration", 60); } + + // ── Punkte-System ───────────────────────────────────────────────────────── + + public int getKillPoints() { return c().getInt("scoring.kill-points", 100); } + public int getBaseAttackPoints() { return c().getInt("scoring.base-attack-points", 60); } + public int getBaseDestroyBonus() { return c().getInt("scoring.base-destroy-bonus", 200); } + public int getStreak3Bonus() { return c().getInt("scoring.streak-3-bonus", 50); } + public int getStreak5Bonus() { return c().getInt("scoring.streak-5-bonus", 100); } + public int getStreak10Bonus() { return c().getInt("scoring.streak-10-bonus", 250); } + + // ── Basen ───────────────────────────────────────────────────────────────── + + public int getBaseHealth() { return c().getInt("base.health", 5); } + public int getBaseWarnHp() { return c().getInt("base.warn-at-hp", 2); } + public boolean isBaseRegenerate() { return c().getBoolean("base.regenerate", false); } + public int getBaseRegenInterval() { return c().getInt("base.regenerate-interval", 60); } + + // ── Anti-Camp ───────────────────────────────────────────────────────────── + + public boolean isAntiCampEnabled() { return c().getBoolean("anti-camp.enabled", true); } + public int getCampMaxIdleSecs() { return c().getInt("anti-camp.max-idle-seconds", 15); } + public double getCampIdleRadius() { return c().getDouble("anti-camp.idle-radius", 5.0); } + public String getCampAction() { return c().getString("anti-camp.action", "WARN_THEN_PUNISH"); } + public String getCampWarnMsg() { return c().getString("anti-camp.warn-message", "§c⚠ CAMPEN VERBOTEN!"); } + public int getCampScorePenalty() { return c().getInt("anti-camp.score-penalty", 10); } + public int getCampWarnDuration() { return c().getInt("anti-camp.warn-duration", 5); } + public double getCampExcludeRadius() { return c().getDouble("anti-camp.exclude-base-radius", 8.0); } + + public Sound getCampWarnSound() { + try { return Sound.valueOf(c().getString("anti-camp.warn-sound", "BLOCK_NOTE_BLOCK_BASS")); } + catch (Exception e) { return Sound.BLOCK_NOTE_BLOCK_BASS; } + } + public float getCampWarnSoundPitch() { return (float) c().getDouble("anti-camp.warn-sound-pitch", 0.5); } + + // ── Mod-Schutz ──────────────────────────────────────────────────────────── + + public boolean isModProtectionEnabled() { return c().getBoolean("mod-protection.enabled", true); } + public boolean isFogOfWarEnabled() { return c().getBoolean("mod-protection.fog-of-war", true); } + public int getFogRadius() { return c().getInt("mod-protection.fog-radius", 48); } + public boolean isArenaBarrierEnabled() { return c().getBoolean("mod-protection.arena-barrier", true); } + public boolean isHideCoordinates() { return c().getBoolean("mod-protection.hide-coordinates",true); } + public boolean isHideFromTab() { return c().getBoolean("mod-protection.hide-from-tab", true); } + public boolean isHideNametags() { return c().getBoolean("mod-protection.hide-nametags", true); } + public boolean isStrictInvisibility() { return c().getBoolean("mod-protection.strict-invisibility", true); } + + // ── Join-Schild ─────────────────────────────────────────────────────────── + + public String getSignTriggerLine() { return c().getString("join-sign.trigger-line", "[Lasertag]"); } + public String getSignColorWaiting() { return c().getString("join-sign.color-waiting", "§a"); } + public String getSignColorStarting(){ return c().getString("join-sign.color-starting","§e"); } + public String getSignColorRunning() { return c().getString("join-sign.color-running", "§c"); } + public String getSignColorFull() { return c().getString("join-sign.color-full", "§8"); } + public int getSignUpdateInterval(){ return c().getInt("join-sign.update-interval", 20); } + + // ── Waffen (dynamisch aus config lesen) ─────────────────────────────────── + + public int getWeaponDamage(String key) { return c().getInt("weapons." + key + ".damage", 25); } + public int getWeaponRange(String key) { return c().getInt("weapons." + key + ".range", 30); } + public int getWeaponCooldown(String key) { return c().getInt("weapons." + key + ".cooldown-ms", 300); } + public int getWeaponPellets(String key) { return c().getInt("weapons." + key + ".pellets", 1); } + public String getWeaponName(String key) { return c().getString("weapons." + key + ".display-name", "§fWaffe"); } + public boolean isWeaponEnabled(String key) { return c().getBoolean("weapons." + key + ".enabled", true); } + public String getWeaponDesc(String key) { return c().getString("weapons." + key + ".description", ""); } + + public Particle getWeaponParticle(String key) { + try { return Particle.valueOf(c().getString("weapons." + key + ".particle", "CRIT")); } + catch (Exception e) { return Particle.CRIT; } + } + + // ── Sounds ──────────────────────────────────────────────────────────────── + + public boolean isSoundsEnabled() { return c().getBoolean("sounds.enabled", true); } + + public Sound getSound(String key, Sound fallback) { + if (!isSoundsEnabled()) return null; + try { return Sound.valueOf(c().getString("sounds." + key, fallback.name())); } + catch (Exception e) { return fallback; } + } + + public float getSoundPitch(String key, float fallback) { + return (float) c().getDouble("sounds." + key, fallback); + } + + public float getSoundVolume(String key, float fallback) { + return (float) c().getDouble("sounds." + key, fallback); + } + + // ── Partikel ────────────────────────────────────────────────────────────── + + public boolean isParticlesEnabled() { return c().getBoolean("particles.enabled", true); } + public boolean isLaserTrailEnabled() { return c().getBoolean("particles.laser-trail", true); } + public boolean isHitEffectEnabled() { return c().getBoolean("particles.hit-effect", true); } + public int getHitParticleCount() { return c().getInt("particles.hit-particle-count", 15); } + public boolean isHealEffectEnabled() { return c().getBoolean("particles.heal-effect", true); } + + // ── Scoreboard ──────────────────────────────────────────────────────────── + + public boolean isScoreboardEnabled() { return c().getBoolean("scoreboard.enabled", true); } + public String getScoreboardTitle() { return c().getString("scoreboard.title", "§b§l⚡ LASERTAG"); } + public boolean showTeamScores() { return c().getBoolean("scoreboard.show-team-scores", true); } + public boolean showBaseHealth() { return c().getBoolean("scoreboard.show-base-health", true); } + public boolean showKillStreak() { return c().getBoolean("scoreboard.show-kill-streak", true); } + public int getScoreboardInterval() { return c().getInt("scoreboard.update-interval", 20); } + + // ── Texte / Nachrichten ─────────────────────────────────────────────────── + + public String getText(String key, String fallback) { + return getPrefix() + c().getString("text." + key, fallback); + } + + /** Ersetzt Platzhalter wie {player}, {team}, {pts} etc. */ + public String getText(String key, String fallback, Object... replacements) { + String s = getText(key, fallback); + for (int i = 0; i + 1 < replacements.length; i += 2) { + s = s.replace("{" + replacements[i] + "}", String.valueOf(replacements[i + 1])); + } + return s; + } + + // ── Intern ─────────────────────────────────────────────────────────────── + + private org.bukkit.configuration.file.FileConfiguration c() { + return plugin.getConfig(); + } +} diff --git a/src/main/java/de/lasertag/game/Game.java b/src/main/java/de/lasertag/game/Game.java new file mode 100644 index 0000000..b8b1a2c --- /dev/null +++ b/src/main/java/de/lasertag/game/Game.java @@ -0,0 +1,736 @@ +package de.lasertag.game; + +import de.lasertag.LasertagPlugin; +import de.lasertag.arena.Arena; +import de.lasertag.camp.AntiCampManager; +import de.lasertag.player.LaserPlayer; +import de.lasertag.protection.ModProtectionManager; +import de.lasertag.weapon.WeaponType; +import de.lasertag.weapon.WeaponUtil; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class Game { + + private final LasertagPlugin plugin; + private final Arena arena; + private final AntiCampManager antiCamp; + private final ModProtectionManager modProtect; + + private GameState state = GameState.WAITING; + private int timeLeft; + private int countdown; + + private final Map players = new ConcurrentHashMap<>(); + private final Map teamScore = new LinkedHashMap<>(); + private final Map shotCooldown= new ConcurrentHashMap<>(); + + private BukkitTask countdownTask, gameTask, healTask; + + public Game(LasertagPlugin plugin, Arena arena) { + this.plugin = plugin; + this.arena = arena; + this.antiCamp = new AntiCampManager(plugin); + this.modProtect = new ModProtectionManager(plugin); + this.timeLeft = plugin.getConfigManager().getGameDuration(); + this.countdown = plugin.getConfigManager().getCountdown(); + for (Team t : Team.values()) teamScore.put(t, 0); + arena.initBaseHealth(plugin.getConfigManager().getBaseHealth()); + } + + // ────────────────────────────────────────────────────────────────────────── + // SPIELER-VERWALTUNG + // ────────────────────────────────────────────────────────────────────────── + + public boolean addPlayer(Player player) { + if (players.containsKey(player.getUniqueId())) return false; + if (!isJoinable()) return false; + int maxTotal = plugin.getConfigManager().getMaxPlayersPerTeam() * 4; + if (players.size() >= maxTotal) return false; + + LaserPlayer lp = new LaserPlayer(player.getUniqueId(), player.getName()); + lp.setTeam(pickBalancedTeam()); + lp.setSavedInventory(player.getInventory().getContents().clone()); + players.put(player.getUniqueId(), lp); + + setupPlayer(player, lp); + sendToSpawn(player, lp.getTeam()); + modProtect.applyProtection(player, this); + + broadcast(plugin.getConfigManager().getText("join", + "§e{player} §7hat Team {team} §7beigetreten.", + "player", player.getName(), "team", lp.getTeam().colored())); + + plugin.getScoreboardManager().update(player, this); + + if (state == GameState.WAITING + && players.size() >= plugin.getConfigManager().getMinPlayers()) { + startCountdown(); + } + return true; + } + + public void removePlayer(Player player) { + LaserPlayer lp = players.remove(player.getUniqueId()); + if (lp == null) return; + shotCooldown.remove(player.getUniqueId()); + antiCamp.remove(player.getUniqueId()); + modProtect.removeProtection(player); + + saveStats(lp, player); + restorePlayer(player, lp); + plugin.getScoreboardManager().remove(player); + + broadcast(plugin.getConfigManager().getText("leave", + "§c{player} §7hat das Spiel verlassen.", "player", player.getName())); + + if (state == GameState.RUNNING + && players.size() < plugin.getConfigManager().getMinPlayers()) { + endGame(); + } + } + + private void saveStats(LaserPlayer lp, Player player) { + var stats = plugin.getPlayerDataManager().getStats(lp.getUuid()); + stats.setName(player.getName()); + stats.apply(lp); + } + + // ────────────────────────────────────────────────────────────────────────── + // SPIEL-ABLAUF + // ────────────────────────────────────────────────────────────────────────── + + public void startCountdown() { + if (state != GameState.WAITING) return; + state = GameState.STARTING; + + final int[] cd = { plugin.getConfigManager().getCountdown() }; + countdownTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + if (cd[0] <= 0) { countdownTask.cancel(); startGame(); return; } + if (cd[0] <= 5 || cd[0] == 10) { + broadcast("§eSpiel startet in §b" + cd[0] + " §eSekunden!"); + playAll(Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1f); + } + float prog = (float) cd[0] / plugin.getConfigManager().getCountdown(); + for (Player p : getOnline()) p.setExp(Math.max(0f, Math.min(1f, prog))); + cd[0]--; + }, 0L, 20L); + } + + public void startGame() { + state = GameState.RUNNING; + placeBaseBlocks(); + + for (Player p : getOnline()) { + p.sendTitle("§b§l⚡ LASERTAG", "§7Verteidige eure Basis!", 10, 50, 10); + p.setExp(1f); + } + playAll(plugin.getConfigManager().getSound("game-start", Sound.ENTITY_ENDER_DRAGON_GROWL), 0.5f, 1.2f); + broadcast(plugin.getConfigManager().getText("game-start", "§a§l⚡ DAS SPIEL BEGINNT!")); + + // Heal-Check + AntiCamp: jede Sekunde + healTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + tickHeal(); + antiCamp.tick(this); + }, 0L, 20L); + + // Haupt-Timer + final int[] tl = { plugin.getConfigManager().getGameDuration() }; + gameTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + if (tl[0] <= 0) { gameTask.cancel(); endGame(); return; } + if (tl[0] == 60) broadcast(plugin.getConfigManager().getText("time-60", "§e⏰ Noch §b60 §eSekunden!")); + if (tl[0] == 30) broadcast(plugin.getConfigManager().getText("time-30", "§c⏰ Noch §b30 §eSekunden!")); + if (tl[0] == 10) broadcast(plugin.getConfigManager().getText("time-10", "§4⏰ Noch §b10 §4Sekunden!")); + + // Basis regenerieren? + if (plugin.getConfigManager().isBaseRegenerate()) { + int interval = plugin.getConfigManager().getBaseRegenInterval(); + if (tl[0] % interval == 0) { + for (Team t : Team.values()) { + int hp = arena.getBaseHealth(t); + int max = plugin.getConfigManager().getBaseHealth(); + if (hp < max && hp > 0) { + arena.damageBase(t); // Missbrauche: erhöhe via setBaseHealth direkt + // Eigentlich: arena.regenBase(t); — wir ergänzen das + } + } + } + } + + timeLeft = tl[0]; + for (Player p : getOnline()) { + plugin.getScoreboardManager().update(p, this); + sendActionBar(p); + } + tl[0]--; + }, 0L, 20L); + } + + public void endGame() { + if (state == GameState.ENDING) return; + state = GameState.ENDING; + cancelTasks(); + removeBaseBlocks(); + modProtect.removeAll(this); + antiCamp.reset(); + + Team winner = getLeader(); + for (Player p : getOnline()) { + LaserPlayer lp = players.get(p.getUniqueId()); + boolean won = winner != null && lp != null && lp.getTeam() == winner; + p.sendTitle(won ? "§a§lSIEG!" : "§c§lNIEDERLAGE!", + winner != null ? "Team " + winner.colored() + " §7gewinnt!" : "§7Unentschieden!", + 10, 80, 20); + p.playSound(p.getLocation(), + won ? Sound.UI_TOAST_CHALLENGE_COMPLETE : Sound.ENTITY_VILLAGER_NO, 1f, 1f); + showEndStats(p, lp); + saveStats(lp, p); + } + plugin.getPlayerDataManager().saveAll(); + + String endMsg = winner != null + ? plugin.getConfigManager().getText("game-end", "§6§l🏆 Team {team} §6§lhat gewonnen!", + "team", winner.colored()) + : plugin.getConfigManager().getText("game-draw", "§7Unentschieden!"); + broadcast(endMsg); + + int delay = plugin.getConfigManager().getEndDisplayTime() * 20; + Bukkit.getScheduler().runTaskLater(plugin, this::reset, delay); + } + + private void reset() { + new ArrayList<>(getOnline()).forEach(this::removePlayer); + state = GameState.WAITING; + timeLeft = plugin.getConfigManager().getGameDuration(); + countdown = plugin.getConfigManager().getCountdown(); + players.clear(); + shotCooldown.clear(); + antiCamp.reset(); + for (Team t : Team.values()) teamScore.put(t, 0); + arena.initBaseHealth(plugin.getConfigManager().getBaseHealth()); + } + + // ────────────────────────────────────────────────────────────────────────── + // SCHUSS-MECHANIK + // ────────────────────────────────────────────────────────────────────────── + + public boolean handleShot(Player shooter, WeaponType weapon) { + LaserPlayer lp = players.get(shooter.getUniqueId()); + if (lp == null || state != GameState.RUNNING) return false; + + if (lp.isHit()) { + sendActionBarMsg(shooter, "§c☠ GETROFFEN – Gehe zur §e" + + lp.getTeam().colored() + " §cBasis!"); + playSound(shooter, Sound.BLOCK_NOTE_BLOCK_DIDGERIDOO, 0.5f, 0.5f); + return false; + } + + long now = System.currentTimeMillis(); + long last = shotCooldown.getOrDefault(shooter.getUniqueId(), 0L); + int cooldown = plugin.getConfigManager().getWeaponCooldown(weapon.getConfigKey()); + if (now - last < cooldown) { + double rem = (cooldown - (now - last)) / 1000.0; + sendActionBarMsg(shooter, "§c⏳ Nachladen... §7(" + String.format("%.1f", rem) + "s)"); + return false; + } + shotCooldown.put(shooter.getUniqueId(), now); + + int pellets = plugin.getConfigManager().getWeaponPellets(weapon.getConfigKey()); + if (pellets > 1) { + for (int i = 0; i < pellets; i++) shootRay(shooter, lp, weapon, spreadDir(shooter)); + } else { + shootRay(shooter, lp, weapon, shooter.getEyeLocation().getDirection()); + } + + Sound shootSnd = plugin.getConfigManager().getSound("shoot", Sound.ENTITY_FIREWORK_ROCKET_BLAST); + float shootPitch = plugin.getConfigManager().getSoundPitch("shoot-pitch", 1.8f); + float shootVol = plugin.getConfigManager().getSoundVolume("shoot-volume", 0.4f); + if (shootSnd != null) shooter.getWorld().playSound(shooter.getLocation(), shootSnd, shootVol, shootPitch); + return true; + } + + private void shootRay(Player shooter, LaserPlayer shooterLp, WeaponType weapon, Vector dir) { + Location eye = shooter.getEyeLocation(); + int range = plugin.getConfigManager().getWeaponRange(weapon.getConfigKey()); + + if (plugin.getConfigManager().isLaserTrailEnabled()) + drawLaser(eye, dir, weapon, range); + + RayTraceResult result = shooter.getWorld().rayTrace( + eye, dir, range, FluidCollisionMode.NEVER, true, 0.5, + e -> e instanceof Player && !e.equals(shooter)); + if (result == null) return; + + // ── Spieler getroffen ───────────────────────────────────────────────── + if (result.getHitEntity() instanceof Player victim) { + LaserPlayer vLp = players.get(victim.getUniqueId()); + if (vLp == null || vLp.isHit()) return; + if (vLp.getTeam() == shooterLp.getTeam()) return; + + vLp.applyHit(); + + // Partikel + if (plugin.getConfigManager().isHitEffectEnabled()) { + victim.getWorld().spawnParticle(Particle.CRIT, + victim.getLocation().add(0,1,0), + plugin.getConfigManager().getHitParticleCount(), + 0.4, 0.4, 0.4, 0.1); + } + + // Sounds + playSound(victim, plugin.getConfigManager().getSound("hit-victim", Sound.ENTITY_PLAYER_HURT), 1f, 0.8f); + playSound(shooter, plugin.getConfigManager().getSound("hit-shooter", Sound.BLOCK_NOTE_BLOCK_BELL), 1f, 2f); + + // Punkte + int pts = plugin.getConfigManager().getKillPoints(); + int streak = shooterLp.getKillStreak() + 1; + int bonus = 0; + if (streak == 3) bonus = plugin.getConfigManager().getStreak3Bonus(); + if (streak == 5) bonus = plugin.getConfigManager().getStreak5Bonus(); + if (streak == 10) bonus = plugin.getConfigManager().getStreak10Bonus(); + + shooterLp.registerKill(); + shooterLp.addScore(pts + bonus); + teamScore.merge(shooterLp.getTeam(), pts + bonus, Integer::sum); + + String streakInfo = streak >= 3 ? " §6[" + streak + "er-Serie!+"+bonus+"]" : ""; + shooter.sendMessage(plugin.getConfigManager().getText("hit-shooter", + "§aDu hast §e{victim} §agetroffen! §7(+{pts} Pkt){streak}", + "victim", victim.getName(), "pts", pts + bonus, "streak", streakInfo)); + victim.sendMessage(plugin.getConfigManager().getText("hit-victim", + "§cDu wurdest von §e{shooter} §cgetroffen!", + "shooter", shooter.getName())); + + // Streak-Broadcast + if (streak == 3) broadcastStreak(shooter, "streak-3", "§6TRIPLE KILL!", 3); + if (streak == 5) broadcastStreak(shooter, "streak-5", "§bPENTA KILL!", 5); + if (streak == 10) broadcastStreak(shooter, "streak-10", "§5§lGODLIKE!", 10); + + sendActionBar(victim); + sendActionBar(shooter); + plugin.getScoreboardManager().update(shooter, this); + plugin.getScoreboardManager().update(victim, this); + return; + } + + // ── Block getroffen (Basisangriff) ──────────────────────────────────── + if (result.getHitBlock() != null) { + Team baseTeam = arena.getBaseTeam(result.getHitBlock().getLocation()); + if (baseTeam != null && baseTeam != shooterLp.getTeam()) { + handleBaseAttack(shooter, shooterLp, baseTeam, + result.getHitBlock().getLocation()); + } + } + } + + // ────────────────────────────────────────────────────────────────────────── + // HEAL-TICK + // ────────────────────────────────────────────────────────────────────────── + + private void tickHeal() { + double baseRadiusSq = Math.pow(plugin.getConfigManager().getBaseRadius(), 2); + int healTimeSec = plugin.getConfigManager().getBaseHealTime(); + + for (Map.Entry e : players.entrySet()) { + Player p = Bukkit.getPlayer(e.getKey()); + LaserPlayer lp = e.getValue(); + if (p == null || !lp.isHit()) continue; + + Location base = arena.getBase(lp.getTeam()); + if (base == null) continue; + + boolean near = p.getLocation().distanceSquared(base) <= baseRadiusSq; + + if (near) { + if (!lp.isHealing()) { + lp.startHealing(); + p.sendMessage(plugin.getConfigManager().getText("heal-start", + "§aHeilung gestartet...", "secs", healTimeSec)); + playSound(p, plugin.getConfigManager().getSound("heal-start", + Sound.BLOCK_ENCHANTMENT_TABLE_USE), 1f, 1f); + } else if (lp.healElapsedMs() >= healTimeSec * 1000L) { + lp.completeHeal(); + p.sendMessage(plugin.getConfigManager().getText("heal-complete", + "§a✔ Du bist wieder einsatzbereit!")); + Sound hs = plugin.getConfigManager().getSound("heal-complete", + Sound.ENTITY_EXPERIENCE_ORB_PICKUP); + float hp = plugin.getConfigManager().getSoundPitch("heal-complete-pitch", 1.5f); + playSound(p, hs, 1f, hp); + p.sendTitle("§a§lBEREIT!", "§7Du kannst wieder schießen!", 5, 30, 5); + if (plugin.getConfigManager().isInvincibleAfterHeal()) { + p.addPotionEffect(new PotionEffect(PotionEffectType.DAMAGE_RESISTANCE, + plugin.getConfigManager().getInvincibleDuration(), 4, false, false)); + } + if (plugin.getConfigManager().isHealEffectEnabled()) { + p.getWorld().spawnParticle(Particle.VILLAGER_HAPPY, + p.getLocation().add(0,1,0), 20, 0.5, 0.5, 0.5, 0.05); + } + } else { + double prog = lp.healElapsedMs() / (healTimeSec * 1000.0); + sendActionBarMsg(p, "§a🛡 Heilung: " + buildBar(prog, 15) + + " §7" + String.format("%.1f", lp.healElapsedMs()/1000.0) + + "§8/" + healTimeSec + "s"); + } + } else { + if (lp.isHealing()) { + lp.stopHealing(); + p.sendMessage(plugin.getConfigManager().getText("heal-interrupted", + "§c⚠ Heilung abgebrochen!")); + } + Location bl = base; + double dist = p.getLocation().distance(bl); + sendActionBarMsg(p, "§c☠ GETROFFEN – Zur §e" + lp.getTeam().colored() + + " §cBasis! §8(" + String.format("%.0f",dist) + "m)"); + } + } + } + + // ────────────────────────────────────────────────────────────────────────── + // BASIS-ANGRIFF + // ────────────────────────────────────────────────────────────────────────── + + private void handleBaseAttack(Player shooter, LaserPlayer lp, Team baseTeam, Location blockLoc) { + if (arena.isBaseDestroyed(baseTeam)) return; + boolean damaged = arena.damageBase(baseTeam); + if (!damaged) return; + + int pts = plugin.getConfigManager().getBaseAttackPoints(); + int hp = arena.getBaseHealth(baseTeam); + int maxHp = plugin.getConfigManager().getBaseHealth(); + lp.addScore(pts); + lp.addBaseAttack(); + teamScore.merge(lp.getTeam(), pts, Integer::sum); + + updateBaseBlock(baseTeam, hp, maxHp); + + if (plugin.getConfigManager().isParticlesEnabled()) { + blockLoc.getWorld().spawnParticle(Particle.EXPLOSION_LARGE, + blockLoc.clone().add(0.5,0.5,0.5), 3, 0.2,0.2,0.2,0.05); + } + Sound bhs = plugin.getConfigManager().getSound("base-hit", Sound.ENTITY_GENERIC_EXPLODE); + if (bhs != null) blockLoc.getWorld().playSound(blockLoc, bhs, 0.5f, 1.5f); + + shooter.sendMessage(plugin.getConfigManager().getPrefix() + "§6⚔ Basis von Team " + + baseTeam.colored() + " §6angegriffen! §7(+" + pts + " Pkt) §8[HP: " + hp + "/" + maxHp + "]"); + + // Warn-HP aus Config + int warnHp = plugin.getConfigManager().getBaseWarnHp(); + + for (Map.Entry e : players.entrySet()) { + if (e.getValue().getTeam() != baseTeam) continue; + Player p = Bukkit.getPlayer(e.getKey()); + if (p == null) continue; + p.sendMessage(plugin.getConfigManager().getText("base-attacked", + "§c⚠ Eure Basis wird angegriffen! §8[HP: {hp}/{max}]", + "hp", hp, "max", maxHp)); + if (hp <= warnHp) + p.sendTitle("§c§l⚠ BASIS KRITISCH!", "§7HP: " + hp + "/" + maxHp, 5, 40, 10); + playSound(p, plugin.getConfigManager().getSound("warning", Sound.BLOCK_BELL_USE), 1f, 0.5f); + } + + broadcast(plugin.getConfigManager().getPrefix() + "§6⚔ §e" + shooter.getName() + + " §6hat die Basis von Team " + baseTeam.colored() + + " §6getroffen! §8(" + hp + "/" + maxHp + ")"); + + if (hp <= 0) { + broadcast(plugin.getConfigManager().getText("base-destroyed", + "§c§l💥 Die Basis von Team {team} §c§lwurde ZERSTÖRT!", + "team", baseTeam.colored())); + int bonus = plugin.getConfigManager().getBaseDestroyBonus(); + lp.addScore(bonus); + teamScore.merge(lp.getTeam(), bonus, Integer::sum); + shooter.sendMessage(plugin.getConfigManager().getPrefix() + + "§6§l+" + bonus + " Bonuspunkte für Basis-Zerstörung!"); + playAll(Sound.ENTITY_ENDER_DRAGON_GROWL, 0.5f, 0.7f); + } + + plugin.getScoreboardManager().update(shooter, this); + } + + // ────────────────────────────────────────────────────────────────────────── + // SPIELER SETUP + // ────────────────────────────────────────────────────────────────────────── + + private void setupPlayer(Player player, LaserPlayer lp) { + player.setGameMode(GameMode.ADVENTURE); + player.getInventory().clear(); + player.setHealth(20); player.setFoodLevel(20); player.setSaturation(20f); + for (PotionEffect e : player.getActivePotionEffects()) + player.removePotionEffect(e.getType()); + + int slot = 0; + for (WeaponType w : WeaponType.values()) { + if (plugin.getConfigManager().isWeaponEnabled(w.getConfigKey())) { + player.getInventory().setItem(slot++, WeaponUtil.create(plugin, w)); + } + } + player.getInventory().setHeldItemSlot(0); + giveArmor(player, lp.getTeam()); + } + + private void giveArmor(Player player, Team team) { + // Helm: farbiger Woll-Block als Kopfbedeckung — auf einen Blick erkennbar + player.getInventory().setHelmet (makeHelm(team)); + player.getInventory().setChestplate(coloredLeather(Material.LEATHER_CHESTPLATE, team)); + player.getInventory().setLeggings (coloredLeather(Material.LEATHER_LEGGINGS, team)); + player.getInventory().setBoots (coloredLeather(Material.LEATHER_BOOTS, team)); + } + + /** + * Helm: Leder-Helm mit kräftiger Teamfarbe (RGB). + * Enchantment-Glanz macht ihn zusätzlich im Dunkeln erkennbar. + */ + private ItemStack makeHelm(Team team) { + ItemStack item = new ItemStack(Material.LEATHER_HELMET); + LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta(); + meta.setColor(team.getArmorColor()); + meta.setDisplayName(team.getChatColor() + "§l" + team.getDisplayName().toUpperCase() + + " §r" + team.getChatColor() + "Helm"); + meta.setLore(java.util.List.of( + "§8Lasertag – " + team.colored() + " §8Team", + "§8Rüstung kann nicht abgelegt werden" + )); + // Glanz damit der Helm optisch hervorsticht + meta.addEnchant(org.bukkit.enchantments.Enchantment.PROTECTION_ENVIRONMENTAL, 1, true); + meta.setUnbreakable(true); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_UNBREAKABLE, + ItemFlag.HIDE_ENCHANTS); + item.setItemMeta(meta); + return item; + } + + /** + * Lederrüstung mit kräftiger Teamfarbe (RGB) und Lore. + * Brustplatte, Hose und Stiefel sind farbig — zusammen mit dem + * Woll-Helm ergibt sich ein eindeutiges Team-Outfit. + */ + private ItemStack coloredLeather(Material mat, Team team) { + String partName = switch (mat) { + case LEATHER_CHESTPLATE -> "Brustplatte"; + case LEATHER_LEGGINGS -> "Hose"; + case LEATHER_BOOTS -> "Stiefel"; + default -> "Rüstung"; + }; + ItemStack item = new ItemStack(mat); + LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta(); + meta.setColor(team.getArmorColor()); + meta.setDisplayName(team.getChatColor() + "Team " + team.getDisplayName() + + " – " + partName); + meta.setLore(java.util.List.of( + "§8Lasertag – " + team.colored() + " §8Team", + "§8Rüstung kann nicht abgelegt werden" + )); + meta.setUnbreakable(true); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_UNBREAKABLE, + ItemFlag.HIDE_ENCHANTS); + item.setItemMeta(meta); + return item; + } + + private void restorePlayer(Player player, LaserPlayer lp) { + player.getInventory().clear(); + if (lp.getSavedInventory() != null) + player.getInventory().setContents(lp.getSavedInventory()); + player.setGameMode(GameMode.SURVIVAL); + player.setHealth(20); player.setFoodLevel(20); + for (PotionEffect e : player.getActivePotionEffects()) + player.removePotionEffect(e.getType()); + Location lobby = plugin.getConfigManager().getLobbyLocation(); + if (lobby != null) player.teleport(lobby); + } + + private void sendToSpawn(Player player, Team team) { + Location spawn = arena.getRandomSpawn(team); + if (spawn != null) player.teleport(spawn); + } + + // ────────────────────────────────────────────────────────────────────────── + // BASIS-BLÖCKE + // ────────────────────────────────────────────────────────────────────────── + + private void placeBaseBlocks() { + for (Team t : Team.values()) { + Location loc = arena.getBase(t); + if (loc != null) loc.getBlock().setType(t.getGlassMat()); + } + } + + private void updateBaseBlock(Team team, int hp, int maxHp) { + Location loc = arena.getBase(team); + if (loc == null) return; + if (hp <= 0) loc.getBlock().setType(Material.OBSIDIAN); + else if (hp < maxHp / 2.0) loc.getBlock().setType(team.getWoolMat()); + if (plugin.getConfigManager().isParticlesEnabled()) + loc.getWorld().spawnParticle(Particle.SMOKE_LARGE, + loc.clone().add(0.5,1.2,0.5), 5, 0.3,0.3,0.3,0.02); + } + + private void removeBaseBlocks() { + for (Team t : Team.values()) { + Location loc = arena.getBase(t); + if (loc == null) continue; + Material m = loc.getBlock().getType(); + if (m == t.getGlassMat() || m == t.getWoolMat() || m == Material.OBSIDIAN) + loc.getBlock().setType(Material.AIR); + } + } + + // ────────────────────────────────────────────────────────────────────────── + // HILFSMETHODEN + // ────────────────────────────────────────────────────────────────────────── + + private void drawLaser(Location start, Vector dir, WeaponType weapon, int range) { + var particle = plugin.getConfigManager().getWeaponParticle(weapon.getConfigKey()); + Location cur = start.clone(); + Vector step = dir.clone().normalize().multiply(0.6); + for (int i = 0; i < range * 1.7; i++) { + cur.add(step); + if (!cur.getBlock().isPassable()) break; + cur.getWorld().spawnParticle(particle, cur.clone(), 1, 0,0,0,0); + } + } + + private Vector spreadDir(Player player) { + Vector base = player.getEyeLocation().getDirection(); + double s = 0.12; + return base.clone().add(new Vector( + (Math.random()-0.5)*s, (Math.random()-0.5)*s*0.7, (Math.random()-0.5)*s + )).normalize(); + } + + private Team pickBalancedTeam() { + Map counts = new LinkedHashMap<>(); + for (Team t : Team.values()) counts.put(t, 0); + for (LaserPlayer lp : players.values()) counts.merge(lp.getTeam(), 1, Integer::sum); + return counts.entrySet().stream().min(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse(Team.RED); + } + + private Team getLeader() { + return teamScore.entrySet().stream().max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey).orElse(null); + } + + private void cancelTasks() { + if (countdownTask != null) countdownTask.cancel(); + if (gameTask != null) gameTask.cancel(); + if (healTask != null) healTask.cancel(); + } + + private void sendActionBar(Player player) { + LaserPlayer lp = players.get(player.getUniqueId()); + if (lp == null) return; + if (!lp.isHit()) { + int idle = antiCamp.getIdleSeconds(player.getUniqueId()); + int max = plugin.getConfigManager().getCampMaxIdleSecs(); + String campBar = idle > 0 + ? " §c| Camp: " + buildBar(1.0 - (double)idle/max, 6) + : ""; + sendActionBarMsg(player, "§7Kills: §a" + lp.getKills() + + " §7Punkte: §6" + lp.getScore() + + " §7Team: " + lp.getTeam().colored() + + " §7Zeit: §b" + formatTime(timeLeft) + + campBar); + } + // Wenn getroffen: wird in tickHeal() gesetzt + } + + private void sendActionBarMsg(Player player, String msg) { + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(msg)); + } + + private void broadcastStreak(Player player, String textKey, String fallbackTitle, int streak) { + broadcast(plugin.getConfigManager().getText(textKey, + fallbackTitle + " §e{player} §7– " + streak + "er Serie!", + "player", player.getName())); + for (Player p : getOnline()) + p.sendTitle(fallbackTitle, "§e" + player.getName(), 5, 40, 10); + } + + private void showEndStats(Player p, LaserPlayer lp) { + if (lp == null) return; + p.sendMessage("§8§l══════════════════════════════"); + p.sendMessage("§b§l ⚡ LASERTAG ERGEBNIS"); + p.sendMessage("§8§l══════════════════════════════"); + p.sendMessage("§7Team: " + lp.getTeam().colored()); + p.sendMessage("§7Kills: §a" + lp.getKills()); + p.sendMessage("§7Tode: §c" + lp.getDeaths()); + p.sendMessage("§7Basis-Angriffe: §6" + lp.getBaseAttacks()); + p.sendMessage("§7Punkte: §6" + lp.getScore()); + p.sendMessage("§7Beste Serie: §b" + lp.getBestStreak()); + p.sendMessage("§8──────────────────────────────"); + p.sendMessage("§7Team-Punkte:"); + teamScore.entrySet().stream() + .sorted((a,b) -> Integer.compare(b.getValue(), a.getValue())) + .forEach(e -> p.sendMessage(" " + e.getKey().colored() + + " §7→ §6" + e.getValue() + " Punkte")); + p.sendMessage("§8§l══════════════════════════════"); + } + + private String buildBar(double progress, int length) { + int filled = Math.max(0, Math.min(length, (int)Math.round(progress * length))); + return "§a" + "█".repeat(filled) + "§8" + "░".repeat(length - filled); + } + + private String formatTime(int secs) { + return String.format("%d:%02d", secs/60, secs%60); + } + + private void broadcast(String msg) { + for (Player p : getOnline()) p.sendMessage(msg); + } + + private void playAll(Sound sound, float vol, float pitch) { + if (sound == null) return; + for (Player p : getOnline()) p.playSound(p.getLocation(), sound, vol, pitch); + } + + private void playSound(Player p, Sound s, float vol, float pitch) { + if (s != null) p.playSound(p.getLocation(), s, vol, pitch); + } + + // ────────────────────────────────────────────────────────────────────────── + // GETTERS + // ────────────────────────────────────────────────────────────────────────── + + public Arena getArena() { return arena; } + public GameState getState() { return state; } + public Map getPlayers() { return players; } + public Map getTeamScore() { return teamScore; } + public int getTimeLeft() { return timeLeft; } + public LaserPlayer getLP(UUID uid) { return players.get(uid); } + public boolean isRunning() { return state == GameState.RUNNING; } + public boolean isJoinable() { return state == GameState.WAITING || state == GameState.STARTING; } + public int getPlayerCount() { return players.size(); } + public int getMaxPlayers() { return plugin.getConfigManager().getMaxPlayersPerTeam() * 4; } + public AntiCampManager getAntiCamp() { return antiCamp; } + + public List getOnline() { + List list = new ArrayList<>(); + for (UUID uid : players.keySet()) { + Player p = Bukkit.getPlayer(uid); + if (p != null) list.add(p); + } + return list; + } + + public void forceEnd() { endGame(); } + public void forceStart() { + if (state == GameState.WAITING) startCountdown(); + else if (state == GameState.STARTING) startGame(); + } +} diff --git a/src/main/java/de/lasertag/game/GameManager.java b/src/main/java/de/lasertag/game/GameManager.java new file mode 100644 index 0000000..50f5b87 --- /dev/null +++ b/src/main/java/de/lasertag/game/GameManager.java @@ -0,0 +1,93 @@ +package de.lasertag.game; + +import de.lasertag.LasertagPlugin; +import de.lasertag.arena.Arena; +import de.lasertag.player.LaserPlayer; +import org.bukkit.entity.Player; + +import java.util.*; + +public class GameManager { + + private final LasertagPlugin plugin; + /** arenaName (lowercase) → Game */ + private final Map games = new LinkedHashMap<>(); + + public GameManager(LasertagPlugin plugin) { + this.plugin = plugin; + // Spiele für alle vorhandenen Arenen erstellen + for (Arena arena : plugin.getArenaManager().getAll()) { + games.put(arena.getName().toLowerCase(), new Game(plugin, arena)); + } + } + + // ─── Spiel-Verwaltung ──────────────────────────────────────────────────── + + /** Wird aufgerufen wenn eine neue Arena erstellt wird. */ + public Game createGame(Arena arena) { + Game g = new Game(plugin, arena); + games.put(arena.getName().toLowerCase(), g); + return g; + } + + public Game getGame(String arenaName) { + return games.get(arenaName.toLowerCase()); + } + + /** Liefert das Spiel in dem der Spieler gerade ist, oder null. */ + public Game getGameOf(Player player) { + return games.values().stream() + .filter(g -> g.getPlayers().containsKey(player.getUniqueId())) + .findFirst().orElse(null); + } + + public LaserPlayer getLaserPlayer(Player player) { + Game g = getGameOf(player); + return g == null ? null : g.getLP(player.getUniqueId()); + } + + // ─── Join / Leave ──────────────────────────────────────────────────────── + + public boolean joinGame(Player player, String arenaName) { + String pre = plugin.getConfig().getString("messages.prefix","§8[§b§lLASERTAG§8] §r"); + if (getGameOf(player) != null) { + player.sendMessage(pre + "§cDu bist bereits in einem Spiel! /lt leave"); + return false; + } + Game g = games.get(arenaName.toLowerCase()); + if (g == null) { player.sendMessage(pre + "§cArena '§e" + arenaName + "§c' nicht gefunden!"); return false; } + if (!g.getArena().isReady()) { player.sendMessage(pre + "§cDiese Arena ist noch nicht fertig eingerichtet!"); return false; } + if (!g.isJoinable()) { player.sendMessage(pre + "§cDas Spiel in dieser Arena läuft bereits!"); return false; } + return g.addPlayer(player); + } + + /** Tritt dem am besten gefüllten, joinbaren Spiel bei. */ + public boolean joinBest(Player player) { + String pre = plugin.getConfig().getString("messages.prefix","§8[§b§lLASERTAG§8] §r"); + if (getGameOf(player) != null) { + player.sendMessage(pre + "§cDu bist bereits in einem Spiel!"); + return false; + } + return games.values().stream() + .filter(g -> g.isJoinable() && g.getArena().isReady()) + .max(Comparator.comparingInt(Game::getPlayerCount)) + .map(g -> g.addPlayer(player)) + .orElseGet(() -> { player.sendMessage(pre + "§cKein verfügbares Spiel! Benutze /lt list"); return false; }); + } + + public void leaveGame(Player player) { + Game g = getGameOf(player); + if (g != null) g.removePlayer(player); + } + + // ─── Admin-Operationen ─────────────────────────────────────────────────── + + public void stopAllGames() { + for (Game g : games.values()) { + List online = new ArrayList<>(g.getOnline()); + online.forEach(g::removePlayer); + } + } + + public Collection getAllGames() { return Collections.unmodifiableCollection(games.values()); } +} diff --git a/src/main/java/de/lasertag/game/GameState.java b/src/main/java/de/lasertag/game/GameState.java new file mode 100644 index 0000000..8a884ba --- /dev/null +++ b/src/main/java/de/lasertag/game/GameState.java @@ -0,0 +1,8 @@ +package de.lasertag.game; + +public enum GameState { + WAITING, // Wartet auf Spieler + STARTING, // Countdown läuft + RUNNING, // Spiel aktiv + ENDING // Ergebnis wird angezeigt +} diff --git a/src/main/java/de/lasertag/game/Team.java b/src/main/java/de/lasertag/game/Team.java new file mode 100644 index 0000000..5b112ec --- /dev/null +++ b/src/main/java/de/lasertag/game/Team.java @@ -0,0 +1,38 @@ +package de.lasertag.game; + +import org.bukkit.ChatColor; +import org.bukkit.Color; +import org.bukkit.Material; + +public enum Team { + + // Name ChatColor Leder-Farbe (kräftig) Wolle Glas + RED ("Rot", ChatColor.RED, Color.fromRGB(220, 20, 20), Material.RED_WOOL, Material.RED_STAINED_GLASS), + BLUE ("Blau", ChatColor.AQUA, Color.fromRGB( 30, 80, 220), Material.BLUE_WOOL, Material.BLUE_STAINED_GLASS), + GREEN ("Grün", ChatColor.GREEN, Color.fromRGB( 20, 160, 20), Material.GREEN_WOOL, Material.GREEN_STAINED_GLASS), + YELLOW("Gelb", ChatColor.YELLOW, Color.fromRGB(220, 180, 0), Material.YELLOW_WOOL, Material.YELLOW_STAINED_GLASS); + + private final String displayName; + private final ChatColor chatColor; + private final Color armorColor; + private final Material woolMat; + private final Material glassMat; + + Team(String displayName, ChatColor chatColor, Color armorColor, + Material woolMat, Material glassMat) { + this.displayName = displayName; + this.chatColor = chatColor; + this.armorColor = armorColor; + this.woolMat = woolMat; + this.glassMat = glassMat; + } + + public String getDisplayName() { return displayName; } + public ChatColor getChatColor() { return chatColor; } + public Color getArmorColor() { return armorColor; } + public Material getWoolMat() { return woolMat; } + public Material getGlassMat() { return glassMat; } + + /** z.B. "§cRot" */ + public String colored() { return chatColor + displayName; } +} diff --git a/src/main/java/de/lasertag/listener/BlockListener.java b/src/main/java/de/lasertag/listener/BlockListener.java new file mode 100644 index 0000000..289ccba --- /dev/null +++ b/src/main/java/de/lasertag/listener/BlockListener.java @@ -0,0 +1,44 @@ +package de.lasertag.listener; + +import de.lasertag.LasertagPlugin; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; + +public class BlockListener implements Listener { + + private final LasertagPlugin plugin; + + public BlockListener(LasertagPlugin plugin) { this.plugin = plugin; } + + @EventHandler + public void onBreak(BlockBreakEvent e) { + Player p = e.getPlayer(); + if (plugin.getGameManager().getGameOf(p) != null) { + e.setCancelled(true); // Kein Block-Abbau per Hand; Basisangriff nur per Schuss + } + } + + @EventHandler + public void onPlace(BlockPlaceEvent e) { + Player p = e.getPlayer(); + if (plugin.getGameManager().getGameOf(p) != null) { + e.setCancelled(true); + } + } + + /** + * Verhindert dass Spieler im Spiel Items aufheben + * (z.B. keine Pfeile, keine zufälligen Drops). + */ + @EventHandler + public void onPickup(EntityPickupItemEvent e) { + if (!(e.getEntity() instanceof Player p)) return; + if (plugin.getGameManager().getGameOf(p) != null) { + e.setCancelled(true); + } + } +} diff --git a/src/main/java/de/lasertag/listener/PlayerListener.java b/src/main/java/de/lasertag/listener/PlayerListener.java new file mode 100644 index 0000000..73f88eb --- /dev/null +++ b/src/main/java/de/lasertag/listener/PlayerListener.java @@ -0,0 +1,70 @@ +package de.lasertag.listener; + +import de.lasertag.LasertagPlugin; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerItemHeldEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.CrossbowMeta; + +public class PlayerListener implements Listener { + + private final LasertagPlugin plugin; + + public PlayerListener(LasertagPlugin plugin) { this.plugin = plugin; } + + /** Schaden komplett sperren — Lasertag verwaltet Tode selbst. */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false) + public void onDamage(EntityDamageEvent e) { + if (!(e.getEntity() instanceof Player p)) return; + if (plugin.getGameManager().getGameOf(p) != null) e.setCancelled(true); + } + + /** Hunger sperren. */ + @EventHandler + public void onFood(FoodLevelChangeEvent e) { + if (!(e.getEntity() instanceof Player p)) return; + if (plugin.getGameManager().getGameOf(p) != null) e.setCancelled(true); + } + + /** Items droppen sperren. */ + @EventHandler + public void onDrop(PlayerDropItemEvent e) { + if (plugin.getGameManager().getGameOf(e.getPlayer()) != null) e.setCancelled(true); + } + + /** Spieler verlässt den Server. */ + @EventHandler + public void onQuit(PlayerQuitEvent e) { + plugin.getGameManager().leaveGame(e.getPlayer()); + } + + /** + * Wenn ein Spieler im Spiel die Crossbow (Sniper) in die Hand nimmt, + * sicherstellen dass sie nicht "geladen" aussieht und keinen Pfeil enthält. + * Crossbows haben eine CrossbowMeta die "charged" sein kann — das resetten wir. + */ + @EventHandler + public void onHeldChange(PlayerItemHeldEvent e) { + Player player = e.getPlayer(); + if (plugin.getGameManager().getGameOf(player) == null) return; + // Kleines Delay damit das Item schon in der Hand ist + org.bukkit.Bukkit.getScheduler().runTaskLater( + de.lasertag.LasertagPlugin.getInstance(), () -> { + ItemStack item = player.getInventory().getItemInMainHand(); + if (item == null) return; + if (item.getItemMeta() instanceof CrossbowMeta cm) { + if (!cm.getChargedProjectiles().isEmpty()) { + cm.setChargedProjectiles(java.util.Collections.emptyList()); + item.setItemMeta(cm); + } + } + }, 1L); + } +} diff --git a/src/main/java/de/lasertag/listener/SignListener.java b/src/main/java/de/lasertag/listener/SignListener.java new file mode 100644 index 0000000..44bc1c4 --- /dev/null +++ b/src/main/java/de/lasertag/listener/SignListener.java @@ -0,0 +1,246 @@ +package de.lasertag.listener; + +import de.lasertag.LasertagPlugin; +import de.lasertag.game.Game; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.block.sign.Side; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.scheduler.BukkitTask; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +/** + * Join-Schild System. + * + * EIN SCHILD ERSTELLEN: + * Schreibe auf Zeile 1 exakt: [Lasertag] + * Schreibe auf Zeile 2: Den Arenannamen + * + * Das Plugin erkennt das Schild automatisch und: + * - Aktualisiert Status/Spielerzahl live + * - Lässt Spieler durch Rechtsklick beitreten + * + * Format: + * Zeile 1: §b[LASERTAG] + * Zeile 2: §eArena-Name + * Zeile 3: §aWartend / §eStartet / §cLäuft / §8Voll + * Zeile 4: §72/16 + */ +public class SignListener implements Listener { + + private final LasertagPlugin plugin; + + /** Location → Arenaname (gespeicherte Schilder) */ + private final Map signs = new HashMap<>(); + private File signsFile; + private BukkitTask updateTask; + + public SignListener(LasertagPlugin plugin) { + this.plugin = plugin; + loadSigns(); + startUpdateTask(); + } + + // ─── Schild erstellen ──────────────────────────────────────────────────── + + @EventHandler + public void onSignChange(SignChangeEvent e) { + // Zeile 1 (Index 0) prüfen + String line0 = e.getLine(0); + if (line0 == null) return; + + String trigger = plugin.getConfigManager().getSignTriggerLine(); + if (!ChatColor.stripColor(line0).equalsIgnoreCase(ChatColor.stripColor(trigger))) return; + + if (!e.getPlayer().hasPermission("lasertag.admin")) { + e.getPlayer().sendMessage(plugin.getConfigManager().getPrefix() + + "§cNur Admins können Lasertag-Schilder erstellen!"); + return; + } + + String arenaName = ChatColor.stripColor(e.getLine(1) != null ? e.getLine(1) : ""); + if (arenaName.isEmpty()) { + e.getPlayer().sendMessage(plugin.getConfigManager().getPrefix() + + "§cZeile 2 muss den Arenannamen enthalten!"); + return; + } + + if (plugin.getArenaManager().getArena(arenaName) == null) { + e.getPlayer().sendMessage(plugin.getConfigManager().getPrefix() + + "§cArena '§e" + arenaName + "§c' nicht gefunden!"); + return; + } + + // Schild registrieren + signs.put(e.getBlock().getLocation(), arenaName); + saveSigns(); + + // Zeilen setzen + e.setLine(0, "§b§l[LASERTAG]"); + e.setLine(1, "§e" + arenaName); + e.setLine(2, "§7Initialisierung..."); + e.setLine(3, "§7..."); + + e.getPlayer().sendMessage(plugin.getConfigManager().getPrefix() + + "§aJoin-Schild für Arena §e" + arenaName + " §aerstellt!"); + + // Sofort aktualisieren + Bukkit.getScheduler().runTaskLater(plugin, () -> updateSign(e.getBlock().getLocation(), arenaName), 2L); + } + + // ─── Schild anklicken (Beitreten) ──────────────────────────────────────── + + @EventHandler + public void onInteract(PlayerInteractEvent e) { + if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return; + if (e.getClickedBlock() == null) return; + if (!(e.getClickedBlock().getState() instanceof Sign)) return; + + Location loc = e.getClickedBlock().getLocation(); + String arenaName = signs.get(loc); + if (arenaName == null) return; + + e.setCancelled(true); + Player player = e.getPlayer(); + + // Ist Spieler bereits in einem Spiel? + if (plugin.getGameManager().getGameOf(player) != null) { + player.sendMessage(plugin.getConfigManager().getPrefix() + + "§cDu bist bereits in einem Spiel! /lt leave"); + return; + } + + // Beitreten versuchen + boolean joined = plugin.getGameManager().joinGame(player, arenaName); + if (joined) { + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.2f); + } + } + + // ─── Schild zerstören ──────────────────────────────────────────────────── + + @EventHandler + public void onBreak(BlockBreakEvent e) { + Location loc = e.getBlock().getLocation(); + if (!signs.containsKey(loc)) return; + + if (!e.getPlayer().hasPermission("lasertag.admin")) { + e.setCancelled(true); + e.getPlayer().sendMessage(plugin.getConfigManager().getPrefix() + + "§cNur Admins können Lasertag-Schilder abbauen!"); + return; + } + + signs.remove(loc); + saveSigns(); + e.getPlayer().sendMessage(plugin.getConfigManager().getPrefix() + + "§aJoin-Schild entfernt."); + } + + // ─── Update-Task ───────────────────────────────────────────────────────── + + private void startUpdateTask() { + int interval = plugin.getConfigManager().getSignUpdateInterval(); + updateTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + for (Map.Entry entry : new HashMap<>(signs).entrySet()) { + updateSign(entry.getKey(), entry.getValue()); + } + }, 20L, interval); + } + + public void stopUpdateTask() { + if (updateTask != null) updateTask.cancel(); + } + + private void updateSign(Location loc, String arenaName) { + Block block = loc.getBlock(); + if (!(block.getState() instanceof Sign sign)) { + // Block ist kein Schild mehr → entfernen + signs.remove(loc); + saveSigns(); + return; + } + + Game game = plugin.getGameManager().getGame(arenaName); + var cfg = plugin.getConfigManager(); + + String line2, line3; + + if (game == null || !game.getArena().isReady() || !game.getArena().isEnabled()) { + line2 = "§c§lNICHT BEREIT"; + line3 = "§8Setup fehlt"; + } else { + boolean full = game.getPlayerCount() >= game.getMaxPlayers(); + String color = switch (game.getState()) { + case WAITING -> full ? cfg.getSignColorFull() : cfg.getSignColorWaiting(); + case STARTING -> cfg.getSignColorStarting(); + case RUNNING -> cfg.getSignColorRunning(); + case ENDING -> "§7"; + }; + String statusText = switch (game.getState()) { + case WAITING -> full ? "Voll" : "Warten"; + case STARTING -> "Startet..."; + case RUNNING -> "Läuft"; + case ENDING -> "Beendet"; + }; + line2 = color + statusText; + line3 = "§7" + game.getPlayerCount() + "§8/§7" + game.getMaxPlayers(); + } + + // Spigot 1.20: Sign hat Vorder- und Rückseite + try { + var signSide = sign.getSide(Side.FRONT); + signSide.setLine(0, "§b§l[LASERTAG]"); + signSide.setLine(1, "§e" + arenaName); + signSide.setLine(2, line2); + signSide.setLine(3, line3); + } catch (Exception ex) { + // Fallback für ältere API + sign.setLine(0, "§b§l[LASERTAG]"); + sign.setLine(1, "§e" + arenaName); + sign.setLine(2, line2); + sign.setLine(3, line3); + } + sign.update(); + } + + // ─── Persistenz ────────────────────────────────────────────────────────── + + private void loadSigns() { + signsFile = new File(plugin.getDataFolder(), "signs.yml"); + if (!signsFile.exists()) return; + FileConfiguration cfg = YamlConfiguration.loadConfiguration(signsFile); + if (!cfg.contains("signs")) return; + for (String key : cfg.getConfigurationSection("signs").getKeys(false)) { + Location loc = cfg.getLocation("signs." + key + ".location"); + String arena = cfg.getString("signs." + key + ".arena"); + if (loc != null && arena != null) signs.put(loc, arena); + } + plugin.getLogger().info("§a" + signs.size() + " Join-Schilder geladen."); + } + + private void saveSigns() { + FileConfiguration cfg = new YamlConfiguration(); + int i = 0; + for (Map.Entry e : signs.entrySet()) { + cfg.set("signs." + i + ".location", e.getKey()); + cfg.set("signs." + i + ".arena", e.getValue()); + i++; + } + try { cfg.save(signsFile); } catch (IOException ex) { ex.printStackTrace(); } + } + + public int getSignCount() { return signs.size(); } +} diff --git a/src/main/java/de/lasertag/listener/WeaponListener.java b/src/main/java/de/lasertag/listener/WeaponListener.java new file mode 100644 index 0000000..0be4f9e --- /dev/null +++ b/src/main/java/de/lasertag/listener/WeaponListener.java @@ -0,0 +1,71 @@ +package de.lasertag.listener; + +import de.lasertag.LasertagPlugin; +import de.lasertag.game.Game; +import de.lasertag.weapon.WeaponType; +import de.lasertag.weapon.WeaponUtil; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.entity.ProjectileLaunchEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; +import org.bukkit.inventory.ItemStack; + +public class WeaponListener implements Listener { + + private final LasertagPlugin plugin; + + public WeaponListener(LasertagPlugin plugin) { this.plugin = plugin; } + + /** + * Schießen: Rechtsklick mit einer Lasertag-Waffe. + * Wir fangen das Event HIGHEST ab damit andere Plugins (z.B. Anticheat) + * nicht zuerst ein normales Crossbow-/Trident-Event auslösen. + */ + @EventHandler(priority = EventPriority.HIGHEST) + public void onInteract(PlayerInteractEvent e) { + if (e.getAction() != Action.RIGHT_CLICK_AIR + && e.getAction() != Action.RIGHT_CLICK_BLOCK) return; + + Player player = e.getPlayer(); + Game game = plugin.getGameManager().getGameOf(player); + if (game == null) return; + + ItemStack item = player.getInventory().getItemInMainHand(); + WeaponType weapon = WeaponUtil.identify(plugin, item); + if (weapon == null) return; + + // Immer abbrechen — verhindert Crossbow-Nachladen, Trident-Wurf etc. + e.setCancelled(true); + + if (game.isRunning()) { + game.handleShot(player, weapon); + } + } + + /** + * Verhindert dass Projektile (Pfeile, Tridents) wirklich abgefeuert werden. + * Crossbow und Trident würden sonst echte Projektile spawnen. + */ + @EventHandler(priority = EventPriority.HIGHEST) + public void onProjectile(ProjectileLaunchEvent e) { + if (!(e.getEntity().getShooter() instanceof Player player)) return; + if (plugin.getGameManager().getGameOf(player) != null) { + e.setCancelled(true); + } + } + + /** + * Hand-Wechsel (F-Taste) im Spiel deaktivieren damit die Waffe + * nicht versehentlich in die Off-Hand wandert. + */ + @EventHandler + public void onSwapHand(PlayerSwapHandItemsEvent e) { + if (plugin.getGameManager().getGameOf(e.getPlayer()) != null) { + e.setCancelled(true); + } + } +} diff --git a/src/main/java/de/lasertag/player/LaserPlayer.java b/src/main/java/de/lasertag/player/LaserPlayer.java new file mode 100644 index 0000000..b915887 --- /dev/null +++ b/src/main/java/de/lasertag/player/LaserPlayer.java @@ -0,0 +1,125 @@ +package de.lasertag.player; + +import de.lasertag.game.Team; +import org.bukkit.inventory.ItemStack; + +import java.util.UUID; + +/** + * Repräsentiert einen Spieler während eines laufenden Spiels. + * + * KERN-MECHANIK: + * - Wird ein Spieler getroffen → isHit() == true + * - Getroffene Spieler können NICHT schießen + * - Getroffene Spieler müssen zur eigenen Basis zurück + * - An der Basis: heal() aufrufen → isHit() == false, kann wieder schießen + */ +public class LaserPlayer { + + private final UUID uuid; + private final String name; + + // Team + private Team team; + + // ─── Hit-Stun Mechanik ─────────────────────────────────────────────────── + /** true = getroffen, muss zur Basis zurück, kann nicht schießen */ + private boolean hit; + /** Zeitstempel des letzten Treffers */ + private long hitTime; + /** Wann der Spieler anfing, sich an der Basis aufzuhalten */ + private long healStart; + /** Heilt gerade an der Basis */ + private boolean healing; + + // ─── Spiel-Statistiken (diese Runde) ──────────────────────────────────── + private int kills; + private int deaths; + private int baseAttacks; // Wie oft hat er eine gegnerische Basis angegriffen + private int score; + private int killStreak; + private int bestStreak; + + // Gespeicherter Zustand vor dem Spiel + private ItemStack[] savedInventory; + + public LaserPlayer(UUID uuid, String name) { + this.uuid = uuid; + this.name = name; + } + + // ─── Hit-Stun API ──────────────────────────────────────────────────────── + + /** Spieler wird getroffen — kann ab sofort nicht mehr schießen. */ + public void applyHit() { + this.hit = true; + this.hitTime = System.currentTimeMillis(); + this.healing = false; + this.healStart = 0; + this.deaths++; + this.killStreak = 0; + } + + /** Spieler betritt die eigene Basis → Heilung beginnt. */ + public void startHealing() { + if (!hit || healing) return; + healing = true; + healStart = System.currentTimeMillis(); + } + + /** Spieler verlässt die Basis → Heilung abbrechen. */ + public void stopHealing() { + healing = false; + healStart = 0; + } + + /** + * Heilung abschließen (wird vom Game-Task nach heal-time Sekunden aufgerufen). + * Der Spieler kann danach wieder schießen. + */ + public void completeHeal() { + hit = false; + healing = false; + healStart = 0; + } + + /** Ist der Spieler gerade getroffen (= gesperrt)? */ + public boolean isHit() { return hit; } + + /** Heilt der Spieler gerade an der Basis? */ + public boolean isHealing() { return healing; } + + /** Millisekunden seit Heilungsbeginn */ + public long healElapsedMs() { + if (!healing || healStart == 0) return 0; + return System.currentTimeMillis() - healStart; + } + + // ─── Kill-Counting ─────────────────────────────────────────────────────── + + public void registerKill() { + kills++; + killStreak++; + if (killStreak > bestStreak) bestStreak = killStreak; + } + + public void addScore(int amount) { score += amount; } + public void addBaseAttack() { baseAttacks++; } + + // ─── Getters / Setters ─────────────────────────────────────────────────── + + public UUID getUuid() { return uuid; } + public String getName() { return name; } + public Team getTeam() { return team; } + public void setTeam(Team team) { this.team = team; } + public int getKills() { return kills; } + public int getDeaths() { return deaths; } + public int getBaseAttacks() { return baseAttacks; } + public int getScore() { return score; } + public int getKillStreak() { return killStreak; } + public int getBestStreak() { return bestStreak; } + public ItemStack[] getSavedInventory() { return savedInventory; } + public void setSavedInventory(ItemStack[] i) { savedInventory = i; } + + public String getKDString() { return kills + "/" + deaths; } +} diff --git a/src/main/java/de/lasertag/player/PlayerDataManager.java b/src/main/java/de/lasertag/player/PlayerDataManager.java new file mode 100644 index 0000000..d8e8134 --- /dev/null +++ b/src/main/java/de/lasertag/player/PlayerDataManager.java @@ -0,0 +1,73 @@ +package de.lasertag.player; + +import de.lasertag.LasertagPlugin; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class PlayerDataManager { + + private final LasertagPlugin plugin; + private final Map map = new HashMap<>(); + private File file; + private FileConfiguration cfg; + + public PlayerDataManager(LasertagPlugin plugin) { + this.plugin = plugin; + load(); + } + + public PlayerStats getStats(UUID uuid) { + return map.computeIfAbsent(uuid, PlayerStats::new); + } + + public List getTopByScore(int limit) { + List list = new ArrayList<>(map.values()); + list.sort((a, b) -> Integer.compare(b.getTotalScore(), a.getTotalScore())); + return list.subList(0, Math.min(limit, list.size())); + } + + private void load() { + file = new File(plugin.getDataFolder(), "stats.yml"); + if (!file.exists()) { + try { plugin.getDataFolder().mkdirs(); file.createNewFile(); } + catch (IOException ex) { ex.printStackTrace(); return; } + } + cfg = YamlConfiguration.loadConfiguration(file); + ConfigurationSection root = cfg.getConfigurationSection("players"); + if (root == null) return; + for (String key : root.getKeys(false)) { + UUID uuid = UUID.fromString(key); + PlayerStats s = new PlayerStats(uuid); + String p = "players." + key + "."; + s.setName(cfg.getString(p + "name", "Unknown")); + s.addKills(cfg.getInt(p + "kills")); + s.addDeaths(cfg.getInt(p + "deaths")); + s.addScore(cfg.getInt(p + "score")); + s.addGames(cfg.getInt(p + "games")); + s.setBestStreak(cfg.getInt(p + "best-streak")); + s.addBaseAttacks(cfg.getInt(p + "base-attacks")); + map.put(uuid, s); + } + } + + public void saveAll() { + cfg.set("players", null); + for (Map.Entry e : map.entrySet()) { + PlayerStats s = e.getValue(); + String p = "players." + e.getKey() + "."; + cfg.set(p + "name", s.getName()); + cfg.set(p + "kills", s.getTotalKills()); + cfg.set(p + "deaths", s.getTotalDeaths()); + cfg.set(p + "score", s.getTotalScore()); + cfg.set(p + "games", s.getGamesPlayed()); + cfg.set(p + "best-streak", s.getBestStreak()); + cfg.set(p + "base-attacks",s.getTotalBaseAttacks()); + } + try { cfg.save(file); } catch (IOException ex) { ex.printStackTrace(); } + } +} diff --git a/src/main/java/de/lasertag/player/PlayerStats.java b/src/main/java/de/lasertag/player/PlayerStats.java new file mode 100644 index 0000000..07a0cc3 --- /dev/null +++ b/src/main/java/de/lasertag/player/PlayerStats.java @@ -0,0 +1,48 @@ +package de.lasertag.player; + +import java.util.UUID; + +public class PlayerStats { + + private final UUID uuid; + private String name = "Unknown"; + private int totalKills; + private int totalDeaths; + private int totalScore; + private int gamesPlayed; + private int bestStreak; + private int totalBaseAttacks; + + public PlayerStats(UUID uuid) { this.uuid = uuid; } + + public void apply(LaserPlayer lp) { + totalKills += lp.getKills(); + totalDeaths += lp.getDeaths(); + totalScore += lp.getScore(); + totalBaseAttacks += lp.getBaseAttacks(); + gamesPlayed++; + if (lp.getBestStreak() > bestStreak) bestStreak = lp.getBestStreak(); + } + + public double getKDR() { + if (totalDeaths == 0) return totalKills; + return Math.round(totalKills * 100.0 / totalDeaths) / 100.0; + } + + // Getters & Setters + public UUID getUuid() { return uuid; } + public String getName() { return name; } + public void setName(String n) { name = n; } + public int getTotalKills() { return totalKills; } + public void addKills(int k) { totalKills += k; } + public int getTotalDeaths() { return totalDeaths; } + public void addDeaths(int d) { totalDeaths += d; } + public int getTotalScore() { return totalScore; } + public void addScore(int s) { totalScore += s; } + public int getGamesPlayed() { return gamesPlayed; } + public void addGames(int g) { gamesPlayed += g; } + public int getBestStreak() { return bestStreak; } + public void setBestStreak(int s) { bestStreak = s; } + public int getTotalBaseAttacks() { return totalBaseAttacks; } + public void addBaseAttacks(int b) { totalBaseAttacks += b; } +} diff --git a/src/main/java/de/lasertag/protection/ModProtectionManager.java b/src/main/java/de/lasertag/protection/ModProtectionManager.java new file mode 100644 index 0000000..0398381 --- /dev/null +++ b/src/main/java/de/lasertag/protection/ModProtectionManager.java @@ -0,0 +1,215 @@ +package de.lasertag.protection; + +import de.lasertag.LasertagPlugin; +import de.lasertag.game.Game; +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.Team; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Mod-Schutz System gegen Minimap-Mods (Xaero, VoxelMap etc.) + * + * WAS MINIMAP-MODS NUTZEN: + * - Spieler-Positionen aus Entity-Tracking Paketen + * - Chunk-Daten für Kartenansicht + * - Tab-Liste für Spielernamen mit Positionen + * + * WAS WIR TUN KÖNNEN (ohne ProtocolLib): + * 1. NAMETAGS verbergen (Scoreboard-Teams mit hidden nametags) + * 2. FOG-OF-WAR simulieren via Blindness-Effekt der zu naher Spieler + * → Macht Karten-Mods wertlos (sie sehen nur was der Client sieht) + * 3. TAB-LISTE leeren für Spieler im Match + * 4. SPIELER-KOLLISION deaktivieren + * + * WAS NICHT MÖGLICH IST (ohne ProtocolLib / NMS): + * - Chunk-Pakete manipulieren + * - Entity-Positionen in Paketen fälschen + * - F3-Koordinaten blockieren + * + * MIT ProtocolLib (optional, erweiterbar): + * - Echte Koordinaten-Verschleierung möglich + * - Spieler-Pakete filtern + */ +public class ModProtectionManager { + + private final LasertagPlugin plugin; + private BukkitTask fogTask; + + // Scoreboard-Team für versteckte Nametags + private org.bukkit.scoreboard.Scoreboard hiddenNametagBoard; + private final Map originalDisplayNames = new HashMap<>(); + + public ModProtectionManager(LasertagPlugin plugin) { + this.plugin = plugin; + } + + /** Wird aufgerufen wenn ein Spieler einem Spiel beitritt. */ + public void applyProtection(Player player, Game game) { + if (!plugin.getConfigManager().isModProtectionEnabled()) return; + + // 1. Nametags verstecken + if (plugin.getConfigManager().isHideNametags()) { + hideNametag(player, game); + } + + // 2. Tab-Liste leeren (nur den eigenen Tab-Name anpassen) + if (plugin.getConfigManager().isHideFromTab()) { + applyTabHide(player, game); + } + + // 3. Fog-of-War: Spieler außerhalb fog-radius unsichtbar machen + if (plugin.getConfigManager().isFogOfWarEnabled()) { + startFogOfWar(game); + } + } + + /** Alle Schutz-Effekte entfernen wenn Spieler das Spiel verlässt. */ + public void removeProtection(Player player) { + // Nametag zurücksetzen + restoreNametag(player); + + // Tab-Liste zurücksetzen + player.setPlayerListName(null); + + // Blindheit entfernen (falls aktiv) + player.removePotionEffect(PotionEffectType.BLINDNESS); + player.removePotionEffect(PotionEffectType.NIGHT_VISION); + } + + /** Alle Schutz-Effekte für alle Spieler entfernen. */ + public void removeAll(Game game) { + if (fogTask != null) { fogTask.cancel(); fogTask = null; } + for (Player p : game.getOnline()) removeProtection(p); + } + + // ─── Nametag verstecken ────────────────────────────────────────────────── + + private void hideNametag(Player player, Game game) { + // Eigenes Scoreboard für jeden Spieler im Spiel + // Wir nutzen das scoreboard das der ScoreboardManager bereits verwaltet — + // dort ein "hidden" Team erstellen + Scoreboard sb = player.getScoreboard(); + if (sb == null) sb = Bukkit.getScoreboardManager().getNewScoreboard(); + + // Team per Spieler-Name (max 16 Zeichen im Team-Namen) + String teamName = "lt_" + player.getName().substring(0, Math.min(player.getName().length(), 13)); + org.bukkit.scoreboard.Team team = sb.getTeam(teamName); + if (team == null) team = sb.registerNewTeam(teamName); + + team.setOption(org.bukkit.scoreboard.Team.Option.NAME_TAG_VISIBILITY, + org.bukkit.scoreboard.Team.OptionStatus.FOR_OTHER_TEAMS); // Eigene Tags nicht verstecken + team.addEntry(player.getName()); + + // Alle anderen Spieler im Spiel aktualisieren + for (Player other : game.getOnline()) { + if (other.equals(player)) continue; + Scoreboard otherSb = other.getScoreboard(); + if (otherSb == null) continue; + String tn = "lt_" + player.getName().substring(0, Math.min(player.getName().length(), 13)); + org.bukkit.scoreboard.Team t = otherSb.getTeam(tn); + if (t == null) t = otherSb.registerNewTeam(tn); + t.setOption(org.bukkit.scoreboard.Team.Option.NAME_TAG_VISIBILITY, + org.bukkit.scoreboard.Team.OptionStatus.NEVER); + t.addEntry(player.getName()); + } + } + + private void restoreNametag(Player player) { + // Nametag-Teams aus Scoreboard entfernen + Scoreboard sb = player.getScoreboard(); + if (sb == null) return; + String teamName = "lt_" + player.getName().substring(0, Math.min(player.getName().length(), 13)); + org.bukkit.scoreboard.Team team = sb.getTeam(teamName); + if (team != null) { + team.removeEntry(player.getName()); + } + } + + // ─── Tab-Liste ──────────────────────────────────────────────────────────── + + private void applyTabHide(Player player, Game game) { + // Spielernamen in der Tab-Liste durch Team-Tag ersetzen + // (versteckt echte Position-Infos, die manche Mods aus der Tab-Liste lesen) + var lp = game.getLP(player.getUniqueId()); + if (lp == null) return; + String teamTag = lp.getTeam().getChatColor() + "[" + lp.getTeam().getDisplayName().substring(0,1) + "] "; + player.setPlayerListName(teamTag + player.getName()); + } + + // ─── Fog-of-War ────────────────────────────────────────────────────────── + + /** + * Fog-of-War: Spieler außerhalb des fog-radius werden für andere + * Spieler unsichtbar gemacht. Das verhindert dass Minimap-Mods + * Gegner-Positionen auf der Karte anzeigen können. + * + * Technisch: Wir nutzen die Spigot hide/show Player API. + * Spieler die außerhalb des Radius sind werden per hidePlayer() ausgeblendet. + */ + private void startFogOfWar(Game game) { + if (fogTask != null) return; // Läuft bereits + int fogRadius = plugin.getConfigManager().getFogRadius(); + long fogRadiusSq = (long) fogRadius * fogRadius; + + fogTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + if (!game.isRunning()) { + fogTask.cancel(); fogTask = null; return; + } + for (Player viewer : game.getOnline()) { + for (Player target : game.getOnline()) { + if (viewer.equals(target)) continue; + boolean sameTeam = false; + var vlp = game.getLP(viewer.getUniqueId()); + var tlp = game.getLP(target.getUniqueId()); + if (vlp != null && tlp != null) sameTeam = vlp.getTeam() == tlp.getTeam(); + + long distSq = (long) viewer.getLocation().distanceSquared(target.getLocation()); + + if (distSq <= fogRadiusSq || sameTeam) { + // In Reichweite oder gleiches Team → sichtbar + viewer.showPlayer(plugin, target); + } else { + // Außerhalb → verstecken (Minimap sieht diesen Spieler nicht) + viewer.hidePlayer(plugin, target); + } + } + } + }, 0L, 10L); // Alle 10 Ticks (0.5s) aktualisieren + } + + // ─── Info-Methode ──────────────────────────────────────────────────────── + + /** + * Sendet eine Erklärung der Mod-Schutz-Maßnahmen an den Spieler. + * Nützlich für Admins. + */ + public void sendProtectionInfo(org.bukkit.command.CommandSender sender) { + sender.sendMessage("§8§l═══════════════════════════════════════"); + sender.sendMessage("§b§l MOD-SCHUTZ — STATUS & ERKLÄRUNG"); + sender.sendMessage("§8§l═══════════════════════════════════════"); + sender.sendMessage((plugin.getConfigManager().isModProtectionEnabled() ? "§a✔" : "§c✘") + + " §7Mod-Schutz aktiv"); + sender.sendMessage((plugin.getConfigManager().isHideNametags() ? "§a✔" : "§c✘") + + " §7Nametags versteckt §8(verhindert Spieler-Tracking)"); + sender.sendMessage((plugin.getConfigManager().isHideFromTab() ? "§a✔" : "§c✘") + + " §7Tab-Liste angepasst §8(versteckt Spielernamen)"); + sender.sendMessage((plugin.getConfigManager().isFogOfWarEnabled() ? "§a✔" : "§c✘") + + " §7Fog-of-War aktiv §8(Radius: §b" + + plugin.getConfigManager().getFogRadius() + " Blöcke§8)"); + sender.sendMessage("§8§l───────────────────────────────────────"); + sender.sendMessage("§7§oWas NICHT blockiert werden kann:"); + sender.sendMessage("§c§o - Chunk-basierte Kartendaten (bräuchte ProtocolLib)"); + sender.sendMessage("§c§o - F3-Koordinaten (Clientseitig)"); + sender.sendMessage("§c§o - Vollständige Minimap-Blöcke"); + sender.sendMessage("§7§oFür vollständigen Schutz: ProtocolLib + PacketBlocker"); + sender.sendMessage("§8§l═══════════════════════════════════════"); + } +} diff --git a/src/main/java/de/lasertag/scoreboard/ScoreboardManager.java b/src/main/java/de/lasertag/scoreboard/ScoreboardManager.java new file mode 100644 index 0000000..33e14d1 --- /dev/null +++ b/src/main/java/de/lasertag/scoreboard/ScoreboardManager.java @@ -0,0 +1,113 @@ +package de.lasertag.scoreboard; + +import de.lasertag.LasertagPlugin; +import de.lasertag.game.Game; +import de.lasertag.game.Team; +import de.lasertag.player.LaserPlayer; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ScoreboardManager { + + private final LasertagPlugin plugin; + private final Map boards = new HashMap<>(); + + public ScoreboardManager(LasertagPlugin plugin) { this.plugin = plugin; } + + public void update(Player player, Game game) { + Scoreboard board = boards.computeIfAbsent(player.getUniqueId(), + k -> Bukkit.getScoreboardManager().getNewScoreboard()); + + // Altes Objective entfernen + Objective old = board.getObjective("lt"); + if (old != null) old.unregister(); + + Objective obj = board.registerNewObjective("lt", "dummy", + ChatColor.AQUA + "" + ChatColor.BOLD + "⚡ LASERTAG"); + obj.setDisplaySlot(DisplaySlot.SIDEBAR); + + LaserPlayer lp = game.getLP(player.getUniqueId()); + + int line = 15; + + // Trennlinie oben + setLine(obj, board, "§b§l─────────────────", line--); + + // Zeit + int secs = game.getTimeLeft(); + String timeStr = String.format("%d:%02d", secs / 60, secs % 60); + setLine(obj, board, "§7⏰ Zeit: §e" + timeStr, line--); + setLine(obj, board, "§7§l─────────────────", line--); + + // Team-Punkte (sortiert nach Punkte) + setLine(obj, board, "§7Team-Punkte:", line--); + Map scores = game.getTeamScore(); + Team[] sorted = Team.values().clone(); + // Bubble sort (nur 4 Teams) + for (int i = 0; i < sorted.length - 1; i++) + for (int j = 0; j < sorted.length - 1 - i; j++) + if (scores.getOrDefault(sorted[j],0) < scores.getOrDefault(sorted[j+1],0)) { + Team tmp = sorted[j]; sorted[j] = sorted[j+1]; sorted[j+1] = tmp; + } + + for (Team t : sorted) { + int hp = game.getArena().getBaseHealth(t); + int maxHp = plugin.getConfig().getInt("game.base-health", 5); + String baseHpBar = buildHpBar(hp, maxHp, 5); + String pts = scores.getOrDefault(t, 0) + "§7pts"; + setLine(obj, board, + t.getChatColor() + "● " + t.getDisplayName() + " §8| §f" + pts + + " §8| " + baseHpBar, line--); + } + + setLine(obj, board, "§7§l─────────────────", line--); + + // Spieler-Stats + if (lp != null) { + String hitStatus = lp.isHit() + ? (lp.isHealing() ? "§e⚕ Heilend..." : "§c☠ Getroffen!") + : "§a✔ Aktiv"; + setLine(obj, board, "§7Status: " + hitStatus, line--); + setLine(obj, board, "§7Kills: §a" + lp.getKills() + " §8| §7Tode: §c" + lp.getDeaths(), line--); + setLine(obj, board, "§7Punkte: §6" + lp.getScore(), line--); + if (lp.getKillStreak() >= 3) + setLine(obj, board, "§6🔥 Serie: §e×" + lp.getKillStreak(), line--); + } + + setLine(obj, board, "§b§l─────────────────", line--); + setLine(obj, board, "§bwww.lasertag.de", line--); + + player.setScoreboard(board); + } + + public void remove(Player player) { + boards.remove(player.getUniqueId()); + player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); + } + + // ─── Hilfsmethoden ─────────────────────────────────────────────────────── + + private int uniqueSuffix = 0; + + private void setLine(Objective obj, Scoreboard board, String text, int score) { + // Scoreboard-Entries müssen einzigartig sein + String entry = text; + while (board.getEntries().contains(entry)) + entry = entry + ChatColor.values()[uniqueSuffix++ % ChatColor.values().length]; + obj.getScore(entry).setScore(score); + } + + private String buildHpBar(int hp, int max, int len) { + if (max <= 0) return "§8-----"; + int filled = (int) Math.round((double) hp / max * len); + filled = Math.max(0, Math.min(len, filled)); + String color = filled > len / 2 ? "§a" : filled > 1 ? "§e" : "§c"; + return color + "█".repeat(filled) + "§8" + "░".repeat(len - filled); + } +} diff --git a/src/main/java/de/lasertag/weapon/WeaponType.java b/src/main/java/de/lasertag/weapon/WeaponType.java new file mode 100644 index 0000000..91dda0d --- /dev/null +++ b/src/main/java/de/lasertag/weapon/WeaponType.java @@ -0,0 +1,34 @@ +package de.lasertag.weapon; + +import org.bukkit.Material; + +/** + * Waffen-Typen. + * + * Item-Auswahl (Minecraft 1.20.1): + * BLAZE_ROD → Laser-Pistole – Leuchtendes orange Item, futuristisch + * CROSSBOW → Sniper – Sieht eindeutig wie eine Fernkampfwaffe aus + * TRIDENT → Shotgun – Groß, wuchtig, kurze Reichweite passend + * LIGHTNING_ROD→ Rapid-Fire – Schmaler Metallstab, elektrisch wirkend + * + * Alle Waffen-Werte (Schaden, Reichweite, Cooldown) kommen aus config.yml. + */ +public enum WeaponType { + + LASER_GUN ("laser-gun", Material.BLAZE_ROD), + SNIPER ("sniper", Material.CROSSBOW), + SHOTGUN ("shotgun", Material.TRIDENT), + RAPID_FIRE("rapid-fire", Material.LIGHTNING_ROD); + + private final String configKey; + private final Material material; + + WeaponType(String configKey, Material material) { + this.configKey = configKey; + this.material = material; + } + + public String getConfigKey() { return configKey; } + public Material getMaterial() { return material; } + public int getSlot() { return ordinal(); } +} diff --git a/src/main/java/de/lasertag/weapon/WeaponUtil.java b/src/main/java/de/lasertag/weapon/WeaponUtil.java new file mode 100644 index 0000000..5515abd --- /dev/null +++ b/src/main/java/de/lasertag/weapon/WeaponUtil.java @@ -0,0 +1,100 @@ +package de.lasertag.weapon; + +import de.lasertag.LasertagPlugin; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.CrossbowMeta; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.Arrays; +import java.util.Collections; + +public final class WeaponUtil { + + private WeaponUtil() {} + + /** + * Erstellt ein Waffen-ItemStack aus der config.yml. + * + * Besonderheiten: + * - CROSSBOW (Sniper): wird als NICHT geladen erstellt damit kein + * Lade-Animationsglitch entsteht. Wir geben ihr einen Glanz-Effekt. + * - TRIDENT (Shotgun): normaler Trident, Wurf wird im Listener blockiert. + * - LIGHTNING_ROD (Rapid-Fire): normaler Rod mit Glanz. + * - BLAZE_ROD (Pistole): leuchtet orange von Natur aus. + */ + public static ItemStack create(LasertagPlugin plugin, WeaponType type) { + var cfg = plugin.getConfigManager(); + String key = type.getConfigKey(); + + ItemStack item = new ItemStack(type.getMaterial()); + ItemMeta meta = item.getItemMeta(); + + meta.setDisplayName(cfg.getWeaponName(key)); + meta.setLore(Arrays.asList( + "§7" + cfg.getWeaponDesc(key), + "§8──────────────────────", + "§7Schaden: §c" + cfg.getWeaponDamage(key), + "§7Reichweite: §e" + cfg.getWeaponRange(key) + " Blöcke", + "§7Cooldown: §b" + String.format("%.2f", cfg.getWeaponCooldown(key) / 1000.0) + "s", + cfg.getWeaponPellets(key) > 1 + ? "§7Pellets: §6" + cfg.getWeaponPellets(key) + "x gleichzeitig" + : "§7Schuss: §61 Projektil", + "§8──────────────────────", + "§8✦ Rechtsklick §8= Schießen" + )); + + // Glanz-Effekt bei allen Waffen damit sie in der Hotbar hervorstechen + meta.addEnchant(Enchantment.DURABILITY, 1, true); + meta.addItemFlags( + ItemFlag.HIDE_ENCHANTS, + ItemFlag.HIDE_ATTRIBUTES, + ItemFlag.HIDE_UNBREAKABLE, + ItemFlag.HIDE_POTION_EFFECTS + ); + meta.setUnbreakable(true); + item.setItemMeta(meta); + + // Crossbow: sicherstellen dass sie NICHT geladen ist (kein Lade-Bug) + if (type.getMaterial() == Material.CROSSBOW) { + CrossbowMeta cm = (CrossbowMeta) item.getItemMeta(); + cm.setChargedProjectiles(Collections.emptyList()); + // Alle oben gesetzten Flags & Lore erneut setzen da wir neue Meta holen + cm.setDisplayName(cfg.getWeaponName(key)); + cm.setLore(Arrays.asList( + "§7" + cfg.getWeaponDesc(key), + "§8──────────────────────", + "§7Schaden: §c" + cfg.getWeaponDamage(key), + "§7Reichweite: §e" + cfg.getWeaponRange(key) + " Blöcke", + "§7Cooldown: §b" + String.format("%.2f", cfg.getWeaponCooldown(key) / 1000.0) + "s", + "§7Schuss: §61 Projektil", + "§8──────────────────────", + "§8✦ Rechtsklick §8= Schießen" + )); + cm.addEnchant(Enchantment.DURABILITY, 1, true); + cm.addItemFlags(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES, + ItemFlag.HIDE_UNBREAKABLE, ItemFlag.HIDE_POTION_EFFECTS); + cm.setUnbreakable(true); + item.setItemMeta(cm); + } + + return item; + } + + /** + * Identifiziert den WeaponType eines Items anhand des Display-Namens. + * Gibt null zurück wenn das Item keine Lasertag-Waffe ist. + */ + public static WeaponType identify(LasertagPlugin plugin, ItemStack item) { + if (item == null || !item.hasItemMeta() || !item.getItemMeta().hasDisplayName()) + return null; + String name = item.getItemMeta().getDisplayName(); + for (WeaponType t : WeaponType.values()) { + if (plugin.getConfigManager().getWeaponName(t.getConfigKey()).equals(name)) + return t; + } + return null; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..4f25792 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,206 @@ +# ============================================================ +# LASERTAG v3.0 — Vollständige Konfiguration +# Alle Werte hier können ohne Neustart geändert werden: +# /ltadmin reload +# ============================================================ + +# ── Allgemeines ────────────────────────────────────────────── +messages: + prefix: "§8[§b§lLASERTAG§8] §r" + language: "DE" # Nur DE unterstützt (erweiterbar) + +lobby: + location: null # Wird per /ltadmin setlobby gesetzt + +# ── Spiel-Grundeinstellungen ───────────────────────────────── +game: + game-duration: 300 # Spielzeit in Sekunden + countdown: 10 # Countdown-Sekunden vor Spielstart + min-players: 2 # Mindestspieler (alle Teams zusammen) + max-players-per-team: 4 # Max. Spieler pro Team (×4 = 16 gesamt) + end-display-time: 8 # Sekunden bis Reset nach Spielende + +# ── Respawn / Hit-Mechanik ─────────────────────────────────── +heal: + base-heal-time: 2 # Sekunden an der Basis zum Heilen + base-radius: 4.0 # Radius (Blöcke) um Basis-Block zum Heilen + invincible-after-heal: true # Kurze Unverwundbarkeit nach Heilung + invincible-duration: 60 # Ticks (60 = 3s) Unverwundbarkeit nach Heal + +# ── Punkte-System ──────────────────────────────────────────── +scoring: + kill-points: 100 # Punkte für Treffer auf Gegner + base-attack-points: 60 # Punkte für Treffer auf Gegnerbasis + base-destroy-bonus: 200 # Bonus wenn Basis vollständig zerstört + streak-3-bonus: 50 # Extra-Punkte bei 3er-Serie + streak-5-bonus: 100 # Extra-Punkte bei 5er-Serie + streak-10-bonus: 250 # Extra-Punkte bei 10er-Serie + +# ── Basen ──────────────────────────────────────────────────── +base: + health: 5 # Treffer bis Basis zerstört + warn-at-hp: 2 # Team warnen wenn Basis-HP auf diesen Wert fällt + regenerate: false # Basis-HP regenerieren während Spiel? + regenerate-interval: 60 # Sekunden zwischen Regen (wenn aktiviert) + +# ── Anti-Camp System ───────────────────────────────────────── +anti-camp: + enabled: true + # Maximale Zeit in Sekunden die ein Spieler im selben Bereich bleiben darf + max-idle-seconds: 15 + # Radius in Blöcken der als "gleicher Bereich" gilt + idle-radius: 5.0 + # Was passiert wenn der Spieler zu lange still steht: + action: WARN_THEN_PUNISH # WARN_ONLY | WARN_THEN_PUNISH | PUNISH_ONLY + # Warn-Nachricht (bei WARN) + warn-message: "§c⚠ CAMPEN VERBOTEN! Bewege dich oder verliere Punkte!" + # Punkte-Abzug pro Sekunde beim Campen (bei PUNISH) + score-penalty: 10 + # Sekunden Warnung bevor Strafe beginnt + warn-duration: 5 + # Basisbereich von Anti-Camp ausschließen? (Spieler können an Basis heilen) + exclude-base-radius: 8.0 + # Sound bei Warnung + warn-sound: BLOCK_NOTE_BLOCK_BASS + warn-sound-pitch: 0.5 + +# ── Minimap / Mod-Schutz ───────────────────────────────────── +mod-protection: + enabled: true + # Xaero's Minimap / VoxelMap etc. blockieren durch Fog-of-War: + # Spieler sehen nur einen begrenzten Radius um sich herum auf der Map + fog-of-war: true + fog-radius: 48 # Blöcke sichtbarer Radius (Render-Distance Trick) + # Regelmäßig unsichtbare Barrier-Blöcke um die Arena spawnen + # um Außenansicht-Cheats zu erschweren + arena-barrier: true + # F3-Debug-Screen blockieren (verhindert Koordinaten-Anzeige) + block-f3: false # experimentell, kann Lag verursachen + # Spieler-Koordinaten aus Tab-Liste entfernen + hide-coordinates: true + # Alle Spieler im Spiel aus der normalen Tab-Liste entfernen + hide-from-tab: true + # Spieler-Nametags im Spiel verbergen + hide-nametags: true + # Unsichtbare Spieler wirklich unsichtbar (verhindert Skeleton-Outline durch Mods) + strict-invisibility: true + +# ── Schild-System (Join-Schild) ────────────────────────────── +join-sign: + # Erste Zeile des Schilds (exakt so schreiben!) + trigger-line: "[Lasertag]" + # Farben im Schild + color-waiting: "§a" # Grün = Wartend + color-starting: "§e" # Gelb = Startet + color-running: "§c" # Rot = Läuft + color-full: "§8" # Grau = Voll + # Format Zeile 2 (Arenaname), Zeile 3 (Status), Zeile 4 (Spieler) + line-arena: "§b{arena}" + line-status: "{color}{status}" + line-players: "§7{players}§8/§7{max}" + # Update-Intervall des Schilds in Ticks (20 = 1s) + update-interval: 20 + +# ── Waffen-Einstellungen ───────────────────────────────────── +weapons: + laser-gun: + enabled: true + display-name: "§b⚡ Laser-Pistole" + damage: 25 + range: 30 + cooldown-ms: 300 + pellets: 1 + particle: END_ROD + description: "Standard Laser-Pistole. Zuverlässig & schnell." + + sniper: + enabled: true + display-name: "§5🎯 Laser-Sniper" + damage: 80 + range: 60 + cooldown-ms: 2000 + pellets: 1 + particle: DRAGON_BREATH + description: "Präzisions-Sniper. Hoher Schaden, sehr lange Reichweite." + + shotgun: + enabled: true + display-name: "§6💥 Laser-Shotgun" + damage: 20 + range: 12 + cooldown-ms: 900 + pellets: 5 + particle: FLAME + description: "Laser-Shotgun. 5 Pellets gleichzeitig, kurze Reichweite." + + rapid-fire: + enabled: true + display-name: "§a⚡⚡ Rapid-Fire" + damage: 12 + range: 22 + cooldown-ms: 120 + pellets: 1 + particle: CRIT + description: "Rapid-Fire Laser. Niedrig Schaden, sehr hohe Feuerrate." + +# ── Sound-Einstellungen ────────────────────────────────────── +sounds: + enabled: true + shoot: ENTITY_FIREWORK_ROCKET_BLAST + shoot-pitch: 1.8 + shoot-volume: 0.4 + hit-shooter: BLOCK_NOTE_BLOCK_BELL + hit-shooter-pitch: 2.0 + hit-victim: ENTITY_PLAYER_HURT + heal-start: BLOCK_ENCHANTMENT_TABLE_USE + heal-complete: ENTITY_EXPERIENCE_ORB_PICKUP + heal-complete-pitch: 1.5 + base-hit: ENTITY_GENERIC_EXPLODE + game-start: ENTITY_ENDER_DRAGON_GROWL + game-end: UI_TOAST_CHALLENGE_COMPLETE + countdown-tick: BLOCK_NOTE_BLOCK_PLING + warning: BLOCK_NOTE_BLOCK_BASS + streak-3: BLOCK_BELL_USE + streak-5: ENTITY_PLAYER_LEVELUP + streak-10: ENTITY_LIGHTNING_BOLT_THUNDER + +# ── Partikel-Einstellungen ─────────────────────────────────── +particles: + enabled: true + laser-trail: true + hit-effect: true + hit-particle-count: 15 + base-hit-effect: true + heal-effect: true + +# ── Scoreboard-Einstellungen ───────────────────────────────── +scoreboard: + enabled: true + title: "§b§l⚡ LASERTAG" + show-team-scores: true + show-base-health: true + show-kill-streak: true # Nur bei Streak ≥ 3 anzeigen + show-player-status: true + update-interval: 20 # Ticks + +# ── Nachrichten (anpassbar) ────────────────────────────────── +text: + join: "§e{player} §7hat Team {team} §7beigetreten." + leave: "§c{player} §7hat das Spiel verlassen." + game-start: "§a§l⚡ DAS SPIEL BEGINNT!" + game-end: "§6§l🏆 Team {team} §6§lhat gewonnen!" + game-draw: "§7Unentschieden!" + hit-shooter: "§aDu hast §e{victim} §agetroffen! §7(+{pts} Pkt){streak}" + hit-victim: "§cDu wurdest von §e{shooter} §cgetroffen! Gehe zur Basis!" + heal-start: "§aHeilung gestartet... §7({secs}s an der Basis bleiben!)" + heal-interrupted: "§c⚠ Heilung abgebrochen! Bleib an der Basis!" + heal-complete: "§a✔ Du bist wieder einsatzbereit!" + base-attacked: "§c⚠ Eure Basis wird angegriffen! §8[HP: {hp}/{max}]" + base-destroyed: "§c§l💥 Die Basis von Team {team} §c§lwurde ZERSTÖRT!" + camp-warn: "§c⚠ CAMPEN VERBOTEN! Bewege dich!" + time-60: "§e⏰ Noch §b60 §eSekunden!" + time-30: "§c⏰ Noch §b30 §eSekunden!" + time-10: "§4⏰ Noch §b10 §4Sekunden!" + streak-3: "§6TRIPLE KILL! §e{player} §7– 3er Serie!" + streak-5: "§bPENTA KILL! §e{player} §7– 5er Serie!" + streak-10: "§5§lGODLIKE! §e{player} §7– 10er Serie!" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..e59d51d --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,24 @@ +name: Lasertag +version: '1.0.0' +main: de.lasertag.LasertagPlugin +api-version: 1.20 +description: Realistic LaserTag Minigame with Base Mechanics +authors: [ M_Viper ] + +commands: + lasertag: + description: Main Lasertag command + aliases: [ lt, laser ] + usage: /lasertag + ltadmin: + description: Lasertag admin commands + usage: /ltadmin + permission: lasertag.admin + +permissions: + lasertag.play: + description: Allows playing Lasertag + default: true + lasertag.admin: + description: Allows admin commands + default: op