Update from Git Manager GUI

This commit is contained in:
2026-02-27 00:44:25 +01:00
parent f74701ecbe
commit e718d06473
20 changed files with 3679 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
package de.fussball.plugin;
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.listeners.*;
import de.fussball.plugin.placeholders.FussballPlaceholders;
import de.fussball.plugin.stats.StatsManager;
import de.fussball.plugin.utils.Messages;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
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;
@Override
public void onEnable() {
instance = this;
ConfigurationSerialization.registerClass(Arena.class);
saveDefaultConfig();
// Manager initialisieren
arenaManager = new ArenaManager(this);
gameManager = new GameManager(this);
statsManager = new StatsManager(this);
signListener = new SignListener(this);
Messages.init(this);
registerCommands();
registerListeners();
// PlaceholderAPI-Integration (optional nur wenn PAPI installiert ist)
if (getServer().getPluginManager().getPlugin("PlaceholderAPI") != null) {
new FussballPlaceholders(this).register();
getLogger().info("[Fussball] PlaceholderAPI-Integration aktiviert!");
} else {
getLogger().info("[Fussball] PlaceholderAPI nicht gefunden Platzhalter deaktiviert.");
}
getLogger().info("⚽ Fußball-Plugin v" + getDescription().getVersion() + " gestartet!");
}
@Override
public void onDisable() {
if (gameManager != null) gameManager.stopAllGames();
if (statsManager != null) statsManager.save();
getLogger().info("⚽ Fußball-Plugin gestoppt!");
}
private void registerCommands() {
FussballCommand cmd = new FussballCommand(this);
getCommand("fussball").setExecutor(cmd);
getCommand("fussball").setTabCompleter(cmd);
}
private void registerListeners() {
getServer().getPluginManager().registerEvents(new BallListener(this), this);
getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
getServer().getPluginManager().registerEvents(new BlockListener(this), this);
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; }
}

View File

@@ -0,0 +1,148 @@
package de.fussball.plugin.stats;
import de.fussball.plugin.Fussball;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* Verwaltet persistente Spielerstatistiken in stats.yml.
* Gespeichert werden: Tore, Schüsse, Siege, Niederlagen, Unentschieden, gespielte Spiele.
*/
public class StatsManager {
private final Fussball plugin;
private final File statsFile;
private FileConfiguration statsConfig;
// In-Memory-Cache für schnellen Zugriff
private final Map<UUID, PlayerStats> cache = new HashMap<>();
public StatsManager(Fussball plugin) {
this.plugin = plugin;
this.statsFile = new File(plugin.getDataFolder(), "stats.yml");
load();
}
// ── Persistenz ──────────────────────────────────────────────────────────
private void load() {
if (!statsFile.exists()) {
try { statsFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); }
}
statsConfig = YamlConfiguration.loadConfiguration(statsFile);
cache.clear();
if (statsConfig.contains("players")) {
for (String uuidStr : statsConfig.getConfigurationSection("players").getKeys(false)) {
try {
UUID uuid = UUID.fromString(uuidStr);
String path = "players." + uuidStr;
PlayerStats stats = new PlayerStats(
statsConfig.getString(path + ".name", "Unbekannt"),
statsConfig.getInt(path + ".goals", 0),
statsConfig.getInt(path + ".kicks", 0),
statsConfig.getInt(path + ".wins", 0),
statsConfig.getInt(path + ".losses", 0),
statsConfig.getInt(path + ".draws", 0),
statsConfig.getInt(path + ".games", 0)
);
cache.put(uuid, stats);
} catch (IllegalArgumentException ignored) {}
}
}
plugin.getLogger().info("[Fussball] Statistiken geladen: " + cache.size() + " Spieler.");
}
public void save() {
statsConfig.set("players", null);
for (Map.Entry<UUID, PlayerStats> entry : cache.entrySet()) {
String path = "players." + entry.getKey();
PlayerStats s = entry.getValue();
statsConfig.set(path + ".name", s.name);
statsConfig.set(path + ".goals", s.goals);
statsConfig.set(path + ".kicks", s.kicks);
statsConfig.set(path + ".wins", s.wins);
statsConfig.set(path + ".losses", s.losses);
statsConfig.set(path + ".draws", s.draws);
statsConfig.set(path + ".games", s.games);
}
try { statsConfig.save(statsFile); } catch (IOException e) { e.printStackTrace(); }
}
// ── Datenzugriff ────────────────────────────────────────────────────────
public PlayerStats getStats(UUID uuid) {
return cache.computeIfAbsent(uuid, k -> new PlayerStats("Unbekannt", 0, 0, 0, 0, 0, 0));
}
public void addGoal(UUID uuid, String name) {
PlayerStats s = getStats(uuid);
s.name = name;
s.goals++;
save();
}
public void addKick(UUID uuid, String name) {
PlayerStats s = getStats(uuid);
s.name = name;
s.kicks++;
// Kein sofortiges Speichern bei jedem Kick Spiel-Ende reicht
}
public void addGameResult(UUID uuid, String name, GameResult result) {
PlayerStats s = getStats(uuid);
s.name = name;
s.games++;
switch (result) {
case WIN -> s.wins++;
case LOSS -> s.losses++;
case DRAW -> s.draws++;
}
save();
}
public void flushKicks(Map<UUID, Integer> kicks, Map<UUID, String> names) {
for (Map.Entry<UUID, Integer> entry : kicks.entrySet()) {
PlayerStats s = getStats(entry.getKey());
if (names.containsKey(entry.getKey())) s.name = names.get(entry.getKey());
s.kicks += entry.getValue();
}
save();
}
/** Gibt die Top-N-Torschützen zurück, sortiert nach Toren */
public List<Map.Entry<UUID, PlayerStats>> getTopScorers(int limit) {
List<Map.Entry<UUID, PlayerStats>> list = new ArrayList<>(cache.entrySet());
list.sort((a, b) -> b.getValue().goals - a.getValue().goals);
return list.subList(0, Math.min(limit, list.size()));
}
/** Gibt die Top-N-Spieler nach Siegen zurück */
public List<Map.Entry<UUID, PlayerStats>> getTopWins(int limit) {
List<Map.Entry<UUID, PlayerStats>> list = new ArrayList<>(cache.entrySet());
list.sort((a, b) -> b.getValue().wins - a.getValue().wins);
return list.subList(0, Math.min(limit, list.size()));
}
// ── Innere Klassen ───────────────────────────────────────────────────────
public static class PlayerStats {
public String name;
public int goals, kicks, wins, losses, draws, games;
public PlayerStats(String name, int goals, int kicks, int wins, int losses, int draws, int games) {
this.name = name; this.goals = goals; this.kicks = kicks;
this.wins = wins; this.losses = losses; this.draws = draws; this.games = games;
}
public double getWinRate() {
if (games == 0) return 0.0;
return (double) wins / games * 100.0;
}
}
public enum GameResult { WIN, LOSS, DRAW }
}

View File

@@ -0,0 +1,253 @@
package de.fussball.plugin.arena;
import de.fussball.plugin.Fussball;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import java.util.*;
public class Arena implements ConfigurationSerializable {
private final String name;
private Location center, redSpawn, blueSpawn, ballSpawn;
private Location redGoalMin, redGoalMax, blueGoalMin, blueGoalMax, lobby;
private Location fieldMin, fieldMax;
private int minPlayers, maxPlayers, gameDuration;
/**
* Neue Arena erstellt Standardwerte werden aus der config.yml gelesen.
* Bereits gespeicherte Arenen laden ihre eigenen Werte über deserialize().
*/
public Arena(String name) {
this.name = name;
Fussball plugin = Fussball.getInstance();
if (plugin != null) {
this.minPlayers = plugin.getConfig().getInt("defaults.min-players", 2);
this.maxPlayers = plugin.getConfig().getInt("defaults.max-players", 10);
this.gameDuration = plugin.getConfig().getInt("defaults.game-duration", 300);
} else {
// Fallback falls getInstance() noch nicht verfügbar (z.B. Deserialisierung)
this.minPlayers = 2;
this.maxPlayers = 10;
this.gameDuration = 300;
}
}
public boolean isSetupComplete() {
return center != null && redSpawn != null && blueSpawn != null && ballSpawn != null
&& redGoalMin != null && redGoalMax != null
&& blueGoalMin != null && blueGoalMax != null && lobby != null;
}
public boolean isInRedGoal(Location loc) { return isInRegion(loc, redGoalMin, redGoalMax); }
public boolean isInBlueGoal(Location loc) { return isInRegion(loc, blueGoalMin, blueGoalMax); }
/**
* BUG FIX: Nur X und Z prüfen (Y ignorieren).
* Vorher führte die Y-Prüfung dazu, dass der Ball beim Anstoß sofort
* als Aus erkannt wurde, weil der ArmorStand über dem Boden schwebt.
*/
public boolean isInField(Location loc) {
if (fieldMin == null || fieldMax == null) return true;
return isInField2D(loc);
}
private boolean isInField2D(Location loc) {
if (fieldMin == null || fieldMax == null || loc == null) return true;
double minX = Math.min(fieldMin.getX(), fieldMax.getX());
double maxX = Math.max(fieldMin.getX(), fieldMax.getX());
double minZ = Math.min(fieldMin.getZ(), fieldMax.getZ());
double maxZ = Math.max(fieldMin.getZ(), fieldMax.getZ());
return loc.getX() >= minX && loc.getX() <= maxX
&& loc.getZ() >= minZ && loc.getZ() <= maxZ;
}
/** Auf welcher Seite hat der Ball das Feld verlassen? (nur XZ, kein Y) */
public String getOutSide(Location loc) {
if (fieldMin == null || fieldMax == null) return null;
if (isInField2D(loc)) return null;
double minX = Math.min(fieldMin.getX(), fieldMax.getX());
double maxX = Math.max(fieldMin.getX(), fieldMax.getX());
double minZ = Math.min(fieldMin.getZ(), fieldMax.getZ());
double maxZ = Math.max(fieldMin.getZ(), fieldMax.getZ());
double lenX = maxX - minX;
double lenZ = maxZ - minZ;
if (lenZ >= lenX) {
if (loc.getZ() < minZ) return "redEnd";
if (loc.getZ() > maxZ) return "blueEnd";
return "side";
} else {
if (loc.getX() < minX) return "redEnd";
if (loc.getX() > maxX) return "blueEnd";
return "side";
}
}
public Location clampToField(Location loc) {
if (fieldMin == null || fieldMax == null) return loc;
double x = Math.max(Math.min(fieldMin.getX(), fieldMax.getX()),
Math.min(loc.getX(), Math.max(fieldMin.getX(), fieldMax.getX())));
double y = loc.getY();
double z = Math.max(Math.min(fieldMin.getZ(), fieldMax.getZ()),
Math.min(loc.getZ(), Math.max(fieldMin.getZ(), fieldMax.getZ())));
return new Location(loc.getWorld(), x, y, z);
}
// Tor-Erkennung: volle 3D-Prüfung (Y ist für das Tor wichtig!)
private boolean isInRegion(Location loc, Location min, Location max) {
if (min == null || max == null || loc == null) return false;
if (loc.getWorld() == null || min.getWorld() == null) return false;
if (!loc.getWorld().equals(min.getWorld())) return false;
return loc.getX() >= Math.min(min.getX(), max.getX()) && loc.getX() <= Math.max(min.getX(), max.getX())
&& loc.getY() >= Math.min(min.getY(), max.getY()) && loc.getY() <= Math.max(min.getY(), max.getY())
&& loc.getZ() >= Math.min(min.getZ(), max.getZ()) && loc.getZ() <= Math.max(min.getZ(), max.getZ());
}
// ── Spielfeld-Achse (für Abseits-Berechnung) ────────────────────────────
/**
* Gibt den Einheitsvektor zurück der vom roten Tor zum blauen Tor zeigt.
* Funktioniert unabhängig davon ob das Feld in X- oder Z-Richtung ausgerichtet ist.
*/
public org.bukkit.util.Vector getFieldDirection() {
if (redGoalMin == null || redGoalMax == null || blueGoalMin == null || blueGoalMax == null) return null;
double rX = (redGoalMin.getX() + redGoalMax.getX()) / 2.0;
double rZ = (redGoalMin.getZ() + redGoalMax.getZ()) / 2.0;
double bX = (blueGoalMin.getX() + blueGoalMax.getX()) / 2.0;
double bZ = (blueGoalMin.getZ() + blueGoalMax.getZ()) / 2.0;
org.bukkit.util.Vector dir = new org.bukkit.util.Vector(bX - rX, 0, bZ - rZ);
double len = dir.length();
if (len < 0.001) return null;
return dir.multiply(1.0 / len);
}
/** Projektionswert einer Location auf die Spielfeld-Achse */
public double getAxisValue(org.bukkit.Location loc) {
org.bukkit.util.Vector dir = getFieldDirection();
if (dir == null || loc == null) return 0;
return loc.getX() * dir.getX() + loc.getZ() * dir.getZ();
}
public double getRedGoalAxisValue() {
if (redGoalMin == null || redGoalMax == null) return 0;
org.bukkit.util.Vector dir = getFieldDirection();
if (dir == null) return 0;
double cx = (redGoalMin.getX() + redGoalMax.getX()) / 2.0;
double cz = (redGoalMin.getZ() + redGoalMax.getZ()) / 2.0;
return cx * dir.getX() + cz * dir.getZ();
}
public double getBlueGoalAxisValue() {
if (blueGoalMin == null || blueGoalMax == null) return 0;
org.bukkit.util.Vector dir = getFieldDirection();
if (dir == null) return 0;
double cx = (blueGoalMin.getX() + blueGoalMax.getX()) / 2.0;
double cz = (blueGoalMin.getZ() + blueGoalMax.getZ()) / 2.0;
return cx * dir.getX() + cz * dir.getZ();
}
public double getCenterAxisValue() {
return (getRedGoalAxisValue() + getBlueGoalAxisValue()) / 2.0;
}
// ── Serialisierung ───────────────────────────────────────────────────────
@Override
public Map<String, Object> serialize() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("name", name);
map.put("minPlayers", minPlayers);
map.put("maxPlayers", maxPlayers);
map.put("gameDuration", gameDuration);
if (lobby != null) map.put("lobby", serLoc(lobby));
if (center != null) map.put("center", serLoc(center));
if (redSpawn != null) map.put("redSpawn", serLoc(redSpawn));
if (blueSpawn != null) map.put("blueSpawn", serLoc(blueSpawn));
if (ballSpawn != null) map.put("ballSpawn", serLoc(ballSpawn));
if (redGoalMin != null) map.put("redGoalMin", serLoc(redGoalMin));
if (redGoalMax != null) map.put("redGoalMax", serLoc(redGoalMax));
if (blueGoalMin != null) map.put("blueGoalMin", serLoc(blueGoalMin));
if (blueGoalMax != null) map.put("blueGoalMax", serLoc(blueGoalMax));
if (fieldMin != null) map.put("fieldMin", serLoc(fieldMin));
if (fieldMax != null) map.put("fieldMax", serLoc(fieldMax));
return map;
}
/**
* Beim Laden gespeicherter Arenen werden die eigenen Werte aus der YAML-Datei
* gelesen NICHT aus der config.yml. So kann jede Arena eigene Werte haben.
*/
public static Arena deserialize(Map<String, Object> map) {
Arena a = new Arena((String) map.get("name"));
// Überschreibe Konstruktor-Defaults mit den gespeicherten Werten
a.minPlayers = getInt(map, "minPlayers", a.minPlayers);
a.maxPlayers = getInt(map, "maxPlayers", a.maxPlayers);
a.gameDuration = getInt(map, "gameDuration", a.gameDuration);
if (map.containsKey("lobby")) a.lobby = desLoc(map.get("lobby"));
if (map.containsKey("center")) a.center = desLoc(map.get("center"));
if (map.containsKey("redSpawn")) a.redSpawn = desLoc(map.get("redSpawn"));
if (map.containsKey("blueSpawn")) a.blueSpawn = desLoc(map.get("blueSpawn"));
if (map.containsKey("ballSpawn")) a.ballSpawn = desLoc(map.get("ballSpawn"));
if (map.containsKey("redGoalMin")) a.redGoalMin = desLoc(map.get("redGoalMin"));
if (map.containsKey("redGoalMax")) a.redGoalMax = desLoc(map.get("redGoalMax"));
if (map.containsKey("blueGoalMin")) a.blueGoalMin = desLoc(map.get("blueGoalMin"));
if (map.containsKey("blueGoalMax")) a.blueGoalMax = desLoc(map.get("blueGoalMax"));
if (map.containsKey("fieldMin")) a.fieldMin = desLoc(map.get("fieldMin"));
if (map.containsKey("fieldMax")) a.fieldMax = desLoc(map.get("fieldMax"));
return a;
}
private static String serLoc(Location l) {
return l.getWorld().getName() + ";" + l.getX() + ";" + l.getY() + ";" + l.getZ() + ";" + l.getYaw() + ";" + l.getPitch();
}
private static Location desLoc(Object obj) {
if (obj == null) return null;
try {
String[] p = obj.toString().split(";");
World world = Bukkit.getWorld(p[0]);
if (world == null) return null;
return new Location(world, Double.parseDouble(p[1]), Double.parseDouble(p[2]), Double.parseDouble(p[3]),
p.length > 4 ? Float.parseFloat(p[4]) : 0f, p.length > 5 ? Float.parseFloat(p[5]) : 0f);
} catch (Exception e) { return null; }
}
private static int getInt(Map<String, Object> map, String key, int def) {
Object v = map.get(key); return v instanceof Number ? ((Number) v).intValue() : def;
}
// ── Getter / Setter ──────────────────────────────────────────────────────
public String getName() { return name; }
public Location getCenter() { return center; }
public void setCenter(Location l) { this.center = l; }
public Location getRedSpawn() { return redSpawn; }
public void setRedSpawn(Location l) { this.redSpawn = l; }
public Location getBlueSpawn() { return blueSpawn; }
public void setBlueSpawn(Location l) { this.blueSpawn = l; }
public Location getBallSpawn() { return ballSpawn; }
public void setBallSpawn(Location l) { this.ballSpawn = l; }
public Location getRedGoalMin() { return redGoalMin; }
public void setRedGoalMin(Location l) { this.redGoalMin = l; }
public Location getRedGoalMax() { return redGoalMax; }
public void setRedGoalMax(Location l) { this.redGoalMax = l; }
public Location getBlueGoalMin() { return blueGoalMin; }
public void setBlueGoalMin(Location l) { this.blueGoalMin = l; }
public Location getBlueGoalMax() { return blueGoalMax; }
public void setBlueGoalMax(Location l) { this.blueGoalMax = l; }
public Location getLobby() { return lobby; }
public void setLobby(Location l) { this.lobby = l; }
public Location getFieldMin() { return fieldMin; }
public void setFieldMin(Location l) { this.fieldMin = l; }
public Location getFieldMax() { return fieldMax; }
public void setFieldMax(Location l) { this.fieldMax = l; }
public int getMinPlayers() { return minPlayers; }
public void setMinPlayers(int n) { this.minPlayers = n; }
public int getMaxPlayers() { return maxPlayers; }
public void setMaxPlayers(int n) { this.maxPlayers = n; }
public int getGameDuration() { return gameDuration; }
public void setGameDuration(int n) { this.gameDuration = n; }
}

View File

@@ -0,0 +1,65 @@
package de.fussball.plugin.arena;
import de.fussball.plugin.Fussball;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.*;
import java.util.*;
public class ArenaManager {
private final Fussball plugin;
private final Map<String, Arena> arenas = new HashMap<>();
private final File arenaFile;
private FileConfiguration arenaConfig;
public ArenaManager(Fussball plugin) {
this.plugin = plugin;
this.arenaFile = new File(plugin.getDataFolder(), "arenas.yml");
loadArenas();
}
public void loadArenas() {
if (!arenaFile.exists()) {
try { arenaFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); }
}
arenaConfig = YamlConfiguration.loadConfiguration(arenaFile);
arenas.clear();
if (arenaConfig.contains("arenas")) {
for (String key : arenaConfig.getConfigurationSection("arenas").getKeys(false)) {
Object obj = arenaConfig.get("arenas." + key);
if (obj instanceof Arena) arenas.put(key.toLowerCase(), (Arena) obj);
}
}
plugin.getLogger().info("Arenen geladen: " + arenas.size());
}
public void saveArenas() {
arenaConfig.set("arenas", null);
for (Map.Entry<String, Arena> e : arenas.entrySet())
arenaConfig.set("arenas." + e.getKey(), e.getValue());
try { arenaConfig.save(arenaFile); } catch (IOException e) { e.printStackTrace(); }
}
public Arena createArena(String name) {
Arena arena = new Arena(name);
arenas.put(name.toLowerCase(), arena);
saveArenas();
return arena;
}
public boolean deleteArena(String name) {
if (arenas.remove(name.toLowerCase()) != null) { saveArenas(); return true; }
return false;
}
public Arena getArena(String name) { return arenas.get(name.toLowerCase()); }
public boolean arenaExists(String name) { return arenas.containsKey(name.toLowerCase()); }
public Collection<Arena> getAllArenas() { return arenas.values(); }
public List<String> getArenaNames() { return new ArrayList<>(arenas.keySet()); }
public void saveArena(Arena arena) {
arenas.put(arena.getName().toLowerCase(), arena);
saveArenas();
}
}

View File

@@ -0,0 +1,333 @@
package de.fussball.plugin.commands;
import de.fussball.plugin.Fussball;
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.stats.StatsManager;
import de.fussball.plugin.utils.MessageUtil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import java.util.*;
public class FussballCommand implements CommandExecutor, TabCompleter {
private final Fussball plugin;
public FussballCommand(Fussball plugin) { this.plugin = plugin; }
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) { sendHelp(sender); return true; }
switch (args[0].toLowerCase()) {
// ── Spieler-Befehle ──────────────────────────────────────────────
case "join" -> {
if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; }
if (args.length < 2) { player.sendMessage(MessageUtil.error("Benutze: /fb join <arena>")); return true; }
Arena arena = plugin.getArenaManager().getArena(args[1]);
if (arena == null) { player.sendMessage(MessageUtil.error("Arena §e" + args[1] + " §cnicht gefunden!")); return true; }
if (!arena.isSetupComplete()) { player.sendMessage(MessageUtil.error("Arena nicht vollständig eingerichtet!")); return true; }
if (plugin.getGameManager().isInAnyGame(player)) { player.sendMessage(MessageUtil.error("Du bist bereits in einem Spiel! Tippe §e/fb leave§c.")); return true; }
plugin.getGameManager().createGame(arena).addPlayer(player);
}
case "leave" -> {
if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; }
// Aus Spiel
Game game = plugin.getGameManager().getPlayerGame(player);
if (game != null) { game.removePlayer(player); player.sendMessage(MessageUtil.success("Du hast das Spiel verlassen!")); return true; }
// Aus Zuschauer
Game specGame = plugin.getGameManager().getSpectatorGame(player);
if (specGame != null) { specGame.removeSpectator(player); player.sendMessage(MessageUtil.success("Du hast das Zuschauen beendet!")); return true; }
// Aus Warteschlange
plugin.getGameManager().removeFromAllQueues(player);
player.sendMessage(MessageUtil.warn("Du bist in keinem Spiel und in keiner Warteschlange."));
}
case "spectate", "spec" -> {
if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; }
if (args.length < 2) { player.sendMessage(MessageUtil.error("Benutze: /fb spectate <arena>")); return true; }
Arena arena = plugin.getArenaManager().getArena(args[1]);
if (arena == null) { player.sendMessage(MessageUtil.error("Arena nicht gefunden!")); return true; }
if (plugin.getGameManager().isInAnyGame(player)) { player.sendMessage(MessageUtil.error("Verlasse zuerst dein aktuelles Spiel!")); return true; }
Game game = plugin.getGameManager().getGame(arena.getName());
if (game == null) { player.sendMessage(MessageUtil.error("Kein laufendes Spiel in dieser Arena!")); return true; }
game.addSpectator(player);
}
case "list" -> {
sender.sendMessage(MessageUtil.header("⚽ Verfügbare Arenen"));
Collection<Arena> arenas = plugin.getArenaManager().getAllArenas();
if (arenas.isEmpty()) { sender.sendMessage(MessageUtil.warn("Keine Arenen vorhanden.")); return true; }
for (Arena a : arenas) {
Game g = plugin.getGameManager().getGame(a.getName());
int queueSize = plugin.getGameManager().getQueueSize(a.getName());
String status = g != null ? statusDot(g.getState()) : "§a●";
String players = g != null ? g.getAllPlayers().size() + "/" + a.getMaxPlayers() : "0/" + a.getMaxPlayers();
String setup = a.isSetupComplete() ? "§a✔" : "§c✗";
String queue = queueSize > 0 ? " §8(§e" + queueSize + " §8warten)" : "";
sender.sendMessage("§7 " + status + " §e" + a.getName() + " §7[" + players + "] " + setup + queue);
}
}
case "stats" -> {
if (!(sender instanceof Player player)) { sender.sendMessage("Nur für Spieler!"); return true; }
Player target = args.length >= 2 ? Bukkit.getPlayer(args[1]) : player;
if (target == null) { player.sendMessage(MessageUtil.error("Spieler §e" + args[1] + " §cnicht gefunden!")); return true; }
StatsManager.PlayerStats s = plugin.getStatsManager().getStats(target.getUniqueId());
player.sendMessage(MessageUtil.header("Statistiken: " + target.getName()));
player.sendMessage("§7 ⚽ Tore: §e" + s.goals);
player.sendMessage("§7 👟 Schüsse: §e" + s.kicks);
player.sendMessage("§7 🏆 Siege: §a" + s.wins);
player.sendMessage("§7 ❌ Niederlagen: §c" + s.losses);
player.sendMessage("§7 Unentschieden: §7" + s.draws);
player.sendMessage("§7 📊 Gespielte Spiele: §e" + s.games);
player.sendMessage("§7 📈 Siegquote: §e" + String.format("%.1f", s.getWinRate()) + "§7%");
// In-Game-Statistik anhängen wenn aktiv
Game inGame = plugin.getGameManager().getPlayerGame(target);
if (inGame != null) {
player.sendMessage("§8--- Aktuelles Spiel ---");
player.sendMessage("§7 Tore heute: §e" + inGame.getGoals().getOrDefault(target.getUniqueId(), 0));
player.sendMessage("§7 Schüsse heute: §e" + inGame.getKicks().getOrDefault(target.getUniqueId(), 0));
}
}
// ── Admin-Befehle ────────────────────────────────────────────────
case "create" -> {
if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; }
if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb create <name>")); return true; }
if (plugin.getArenaManager().arenaExists(args[1])) { sender.sendMessage(MessageUtil.error("Arena §e" + args[1] + " §cexistiert bereits!")); return true; }
plugin.getArenaManager().createArena(args[1]);
sender.sendMessage(MessageUtil.success("Arena §e" + args[1] + " §aerstellt! Richte sie mit §e/fb setup §aein."));
}
case "delete" -> {
if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; }
if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb delete <arena>")); return true; }
sender.sendMessage(plugin.getArenaManager().deleteArena(args[1])
? MessageUtil.success("Arena §e" + args[1] + " §agelöscht!") : MessageUtil.error("Arena nicht gefunden!"));
}
case "setup" -> {
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 < 3) { sendSetupHelp(player); return true; }
handleSetup(player, args);
}
case "stop" -> {
if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; }
if (args.length < 2) { sender.sendMessage(MessageUtil.error("Benutze: /fb stop <arena>")); return true; }
Game game = plugin.getGameManager().getGame(args[1]);
if (game == null) { sender.sendMessage(MessageUtil.error("Kein aktives Spiel in §e" + args[1] + "§c!")); return true; }
game.endGame(null);
sender.sendMessage(MessageUtil.success("Spiel in §e" + args[1] + " §aberendet."));
}
case "top" -> {
if (args.length < 2) {
sender.sendMessage(MessageUtil.error("Benutze: /fussball top goals|wins|kicks"));
return true;
}
switch (args[1].toLowerCase()) {
case "goals" -> {
sender.sendMessage(MessageUtil.header("🏆 Top Torschützen"));
var list = plugin.getStatsManager().getTopScorers(10);
if (list.isEmpty()) { sender.sendMessage(MessageUtil.warn("Noch keine Daten.")); break; }
for (int i = 0; i < list.size(); i++) {
var e = list.get(i);
sender.sendMessage("§e#" + (i+1) + " §f" + e.getValue().name
+ " §7— §e" + e.getValue().goals + " §7Tore"
+ " §8(§7" + e.getValue().games + " Spiele§8)");
}
}
case "wins" -> {
sender.sendMessage(MessageUtil.header("🏆 Top Gewinner"));
var list = plugin.getStatsManager().getTopWins(10);
if (list.isEmpty()) { sender.sendMessage(MessageUtil.warn("Noch keine Daten.")); break; }
for (int i = 0; i < list.size(); i++) {
var e = list.get(i);
sender.sendMessage("§e#" + (i+1) + " §f" + e.getValue().name
+ " §7— §a" + e.getValue().wins + " §7Siege"
+ " §8(§7" + String.format("%.0f", e.getValue().getWinRate()) + "% WR§8)");
}
}
case "kicks" -> {
sender.sendMessage(MessageUtil.header("🏆 Top Schützen (Schüsse)"));
var list = plugin.getStatsManager().getTopScorers(10);
// Nutze getTopScorers und zeige kicks
var kickList = new java.util.ArrayList<>(plugin.getStatsManager().getTopScorers(100));
kickList.sort((a, b) -> b.getValue().kicks - a.getValue().kicks);
for (int i = 0; i < Math.min(10, kickList.size()); i++) {
var e = kickList.get(i);
sender.sendMessage("§e#" + (i+1) + " §f" + e.getValue().name
+ " §7— §e" + e.getValue().kicks + " §7Schüsse");
}
}
default -> sender.sendMessage(MessageUtil.error("Gültig: goals | wins | kicks"));
}
}
case "debug" -> {
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) { sender.sendMessage(MessageUtil.error("Benutze: /fb debug <arena>")); return true; }
Arena arena = plugin.getArenaManager().getArena(args[1]);
if (arena == null) { sender.sendMessage(MessageUtil.error("Arena nicht gefunden!")); return true; }
handleDebug(player, arena);
}
default -> sendHelp(sender);
}
return true;
}
// ── Setup-Handler ────────────────────────────────────────────────────────
private void handleSetup(Player player, String[] args) {
Arena arena = plugin.getArenaManager().getArena(args[1]);
if (arena == null) { player.sendMessage(MessageUtil.error("Arena §e" + args[1] + " §cnicht gefunden!")); return; }
switch (args[2].toLowerCase()) {
case "lobby" -> { arena.setLobby(player.getLocation()); player.sendMessage(MessageUtil.success("Lobby gesetzt: " + locStr(player.getLocation()))); }
case "redspawn" -> { arena.setRedSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Roter Spawn gesetzt: " + locStr(player.getLocation()))); }
case "bluespawn" -> { arena.setBlueSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Blauer Spawn gesetzt: " + locStr(player.getLocation()))); }
case "ballspawn" -> { arena.setBallSpawn(player.getLocation()); player.sendMessage(MessageUtil.success("Ball-Spawn gesetzt: " + locStr(player.getLocation()))); }
case "center" -> { arena.setCenter(player.getLocation()); player.sendMessage(MessageUtil.success("Mittelpunkt gesetzt: " + locStr(player.getLocation()))); }
case "redgoalmin" -> { arena.setRedGoalMin(player.getLocation()); player.sendMessage(MessageUtil.success("Rotes Tor Min gesetzt: " + locStr(player.getLocation()))); }
case "redgoalmax" -> { arena.setRedGoalMax(player.getLocation()); player.sendMessage(MessageUtil.success("Rotes Tor Max gesetzt: " + locStr(player.getLocation()))); }
case "bluegoalmin" -> { arena.setBlueGoalMin(player.getLocation()); player.sendMessage(MessageUtil.success("Blaues Tor Min gesetzt: " + locStr(player.getLocation()))); }
case "bluegoalmax" -> { arena.setBlueGoalMax(player.getLocation()); player.sendMessage(MessageUtil.success("Blaues Tor Max gesetzt: " + locStr(player.getLocation()))); }
case "fieldmin" -> { arena.setFieldMin(player.getLocation()); player.sendMessage(MessageUtil.success("Spielfeld Min gesetzt: " + locStr(player.getLocation()))); }
case "fieldmax" -> { arena.setFieldMax(player.getLocation()); player.sendMessage(MessageUtil.success("Spielfeld Max 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")); }
case "info" -> {
player.sendMessage(MessageUtil.header("Arena: " + arena.getName()));
player.sendMessage("§7 Lobby: " + check(arena.getLobby()));
player.sendMessage("§7 Roter Spawn: " + check(arena.getRedSpawn()));
player.sendMessage("§7 Blauer Spawn: " + check(arena.getBlueSpawn()));
player.sendMessage("§7 Ball-Spawn: " + check(arena.getBallSpawn()));
player.sendMessage("§7 Mittelpunkt: " + check(arena.getCenter()));
player.sendMessage("§7 Rotes Tor: " + check(arena.getRedGoalMin(), arena.getRedGoalMax()));
player.sendMessage("§7 Blaues Tor: " + check(arena.getBlueGoalMin(), arena.getBlueGoalMax()));
player.sendMessage("§7 Spielfeld: " + check(arena.getFieldMin(), arena.getFieldMax()) + " §8(optional)");
player.sendMessage("§7 Min. Spieler: §e" + arena.getMinPlayers());
player.sendMessage("§7 Max. Spieler: §e" + arena.getMaxPlayers());
player.sendMessage("§7 Spieldauer: §e" + arena.getGameDuration() + "s");
player.sendMessage("§7 Setup komplett: " + (arena.isSetupComplete() ? "§a✔ JA" : "§c✗ NEIN"));
return;
}
default -> { sendSetupHelp(player); return; }
}
plugin.getArenaManager().saveArena(arena);
}
// ── Debug-Handler ────────────────────────────────────────────────────────
private void handleDebug(Player player, Arena arena) {
player.sendMessage(MessageUtil.header("DEBUG: " + arena.getName()));
printRegion(player, "§cROTES TOR", arena.getRedGoalMin(), arena.getRedGoalMax());
printRegion(player, "§9BLAUES TOR", arena.getBlueGoalMin(), arena.getBlueGoalMax());
printRegion(player, "§aSPIELFELD", arena.getFieldMin(), arena.getFieldMax());
Game game = plugin.getGameManager().getGame(arena.getName());
if (game != null && game.getBall() != null && game.getBall().isActive()) {
Ball ball = game.getBall();
Location bl = ball.getEntity().getLocation();
Location bh = bl.clone().add(0, 1.4, 0);
player.sendMessage("§e--- BALL ---");
player.sendMessage("§7Fuß: §f" + locStr(bl));
player.sendMessage("§7Kopf: §f" + locStr(bh));
player.sendMessage("§7RotesTor: Fuß=" + yn(arena.isInRedGoal(bl)) + " Kopf=" + yn(arena.isInRedGoal(bh)));
player.sendMessage("§7BlauesTor: Fuß=" + yn(arena.isInBlueGoal(bl)) + " Kopf=" + yn(arena.isInBlueGoal(bh)));
player.sendMessage("§7Im Feld: " + yn(arena.isInField(bl)));
player.sendMessage("§7Aus-Seite: §f" + (arena.getOutSide(bl) != null ? arena.getOutSide(bl) : "keine"));
} else {
player.sendMessage("§7(Kein aktiver Ball)");
}
player.sendMessage("§e--- DEINE POSITION ---");
player.sendMessage("§f" + locStr(player.getLocation()));
player.sendMessage("§7RotesTor: " + yn(arena.isInRedGoal(player.getLocation())));
player.sendMessage("§7BlauesTor: " + yn(arena.isInBlueGoal(player.getLocation())));
player.sendMessage("§7Im Feld: " + yn(arena.isInField(player.getLocation())));
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private String statusDot(GameState s) {
return switch (s) {
case WAITING -> "§a●";
case STARTING -> "§e●";
case RUNNING, GOAL, HALFTIME, OVERTIME -> "§c●";
case PENALTY -> "§d●";
case ENDING -> "§7●";
};
}
private void printRegion(Player p, String label, Location min, Location max) {
p.sendMessage("§e--- " + label + " §e---");
if (min == null || max == null) { p.sendMessage("§cNICHT GESETZT"); return; }
p.sendMessage("§7Min: §f" + locStr(min) + " §7Max: §f" + locStr(max));
p.sendMessage("§7X: §f" + fmt(Math.min(min.getX(), max.getX())) + "§7─§f" + fmt(Math.max(min.getX(), max.getX())));
p.sendMessage("§7Y: §f" + fmt(Math.min(min.getY(), max.getY())) + "§7─§f" + fmt(Math.max(min.getY(), max.getY())));
p.sendMessage("§7Z: §f" + fmt(Math.min(min.getZ(), max.getZ())) + "§7─§f" + fmt(Math.max(min.getZ(), max.getZ())));
}
private void sendHelp(CommandSender s) {
s.sendMessage(MessageUtil.header("⚽ Fußball Plugin"));
s.sendMessage("§e/fb join <arena> §7- Spiel beitreten");
s.sendMessage("§e/fb leave §7- Spiel / Zuschauer verlassen");
s.sendMessage("§e/fb spectate <arena> §7- Spiel zuschauen");
s.sendMessage("§e/fb list §7- Arenen anzeigen");
s.sendMessage("§e/fb stats [spieler] §7- Statistiken anzeigen");
s.sendMessage("§e/fb top [goals|wins] §7- Bestenliste");
if (s.hasPermission("fussball.admin")) {
s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug");
}
}
private void sendSetupHelp(Player p) {
p.sendMessage(MessageUtil.header("Setup-Optionen"));
p.sendMessage("§e/fb setup <arena> lobby|redspawn|bluespawn|ballspawn|center");
p.sendMessage("§e/fb setup <arena> redgoalmin|redgoalmax|bluegoalmin|bluegoalmax");
p.sendMessage("§e/fb setup <arena> fieldmin|fieldmax §8(optional Aus-Erkennung)");
p.sendMessage("§e/fb setup <arena> minplayers <n>|maxplayers <n>|duration <s>|info");
}
private String check(Location l) { return l != null ? "§a✔ " + locStr(l) : "§c✗ nicht gesetzt"; }
private String check(Location a, Location b) { return (a != null && b != null) ? "§a✔ gesetzt" : "§c✗ nicht gesetzt"; }
private String yn(boolean b) { return b ? "§aJA" : "§cNEIN"; }
private String locStr(Location l) { return fmt(l.getX()) + " / " + fmt(l.getY()) + " / " + fmt(l.getZ()); }
private String fmt(double d) { return String.format("%.1f", d); }
// ── Tab-Completion ───────────────────────────────────────────────────────
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) {
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", "debug"));
} else if (args.length == 2 && List.of("join","delete","setup","stop","debug","spectate").contains(args[0].toLowerCase())) {
list.addAll(plugin.getArenaManager().getArenaNames());
} else if (args.length == 3 && args[0].equalsIgnoreCase("setup")) {
list.addAll(List.of("lobby","redspawn","bluespawn","ballspawn","center",
"redgoalmin","redgoalmax","bluegoalmin","bluegoalmax",
"fieldmin","fieldmax","minplayers","maxplayers","duration","info"));
} else if (args.length == 2 && args[0].equalsIgnoreCase("top")) {
list.addAll(List.of("goals", "wins"));
}
String input = args[args.length - 1].toLowerCase();
list.removeIf(s -> !s.toLowerCase().startsWith(input));
return list;
}
}

View File

@@ -0,0 +1,256 @@
package de.fussball.plugin.game;
import de.fussball.plugin.Fussball;
import de.fussball.plugin.utils.MessageUtil;
import org.bukkit.*;
import org.bukkit.entity.*;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.profile.PlayerTextures;
import org.bukkit.util.Vector;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID;
public class Ball {
private static final String BALL_TEXTURE_URL =
"https://textures.minecraft.net/texture/451f8cfcfb85d77945dc6a3618414093e70436b46d2577b28c727f1329b7265e";
// Reibung: jeder Tick wird horizontale Geschwindigkeit um diesen Faktor reduziert
private static final double FRICTION = 0.82;
// Unter dieser Geschwindigkeit → Ball stoppt komplett
private static final double MIN_VELOCITY = 0.04;
private final Game game;
private final Fussball plugin;
private ArmorStand entity;
private final Location spawnLocation;
private boolean active = false;
// ── Torwart-Halten ───────────────────────────────────────────────────────
private boolean heldByGoalkeeper = false;
private Player holdingPlayer = null;
public Ball(Game game, Fussball plugin, Location spawnLocation) {
this.game = game;
this.plugin = plugin;
this.spawnLocation = spawnLocation.clone();
}
// ── Config-Helfer ────────────────────────────────────────────────────────
private double cfg(String path, double def) {
return plugin.getConfig().getDouble(path, def);
}
// ── Spawnen ──────────────────────────────────────────────────────────────
public void spawn() {
if (spawnLocation == null || spawnLocation.getWorld() == null) {
plugin.getLogger().severe("[Fussball] Ball-Spawn Location ist null!"); return;
}
if (entity != null && !entity.isDead()) entity.remove();
spawnLocation.getWorld().loadChunk(spawnLocation.getBlockX() >> 4, spawnLocation.getBlockZ() >> 4);
entity = spawnLocation.getWorld().spawn(
spawnLocation, ArmorStand.class, CreatureSpawnEvent.SpawnReason.CUSTOM, false,
stand -> {
stand.setVisible(false);
stand.setGravity(true);
stand.setCollidable(false);
stand.setInvulnerable(true);
stand.setPersistent(false);
stand.setSilent(true);
stand.setSmall(true);
stand.setArms(false);
stand.setBasePlate(false);
stand.setCustomName("§e⚽ Fußball");
stand.setCustomNameVisible(true);
stand.getEquipment().setHelmet(createBallItem());
}
);
if (entity == null) {
plugin.getLogger().severe("[Fussball] ArmorStand konnte nicht gespawnt werden!"); return;
}
active = true;
}
private ItemStack createBallItem() {
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta == null) return skull;
meta.setDisplayName("§e⚽ Fußball");
try {
PlayerProfile profile = Bukkit.createPlayerProfile(
UUID.nameUUIDFromBytes("FussballBall".getBytes()), "FussballBall");
PlayerTextures textures = profile.getTextures();
textures.setSkin(new URL(BALL_TEXTURE_URL));
profile.setTextures(textures);
meta.setOwnerProfile(profile);
} catch (MalformedURLException e) {
plugin.getLogger().warning("[Fussball] Ball-Textur URL ungültig: " + e.getMessage());
}
skull.setItemMeta(meta);
return skull;
}
// ── Schuss ───────────────────────────────────────────────────────────────
/** Normaler Schuss (Rechtsklick) */
public void kick(Player player) {
if (entity == null || entity.isDead() || !active) return;
Location ballLoc = entity.getLocation();
Vector dir = getKickDirection(player);
double kickVertical = cfg("ball.kick-vertical", 0.3);
double kickPower = cfg("ball.kick-power", 1.1);
double sprintKickPower = cfg("ball.sprint-kick-power", 1.8);
dir.setY(kickVertical);
dir.multiply(player.isSprinting() ? sprintKickPower : kickPower);
applyKick(dir, ballLoc, 1.8f);
}
/**
* Aufgeladener Schuss (Shift gedrückt halten → loslassen)
* power = 0.0 (kurz gehalten) bis 1.0 (voll aufgeladen, ~1.5s)
*/
public void chargedKick(Player player, double power) {
if (entity == null || entity.isDead() || !active) return;
Location ballLoc = entity.getLocation();
Vector dir = getKickDirection(player);
double minPower = cfg("ball.charged-min-power", 1.3);
double maxPower = cfg("ball.charged-max-power", 3.8);
double actualMultiplier = minPower + (maxPower - minPower) * power;
dir.setY(0.25 + power * 0.25); // mehr Loft bei vollem Schuss
dir.multiply(actualMultiplier);
float pitch = 1.0f + (float) (power * 0.8f);
applyKick(dir, ballLoc, pitch);
// Feuer-Partikel ab 70% Ladung
if (power > 0.7) {
ballLoc.getWorld().spawnParticle(Particle.FLAME, ballLoc, 12, 0.2, 0.2, 0.2, 0.06);
}
}
private Vector getKickDirection(Player player) {
Vector dir = entity.getLocation().toVector().subtract(player.getLocation().toVector());
if (dir.lengthSquared() < 0.0001) dir = player.getLocation().getDirection();
dir.normalize();
return dir;
}
private void applyKick(Vector dir, Location ballLoc, float soundPitch) {
entity.setVelocity(dir);
ballLoc.getWorld().playSound(ballLoc, Sound.ENTITY_GENERIC_SMALL_FALL, 1.0f, soundPitch);
ballLoc.getWorld().spawnParticle(Particle.POOF, ballLoc, 5, 0.1, 0.1, 0.1, 0.05);
}
// ── Physik ───────────────────────────────────────────────────────────────
public void applyFriction() {
if (heldByGoalkeeper) return; // Kein Reibung wenn der TW den Ball hält
if (entity == null || entity.isDead() || !active) return;
Vector vel = entity.getVelocity();
double hx = vel.getX() * FRICTION;
double hz = vel.getZ() * FRICTION;
if (Math.abs(hx) < MIN_VELOCITY) hx = 0;
if (Math.abs(hz) < MIN_VELOCITY) hz = 0;
entity.setVelocity(new Vector(hx, vel.getY(), hz));
}
// ── Torwart-Mechanik ─────────────────────────────────────────────────────
/**
* Torwart greift den Ball Ball schwebt vor ihm und folgt ihm.
* Prüft vorher, ob er im erlaubten Bereich ist.
*/
public void holdBall(Player goalkeeper) {
if (entity == null || entity.isDead() || !active) return;
// NEUE REGEL: Prüfen, ob Spieler im 2-Block-Radius am Tor ist
if (!game.isAllowedToHoldBall(goalkeeper)) {
goalkeeper.sendMessage(MessageUtil.error("Du kannst den Ball nur innerhalb von 2 Blöcken am Tor halten!"));
return;
}
this.heldByGoalkeeper = true;
this.holdingPlayer = goalkeeper;
entity.setGravity(false);
entity.setVelocity(new Vector(0, 0, 0));
}
/**
* Muss jeden Tick aufgerufen werden damit der Ball dem TW folgt.
*/
public void updateHeldPosition() {
if (!heldByGoalkeeper || holdingPlayer == null || entity == null || entity.isDead()) return;
Location hold = holdingPlayer.getLocation().clone();
hold.add(holdingPlayer.getLocation().getDirection().normalize().multiply(0.8));
hold.setY(hold.getY() + 1.0);
entity.teleport(hold);
entity.setVelocity(new Vector(0, 0, 0));
}
/**
* Torwart lässt den Ball los ohne zu werfen (z.B. fallen lassen).
*/
public void releaseBall() {
this.heldByGoalkeeper = false;
this.holdingPlayer = null;
if (entity != null && !entity.isDead()) {
entity.setGravity(true);
}
}
/**
* Torwart wirft den Ball mit gegebener Stärke (0.5 2.5).
*/
public void throwBall(Player goalkeeper, double power) {
releaseBall();
if (entity == null || entity.isDead()) return;
Vector dir = goalkeeper.getLocation().getDirection();
dir.setY(dir.getY() + 0.25);
dir.multiply(power);
entity.setVelocity(dir);
Location loc = entity.getLocation();
loc.getWorld().playSound(loc, Sound.ENTITY_SNOWBALL_THROW, 1f, 1.1f);
loc.getWorld().spawnParticle(Particle.POOF, loc, 5, 0.1, 0.1, 0.1, 0.04);
}
public boolean isHeld() { return heldByGoalkeeper; }
public Player getHoldingPlayer() { return holdingPlayer; }
// ── Util ─────────────────────────────────────────────────────────────────
public void returnToCenter() {
if (entity != null && !entity.isDead()) {
entity.teleport(spawnLocation);
entity.setVelocity(new Vector(0, 0, 0));
}
}
public void remove() {
if (entity != null && !entity.isDead()) entity.remove();
entity = null;
active = false;
}
public ArmorStand getEntity() { return entity; }
public boolean isActive() { return active; }
public Location getSpawnLocation() { return spawnLocation; }
public double getDistanceTo(Player player) {
if (entity == null || entity.isDead()) return Double.MAX_VALUE;
if (!entity.getWorld().equals(player.getWorld())) return Double.MAX_VALUE;
return entity.getLocation().distance(player.getLocation());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
package de.fussball.plugin.game;
import de.fussball.plugin.Fussball;
import de.fussball.plugin.arena.Arena;
import de.fussball.plugin.utils.MessageUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
public class GameManager {
private final Fussball plugin;
private final Map<String, Game> games = new HashMap<>();
// Warteschlange pro Arena: FIFO-Queue mit Spieler-UUIDs
private final Map<String, Queue<UUID>> queues = new HashMap<>();
public GameManager(Fussball plugin) { this.plugin = plugin; }
// ── Spiel-Verwaltung ────────────────────────────────────────────────────
public Game createGame(Arena arena) {
String key = arena.getName().toLowerCase();
if (games.containsKey(key)) return games.get(key);
Game game = new Game(plugin, arena);
games.put(key, game);
return game;
}
public Game getGame(String arenaName) { return games.get(arenaName.toLowerCase()); }
public void removeGame(String arenaName) {
games.remove(arenaName.toLowerCase());
// Warteschlange abarbeiten nächster Spieler darf joinen
processQueue(arenaName);
}
public Collection<Game> getAllGames() { return games.values(); }
public Game getPlayerGame(Player player) {
for (Game game : games.values()) if (game.isInGame(player)) return game;
return null;
}
public boolean isInGame(Player player) { return getPlayerGame(player) != null; }
/** Gibt zurück ob der Spieler als Zuschauer in einem Spiel sitzt */
public Game getSpectatorGame(Player player) {
for (Game game : games.values()) if (game.isSpectator(player)) return game;
return null;
}
public boolean isInAnyGame(Player player) {
return isInGame(player) || getSpectatorGame(player) != null;
}
public void stopAllGames() {
for (Game game : new ArrayList<>(games.values())) game.endGame(null);
games.clear();
queues.clear();
}
// ── Warteschlange ───────────────────────────────────────────────────────
/** Spieler zur Warteschlange einer Arena hinzufügen */
public void addToQueue(String arenaName, Player player) {
String key = arenaName.toLowerCase();
queues.computeIfAbsent(key, k -> new LinkedList<>());
Queue<UUID> queue = queues.get(key);
if (queue.contains(player.getUniqueId())) {
player.sendMessage(MessageUtil.warn("Du bist bereits in der Warteschlange für §e" + arenaName + "§e!"));
return;
}
queue.add(player.getUniqueId());
int pos = getQueuePosition(arenaName, player);
player.sendMessage(MessageUtil.info("Du bist in der Warteschlange für §e" + arenaName + " §7(Position §e" + pos + "§7)"));
}
/** Spieler aus der Warteschlange entfernen */
public void removeFromQueue(String arenaName, Player player) {
Queue<UUID> queue = queues.get(arenaName.toLowerCase());
if (queue != null) queue.remove(player.getUniqueId());
}
/** Entfernt einen Spieler aus ALLEN Warteschlangen */
public void removeFromAllQueues(Player player) {
for (Queue<UUID> q : queues.values()) q.remove(player.getUniqueId());
}
public int getQueuePosition(String arenaName, Player player) {
Queue<UUID> queue = queues.get(arenaName.toLowerCase());
if (queue == null) return -1;
int pos = 1;
for (UUID uuid : queue) {
if (uuid.equals(player.getUniqueId())) return pos;
pos++;
}
return -1;
}
public int getQueueSize(String arenaName) {
Queue<UUID> queue = queues.get(arenaName.toLowerCase());
return queue == null ? 0 : queue.size();
}
/**
* Wenn ein Spiel endet oder Platz frei wird → nächsten Spieler aus der
* Warteschlange in das (neue) Spiel einladen.
*/
private void processQueue(String arenaName) {
String key = arenaName.toLowerCase();
Queue<UUID> queue = queues.get(key);
if (queue == null || queue.isEmpty()) return;
Arena arena = plugin.getArenaManager().getArena(arenaName);
if (arena == null || !arena.isSetupComplete()) return;
// Warte kurz, bis das alte Spiel vollständig aufgeräumt ist
org.bukkit.scheduler.BukkitRunnable task = new org.bukkit.scheduler.BukkitRunnable() {
public void run() {
Queue<UUID> q = queues.get(key);
if (q == null || q.isEmpty()) return;
UUID next = q.poll();
if (next == null) return;
Player p = Bukkit.getPlayer(next);
if (p == null || !p.isOnline()) { run(); return; } // überspringe Offline-Spieler
if (isInAnyGame(p)) return;
p.sendMessage(MessageUtil.success("§e⚽ Dein Platz in §e" + arenaName + " §aist frei! Du wirst hinzugefügt..."));
createGame(arena).addPlayer(p);
}
};
task.runTaskLater(plugin, 120L); // 6s nach Spielende
}
}

View File

@@ -0,0 +1,12 @@
package de.fussball.plugin.game;
public enum GameState {
WAITING, // Warte auf Spieler
STARTING, // Countdown läuft
RUNNING, // Spiel läuft
GOAL, // Tor-Pause
HALFTIME, // Halbzeit-Pause
OVERTIME, // Verlängerung
PENALTY, // Elfmeterschießen
ENDING // Spiel beendet
}

View File

@@ -0,0 +1,22 @@
package de.fussball.plugin.game;
import org.bukkit.ChatColor;
public enum Team {
RED("Rot", ChatColor.RED, "§c"),
BLUE("Blau", ChatColor.BLUE, "§9");
private final String displayName;
private final ChatColor color;
private final String colorCode;
Team(String displayName, ChatColor color, String colorCode) {
this.displayName = displayName;
this.color = color;
this.colorCode = colorCode;
}
public String getDisplayName() { return displayName; }
public ChatColor getColor() { return color; }
public String getColorCode() { return colorCode; }
public Team getOpponent() { return this == RED ? BLUE : RED; }
}

View File

@@ -0,0 +1,222 @@
package de.fussball.plugin.listeners;
import de.fussball.plugin.Fussball;
import de.fussball.plugin.game.Ball;
import de.fussball.plugin.game.Game;
import de.fussball.plugin.game.GameState;
import de.fussball.plugin.game.Team;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.entity.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Verwaltet Ball-Interaktionen:
* • Normaler Schuss Rechtsklick auf Ball
* • Aufgeladener Schuss Shift halten → loslassen
* • Torwart Rechtsklick = halten, nochmal Rechtsklick/Shift = werfen
* • Foul-Erkennung Spieler trifft gegnerischen Spieler
*/
public class BallListener implements Listener {
private final Fussball plugin;
// UUID → Timestamp Lade-Beginn (ms)
private final Map<UUID, Long> chargeMap = new HashMap<>();
public BallListener(Fussball plugin) { this.plugin = plugin; }
// ── Rechtsklick auf Ball ─────────────────────────────────────────────────
@EventHandler
public void onInteract(PlayerInteractAtEntityEvent event) {
if (!(event.getRightClicked() instanceof ArmorStand stand)) return;
Player player = event.getPlayer();
Game game = plugin.getGameManager().getPlayerGame(player);
if (game == null) return;
Ball ball = game.getBall();
if (ball == null || !stand.equals(ball.getEntity())) return;
event.setCancelled(true);
if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) return;
// ── Torwart hält Ball bereits → werfen ──────────────────────────────
if (ball.isHeld() && player.equals(ball.getHoldingPlayer())) {
double power = plugin.getConfig().getDouble("gameplay.goalkeeper-throw-power", 1.8);
game.setLastKicker(player.getUniqueId());
ball.throwBall(player, power);
game.clearThrowIn();
game.broadcastAll(plugin.getConfig().getString("messages.goalkeeper-throw",
"§6TW §f{player} §7wirft den Ball!").replace("{player}", player.getName()));
return;
}
// ── Torwart greift Ball (noch nicht haltend) ────────────────────────
if (game.isGoalkeeper(player) && !player.isSneaking()) {
if (!plugin.getConfig().getBoolean("gameplay.foul-detection-enabled", true)) {
// Falls Torwart-Mechanik in Config deaktiviert → normaler Schuss
} else if (ball.getDistanceTo(player) <= plugin.getConfig().getDouble("gameplay.goalkeeper-hold-range", 2.5)) {
if (game.isInOwnHalf(player)) {
if (game.getThrowInTeam() == null || game.getThrowInTeam() == game.getTeam(player)) {
game.clearThrowIn();
game.setLastKicker(player.getUniqueId());
ball.holdBall(player);
game.broadcastAll(plugin.getConfig().getString("messages.goalkeeper-hold",
"§6TW §f{player} §7hält den Ball!").replace("{player}", player.getName()));
return;
}
} else {
player.sendMessage(plugin.getConfig().getString("messages.goalkeeper-no-hold",
"§cDu kannst den Ball nur in deiner eigenen Hälfte halten!"));
// Fällt durch zum normalen Schuss
}
}
}
// ── Normaler Schuss (kein Shift) ────────────────────────────────────
if (player.isSneaking()) return;
if (game.getThrowInTeam() != null && game.getThrowInTeam() != game.getTeam(player)) {
player.sendMessage("§cDu bist nicht dran! Warte auf den Einwurf des anderen Teams.");
return;
}
game.clearThrowIn();
game.setLastKicker(player.getUniqueId());
ball.kick(player);
}
// ── Linksklick / Schaden am Ball ────────────────────────────────────────
@EventHandler
public void onDamage(EntityDamageByEntityEvent event) {
if (!(event.getEntity() instanceof ArmorStand)) return;
if (!(event.getDamager() instanceof Player player)) return;
Game game = plugin.getGameManager().getPlayerGame(player);
if (game == null) return;
Ball ball = game.getBall();
if (ball == null || !ball.getEntity().equals(event.getEntity())) return;
event.setCancelled(true);
if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) return;
if (player.isSneaking()) return;
if (game.getThrowInTeam() != null && game.getThrowInTeam() != game.getTeam(player)) {
player.sendMessage("§cDu bist nicht dran!");
return;
}
game.clearThrowIn();
game.setLastKicker(player.getUniqueId());
ball.kick(player);
}
// ── Foul-Erkennung (Spieler trifft gegnerischen Spieler) ─────────────────
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerHitPlayer(EntityDamageByEntityEvent event) {
if (!(event.getEntity() instanceof Player victim)) return;
Player damager;
if (event.getDamager() instanceof Player p) {
damager = p;
} else if (event.getDamager() instanceof Projectile proj && proj.getShooter() instanceof Player p) {
damager = p;
} else return;
if (!plugin.getConfig().getBoolean("gameplay.foul-detection-enabled", true)) return;
Game game = plugin.getGameManager().getPlayerGame(damager);
if (game == null || !game.isInGame(victim)) return;
if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) return;
Team damagerTeam = game.getTeam(damager);
Team victimTeam = game.getTeam(victim);
if (damagerTeam == null || victimTeam == null || damagerTeam == victimTeam) return;
// Schaden canceln (kein PvP im Fußball)
event.setCancelled(true);
// Foul registrieren
double damage = event.getFinalDamage();
boolean directRedCard = damage >= 8.0; // Sehr harter Schlag → direkt Rot
game.handleFoul(damager, victim, victim.getLocation(), directRedCard);
}
// ── Aufgeladener Schuss (Shift-System) ──────────────────────────────────
@EventHandler
public void onSneak(PlayerToggleSneakEvent event) {
Player player = event.getPlayer();
Game game = plugin.getGameManager().getPlayerGame(player);
if (game == null) return;
if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) {
chargeMap.remove(player.getUniqueId()); return;
}
Ball ball = game.getBall();
// ── Torwart hält Ball → Shift loslassen = werfen ────────────────────
if (!event.isSneaking() && ball != null && ball.isHeld() && player.equals(ball.getHoldingPlayer())) {
double power = plugin.getConfig().getDouble("gameplay.goalkeeper-throw-power", 1.8);
game.setLastKicker(player.getUniqueId());
ball.throwBall(player, power);
game.clearThrowIn();
game.broadcastAll(plugin.getConfig().getString("messages.goalkeeper-throw",
"§6TW §f{player} §7wirft den Ball!").replace("{player}", player.getName()));
return;
}
if (event.isSneaking()) {
// ── Shift gedrückt: Laden beginnen ──────────────────────────────
if (ball == null || ball.getDistanceTo(player) > 2.5) return;
chargeMap.put(player.getUniqueId(), System.currentTimeMillis());
startChargeDisplay(player, game);
} else {
// ── Shift losgelassen: Schuss abfeuern ──────────────────────────
Long startTime = chargeMap.remove(player.getUniqueId());
if (startTime == null) return;
if (ball == null || ball.getDistanceTo(player) > 2.5) return;
if (game.getThrowInTeam() != null && game.getThrowInTeam() != game.getTeam(player)) {
player.sendMessage("§cDu bist nicht dran!");
return;
}
game.clearThrowIn();
long held = System.currentTimeMillis() - startTime;
double power = Math.min(held / 1500.0, 1.0);
game.setLastKicker(player.getUniqueId());
ball.chargedKick(player, power);
}
}
private void startChargeDisplay(Player player, Game game) {
new BukkitRunnable() {
@Override
public void run() {
if (!player.isSneaking() || !chargeMap.containsKey(player.getUniqueId())) { cancel(); return; }
if (game.getState() != GameState.RUNNING && game.getState() != GameState.OVERTIME) {
chargeMap.remove(player.getUniqueId()); cancel(); return;
}
Ball ball = game.getBall();
if (ball == null || ball.getDistanceTo(player) > 2.5) {
chargeMap.remove(player.getUniqueId()); cancel(); return;
}
long elapsed = System.currentTimeMillis() - chargeMap.get(player.getUniqueId());
double power = Math.min(elapsed / 1500.0, 1.0);
int filled = (int) Math.round(power * 10);
String color = power < 0.4 ? "§a" : power < 0.8 ? "§e" : "§c";
String bar = color + "".repeat(filled) + "§8" + "".repeat(10 - filled);
player.spigot().sendMessage(ChatMessageType.ACTION_BAR,
TextComponent.fromLegacyText("§e⚽ Schuss-Power: " + bar + " §f" + (int)(power*100) + "%"));
if (power >= 1.0) {
chargeMap.remove(player.getUniqueId());
game.setLastKicker(player.getUniqueId());
ball.chargedKick(player, 1.0);
cancel();
}
}
}.runTaskTimer(plugin, 1L, 2L);
}
}

View File

@@ -0,0 +1,22 @@
package de.fussball.plugin.listeners;
import de.fussball.plugin.Fussball;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
public class BlockListener implements Listener {
private final Fussball plugin;
public BlockListener(Fussball plugin) { this.plugin = plugin; }
@EventHandler
public void onBreak(BlockBreakEvent event) {
if (plugin.getGameManager().isInGame(event.getPlayer())) event.setCancelled(true);
}
@EventHandler
public void onPlace(BlockPlaceEvent event) {
if (plugin.getGameManager().isInGame(event.getPlayer())) event.setCancelled(true);
}
}

View File

@@ -0,0 +1,89 @@
package de.fussball.plugin.listeners;
import de.fussball.plugin.Fussball;
import de.fussball.plugin.game.Game;
import de.fussball.plugin.game.Team;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.player.*;
public class PlayerListener implements Listener {
private final Fussball plugin;
public PlayerListener(Fussball plugin) { this.plugin = plugin; }
/** Spieler disconnected → aus dem Spiel entfernen */
@EventHandler
public void onQuit(PlayerQuitEvent event) {
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);
}
/** Spieler stirbt → heilen und zurückteleportieren statt Tod */
@EventHandler
public void onDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
Game game = plugin.getGameManager().getPlayerGame(player);
if (game == null) return;
if (player.getHealth() - event.getFinalDamage() <= 0) {
event.setCancelled(true);
player.setHealth(20.0);
Team team = game.getTeam(player);
if (team == Team.RED) player.teleport(game.getArena().getRedSpawn());
else if (team == Team.BLUE) player.teleport(game.getArena().getBlueSpawn());
}
}
/** Hunger deaktivieren */
@EventHandler
public void onFood(FoodLevelChangeEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
if (plugin.getGameManager().isInAnyGame(player)) event.setCancelled(true);
}
/** Items droppen verhindern */
@EventHandler
public void onDrop(PlayerDropItemEvent event) {
if (plugin.getGameManager().isInAnyGame(event.getPlayer())) event.setCancelled(true);
}
/** Items aufheben verhindern */
@EventHandler
public void onPickup(EntityPickupItemEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
if (plugin.getGameManager().isInAnyGame(player)) event.setCancelled(true);
}
/**
* 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).
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
Game game = plugin.getGameManager().getPlayerGame(player);
if (game == null) return;
event.setCancelled(true);
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);
}
}

View File

@@ -0,0 +1,216 @@
package de.fussball.plugin.listeners;
import de.fussball.plugin.Fussball;
import de.fussball.plugin.arena.Arena;
import de.fussball.plugin.game.Game;
import de.fussball.plugin.utils.MessageUtil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* Fußball-Join-Schilder
*
* Format beim Beschriften (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.
*/
public class SignListener implements Listener {
private static final String TAG = "[Fussball]";
private static final String TAG_FORMATTED = "§8[§e⚽§8]";
private final Fussball plugin;
// Location → ArenaName
private final Map<String, String> signs = new HashMap<>(); // key = "world;x;y;z"
private final File signFile;
private FileConfiguration signConfig;
public SignListener(Fussball plugin) {
this.plugin = plugin;
this.signFile = new File(plugin.getDataFolder(), "signs.yml");
loadSigns();
}
// ── Persistenz ──────────────────────────────────────────────────────────
private void loadSigns() {
if (!signFile.exists()) {
try { signFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); }
}
signConfig = YamlConfiguration.loadConfiguration(signFile);
signs.clear();
if (signConfig.contains("signs")) {
for (String key : signConfig.getConfigurationSection("signs").getKeys(false)) {
String arenaName = signConfig.getString("signs." + key);
signs.put(key, arenaName);
}
}
plugin.getLogger().info("[Fussball] " + signs.size() + " Schilder geladen.");
}
private void saveSigns() {
signConfig.set("signs", null);
for (Map.Entry<String, String> e : signs.entrySet()) {
signConfig.set("signs." + e.getKey(), e.getValue());
}
try { signConfig.save(signFile); } catch (IOException e) { e.printStackTrace(); }
}
private String locKey(Location l) {
return l.getWorld().getName() + ";" + l.getBlockX() + ";" + l.getBlockY() + ";" + l.getBlockZ();
}
// ── 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;
Player player = event.getPlayer();
if (!player.hasPermission("fussball.admin")) {
player.sendMessage(MessageUtil.error("Keine Berechtigung für Fußball-Schilder!"));
event.setCancelled(true);
return;
}
String arenaName = event.getLine(1);
if (arenaName == null || arenaName.isEmpty()) {
player.sendMessage(MessageUtil.error("Zeile 2 muss den Arena-Namen enthalten!"));
event.setCancelled(true);
return;
}
Arena arena = plugin.getArenaManager().getArena(arenaName);
if (arena == null) {
player.sendMessage(MessageUtil.error("Arena §e" + arenaName + " §cnicht gefunden!"));
event.setCancelled(true);
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!"));
}
/** 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;
String key = locKey(block.getLocation());
if (!signs.containsKey(key)) return;
event.setCancelled(true);
Player player = event.getPlayer();
String arenaName = signs.get(key);
Arena arena = plugin.getArenaManager().getArena(arenaName);
if (arena == null) {
player.sendMessage(MessageUtil.error("Arena nicht gefunden!"));
signs.remove(key);
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);
}
/** Schild abbauen → aus Liste entfernen */
@EventHandler
public void onBreak(BlockBreakEvent event) {
String key = locKey(event.getBlock().getLocation());
if (signs.containsKey(key)) {
signs.remove(key);
saveSigns();
event.getPlayer().sendMessage(MessageUtil.info("Fußball-Schild entfernt."));
}
}
// ── Ö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;
Location loc = keyToLocation(entry.getKey());
if (loc == null) continue;
Block block = loc.getBlock();
if (!(block.getState() instanceof Sign sign)) continue;
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");
sign.update();
}
}
private String buildStatusLine(Arena arena) {
Game game = plugin.getGameManager().getGame(arena.getName());
if (game == null) return "§a● §70/" + arena.getMaxPlayers();
int players = game.getAllPlayers().size();
String dot = switch (game.getState()) {
case WAITING -> "§a●";
case STARTING -> "§e●";
case RUNNING, GOAL -> "§c●";
case HALFTIME, OVERTIME,
PENALTY -> "§6●";
case ENDING -> "§7●";
};
return dot + " §7" + players + "/" + arena.getMaxPlayers();
}
private Location keyToLocation(String key) {
try {
String[] p = key.split(";");
org.bukkit.World world = Bukkit.getWorld(p[0]);
if (world == null) return null;
return new Location(world, Integer.parseInt(p[1]), Integer.parseInt(p[2]), Integer.parseInt(p[3]));
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,69 @@
package de.fussball.plugin.placeholders;
import de.fussball.plugin.Fussball;
import de.fussball.plugin.stats.StatsManager;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
/**
* PlaceholderAPI-Erweiterung für das Fußball-Plugin.
*
* Verfügbare Platzhalter:
* %fussball_goals% - Tore des Spielers (gesamt)
* %fussball_kicks% - Schüsse des Spielers (gesamt)
* %fussball_wins% - Siege des Spielers
* %fussball_losses% - Niederlagen des Spielers
* %fussball_draws% - Unentschieden des Spielers
* %fussball_games% - Gespielte Spiele des Spielers
* %fussball_winrate% - Siegquote in Prozent (z.B. "67.5")
* %fussball_ingame% - "true" / "false" ob Spieler gerade im Spiel ist
* %fussball_arena% - Name der aktuellen Arena (oder "")
* %fussball_score% - Aktueller Spielstand (z.B. "2 : 1") oder ""
*/
public class FussballPlaceholders extends PlaceholderExpansion {
private final Fussball plugin;
public FussballPlaceholders(Fussball plugin) {
this.plugin = plugin;
}
@Override
public String getIdentifier() { return "fussball"; }
@Override
public String getAuthor() { return "M_Viper"; }
@Override
public String getVersion() { return plugin.getDescription().getVersion(); }
@Override
public boolean persist() { return true; }
@Override
public String onPlaceholderRequest(Player player, String identifier) {
if (player == null) return "";
StatsManager stats = plugin.getStatsManager();
StatsManager.PlayerStats s = stats.getStats(player.getUniqueId());
return switch (identifier.toLowerCase()) {
case "goals" -> String.valueOf(s.goals);
case "kicks" -> String.valueOf(s.kicks);
case "wins" -> String.valueOf(s.wins);
case "losses" -> String.valueOf(s.losses);
case "draws" -> String.valueOf(s.draws);
case "games" -> String.valueOf(s.games);
case "winrate" -> String.format("%.1f", s.getWinRate());
case "ingame" -> String.valueOf(plugin.getGameManager().isInGame(player));
case "arena" -> {
var game = plugin.getGameManager().getPlayerGame(player);
yield game != null ? game.getArena().getName() : "";
}
case "score" -> {
var game = plugin.getGameManager().getPlayerGame(player);
yield game != null ? game.getRedScore() + " : " + game.getBlueScore() : "";
}
default -> null;
};
}
}

View File

@@ -0,0 +1,137 @@
package de.fussball.plugin.scoreboard;
import de.fussball.plugin.game.Game;
import de.fussball.plugin.game.GameState;
import de.fussball.plugin.game.Team;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.*;
import java.util.*;
public class FussballScoreboard {
private final Game game;
private final Map<UUID, Scoreboard> boards = new HashMap<>();
public FussballScoreboard(Game game) { this.game = game; }
// ── Scoreboard geben / entfernen ─────────────────────────────────────────
public void give(Player player) {
Scoreboard board = Bukkit.getScoreboardManager().getNewScoreboard();
Objective obj = board.registerNewObjective("fussball", "dummy", "§e§l⚽ FUSSBALL");
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
// Team-Namensschilder registrieren
registerTeamTags(board);
boards.put(player.getUniqueId(), board);
player.setScoreboard(board);
update(player, board);
}
public void remove(Player player) {
boards.remove(player.getUniqueId());
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
}
public void updateAll() {
for (UUID uuid : game.getAllPlayers()) {
Player p = Bukkit.getPlayer(uuid);
if (p != null && boards.containsKey(uuid)) {
updateTeamTags(boards.get(uuid)); // Team-Tags aktuell halten
update(p, boards.get(uuid));
}
}
// Auch Zuschauer-Scoreboards updaten
for (UUID uuid : game.getSpectators()) {
Player p = Bukkit.getPlayer(uuid);
if (p != null && boards.containsKey(uuid)) update(p, boards.get(uuid));
}
}
// ── Anzeige ──────────────────────────────────────────────────────────────
private void update(Player player, Scoreboard board) {
Objective obj = board.getObjective("fussball");
if (obj == null) return;
for (String entry : board.getEntries()) board.resetScores(entry);
int l = 14;
set(obj, "§r", l--);
set(obj, "§c▶ ROT §f" + game.getRedScore() + " §7: §f" + game.getBlueScore() + " §9BLAU ◀", l--);
set(obj, "§r§r", l--);
// Zeit / Status-Zeile
switch (game.getState()) {
case RUNNING, GOAL -> {
int m = game.getTimeLeft() / 60, s = game.getTimeLeft() % 60;
String half = game.isSecondHalf() ? "§72. HZ" : "§71. HZ";
set(obj, "§e⏱ " + String.format("%02d:%02d", m, s) + " " + half, l--);
}
case HALFTIME -> set(obj, "§6⏸ HALBZEIT", l--);
case OVERTIME -> {
int m = game.getTimeLeft() / 60, s = game.getTimeLeft() % 60;
set(obj, "§e⏱ " + String.format("%02d:%02d", m, s) + " §6VL", l--);
}
case PENALTY -> set(obj, "§c⚽ ELFMETER R" + game.getPenaltyRedGoals() + ":B" + game.getPenaltyBlueGoals(), l--);
case STARTING -> set(obj, "§e⏱ §6Startet gleich...", l--);
default -> set(obj, "§7⏳ Warte auf Spieler...", l--);
}
set(obj, "§r§r§r", l--);
// Team
Team team = game.getTeam(player);
if (team != null) {
String c = team == Team.RED ? "§c" : "§9";
set(obj, "§7Dein Team: " + c + team.getDisplayName(), l--);
} else if (game.isSpectator(player)) {
set(obj, "§7Rolle: §8Zuschauer", l--);
}
set(obj, "§7Spieler: §f" + game.getAllPlayers().size() + "/" + game.getArena().getMaxPlayers(), l--);
set(obj, "§r§r§r§r", l--);
set(obj, "§7Arena: §e" + game.getArena().getName(), l--);
set(obj, "§r§r§r§r§r", l--);
set(obj, "§6§lFußball-Plugin", l);
}
// ── Team-Namensschilder (farbige Namen über dem Kopf) ────────────────────
/** Registriert die Scoreboard-Teams auf dem übergebenen Board (1x bei give()) */
private void registerTeamTags(Scoreboard board) {
if (board.getTeam("fb_red") == null) {
org.bukkit.scoreboard.Team red = board.registerNewTeam("fb_red");
red.setPrefix("§c"); red.setColor(org.bukkit.ChatColor.RED);
}
if (board.getTeam("fb_blue") == null) {
org.bukkit.scoreboard.Team blue = board.registerNewTeam("fb_blue");
blue.setPrefix("§9"); blue.setColor(org.bukkit.ChatColor.BLUE);
}
}
/** Aktualisiert die Spieler-Einträge in den Teams (wenn jemand joint/geht) */
public void updateTeamTags(Scoreboard board) {
org.bukkit.scoreboard.Team red = board.getTeam("fb_red");
org.bukkit.scoreboard.Team blue = board.getTeam("fb_blue");
if (red == null || blue == null) return;
// Alle vorherigen Einträge leeren
for (String e : new HashSet<>(red.getEntries())) red.removeEntry(e);
for (String e : new HashSet<>(blue.getEntries())) blue.removeEntry(e);
// Neu befüllen
for (UUID uuid : game.getRedTeam()) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) red.addEntry(p.getName());
}
for (UUID uuid : game.getBlueTeam()) {
Player p = Bukkit.getPlayer(uuid);
if (p != null) blue.addEntry(p.getName());
}
}
private void set(Objective obj, String text, int value) {
obj.getScore(text).setScore(value);
}
}

View File

@@ -0,0 +1,31 @@
package de.fussball.plugin.utils;
import org.bukkit.ChatColor;
public class MessageUtil {
public static final String PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.YELLOW + "" + ChatColor.DARK_GRAY + "] " + ChatColor.GRAY;
public static final String ERROR_PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.RED + "" + ChatColor.DARK_GRAY + "] " + ChatColor.RED;
public static final String SUCCESS_PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.GREEN + "" + ChatColor.DARK_GRAY + "] " + ChatColor.GREEN;
public static final String WARN_PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.YELLOW + "!" + ChatColor.DARK_GRAY + "] " + ChatColor.YELLOW;
public static String error(String msg) {
return ERROR_PREFIX + msg;
}
public static String success(String msg) {
return SUCCESS_PREFIX + msg;
}
public static String warn(String msg) {
return WARN_PREFIX + msg;
}
public static String info(String msg) {
return PREFIX + msg;
}
public static String header(String title) {
return ChatColor.GRAY.toString() + ChatColor.STRIKETHROUGH + "-----------------" + ChatColor.RESET + " " + ChatColor.YELLOW + title + ChatColor.GRAY + " " + ChatColor.STRIKETHROUGH + "-----------------";
}
}

View File

@@ -0,0 +1,31 @@
package de.fussball.plugin.utils;
import de.fussball.plugin.Fussball;
/**
* Liest alle Broadcast-Texte aus config.yml.
* Platzhalter: {player}, {team}, {score}, {time}, {reason}, {n}
*/
public class Messages {
private static Fussball plugin;
public static void init(Fussball p) { plugin = p; }
/** Liest eine Nachricht aus config.yml → messages.<key> */
public static String get(String key) {
if (plugin == null) return "§c[MSG:" + key + "]";
return plugin.getConfig().getString("messages." + key, "§c[MSG:" + key + "]");
}
/** Liest Nachricht und ersetzt Platzhalter: replace("player", "Hans", "team", "Rot", ...) */
public static String get(String key, String... pairs) {
String msg = get(key);
for (int i = 0; i + 1 < pairs.length; i += 2) {
msg = msg.replace("{" + pairs[i] + "}", pairs[i + 1]);
}
return msg;
}
public static String prefix() { return get("prefix"); }
}