Update from Git Manager GUI

This commit is contained in:
2026-02-28 00:50:00 +01:00
parent 622f72e7f3
commit 27d6804717
8 changed files with 1004 additions and 80 deletions

View File

@@ -4,6 +4,7 @@ import de.fussball.plugin.arena.Arena;
import de.fussball.plugin.arena.ArenaManager;
import de.fussball.plugin.commands.FussballCommand;
import de.fussball.plugin.game.GameManager;
import de.fussball.plugin.hologram.HologramManager;
import de.fussball.plugin.listeners.*;
import de.fussball.plugin.placeholders.FussballPlaceholders;
import de.fussball.plugin.stats.StatsManager;
@@ -14,10 +15,11 @@ import org.bukkit.plugin.java.JavaPlugin;
public class Fussball extends JavaPlugin {
private static Fussball instance;
private ArenaManager arenaManager;
private GameManager gameManager;
private StatsManager statsManager;
private SignListener signListener;
private ArenaManager arenaManager;
private GameManager gameManager;
private StatsManager statsManager;
private SignListener signListener;
private HologramManager hologramManager;
@Override
public void onEnable() {
@@ -26,10 +28,11 @@ public class Fussball extends JavaPlugin {
saveDefaultConfig();
// Manager initialisieren
arenaManager = new ArenaManager(this);
gameManager = new GameManager(this);
statsManager = new StatsManager(this);
signListener = new SignListener(this);
arenaManager = new ArenaManager(this);
gameManager = new GameManager(this);
statsManager = new StatsManager(this);
signListener = new SignListener(this);
hologramManager = new HologramManager(this);
Messages.init(this);
registerCommands();
@@ -48,8 +51,9 @@ public class Fussball extends JavaPlugin {
@Override
public void onDisable() {
if (gameManager != null) gameManager.stopAllGames();
if (statsManager != null) statsManager.save();
if (gameManager != null) gameManager.stopAllGames();
if (statsManager != null) statsManager.save();
if (hologramManager != null) hologramManager.removeAll(); // Entities sauber entfernen
getLogger().info("⚽ Fußball-Plugin gestoppt!");
}
@@ -66,9 +70,10 @@ public class Fussball extends JavaPlugin {
getServer().getPluginManager().registerEvents(signListener, this);
}
public static Fussball getInstance() { return instance; }
public ArenaManager getArenaManager() { return arenaManager; }
public GameManager getGameManager() { return gameManager; }
public StatsManager getStatsManager() { return statsManager; }
public SignListener getSignListener() { return signListener; }
public static Fussball getInstance() { return instance; }
public ArenaManager getArenaManager() { return arenaManager; }
public GameManager getGameManager() { return gameManager; }
public StatsManager getStatsManager() { return statsManager; }
public SignListener getSignListener() { return signListener; }
public HologramManager getHologramManager() { return hologramManager; }
}

View File

@@ -12,6 +12,7 @@ public class Arena implements ConfigurationSerializable {
private final String name;
private Location center, redSpawn, blueSpawn, ballSpawn;
private Location redGoalMin, redGoalMax, blueGoalMin, blueGoalMax, lobby;
private Location spectatorSpawn; // Zuschauer-Spawn (optional Fallback: Spielfeldrand)
private Location fieldMin, fieldMax;
// Strafräume optional manuell gesetzt; sonst auto-berechnet aus Tor + config
private Location redPenaltyMin, redPenaltyMax, bluePenaltyMin, bluePenaltyMax;
@@ -259,6 +260,7 @@ public class Arena implements ConfigurationSerializable {
if (bluePenaltyMax != null) map.put("bluePenaltyMax", serLoc(bluePenaltyMax));
if (redPenaltySpot != null) map.put("redPenaltySpot", serLoc(redPenaltySpot));
if (bluePenaltySpot != null) map.put("bluePenaltySpot", serLoc(bluePenaltySpot));
if (spectatorSpawn != null) map.put("spectatorSpawn", serLoc(spectatorSpawn));
return map;
}
@@ -289,6 +291,7 @@ public class Arena implements ConfigurationSerializable {
if (map.containsKey("bluePenaltyMax")) a.bluePenaltyMax = desLoc(map.get("bluePenaltyMax"));
if (map.containsKey("redPenaltySpot")) a.redPenaltySpot = desLoc(map.get("redPenaltySpot"));
if (map.containsKey("bluePenaltySpot")) a.bluePenaltySpot = desLoc(map.get("bluePenaltySpot"));
if (map.containsKey("spectatorSpawn")) a.spectatorSpawn = desLoc(map.get("spectatorSpawn"));
return a;
}
@@ -364,6 +367,8 @@ public class Arena implements ConfigurationSerializable {
}
public boolean hasManualPenaltyAreas() { return redPenaltyMin != null && redPenaltyMax != null
&& bluePenaltyMin != null && bluePenaltyMax != null; }
public Location getSpectatorSpawn() { return spectatorSpawn; }
public void setSpectatorSpawn(Location l) { this.spectatorSpawn = l; }
public int getMinPlayers() { return minPlayers; }
public void setMinPlayers(int n) { this.minPlayers = n; }
public int getMaxPlayers() { return maxPlayers; }

View File

@@ -5,6 +5,8 @@ import de.fussball.plugin.arena.Arena;
import de.fussball.plugin.game.Ball;
import de.fussball.plugin.game.Game;
import de.fussball.plugin.game.GameState;
import de.fussball.plugin.hologram.FussballHologram;
import de.fussball.plugin.hologram.HologramManager;
import de.fussball.plugin.stats.StatsManager;
import de.fussball.plugin.utils.MessageUtil;
import org.bukkit.Bukkit;
@@ -200,6 +202,76 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
handleDebug(player, arena);
}
// ── Hologramm-Verwaltung ─────────────────────────────────────────
// /fb hologram set <id> goals|wins Hologramm erstellen
// /fb hologram remove Nächstes Hologramm (< 5 Blöcke) entfernen
// /fb hologram delete <id> Hologramm nach ID löschen
// /fb hologram reload Alle Hologramme neu spawnen
// /fb hologram list Alle Hologramme anzeigen
case "hologram", "holo" -> {
if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; }
if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; }
if (args.length < 2) {
player.sendMessage(MessageUtil.header("Hologramm-Befehle"));
player.sendMessage("§e/fb hologram set <id> goals|wins §7 Hologramm setzen");
player.sendMessage("§e/fb hologram remove §7 Nächstes entfernen (< 5 Blöcke)");
player.sendMessage("§e/fb hologram delete <id> §7 Nach ID löschen");
player.sendMessage("§e/fb hologram reload §7 Alle neu laden");
player.sendMessage("§e/fb hologram list §7 Alle anzeigen");
player.sendMessage("§7Gesamt: §e" + plugin.getHologramManager().getCount() + " §7Hologramme");
player.sendMessage("§7§oRechtsklick auf Hologramm → Tore ↔ Siege wechseln");
return true;
}
switch (args[1].toLowerCase()) {
case "set" -> {
if (args.length < 4) {
player.sendMessage(MessageUtil.error("Benutze: /fb hologram set <id> goals|wins"));
return true;
}
String id = args[2];
FussballHologram.HoloType type = switch (args[3].toLowerCase()) {
case "wins", "siege" -> FussballHologram.HoloType.WINS;
default -> FussballHologram.HoloType.GOALS;
};
plugin.getHologramManager().createHologram(id, player.getLocation(), type);
String holoLabel = type == FussballHologram.HoloType.WINS ? "Top-10-Siege" : "Top-10-Tore";
player.sendMessage(MessageUtil.success("§e" + id + " §a(" + holoLabel + ") Hologramm gesetzt!"));
player.sendMessage("§7§oRechtsklick auf das Hologramm wechselt zwischen Tore und Siege.");
}
case "remove" -> {
String removed = plugin.getHologramManager().removeNearest(player.getLocation());
if (removed != null) {
player.sendMessage(MessageUtil.success("Hologramm §e" + removed + " §aentfernt!"));
} else {
player.sendMessage(MessageUtil.error("Kein Hologramm innerhalb von 5 Blöcken gefunden!"));
}
}
case "delete" -> {
if (args.length < 3) { player.sendMessage(MessageUtil.error("Benutze: /fb hologram delete <id>")); return true; }
if (plugin.getHologramManager().removeHologram(args[2])) {
player.sendMessage(MessageUtil.success("Hologramm §e" + args[2] + " §agelöscht!"));
} else {
player.sendMessage(MessageUtil.error("Kein Hologramm mit ID §e" + args[2] + "§c gefunden!"));
}
}
case "reload" -> {
plugin.getHologramManager().reload();
player.sendMessage(MessageUtil.success("Hologramme neu geladen! §7(" + plugin.getHologramManager().getCount() + " gesamt)"));
}
case "list" -> {
player.sendMessage(MessageUtil.header("Hologramme (" + plugin.getHologramManager().getCount() + ")"));
if (plugin.getHologramManager().getCount() == 0) {
player.sendMessage(MessageUtil.warn("Keine Hologramme vorhanden."));
} else {
for (String id : plugin.getHologramManager().getHologramIds()) {
player.sendMessage("§7 • §e" + id);
}
}
}
default -> player.sendMessage(MessageUtil.error("Gültig: set <id> goals|wins | remove | delete <id> | reload | list"));
}
}
default -> sendHelp(sender);
}
return true;
@@ -229,6 +301,7 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
case "bluepenaltymax" -> { arena.setBluePenaltyMax(player.getLocation()); player.sendMessage(MessageUtil.success("Blauer Strafraum Max gesetzt: "+ locStr(player.getLocation()))); }
case "redpenaltyspot" -> { arena.setRedPenaltySpot(player.getLocation()); player.sendMessage(MessageUtil.success("Roter Elfmeter-Punkt gesetzt: " + locStr(player.getLocation()))); }
case "bluepenaltyspot" -> { arena.setBluePenaltySpot(player.getLocation()); player.sendMessage(MessageUtil.success("Blauer Elfmeter-Punkt gesetzt: " + locStr(player.getLocation()))); }
case "spectatorspawn" -> { arena.setSpectatorSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Zuschauer-Spawn gesetzt: " + locStr(player.getLocation()))); }
case "minplayers" -> { if (args.length < 4) return; arena.setMinPlayers(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Min-Spieler: §e" + args[3])); }
case "maxplayers" -> { if (args.length < 4) return; arena.setMaxPlayers(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Max-Spieler: §e" + args[3])); }
case "duration" -> { if (args.length < 4) return; arena.setGameDuration(Integer.parseInt(args[3])); player.sendMessage(MessageUtil.success("Spieldauer: §e" + args[3] + "s")); }
@@ -246,7 +319,7 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
+ " §8(optional sonst auto-berechnet)");
player.sendMessage("§7 Blauer Strafraum: " + check(arena.getBluePenaltyMin(), arena.getBluePenaltyMax())
+ " §8(optional sonst auto-berechnet)");
player.sendMessage("§7 Roter Elfmeter-Punkt: " + check(arena.getRedPenaltySpot()) + " §8(optional sonst ball-spawn)");
player.sendMessage("§7 Zuschauer-Spawn: " + check(arena.getSpectatorSpawn()) + " §8(optional sonst Spielfeldrand)");
player.sendMessage("§7 Blauer Elfmeter-Punkt: " + check(arena.getBluePenaltySpot()) + " §8(optional sonst ball-spawn)");
player.sendMessage("§7 Min. Spieler: §e" + arena.getMinPlayers());
player.sendMessage("§7 Max. Spieler: §e" + arena.getMaxPlayers());
@@ -320,6 +393,7 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
s.sendMessage("§e/fb top [goals|wins] §7- Bestenliste");
if (s.hasPermission("fussball.admin")) {
s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug");
s.sendMessage("§c§lAdmin: §chologram set goals|wins / remove / reload");
}
}
@@ -345,9 +419,17 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
List<String> list = new ArrayList<>();
if (args.length == 1) {
list.addAll(List.of("join", "leave", "list", "stats", "top", "spectate"));
if (sender.hasPermission("fussball.admin")) list.addAll(List.of("create", "delete", "setup", "stop", "setgk", "debug"));
if (sender.hasPermission("fussball.admin")) list.addAll(List.of("create", "delete", "setup", "stop", "setgk", "debug", "hologram"));
} else if (args.length == 2 && List.of("join","delete","setup","stop","setgk","debug","spectate").contains(args[0].toLowerCase())) {
list.addAll(plugin.getArenaManager().getArenaNames());
} else if (args.length == 2 && args[0].equalsIgnoreCase("hologram")) {
list.addAll(List.of("set", "remove", "delete", "reload", "list"));
} else if (args.length == 3 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("set")) {
list.addAll(plugin.getArenaManager().getArenaNames()); // id-Vorschläge (frei wählbar, aber arena-namen passen)
} else if (args.length == 4 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("set")) {
list.addAll(List.of("goals", "wins"));
} else if (args.length == 3 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("delete")) {
list.addAll(plugin.getHologramManager().getHologramIds());
} else if (args.length == 3 && args[0].equalsIgnoreCase("setgk")) {
// Spielernamen aus dem aktiven Spiel vorschlagen
Game gkGame = plugin.getGameManager().getGame(args[1]);
@@ -362,7 +444,7 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
"redgoalmin","redgoalmax","bluegoalmin","bluegoalmax",
"fieldmin","fieldmax",
"redpenaltymin","redpenaltymax","bluepenaltymin","bluepenaltymax",
"redpenaltyspot","bluepenaltyspot",
"redpenaltyspot","bluepenaltyspot","spectatorspawn",
"minplayers","maxplayers","duration","info"));
} else if (args.length == 2 && args[0].equalsIgnoreCase("top")) {
list.addAll(List.of("goals", "wins"));

View File

@@ -52,6 +52,10 @@ public class Game {
private final Map<UUID, Integer> outOfBoundsCountdown = new HashMap<>();
// ────────────────────────────────────────────────────────────────────────
// ── AFK-Erkennung ──────────────────────────────────────────────────────
private final Map<UUID, Location> lastPosition = new HashMap<>(); // letzte gemessene Position
private final Map<UUID, Integer> afkTicks = new HashMap<>(); // Ticks ohne Bewegung
private UUID lastKicker = null;
private UUID secondLastKicker = null; // für Assist-Erkennung
private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt)
@@ -161,15 +165,29 @@ public class Game {
public boolean addSpectator(Player player) {
if (isInGame(player)) { player.sendMessage(MessageUtil.error("Du bist bereits Spieler!")); return false; }
if (isSpectator(player)) { player.sendMessage(MessageUtil.error("Du schaust bereits zu!")); return false; }
if (state == GameState.WAITING || state == GameState.STARTING || state == GameState.ENDING) {
player.sendMessage(MessageUtil.error("Kein laufendes Spiel zum Zuschauen!")); return false;
if (state == GameState.ENDING) {
player.sendMessage(MessageUtil.error("Das Spiel ist gerade dabei zu enden!")); return false;
}
spectators.add(player.getUniqueId());
player.setGameMode(GameMode.SPECTATOR);
player.teleport(arena.getBallSpawn() != null ? arena.getBallSpawn() : arena.getLobby());
player.setGameMode(GameMode.ADVENTURE);
// Gesetzter Zuschauer-Spawn hat Priorität, danach Fallback über getSpectatorSpawn()
Location spectatorLoc = getSpectatorSpawn();
if (spectatorLoc != null) player.teleport(spectatorLoc);
// Zuschauer bleiben sichtbar die Arena-Grenzen verhindern das Betreten des Feldes.
// Inventar leeren und Hunger/Schaden deaktivieren (bereits durch PlayerListener)
player.getInventory().clear();
player.setHealth(20.0);
player.setFoodLevel(20);
for (PotionEffect e : player.getActivePotionEffects()) player.removePotionEffect(e.getType());
scoreboard.give(player);
if (bossBar != null) bossBar.addPlayer(player);
player.sendMessage(MessageUtil.success("Du schaust jetzt §e" + arena.getName() + " §azu! §7(/fb leave zum Beenden)"));
String stateHint = (state == GameState.WAITING || state == GameState.STARTING)
? " §7(Spiel startet gleich)" : "";
player.sendMessage(MessageUtil.success("Du schaust jetzt §e" + arena.getName() + " §azu!" + stateHint + " §7(/fb leave zum Beenden)"));
player.sendMessage(MessageUtil.info("§7Du kannst die Arena nicht betreten. Viel Spaß beim Zuschauen!"));
return true;
}
@@ -177,6 +195,7 @@ public class Game {
spectators.remove(player.getUniqueId());
if (bossBar != null) bossBar.removePlayer(player);
scoreboard.remove(player);
resetPlayer(player);
}
@@ -225,11 +244,13 @@ public class Game {
player.getInventory().setLeggings(armor[2]); player.getInventory().setBoots(armor[3]);
}
/** BUG FIX: Teleportiert zur Lobby statt Welt-Spawn */
/** Setzt einen Spieler oder Zuschauer zurück (Lobby, ADVENTURE, Inventar leer) */
private void resetPlayer(Player player) {
player.getInventory().clear();
for (PotionEffect e : player.getActivePotionEffects()) player.removePotionEffect(e.getType());
player.setGameMode(GameMode.SURVIVAL);
player.setGameMode(GameMode.ADVENTURE); // ADVENTURE kein SURVIVAL (kein PvP / kein Hunger)
player.setHealth(20.0);
player.setFoodLevel(20);
Location tp = arena.getLobby() != null ? arena.getLobby() : Bukkit.getWorlds().get(0).getSpawnLocation();
player.teleport(tp);
}
@@ -578,6 +599,9 @@ public class Game {
if (gameTask != null) { gameTask.cancel(); gameTask = null; }
gameTask = new BukkitRunnable() {
public void run() {
// Zuschauer-Grenzen immer prüfen (unabhängig vom Spielzustand)
checkSpectatorBoundaries();
if (state != GameState.RUNNING && state != GameState.OVERTIME) return;
// Kopfball-Abklingzeiten herunterzählen
@@ -615,6 +639,7 @@ public class Game {
checkPlayerBallInteraction();
checkPlayerBoundaries();
checkHeaderOpportunities();
checkAfkPlayers();
// Freistoß-Abstandsdurchsetzung
if (freekickLocation != null) {
@@ -849,7 +874,7 @@ public class Game {
for (UUID uuid : allPlayers) {
Player p = Bukkit.getPlayer(uuid);
if (p == null) continue;
if (ball.getDistanceTo(p) >= 1.5) continue;
if (ball.getDistanceTo(p) >= 2.2) continue;
if (throwInTeam != null && getTeam(p) != throwInTeam) continue;
// ── Rückpass-Regel für Torwarte ──────────────────────────────────
@@ -1336,9 +1361,199 @@ public class Game {
}
// ════════════════════════════════════════════════════════════════════════
// KOPFBALL
// AFK-ERKENNUNG
// ════════════════════════════════════════════════════════════════════════
/**
* Erkennt Spieler die zu lange stillstehen und ermahnt / kickt sie.
*
* Konfiguration (config.yml):
* gameplay.afk-warn-seconds: 20 → erste Warnung nach 20s Stillstand
* gameplay.afk-kick-seconds: 40 → Disqualifikation nach 40s Stillstand
* gameplay.afk-move-threshold: 0.5 → Mindestbewegung in Blöcken pro Sekunde
*
* Läuft sekündlich im Game-Loop (nur bei RUNNING / OVERTIME).
*/
private void checkAfkPlayers() {
if (state != GameState.RUNNING && state != GameState.OVERTIME) return;
int warnSecs = plugin.getConfig().getInt("gameplay.afk-warn-seconds", 20);
int kickSecs = plugin.getConfig().getInt("gameplay.afk-kick-seconds", 40);
double threshold = plugin.getConfig().getDouble("gameplay.afk-move-threshold", 0.5);
for (UUID uuid : new ArrayList<>(allPlayers)) {
Player p = Bukkit.getPlayer(uuid);
if (p == null) continue;
Location current = p.getLocation();
Location last = lastPosition.get(uuid);
// Erste Messung → Position speichern, kein AFK-Zähler
if (last == null) {
lastPosition.put(uuid, current.clone());
continue;
}
double dist = current.distanceSquared(last); // Quadrat reicht zum Vergleich
if (dist >= threshold * threshold) {
// Spieler hat sich bewegt → AFK-Counter zurücksetzen
afkTicks.remove(uuid);
lastPosition.put(uuid, current.clone());
} else {
// Spieler steht still → Counter erhöhen
int ticks = afkTicks.merge(uuid, 1, Integer::sum);
lastPosition.put(uuid, current.clone());
if (ticks == warnSecs) {
// ── Erste Warnung ────────────────────────────────────────
p.sendTitle("§e§l⚠ AFK?", "§7Bewege dich du wirst sonst disqualifiziert!", 5, 50, 10);
p.sendMessage(MessageUtil.warn("§7Du stehst seit §e" + warnSecs + "s §7still! Bewege dich oder du wirst rausgeworfen!"));
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.5f);
broadcastAll(MessageUtil.warn("§e" + p.getName() + " §7scheint AFK zu sein!"));
} else if (ticks > warnSecs && ticks < kickSecs && (ticks - warnSecs) % 5 == 0) {
// ── Erinnerungen alle 5s ─────────────────────────────────
int remaining = kickSecs - ticks;
p.sendTitle("§c§l⚠ NOCH " + remaining + "s!", "§7Bewege dich oder du wirst disqualifiziert!", 5, 25, 5);
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 0.3f);
} else if (ticks >= kickSecs) {
// ── Disqualifikation ─────────────────────────────────────
afkTicks.remove(uuid);
lastPosition.remove(uuid);
broadcastAll(MessageUtil.warn("§e" + p.getName() + " §7wurde wegen AFK disqualifiziert!"));
p.sendTitle("§c§lDISQUALIFIZIERT!", "§7Du warst zu lange AFK!", 10, 80, 20);
disqualifyPlayer(p);
}
}
}
}
// ════════════════════════════════════════════════════════════════════════
// ZUSCHAUER-GRENZEN
// ════════════════════════════════════════════════════════════════════════
/**
* Zuschauer müssen sich AUSSERHALB des Spielfeldes aufhalten (mind. 2 Blöcke Abstand),
* dürfen aber einen Außenpuffer von 15 Blöcken um das Spielfeld nicht verlassen.
*
* Zu nah am Feld (< 2 Blöcke) → wird nach außen geschoben
* Zu weit vom Feld (> 15 Blöcke) → wird zum Spielfeldrand zurückteleportiert
* Falsche Welt → sofort zurück
*/
private void checkSpectatorBoundaries() {
if (spectators.isEmpty()) return;
if (arena.getFieldMin() == null || arena.getFieldMax() == null) return;
final double INNER_BUFFER = 2.0; // Mindestabstand zum Spielfeld (außen)
final double OUTER_BUFFER = 15.0; // Maximaler Abstand vom Spielfeld
// Spielfeld-Grenzen
double fMinX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double fMaxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX());
double fMinZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double fMaxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double fMinY = Math.min(arena.getFieldMin().getY(), arena.getFieldMax().getY());
double fMaxY = Math.max(arena.getFieldMin().getY(), arena.getFieldMax().getY());
// Innere Grenze (Spielfeld + INNER_BUFFER) Zuschauer NICHT erlaubt
double innerMinX = fMinX - INNER_BUFFER; double innerMaxX = fMaxX + INNER_BUFFER;
double innerMinZ = fMinZ - INNER_BUFFER; double innerMaxZ = fMaxZ + INNER_BUFFER;
// Äußere Grenze (Spielfeld + OUTER_BUFFER) Zuschauer MÜSSEN innerhalb sein
double outerMinX = fMinX - OUTER_BUFFER; double outerMaxX = fMaxX + OUTER_BUFFER;
double outerMinZ = fMinZ - OUTER_BUFFER; double outerMaxZ = fMaxZ + OUTER_BUFFER;
double outerMinY = fMinY - 5; double outerMaxY = fMaxY + 30;
// Rückteleport-Ziel: Spielfeldrand (+ INNER_BUFFER + 1, damit klar außerhalb)
// Wir teleportieren zur Mitte einer Seitenlinie
double centerX = (fMinX + fMaxX) / 2.0;
double centerZ = (fMinZ + fMaxZ) / 2.0;
double baseY = fMinY;
// Rand-Spawn: an der langen Seite des Feldes, außen
Location spectatorEdge = new Location(
arena.getFieldMin().getWorld(),
centerX,
baseY,
fMaxZ + INNER_BUFFER + 1,
0f, 0f
);
// Fallback: Ball-Spawn / Lobby
Location fallback = arena.getBallSpawn() != null ? arena.getBallSpawn() : arena.getLobby();
for (UUID uuid : new ArrayList<>(spectators)) {
Player p = Bukkit.getPlayer(uuid);
if (p == null) continue;
// ── Falsche Welt ──────────────────────────────────────────────────
if (!p.getWorld().equals(arena.getFieldMin().getWorld())) {
Location tp = fallback != null ? fallback : spectatorEdge;
p.teleport(tp);
p.sendTitle("§c⚠ FALSCHE WELT!", "§7Zurück zur Arena teleportiert!", 5, 40, 10);
continue;
}
Location loc = p.getLocation();
double px = loc.getX(), pz = loc.getZ(), py = loc.getY();
boolean insideInner = px > innerMinX && px < innerMaxX
&& pz > innerMinZ && pz < innerMaxZ;
boolean outsideOuter = px < outerMinX || px > outerMaxX
|| pz < outerMinZ || pz > outerMaxZ
|| py < outerMinY || py > outerMaxY;
if (insideInner) {
// ── Zu nah / auf dem Spielfeld → raus schieben ───────────────
// Nächsten Punkt auf dem inneren Rand berechnen und leicht weiter raus
double pushX = Math.max(innerMinX, Math.min(px, innerMaxX));
double pushZ = Math.max(innerMinZ, Math.min(pz, innerMaxZ));
// Richtung nach außen bestimmen
double dxMin = Math.abs(px - innerMinX), dxMax = Math.abs(px - innerMaxX);
double dzMin = Math.abs(pz - innerMinZ), dzMax = Math.abs(pz - innerMaxZ);
double minDist = Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax));
Location push;
if (minDist == dxMin) push = new Location(loc.getWorld(), innerMinX - 1, loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
else if (minDist == dxMax) push = new Location(loc.getWorld(), innerMaxX + 1, loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
else if (minDist == dzMin) push = new Location(loc.getWorld(), loc.getX(), loc.getY(), innerMinZ - 1, loc.getYaw(), loc.getPitch());
else push = new Location(loc.getWorld(), loc.getX(), loc.getY(), innerMaxZ + 1, loc.getYaw(), loc.getPitch());
p.teleport(push);
p.sendTitle("§c⚠ SPIELFELD!", "§7Zuschauer müssen außerhalb des Feldes bleiben!", 5, 30, 5);
} else if (outsideOuter) {
// ── Zu weit draußen → zum Spielfeldrand zurück ───────────────
p.teleport(spectatorEdge);
p.sendTitle("§c⚠ ARENAGRENZE!", "§7Zuschauer dürfen die Arena nicht verlassen!", 5, 40, 10);
p.sendMessage(MessageUtil.warn("§7Als Zuschauer darfst du die Arena nicht verlassen!"));
}
}
}
/** Für PlayerListener: prüft ob Zuschauer außerhalb des erlaubten Bereichs ist */
public boolean isSpectatorOutOfBounds(Player player) {
if (!isSpectator(player)) return false;
if (arena.getFieldMin() == null || arena.getFieldMax() == null) return false;
final double OUTER_BUFFER = 15.0;
Location loc = player.getLocation();
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX()) - OUTER_BUFFER;
double maxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX()) + OUTER_BUFFER;
double minZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ()) - OUTER_BUFFER;
double maxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ()) + OUTER_BUFFER;
return loc.getX() < minX || loc.getX() > maxX || loc.getZ() < minZ || loc.getZ() > maxZ;
}
public Location getSpectatorSpawn() {
// Manuell gesetzter Zuschauer-Spawn hat Priorität
if (arena.getSpectatorSpawn() != null) return arena.getSpectatorSpawn();
// Fallback: Spielfeldrand (Mitte der langen Seite, 3 Blöcke außen)
if (arena.getFieldMin() != null && arena.getFieldMax() != null) {
double fMaxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
double centerX = (arena.getFieldMin().getX() + arena.getFieldMax().getX()) / 2.0;
double baseY = Math.min(arena.getFieldMin().getY(), arena.getFieldMax().getY());
return new Location(arena.getFieldMin().getWorld(), centerX, baseY, fMaxZ + 3, 0f, 0f);
}
return arena.getBallSpawn() != null ? arena.getBallSpawn() : arena.getLobby();
}
/**
* Prüft jeden Sekunden-Tick ob ein Spieler den Ball köpfen kann.
* Bedingungen: Spieler ist in der Luft, Ball befindet sich auf Kopfhöhe,

View File

@@ -0,0 +1,220 @@
package de.fussball.plugin.hologram;
import de.fussball.plugin.Fussball;
import de.fussball.plugin.stats.StatsManager;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.entity.Display;
import org.bukkit.entity.Interaction;
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Ein einzelnes Fußball-Statistik-Hologramm.
*
* Basiert auf NexusHologram (NexusLobby):
* • Pro Spieler eine eigene TextDisplay-Entity → nur er sieht sie
* • Interaction-Entity als Hitbox → Rechtsklick wechselt zwischen Seiten
* • Seiten: GOALS (Top-10 Torschützen) und WINS (Top-10 Gewinner)
* • Distanz-Check: > 48 Blöcke → Entity entfernen (Bandbreite sparen)
*/
public class FussballHologram {
/** Render-Radius: 48 Blöcke (2304 = 48²) */
private static final double RENDER_RADIUS_SQ = 2304.0;
public enum HoloType { GOALS, WINS }
private final String id;
private final Location location;
private HoloType type; // mutable nicht final, damit Admin den Typ ändern kann
// UUID des Spielers → seine persönliche Entity
private final Map<UUID, TextDisplay> playerEntities = new ConcurrentHashMap<>();
private final Map<UUID, Interaction> playerInteractions = new ConcurrentHashMap<>();
/** Aktuell angezeigte Seite (0 = GOALS, 1 = WINS) pro Spieler */
private final Map<UUID, Integer> currentPage = new ConcurrentHashMap<>();
private final Fussball plugin;
public FussballHologram(String id, Location location, HoloType type, Fussball plugin) {
this.id = id;
this.location = location.clone();
this.type = type;
this.plugin = plugin;
}
// ── Seiten-Wechsel ───────────────────────────────────────────────────────
/** Rechtsklick → zur nächsten Seite (GOALS ↔ WINS) */
public void nextPage(Player player) {
// Wir haben 2 Seiten: 0 = GOALS, 1 = WINS
int next = (currentPage.getOrDefault(player.getUniqueId(), 0) + 1) % 2;
currentPage.put(player.getUniqueId(), next);
renderForPlayer(player);
}
// ── Render ───────────────────────────────────────────────────────────────
/**
* Rendert das Hologramm für einen Spieler oder entfernt es wenn er zu weit weg ist.
* Wird alle 5 Ticks vom HologramManager aufgerufen.
*/
public void renderForPlayer(Player player) {
// Falsche Welt oder zu weit → entfernen
if (!player.getWorld().equals(location.getWorld())
|| player.getLocation().distanceSquared(location) > RENDER_RADIUS_SQ) {
removeForPlayer(player);
return;
}
// Seite bestimmen
int pageIdx = currentPage.getOrDefault(player.getUniqueId(), type == HoloType.WINS ? 1 : 0);
HoloType displayType = pageIdx == 1 ? HoloType.WINS : HoloType.GOALS;
String text = buildText(displayType);
TextDisplay display = playerEntities.get(player.getUniqueId());
if (display == null || !display.isValid()) {
// ── Neue TextDisplay-Entity spawnen ─────────────────────────────
display = location.getWorld().spawn(location, TextDisplay.class, entity -> {
entity.setCustomName("fb_holo_" + id + "_" + player.getName());
entity.setCustomNameVisible(false);
entity.setPersistent(false);
entity.setBillboard(Display.Billboard.CENTER);
entity.setBackgroundColor(Color.fromARGB(0, 0, 0, 0)); // komplett transparent
entity.setDefaultBackground(false); // Standard-Grau-Panel entfernen
entity.setText(text);
entity.setInvulnerable(true);
entity.setSeeThrough(false);
});
// ── Interaction-Entity spawnen (Hitbox für Rechtsklick) ──────────
Interaction interact = location.getWorld().spawn(location, Interaction.class, entity -> {
entity.setInteractionWidth(2.5f);
entity.setInteractionHeight(2.5f);
entity.setCustomNameVisible(false);
entity.setPersistent(false);
});
// ── Nur für diesen Spieler sichtbar machen ───────────────────────
TextDisplay finalDisplay = display;
Interaction finalInteract = interact;
for (Player other : Bukkit.getOnlinePlayers()) {
if (!other.getUniqueId().equals(player.getUniqueId())) {
other.hideEntity(plugin, finalDisplay);
other.hideEntity(plugin, finalInteract);
}
}
playerEntities.put(player.getUniqueId(), display);
playerInteractions.put(player.getUniqueId(), interact);
} else {
// ── Bestehende Entity nur aktualisieren (kein Re-Spawn) ──────────
if (!display.getText().equals(text)) {
display.setText(text);
}
}
}
// ── Entfernen ────────────────────────────────────────────────────────────
/** Entfernt die Entities eines einzelnen Spielers (Disconnect / Worldwechsel / zu weit) */
public void removeForPlayer(Player player) {
TextDisplay display = playerEntities.remove(player.getUniqueId());
if (display != null) display.remove();
Interaction interact = playerInteractions.remove(player.getUniqueId());
if (interact != null) interact.remove();
currentPage.remove(player.getUniqueId());
}
/** Entfernt ALLE Entities (Plugin-Disable / Hologramm gelöscht) */
public void removeAll() {
playerEntities.values().forEach(TextDisplay::remove);
playerInteractions.values().forEach(Interaction::remove);
playerEntities.clear();
playerInteractions.clear();
currentPage.clear();
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
/**
* Prüft, ob die gegebene Entity-UUID eine Interaction-Entity dieses Hologramms ist.
* (Wird in HologramManager genutzt um Klick-Events zuzuordnen)
*/
public boolean isInteractionEntity(UUID entityId) {
return playerInteractions.values().stream()
.anyMatch(i -> i.getUniqueId().equals(entityId));
}
/** Baut den anzuzeigenden Text aus den aktuellen Top-10-Statistiken */
private String buildText(HoloType showType) {
StringBuilder sb = new StringBuilder();
if (showType == HoloType.GOALS) {
sb.append("§6§l⚽ TOP 10 TORSCHÜTZEN ⚽\n");
sb.append("§8§m══════════════════════§r\n");
var list = plugin.getStatsManager().getTopScorers(10);
if (list.isEmpty()) {
sb.append("§8Noch keine Statistiken vorhanden.");
} else {
for (int i = 0; i < list.size(); i++) {
StatsManager.PlayerStats s = list.get(i).getValue();
sb.append(medal(i + 1))
.append(" §0").append(s.name)
.append(" §4").append(s.goals).append(" §8Tore");
if (i < list.size() - 1) sb.append("\n");
}
}
sb.append("\n§8§m══════════════════════§r");
sb.append("\n§8§o[Rechtsklick → Siege anzeigen]");
} else {
sb.append("§2§l🏆 TOP 10 GEWINNER 🏆\n");
sb.append("§8§m══════════════════════§r\n");
var list = plugin.getStatsManager().getTopWins(10);
if (list.isEmpty()) {
sb.append("§8Noch keine Statistiken vorhanden.");
} else {
for (int i = 0; i < list.size(); i++) {
StatsManager.PlayerStats s = list.get(i).getValue();
sb.append(medal(i + 1))
.append(" §0").append(s.name)
.append(" §2").append(s.wins).append(" §8Siege")
.append(" §8(").append(String.format("%.0f", s.getWinRate())).append("%)");
if (i < list.size() - 1) sb.append("\n");
}
}
sb.append("\n§8§m══════════════════════§r");
sb.append("\n§8§o[Rechtsklick → Tore anzeigen]");
}
return sb.toString();
}
private String medal(int rank) {
return switch (rank) {
case 1 -> "§6§l#1"; // Gold bleibt hebt sich gut ab
case 2 -> "§8§l#2"; // Dunkelgrau
case 3 -> "§4§l#3"; // Dunkelrot
default -> "§8#" + rank;
};
}
// ── Getter ───────────────────────────────────────────────────────────────
public String getId() { return id; }
public Location getLocation() { return location.clone(); }
public HoloType getType() { return type; }
public void setType(HoloType t) { this.type = t; }
}

View File

@@ -0,0 +1,293 @@
package de.fussball.plugin.hologram;
import de.fussball.plugin.Fussball;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Interaction;
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Verwaltet alle Fußball-Statistik-Hologramme.
*
* Adaption von HologramModule (NexusLobby):
* • Pro Spieler eine eigene TextDisplay-Entity (nur er sieht sie)
* • Interaction-Entity als Klick-Hitbox → Seiten wechseln (Tore ↔ Siege)
* • Render-Task alle 5 Ticks → Distanzprüfung + Text-Update
* • Cleanup bei Join / Quit / Weltenwechsel
* • Persistierung in holograms.yml
*
* Befehle (FussballCommand):
* /fb hologram set goals|wins Hologramm an Spielerposition erstellen
* /fb hologram remove Nächstes Hologramm (< 5 Blöcke) entfernen
* /fb hologram reload Alle Hologramme neu laden (nach Restart)
* /fb hologram list Alle Hologramme auflisten
*/
public class HologramManager implements Listener {
private final Fussball plugin;
private final Map<String, FussballHologram> holograms = new ConcurrentHashMap<>();
private File holoFile;
private FileConfiguration holoConfig;
private BukkitTask renderTask;
public HologramManager(Fussball plugin) {
this.plugin = plugin;
loadConfig();
loadHolograms();
startRenderTask();
cleanupStrayEntities();
Bukkit.getPluginManager().registerEvents(this, plugin);
}
// ── Konfiguration ────────────────────────────────────────────────────────
private void loadConfig() {
holoFile = new File(plugin.getDataFolder(), "holograms.yml");
if (!holoFile.exists()) {
try { holoFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); }
}
holoConfig = YamlConfiguration.loadConfiguration(holoFile);
}
private void saveConfig() {
try {
holoConfig.save(holoFile);
} catch (IOException e) {
plugin.getLogger().severe("[Hologram] Konnte holograms.yml nicht speichern: " + e.getMessage());
}
}
// ── Laden / Erstellen / Entfernen ────────────────────────────────────────
private void loadHolograms() {
// Vorherige Instanzen sauber entfernen
holograms.values().forEach(FussballHologram::removeAll);
holograms.clear();
if (!holoConfig.contains("holograms")) return;
for (String id : holoConfig.getConfigurationSection("holograms").getKeys(false)) {
String path = "holograms." + id;
String worldName = holoConfig.getString(path + ".world");
if (worldName == null) continue;
World world = Bukkit.getWorld(worldName);
if (world == null) {
plugin.getLogger().warning("[Hologram] Welt '" + worldName
+ "' nicht gefunden Hologramm '" + id + "' übersprungen.");
continue;
}
Location loc = new Location(
world,
holoConfig.getDouble(path + ".x"),
holoConfig.getDouble(path + ".y"),
holoConfig.getDouble(path + ".z")
);
String typeStr = holoConfig.getString(path + ".type", "GOALS");
FussballHologram.HoloType type =
"WINS".equalsIgnoreCase(typeStr)
? FussballHologram.HoloType.WINS
: FussballHologram.HoloType.GOALS;
holograms.put(id, new FussballHologram(id, loc, type, plugin));
}
plugin.getLogger().info("[Hologram] " + holograms.size() + " Hologramme geladen.");
}
/**
* Erstellt ein neues Hologramm und speichert es in holograms.yml.
* Falls die ID bereits existiert, wird das alte sauber entfernt.
*/
public boolean createHologram(String id, Location loc, FussballHologram.HoloType type) {
if (holograms.containsKey(id)) removeHologram(id);
String path = "holograms." + id;
holoConfig.set(path + ".world", loc.getWorld().getName());
holoConfig.set(path + ".x", loc.getX());
holoConfig.set(path + ".y", loc.getY());
holoConfig.set(path + ".z", loc.getZ());
holoConfig.set(path + ".type", type.name());
saveConfig();
FussballHologram holo = new FussballHologram(id, loc, type, plugin);
holograms.put(id, holo);
// Sofort für alle Online-Spieler rendern
for (Player player : Bukkit.getOnlinePlayers()) holo.renderForPlayer(player);
return true;
}
/**
* Entfernt ein Hologramm anhand seiner ID.
* @return true wenn gefunden und entfernt
*/
public boolean removeHologram(String id) {
FussballHologram holo = holograms.remove(id);
if (holo == null) return false;
// Erst für alle Online-Spieler visuell entfernen
for (Player player : Bukkit.getOnlinePlayers()) holo.removeForPlayer(player);
// Dann alle Entities serverseitig löschen
holo.removeAll();
holoConfig.set("holograms." + id, null);
saveConfig();
return true;
}
/**
* Entfernt das nächste Hologramm innerhalb von 5 Blöcken zur gegebenen Location.
* @return ID des entfernten Hologramms oder null wenn keines gefunden
*/
public String removeNearest(Location loc) {
String nearest = null;
double nearestDist = 5.0;
for (Map.Entry<String, FussballHologram> entry : holograms.entrySet()) {
Location holoLoc = entry.getValue().getLocation();
if (!holoLoc.getWorld().equals(loc.getWorld())) continue;
double dist = holoLoc.distance(loc);
if (dist < nearestDist) {
nearestDist = dist;
nearest = entry.getKey();
}
}
if (nearest == null) return null;
removeHologram(nearest);
return nearest;
}
/** Alle Hologramme neu laden (z.B. nach /fb hologram reload) */
public void reload() {
loadConfig();
loadHolograms();
// Für alle Online-Spieler sofort rendern
for (Player player : Bukkit.getOnlinePlayers()) {
holograms.values().forEach(h -> h.renderForPlayer(player));
}
}
// ── Render-Task ──────────────────────────────────────────────────────────
/**
* Alle 5 Ticks: Distanzprüfung + Text-Update.
* Text wird in FussballHologram nur neu gesetzt wenn er sich geändert hat (==Check).
*/
private void startRenderTask() {
if (renderTask != null) renderTask.cancel();
renderTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
holograms.values().forEach(h -> h.renderForPlayer(player));
}
}, 20L, 5L);
}
// ── Events ───────────────────────────────────────────────────────────────
/** Rechtsklick auf Interaction-Entity → Seite wechseln (Tore ↔ Siege) */
@EventHandler
public void onInteract(PlayerInteractEntityEvent event) {
if (!(event.getRightClicked() instanceof Interaction)) return;
for (FussballHologram holo : holograms.values()) {
if (holo.isInteractionEntity(event.getRightClicked().getUniqueId())) {
holo.nextPage(event.getPlayer());
break;
}
}
}
/** Spieler verlässt den Server → Entities entfernen */
@EventHandler
public void onQuit(PlayerQuitEvent event) {
holograms.values().forEach(h -> h.removeForPlayer(event.getPlayer()));
}
/** Weltenwechsel → Entities aus der alten Welt entfernen */
@EventHandler
public void onWorldChange(PlayerChangedWorldEvent event) {
holograms.values().forEach(h -> h.removeForPlayer(event.getPlayer()));
}
/**
* Spieler joint → verbleibende verwaiste fb_holo_*-Entities verstecken,
* dann Hologramme für ihn rendern.
*/
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Player p = event.getPlayer();
// Crash-Überreste aus der Welt verstecken
for (Entity entity : p.getWorld().getEntities()) {
String name = entity.getCustomName();
if (name == null || !name.startsWith("fb_holo_")) continue;
if (!name.endsWith("_" + p.getName())) {
p.hideEntity(plugin, entity);
}
}
// Sofort für ihn rendern
holograms.values().forEach(h -> h.renderForPlayer(p));
}, 10L);
}
// ── Shutdown ─────────────────────────────────────────────────────────────
/** Sauber herunterfahren (Plugin-Disable) */
public void removeAll() {
if (renderTask != null) { renderTask.cancel(); renderTask = null; }
HandlerList.unregisterAll(this);
for (Player player : Bukkit.getOnlinePlayers()) {
holograms.values().forEach(h -> h.removeForPlayer(player));
}
holograms.values().forEach(FussballHologram::removeAll);
holograms.clear();
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
/**
* Entfernt beim Serverstart verbleibende fb_holo_*-Entities aus allen Welten.
* (Überreste nach einem Crash non-persistent Entities sollten eigentlich weg sein,
* aber besser einmal zu viel prüfen)
*/
private void cleanupStrayEntities() {
for (World world : Bukkit.getWorlds()) {
for (Entity entity : world.getEntities()) {
String name = entity.getCustomName();
if (name != null && name.startsWith("fb_holo_")) {
entity.remove();
}
}
}
}
/** @return Anzahl der registrierten Hologramme */
public int getCount() { return holograms.size(); }
/** @return Set aller Hologramm-IDs (für Tab-Completion) */
public Set<String> getHologramIds() { return holograms.keySet(); }
}

View File

@@ -23,10 +23,8 @@ public class PlayerListener implements Listener {
Player player = event.getPlayer();
Game game = plugin.getGameManager().getPlayerGame(player);
if (game != null) game.removePlayer(player);
// Auch als Zuschauer entfernen
Game spectatorGame = plugin.getGameManager().getSpectatorGame(player);
if (spectatorGame != null) spectatorGame.removeSpectator(player);
// Aus Warteschlangen entfernen
plugin.getGameManager().removeFromAllQueues(player);
}
@@ -66,24 +64,81 @@ public class PlayerListener implements Listener {
}
/**
* Team-Chat: Nachrichten von Spielern im Spiel werden NUR ans eigene Team gesendet.
* Mit "!" am Anfang können Admins global ins Spiel broadcasten.
* Zuschauer sehen alle Team-Chats (mit Label).
* Zuschauer dürfen die Arena nicht verlassen (Weltenwechsel blockieren).
* GM3 erlaubt Portale das verhindert, dass ein Zuschauer durch ein Portal
* aus der Welt heraus teleportiert wird.
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@EventHandler(priority = EventPriority.HIGH)
public void onPortal(PlayerPortalEvent event) {
Player player = event.getPlayer();
if (plugin.getGameManager().getSpectatorGame(player) != null) {
event.setCancelled(true);
player.sendMessage("\u00a7cZuschauer d\u00fcrfen keine Portale benutzen!");
}
}
/**
* Zuschauer-Grenzen bei Bewegung prüfen.
* Nur bei echtem Block-Wechsel (reduziert Event-Last drastisch).
* Der Game-Loop prüft ebenfalls jede Sekunde dieser Handler greift
* als sofortiger Schutz bei schnellem Fliegen.
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onMove(PlayerMoveEvent event) {
// Nur bei echtem Block-Wechsel auswerten (performance)
if (event.getFrom().getBlockX() == event.getTo().getBlockX()
&& event.getFrom().getBlockY() == event.getTo().getBlockY()
&& event.getFrom().getBlockZ() == event.getTo().getBlockZ()) return;
Player player = event.getPlayer();
Game game = plugin.getGameManager().getSpectatorGame(player);
if (game == null) return;
// Weltenwechsel durch Bewegung abfangen
if (!event.getFrom().getWorld().equals(event.getTo().getWorld())) {
event.setCancelled(true);
return;
}
// Grenzen prüfen Game.java berechnet die erlaubte Zone
if (game.isSpectatorOutOfBounds(player)) {
event.setCancelled(true);
// Zum Zuschauer-Spawn zurückteleportieren
player.teleport(game.getSpectatorSpawn());
player.sendTitle("§c\u26a0 ARENAGRENZE!", "§7Zuschauer d\u00fcrfen die Arena nicht verlassen!", 5, 40, 10);
}
}
/**
* Team-Chat: Nachrichten von Spielern im Spiel werden NUR ans eigene Team gesendet.
*
* LOWEST-Priorität: Event wird sofort gecancelt, BEVOR Chat-Formatter-Plugins
* (EssentialsChat, CMI, ...) die Nachricht mit ihrem Prefix versenden können.
* Das verhindert Doppelnachrichten wie "[Rot] msg" + "[Owner] msg".
*
* Da AsyncPlayerChatEvent asynchron läuft, wird die eigentliche Team-Nachricht
* per runTask auf den Haupt-Thread verlagert.
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
Game game = plugin.getGameManager().getPlayerGame(player);
if (game == null) return;
// Sofort canceln noch bevor andere Plugins das Event verarbeiten
event.setCancelled(true);
event.getRecipients().clear();
String message = event.getMessage();
// Admin-Global-Broadcast
if (message.startsWith("!") && player.hasPermission("fussball.admin")) {
game.broadcastAll("§6[Global] §f" + player.getName() + "§7: " + message.substring(1).trim());
return;
}
game.sendTeamMessage(player, message);
// Nachricht auf dem Haupt-Thread versenden (Bukkit-API ist nicht thread-safe)
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
// Admin-Global-Broadcast
if (message.startsWith("!") && player.hasPermission("fussball.admin")) {
game.broadcastAll("§6[Global] §f" + player.getName() + "§7: " + message.substring(1).trim());
return;
}
game.sendTeamMessage(player, message);
});
}
}

View File

@@ -23,24 +23,33 @@ import java.io.IOException;
import java.util.*;
/**
* Fußball-Join-Schilder
* Fußball-Schilder (Join + Zuschauer)
*
* Format beim Beschriften (braucht fussball.admin):
* JOIN-Schild (braucht fussball.admin):
* Zeile 1: [Fussball]
* Zeile 2: <ArenaName>
*
* Schilder werden in signs.yml gespeichert und überleben Server-Neustarts.
* Aktualisierung erfolgt automatisch bei Spieler-Join/Leave und Spielstart/-ende.
* ZUSCHAUER-Schild (braucht fussball.admin):
* Zeile 1: [FussballSpec]
* Zeile 2: <ArenaName>
*
* Beide Schild-Typen werden in signs.yml gespeichert.
* Der Typ wird als Prefix im Key gespeichert: "join:" oder "spec:".
*/
public class SignListener implements Listener {
private static final String TAG = "[Fussball]";
private static final String TAG_FORMATTED = "§8[§e⚽§8]";
// Join-Schild Tags
private static final String TAG_JOIN = "[Fussball]";
private static final String TAG_JOIN_FMT = "§8[§e⚽§8]";
// Zuschauer-Schild Tags
private static final String TAG_SPEC = "[FussballSpec]";
private static final String TAG_SPEC_FMT = "§8[§b👁§8]";
private final Fussball plugin;
// Location → ArenaName
private final Map<String, String> signs = new HashMap<>(); // key = "world;x;y;z"
// locKey → "join:<ArenaName>" oder "spec:<ArenaName>"
private final Map<String, String> signs = new HashMap<>();
private final File signFile;
private FileConfiguration signConfig;
@@ -61,8 +70,12 @@ public class SignListener implements Listener {
signs.clear();
if (signConfig.contains("signs")) {
for (String key : signConfig.getConfigurationSection("signs").getKeys(false)) {
String arenaName = signConfig.getString("signs." + key);
signs.put(key, arenaName);
String value = signConfig.getString("signs." + key);
// Legacy-Migration: alte Einträge ohne Prefix → "join:"
if (value != null && !value.startsWith("join:") && !value.startsWith("spec:")) {
value = "join:" + value;
}
signs.put(key, value);
}
}
plugin.getLogger().info("[Fussball] " + signs.size() + " Schilder geladen.");
@@ -82,13 +95,16 @@ public class SignListener implements Listener {
// ── Events ───────────────────────────────────────────────────────────────
/** Schild beschriften → Fußball-Schild erstellen */
@EventHandler
public void onSignChange(SignChangeEvent event) {
String line0 = event.getLine(0);
if (line0 == null || !line0.equalsIgnoreCase(TAG)) return;
if (line0 == null) return;
Player player = event.getPlayer();
boolean isJoin = line0.equalsIgnoreCase(TAG_JOIN);
boolean isSpec = line0.equalsIgnoreCase(TAG_SPEC);
if (!isJoin && !isSpec) return;
if (!player.hasPermission("fussball.admin")) {
player.sendMessage(MessageUtil.error("Keine Berechtigung für Fußball-Schilder!"));
event.setCancelled(true);
@@ -109,31 +125,40 @@ public class SignListener implements Listener {
return;
}
event.setLine(0, TAG_FORMATTED);
event.setLine(1, "§e" + arena.getName());
event.setLine(2, buildStatusLine(arena));
event.setLine(3, "§7Klick zum Joinen");
String key = locKey(event.getBlock().getLocation());
signs.put(key, arena.getName());
saveSigns();
player.sendMessage(MessageUtil.success("Fußball-Schild für §e" + arena.getName() + " §aerstellt!"));
if (isJoin) {
event.setLine(0, TAG_JOIN_FMT);
event.setLine(1, "§e" + arena.getName());
event.setLine(2, buildStatusLine(arena));
event.setLine(3, "§7Joinen");
signs.put(key, "join:" + arena.getName());
player.sendMessage(MessageUtil.success("Join-Schild für §e" + arena.getName() + " §aerstellt!"));
} else {
event.setLine(0, TAG_SPEC_FMT);
event.setLine(1, "§b" + arena.getName());
event.setLine(2, buildStatusLine(arena));
event.setLine(3, "§7Zuschauen");
signs.put(key, "spec:" + arena.getName());
player.sendMessage(MessageUtil.success("Zuschauer-Schild für §e" + arena.getName() + " §aerstellt!"));
}
saveSigns();
}
/** Rechtsklick → Spieler joinen */
@EventHandler
public void onInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
Block block = event.getClickedBlock();
if (block == null || !(block.getState() instanceof Sign sign)) return;
if (block == null || !(block.getState() instanceof Sign)) return;
String key = locKey(block.getLocation());
if (!signs.containsKey(key)) return;
event.setCancelled(true);
Player player = event.getPlayer();
String arenaName = signs.get(key);
String value = signs.get(key);
boolean isSpec = value.startsWith("spec:");
String arenaName = value.substring(value.indexOf(':') + 1);
Arena arena = plugin.getArenaManager().getArena(arenaName);
if (arena == null) {
@@ -142,20 +167,34 @@ public class SignListener implements Listener {
saveSigns();
return;
}
if (!arena.isSetupComplete()) {
player.sendMessage(MessageUtil.error("Diese Arena ist noch nicht fertig eingerichtet!"));
return;
}
if (plugin.getGameManager().isInGame(player)) {
player.sendMessage(MessageUtil.error("Du bist bereits in einem Spiel!"));
return;
}
plugin.getGameManager().createGame(arena).addPlayer(player);
refreshSignsForArena(arenaName);
if (isSpec) {
// ── ZUSCHAUER-SCHILD ────────────────────────────────────────────
if (plugin.getGameManager().isInAnyGame(player)) {
player.sendMessage(MessageUtil.error("Verlasse zuerst dein aktuelles Spiel!"));
return;
}
Game game = plugin.getGameManager().getGame(arenaName);
if (game == null) {
player.sendMessage(MessageUtil.error("Kein laufendes Spiel in §e" + arenaName + "§c!"));
return;
}
game.addSpectator(player);
} else {
// ── JOIN-SCHILD ─────────────────────────────────────────────────
if (!arena.isSetupComplete()) {
player.sendMessage(MessageUtil.error("Diese Arena ist noch nicht fertig eingerichtet!"));
return;
}
if (plugin.getGameManager().isInGame(player)) {
player.sendMessage(MessageUtil.error("Du bist bereits in einem Spiel!"));
return;
}
plugin.getGameManager().createGame(arena).addPlayer(player);
refreshSignsForArena(arenaName);
}
}
/** Schild abbauen → aus Liste entfernen */
@EventHandler
public void onBreak(BlockBreakEvent event) {
String key = locKey(event.getBlock().getLocation());
@@ -168,10 +207,13 @@ public class SignListener implements Listener {
// ── Öffentliche Methode: von Game.java aufrufen ──────────────────────────
/** Aktualisiert alle Schilder einer Arena. Wird von Game.java aufgerufen. */
public void refreshSignsForArena(String arenaName) {
for (Map.Entry<String, String> entry : signs.entrySet()) {
if (!entry.getValue().equalsIgnoreCase(arenaName)) continue;
String value = entry.getValue();
String entryArena = value.substring(value.indexOf(':') + 1);
if (!entryArena.equalsIgnoreCase(arenaName)) continue;
boolean isSpec = value.startsWith("spec:");
Location loc = keyToLocation(entry.getKey());
if (loc == null) continue;
Block block = loc.getBlock();
@@ -180,10 +222,17 @@ public class SignListener implements Listener {
Arena arena = plugin.getArenaManager().getArena(arenaName);
if (arena == null) continue;
sign.setLine(0, TAG_FORMATTED);
sign.setLine(1, "§e" + arena.getName());
sign.setLine(2, buildStatusLine(arena));
sign.setLine(3, "§7Klick zum Joinen");
if (isSpec) {
sign.setLine(0, TAG_SPEC_FMT);
sign.setLine(1, "§b" + arena.getName());
sign.setLine(2, buildStatusLine(arena));
sign.setLine(3, "§7Zuschauen");
} else {
sign.setLine(0, TAG_JOIN_FMT);
sign.setLine(1, "§e" + arena.getName());
sign.setLine(2, buildStatusLine(arena));
sign.setLine(3, "§7Joinen");
}
sign.update();
}
}