Update from Git Manager GUI
This commit is contained in:
@@ -4,6 +4,7 @@ import de.fussball.plugin.arena.Arena;
|
|||||||
import de.fussball.plugin.arena.ArenaManager;
|
import de.fussball.plugin.arena.ArenaManager;
|
||||||
import de.fussball.plugin.commands.FussballCommand;
|
import de.fussball.plugin.commands.FussballCommand;
|
||||||
import de.fussball.plugin.game.GameManager;
|
import de.fussball.plugin.game.GameManager;
|
||||||
|
import de.fussball.plugin.hologram.HologramManager;
|
||||||
import de.fussball.plugin.listeners.*;
|
import de.fussball.plugin.listeners.*;
|
||||||
import de.fussball.plugin.placeholders.FussballPlaceholders;
|
import de.fussball.plugin.placeholders.FussballPlaceholders;
|
||||||
import de.fussball.plugin.stats.StatsManager;
|
import de.fussball.plugin.stats.StatsManager;
|
||||||
@@ -18,6 +19,7 @@ public class Fussball extends JavaPlugin {
|
|||||||
private GameManager gameManager;
|
private GameManager gameManager;
|
||||||
private StatsManager statsManager;
|
private StatsManager statsManager;
|
||||||
private SignListener signListener;
|
private SignListener signListener;
|
||||||
|
private HologramManager hologramManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -30,6 +32,7 @@ public class Fussball extends JavaPlugin {
|
|||||||
gameManager = new GameManager(this);
|
gameManager = new GameManager(this);
|
||||||
statsManager = new StatsManager(this);
|
statsManager = new StatsManager(this);
|
||||||
signListener = new SignListener(this);
|
signListener = new SignListener(this);
|
||||||
|
hologramManager = new HologramManager(this);
|
||||||
Messages.init(this);
|
Messages.init(this);
|
||||||
|
|
||||||
registerCommands();
|
registerCommands();
|
||||||
@@ -50,6 +53,7 @@ public class Fussball extends JavaPlugin {
|
|||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
if (gameManager != null) gameManager.stopAllGames();
|
if (gameManager != null) gameManager.stopAllGames();
|
||||||
if (statsManager != null) statsManager.save();
|
if (statsManager != null) statsManager.save();
|
||||||
|
if (hologramManager != null) hologramManager.removeAll(); // Entities sauber entfernen
|
||||||
getLogger().info("⚽ Fußball-Plugin gestoppt!");
|
getLogger().info("⚽ Fußball-Plugin gestoppt!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +75,5 @@ public class Fussball extends JavaPlugin {
|
|||||||
public GameManager getGameManager() { return gameManager; }
|
public GameManager getGameManager() { return gameManager; }
|
||||||
public StatsManager getStatsManager() { return statsManager; }
|
public StatsManager getStatsManager() { return statsManager; }
|
||||||
public SignListener getSignListener() { return signListener; }
|
public SignListener getSignListener() { return signListener; }
|
||||||
|
public HologramManager getHologramManager() { return hologramManager; }
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ public class Arena implements ConfigurationSerializable {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private Location center, redSpawn, blueSpawn, ballSpawn;
|
private Location center, redSpawn, blueSpawn, ballSpawn;
|
||||||
private Location redGoalMin, redGoalMax, blueGoalMin, blueGoalMax, lobby;
|
private Location redGoalMin, redGoalMax, blueGoalMin, blueGoalMax, lobby;
|
||||||
|
private Location spectatorSpawn; // Zuschauer-Spawn (optional – Fallback: Spielfeldrand)
|
||||||
private Location fieldMin, fieldMax;
|
private Location fieldMin, fieldMax;
|
||||||
// Strafräume – optional manuell gesetzt; sonst auto-berechnet aus Tor + config
|
// Strafräume – optional manuell gesetzt; sonst auto-berechnet aus Tor + config
|
||||||
private Location redPenaltyMin, redPenaltyMax, bluePenaltyMin, bluePenaltyMax;
|
private Location redPenaltyMin, redPenaltyMax, bluePenaltyMin, bluePenaltyMax;
|
||||||
@@ -259,6 +260,7 @@ public class Arena implements ConfigurationSerializable {
|
|||||||
if (bluePenaltyMax != null) map.put("bluePenaltyMax", serLoc(bluePenaltyMax));
|
if (bluePenaltyMax != null) map.put("bluePenaltyMax", serLoc(bluePenaltyMax));
|
||||||
if (redPenaltySpot != null) map.put("redPenaltySpot", serLoc(redPenaltySpot));
|
if (redPenaltySpot != null) map.put("redPenaltySpot", serLoc(redPenaltySpot));
|
||||||
if (bluePenaltySpot != null) map.put("bluePenaltySpot", serLoc(bluePenaltySpot));
|
if (bluePenaltySpot != null) map.put("bluePenaltySpot", serLoc(bluePenaltySpot));
|
||||||
|
if (spectatorSpawn != null) map.put("spectatorSpawn", serLoc(spectatorSpawn));
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +291,7 @@ public class Arena implements ConfigurationSerializable {
|
|||||||
if (map.containsKey("bluePenaltyMax")) a.bluePenaltyMax = desLoc(map.get("bluePenaltyMax"));
|
if (map.containsKey("bluePenaltyMax")) a.bluePenaltyMax = desLoc(map.get("bluePenaltyMax"));
|
||||||
if (map.containsKey("redPenaltySpot")) a.redPenaltySpot = desLoc(map.get("redPenaltySpot"));
|
if (map.containsKey("redPenaltySpot")) a.redPenaltySpot = desLoc(map.get("redPenaltySpot"));
|
||||||
if (map.containsKey("bluePenaltySpot")) a.bluePenaltySpot = desLoc(map.get("bluePenaltySpot"));
|
if (map.containsKey("bluePenaltySpot")) a.bluePenaltySpot = desLoc(map.get("bluePenaltySpot"));
|
||||||
|
if (map.containsKey("spectatorSpawn")) a.spectatorSpawn = desLoc(map.get("spectatorSpawn"));
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +367,8 @@ public class Arena implements ConfigurationSerializable {
|
|||||||
}
|
}
|
||||||
public boolean hasManualPenaltyAreas() { return redPenaltyMin != null && redPenaltyMax != null
|
public boolean hasManualPenaltyAreas() { return redPenaltyMin != null && redPenaltyMax != null
|
||||||
&& bluePenaltyMin != null && bluePenaltyMax != null; }
|
&& bluePenaltyMin != null && bluePenaltyMax != null; }
|
||||||
|
public Location getSpectatorSpawn() { return spectatorSpawn; }
|
||||||
|
public void setSpectatorSpawn(Location l) { this.spectatorSpawn = l; }
|
||||||
public int getMinPlayers() { return minPlayers; }
|
public int getMinPlayers() { return minPlayers; }
|
||||||
public void setMinPlayers(int n) { this.minPlayers = n; }
|
public void setMinPlayers(int n) { this.minPlayers = n; }
|
||||||
public int getMaxPlayers() { return maxPlayers; }
|
public int getMaxPlayers() { return maxPlayers; }
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import de.fussball.plugin.arena.Arena;
|
|||||||
import de.fussball.plugin.game.Ball;
|
import de.fussball.plugin.game.Ball;
|
||||||
import de.fussball.plugin.game.Game;
|
import de.fussball.plugin.game.Game;
|
||||||
import de.fussball.plugin.game.GameState;
|
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.stats.StatsManager;
|
||||||
import de.fussball.plugin.utils.MessageUtil;
|
import de.fussball.plugin.utils.MessageUtil;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
@@ -200,6 +202,76 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
|||||||
handleDebug(player, arena);
|
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);
|
default -> sendHelp(sender);
|
||||||
}
|
}
|
||||||
return true;
|
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 "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 "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 "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 "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 "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")); }
|
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)");
|
+ " §8(optional – sonst auto-berechnet)");
|
||||||
player.sendMessage("§7 Blauer Strafraum: " + check(arena.getBluePenaltyMin(), arena.getBluePenaltyMax())
|
player.sendMessage("§7 Blauer Strafraum: " + check(arena.getBluePenaltyMin(), arena.getBluePenaltyMax())
|
||||||
+ " §8(optional – sonst auto-berechnet)");
|
+ " §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 Blauer Elfmeter-Punkt: " + check(arena.getBluePenaltySpot()) + " §8(optional – sonst ball-spawn)");
|
||||||
player.sendMessage("§7 Min. Spieler: §e" + arena.getMinPlayers());
|
player.sendMessage("§7 Min. Spieler: §e" + arena.getMinPlayers());
|
||||||
player.sendMessage("§7 Max. Spieler: §e" + arena.getMaxPlayers());
|
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");
|
s.sendMessage("§e/fb top [goals|wins] §7- Bestenliste");
|
||||||
if (s.hasPermission("fussball.admin")) {
|
if (s.hasPermission("fussball.admin")) {
|
||||||
s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug");
|
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<>();
|
List<String> list = new ArrayList<>();
|
||||||
if (args.length == 1) {
|
if (args.length == 1) {
|
||||||
list.addAll(List.of("join", "leave", "list", "stats", "top", "spectate"));
|
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())) {
|
} else if (args.length == 2 && List.of("join","delete","setup","stop","setgk","debug","spectate").contains(args[0].toLowerCase())) {
|
||||||
list.addAll(plugin.getArenaManager().getArenaNames());
|
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")) {
|
} else if (args.length == 3 && args[0].equalsIgnoreCase("setgk")) {
|
||||||
// Spielernamen aus dem aktiven Spiel vorschlagen
|
// Spielernamen aus dem aktiven Spiel vorschlagen
|
||||||
Game gkGame = plugin.getGameManager().getGame(args[1]);
|
Game gkGame = plugin.getGameManager().getGame(args[1]);
|
||||||
@@ -362,7 +444,7 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
|||||||
"redgoalmin","redgoalmax","bluegoalmin","bluegoalmax",
|
"redgoalmin","redgoalmax","bluegoalmin","bluegoalmax",
|
||||||
"fieldmin","fieldmax",
|
"fieldmin","fieldmax",
|
||||||
"redpenaltymin","redpenaltymax","bluepenaltymin","bluepenaltymax",
|
"redpenaltymin","redpenaltymax","bluepenaltymin","bluepenaltymax",
|
||||||
"redpenaltyspot","bluepenaltyspot",
|
"redpenaltyspot","bluepenaltyspot","spectatorspawn",
|
||||||
"minplayers","maxplayers","duration","info"));
|
"minplayers","maxplayers","duration","info"));
|
||||||
} else if (args.length == 2 && args[0].equalsIgnoreCase("top")) {
|
} else if (args.length == 2 && args[0].equalsIgnoreCase("top")) {
|
||||||
list.addAll(List.of("goals", "wins"));
|
list.addAll(List.of("goals", "wins"));
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ public class Game {
|
|||||||
private final Map<UUID, Integer> outOfBoundsCountdown = new HashMap<>();
|
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 lastKicker = null;
|
||||||
private UUID secondLastKicker = null; // für Assist-Erkennung
|
private UUID secondLastKicker = null; // für Assist-Erkennung
|
||||||
private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt)
|
private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt)
|
||||||
@@ -161,15 +165,29 @@ public class Game {
|
|||||||
public boolean addSpectator(Player player) {
|
public boolean addSpectator(Player player) {
|
||||||
if (isInGame(player)) { player.sendMessage(MessageUtil.error("Du bist bereits Spieler!")); return false; }
|
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 (isSpectator(player)) { player.sendMessage(MessageUtil.error("Du schaust bereits zu!")); return false; }
|
||||||
if (state == GameState.WAITING || state == GameState.STARTING || state == GameState.ENDING) {
|
if (state == GameState.ENDING) {
|
||||||
player.sendMessage(MessageUtil.error("Kein laufendes Spiel zum Zuschauen!")); return false;
|
player.sendMessage(MessageUtil.error("Das Spiel ist gerade dabei zu enden!")); return false;
|
||||||
}
|
}
|
||||||
spectators.add(player.getUniqueId());
|
spectators.add(player.getUniqueId());
|
||||||
player.setGameMode(GameMode.SPECTATOR);
|
player.setGameMode(GameMode.ADVENTURE);
|
||||||
player.teleport(arena.getBallSpawn() != null ? arena.getBallSpawn() : arena.getLobby());
|
|
||||||
|
// 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);
|
scoreboard.give(player);
|
||||||
if (bossBar != null) bossBar.addPlayer(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +195,7 @@ public class Game {
|
|||||||
spectators.remove(player.getUniqueId());
|
spectators.remove(player.getUniqueId());
|
||||||
if (bossBar != null) bossBar.removePlayer(player);
|
if (bossBar != null) bossBar.removePlayer(player);
|
||||||
scoreboard.remove(player);
|
scoreboard.remove(player);
|
||||||
|
|
||||||
resetPlayer(player);
|
resetPlayer(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +244,13 @@ public class Game {
|
|||||||
player.getInventory().setLeggings(armor[2]); player.getInventory().setBoots(armor[3]);
|
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) {
|
private void resetPlayer(Player player) {
|
||||||
player.getInventory().clear();
|
player.getInventory().clear();
|
||||||
for (PotionEffect e : player.getActivePotionEffects()) player.removePotionEffect(e.getType());
|
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();
|
Location tp = arena.getLobby() != null ? arena.getLobby() : Bukkit.getWorlds().get(0).getSpawnLocation();
|
||||||
player.teleport(tp);
|
player.teleport(tp);
|
||||||
}
|
}
|
||||||
@@ -578,6 +599,9 @@ public class Game {
|
|||||||
if (gameTask != null) { gameTask.cancel(); gameTask = null; }
|
if (gameTask != null) { gameTask.cancel(); gameTask = null; }
|
||||||
gameTask = new BukkitRunnable() {
|
gameTask = new BukkitRunnable() {
|
||||||
public void run() {
|
public void run() {
|
||||||
|
// Zuschauer-Grenzen immer prüfen (unabhängig vom Spielzustand)
|
||||||
|
checkSpectatorBoundaries();
|
||||||
|
|
||||||
if (state != GameState.RUNNING && state != GameState.OVERTIME) return;
|
if (state != GameState.RUNNING && state != GameState.OVERTIME) return;
|
||||||
|
|
||||||
// Kopfball-Abklingzeiten herunterzählen
|
// Kopfball-Abklingzeiten herunterzählen
|
||||||
@@ -615,6 +639,7 @@ public class Game {
|
|||||||
checkPlayerBallInteraction();
|
checkPlayerBallInteraction();
|
||||||
checkPlayerBoundaries();
|
checkPlayerBoundaries();
|
||||||
checkHeaderOpportunities();
|
checkHeaderOpportunities();
|
||||||
|
checkAfkPlayers();
|
||||||
|
|
||||||
// Freistoß-Abstandsdurchsetzung
|
// Freistoß-Abstandsdurchsetzung
|
||||||
if (freekickLocation != null) {
|
if (freekickLocation != null) {
|
||||||
@@ -849,7 +874,7 @@ public class Game {
|
|||||||
for (UUID uuid : allPlayers) {
|
for (UUID uuid : allPlayers) {
|
||||||
Player p = Bukkit.getPlayer(uuid);
|
Player p = Bukkit.getPlayer(uuid);
|
||||||
if (p == null) continue;
|
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;
|
if (throwInTeam != null && getTeam(p) != throwInTeam) continue;
|
||||||
|
|
||||||
// ── Rückpass-Regel für Torwarte ──────────────────────────────────
|
// ── 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.
|
* 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,
|
* Bedingungen: Spieler ist in der Luft, Ball befindet sich auf Kopfhöhe,
|
||||||
|
|||||||
220
src/main/java/de/fussball/plugin/hologram/FussballHologram.java
Normal file
220
src/main/java/de/fussball/plugin/hologram/FussballHologram.java
Normal 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; }
|
||||||
|
}
|
||||||
293
src/main/java/de/fussball/plugin/hologram/HologramManager.java
Normal file
293
src/main/java/de/fussball/plugin/hologram/HologramManager.java
Normal 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(); }
|
||||||
|
}
|
||||||
@@ -23,10 +23,8 @@ public class PlayerListener implements Listener {
|
|||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
Game game = plugin.getGameManager().getPlayerGame(player);
|
Game game = plugin.getGameManager().getPlayerGame(player);
|
||||||
if (game != null) game.removePlayer(player);
|
if (game != null) game.removePlayer(player);
|
||||||
// Auch als Zuschauer entfernen
|
|
||||||
Game spectatorGame = plugin.getGameManager().getSpectatorGame(player);
|
Game spectatorGame = plugin.getGameManager().getSpectatorGame(player);
|
||||||
if (spectatorGame != null) spectatorGame.removeSpectator(player);
|
if (spectatorGame != null) spectatorGame.removeSpectator(player);
|
||||||
// Aus Warteschlangen entfernen
|
|
||||||
plugin.getGameManager().removeFromAllQueues(player);
|
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.
|
* Zuschauer dürfen die Arena nicht verlassen (Weltenwechsel blockieren).
|
||||||
* Mit "!" am Anfang können Admins global ins Spiel broadcasten.
|
* GM3 erlaubt Portale – das verhindert, dass ein Zuschauer durch ein Portal
|
||||||
* Zuschauer sehen alle Team-Chats (mit Label).
|
* 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) {
|
public void onChat(AsyncPlayerChatEvent event) {
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
Game game = plugin.getGameManager().getPlayerGame(player);
|
Game game = plugin.getGameManager().getPlayerGame(player);
|
||||||
if (game == null) return;
|
if (game == null) return;
|
||||||
|
|
||||||
|
// Sofort canceln – noch bevor andere Plugins das Event verarbeiten
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
|
event.getRecipients().clear();
|
||||||
|
|
||||||
String message = event.getMessage();
|
String message = event.getMessage();
|
||||||
|
|
||||||
|
// Nachricht auf dem Haupt-Thread versenden (Bukkit-API ist nicht thread-safe)
|
||||||
|
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
// Admin-Global-Broadcast
|
// Admin-Global-Broadcast
|
||||||
if (message.startsWith("!") && player.hasPermission("fussball.admin")) {
|
if (message.startsWith("!") && player.hasPermission("fussball.admin")) {
|
||||||
game.broadcastAll("§6[Global] §f" + player.getName() + "§7: " + message.substring(1).trim());
|
game.broadcastAll("§6[Global] §f" + player.getName() + "§7: " + message.substring(1).trim());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
game.sendTeamMessage(player, message);
|
game.sendTeamMessage(player, message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,24 +23,33 @@ import java.io.IOException;
|
|||||||
import java.util.*;
|
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 1: [Fussball]
|
||||||
* Zeile 2: <ArenaName>
|
* Zeile 2: <ArenaName>
|
||||||
*
|
*
|
||||||
* Schilder werden in signs.yml gespeichert und überleben Server-Neustarts.
|
* ZUSCHAUER-Schild (braucht fussball.admin):
|
||||||
* Aktualisierung erfolgt automatisch bei Spieler-Join/Leave und Spielstart/-ende.
|
* 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 {
|
public class SignListener implements Listener {
|
||||||
|
|
||||||
private static final String TAG = "[Fussball]";
|
// Join-Schild Tags
|
||||||
private static final String TAG_FORMATTED = "§8[§e⚽§8]";
|
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;
|
private final Fussball plugin;
|
||||||
|
|
||||||
// Location → ArenaName
|
// locKey → "join:<ArenaName>" oder "spec:<ArenaName>"
|
||||||
private final Map<String, String> signs = new HashMap<>(); // key = "world;x;y;z"
|
private final Map<String, String> signs = new HashMap<>();
|
||||||
|
|
||||||
private final File signFile;
|
private final File signFile;
|
||||||
private FileConfiguration signConfig;
|
private FileConfiguration signConfig;
|
||||||
@@ -61,8 +70,12 @@ public class SignListener implements Listener {
|
|||||||
signs.clear();
|
signs.clear();
|
||||||
if (signConfig.contains("signs")) {
|
if (signConfig.contains("signs")) {
|
||||||
for (String key : signConfig.getConfigurationSection("signs").getKeys(false)) {
|
for (String key : signConfig.getConfigurationSection("signs").getKeys(false)) {
|
||||||
String arenaName = signConfig.getString("signs." + key);
|
String value = signConfig.getString("signs." + key);
|
||||||
signs.put(key, arenaName);
|
// 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.");
|
plugin.getLogger().info("[Fussball] " + signs.size() + " Schilder geladen.");
|
||||||
@@ -82,13 +95,16 @@ public class SignListener implements Listener {
|
|||||||
|
|
||||||
// ── Events ───────────────────────────────────────────────────────────────
|
// ── Events ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Schild beschriften → Fußball-Schild erstellen */
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onSignChange(SignChangeEvent event) {
|
public void onSignChange(SignChangeEvent event) {
|
||||||
String line0 = event.getLine(0);
|
String line0 = event.getLine(0);
|
||||||
if (line0 == null || !line0.equalsIgnoreCase(TAG)) return;
|
if (line0 == null) return;
|
||||||
|
|
||||||
Player player = event.getPlayer();
|
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")) {
|
if (!player.hasPermission("fussball.admin")) {
|
||||||
player.sendMessage(MessageUtil.error("Keine Berechtigung für Fußball-Schilder!"));
|
player.sendMessage(MessageUtil.error("Keine Berechtigung für Fußball-Schilder!"));
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
@@ -109,31 +125,40 @@ public class SignListener implements Listener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.setLine(0, TAG_FORMATTED);
|
String key = locKey(event.getBlock().getLocation());
|
||||||
|
|
||||||
|
if (isJoin) {
|
||||||
|
event.setLine(0, TAG_JOIN_FMT);
|
||||||
event.setLine(1, "§e" + arena.getName());
|
event.setLine(1, "§e" + arena.getName());
|
||||||
event.setLine(2, buildStatusLine(arena));
|
event.setLine(2, buildStatusLine(arena));
|
||||||
event.setLine(3, "§7Klick zum Joinen");
|
event.setLine(3, "§7Joinen");
|
||||||
|
signs.put(key, "join:" + arena.getName());
|
||||||
String key = locKey(event.getBlock().getLocation());
|
player.sendMessage(MessageUtil.success("Join-Schild für §e" + arena.getName() + " §aerstellt!"));
|
||||||
signs.put(key, arena.getName());
|
} 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();
|
saveSigns();
|
||||||
|
|
||||||
player.sendMessage(MessageUtil.success("Fußball-Schild für §e" + arena.getName() + " §aerstellt!"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rechtsklick → Spieler joinen */
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onInteract(PlayerInteractEvent event) {
|
public void onInteract(PlayerInteractEvent event) {
|
||||||
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
|
||||||
Block block = event.getClickedBlock();
|
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());
|
String key = locKey(block.getLocation());
|
||||||
if (!signs.containsKey(key)) return;
|
if (!signs.containsKey(key)) return;
|
||||||
|
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
Player player = event.getPlayer();
|
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);
|
Arena arena = plugin.getArenaManager().getArena(arenaName);
|
||||||
if (arena == null) {
|
if (arena == null) {
|
||||||
@@ -142,6 +167,21 @@ public class SignListener implements Listener {
|
|||||||
saveSigns();
|
saveSigns();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()) {
|
if (!arena.isSetupComplete()) {
|
||||||
player.sendMessage(MessageUtil.error("Diese Arena ist noch nicht fertig eingerichtet!"));
|
player.sendMessage(MessageUtil.error("Diese Arena ist noch nicht fertig eingerichtet!"));
|
||||||
return;
|
return;
|
||||||
@@ -150,12 +190,11 @@ public class SignListener implements Listener {
|
|||||||
player.sendMessage(MessageUtil.error("Du bist bereits in einem Spiel!"));
|
player.sendMessage(MessageUtil.error("Du bist bereits in einem Spiel!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.getGameManager().createGame(arena).addPlayer(player);
|
plugin.getGameManager().createGame(arena).addPlayer(player);
|
||||||
refreshSignsForArena(arenaName);
|
refreshSignsForArena(arenaName);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Schild abbauen → aus Liste entfernen */
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onBreak(BlockBreakEvent event) {
|
public void onBreak(BlockBreakEvent event) {
|
||||||
String key = locKey(event.getBlock().getLocation());
|
String key = locKey(event.getBlock().getLocation());
|
||||||
@@ -168,10 +207,13 @@ public class SignListener implements Listener {
|
|||||||
|
|
||||||
// ── Öffentliche Methode: von Game.java aufrufen ──────────────────────────
|
// ── Öffentliche Methode: von Game.java aufrufen ──────────────────────────
|
||||||
|
|
||||||
/** Aktualisiert alle Schilder einer Arena. Wird von Game.java aufgerufen. */
|
|
||||||
public void refreshSignsForArena(String arenaName) {
|
public void refreshSignsForArena(String arenaName) {
|
||||||
for (Map.Entry<String, String> entry : signs.entrySet()) {
|
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());
|
Location loc = keyToLocation(entry.getKey());
|
||||||
if (loc == null) continue;
|
if (loc == null) continue;
|
||||||
Block block = loc.getBlock();
|
Block block = loc.getBlock();
|
||||||
@@ -180,10 +222,17 @@ public class SignListener implements Listener {
|
|||||||
Arena arena = plugin.getArenaManager().getArena(arenaName);
|
Arena arena = plugin.getArenaManager().getArena(arenaName);
|
||||||
if (arena == null) continue;
|
if (arena == null) continue;
|
||||||
|
|
||||||
sign.setLine(0, TAG_FORMATTED);
|
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(1, "§e" + arena.getName());
|
||||||
sign.setLine(2, buildStatusLine(arena));
|
sign.setLine(2, buildStatusLine(arena));
|
||||||
sign.setLine(3, "§7Klick zum Joinen");
|
sign.setLine(3, "§7Joinen");
|
||||||
|
}
|
||||||
sign.update();
|
sign.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user