Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0085ba5bd | |||
| e10f1313bd | |||
| 960290dfa8 | |||
| 3f6d91cdc7 | |||
| 54414cbc2e | |||
| 885fd9792d | |||
| a6d17f4d64 | |||
| 22c5837455 | |||
| 266268bd0b | |||
| c837c963af | |||
| d7ed7b00c9 |
114
README.md
114
README.md
@@ -23,6 +23,7 @@ Ein professionelles Fußball-Plugin für Spigot/Paper 1.21+ mit echtem Ball-Phys
|
||||
| 🙋 Teamwahl | Spieler können per `/fb team rot|blau` ihr Wunsch-Team wählen (vor Spielstart) |
|
||||
| 👕 Team-Rüstung | Farbige Leder-Rüstung mit **Trikot-Nummer** (#1, #2, …) auf dem Brustpanzer |
|
||||
| 🥅 Tor-Erkennung | Region-basierte Tore mit Trajectory-Check (Schritt-für-Schritt Bahnverfolgung) |
|
||||
| 💠 Tor-Beacons | Beacon-Farbe am linken/rechten Tor zeigt das verteidigende Team (Rot/Blau) und wechselt automatisch zur Halbzeit mit den Seiten |
|
||||
| 🎉 Tor-Effekte | Feuerwerk, Titel, Action-Bar Tor-Replay, Sound |
|
||||
| 🎶 Stadionatmosphäre | Jubel-Sounds & mehrfache Feuerwerke beim Tor; Enttäuschungs-Sound für das andere Team |
|
||||
| 📊 Scoreboard | Live-Spielstand, Zeit, Halbzeit-Anzeige, Ballbesitz, farbige Spielernamen über Kopf |
|
||||
@@ -48,7 +49,8 @@ Ein professionelles Fußball-Plugin für Spigot/Paper 1.21+ mit echtem Ball-Phys
|
||||
| 🏟 Strafraum | Auto-berechnet aus Tor-Koordinaten oder manuell setzbar |
|
||||
| 📋 Matchbericht | Tore, Karten, Fouls, Abseits, Ballbesitz und MVP am Spielende |
|
||||
| 🏆 MVP-System | Bester Torschütze wird nach dem Spiel bekannt gegeben |
|
||||
| 📈 Persistente Stats | Tore, Eigentore, Vorlagen, Schüsse, Siege, Niederlagen, Siegquote |
|
||||
| 📈 Persistente Stats | Tore, Eigentore, Vorlagen, Schüsse, Siege, Niederlagen, Siegquote; per Admin-Befehl resetbar |
|
||||
| 🪄 Hologramme | Platzierbare Ingame-Hologramme für Top-Tore, Top-Siege und Live-Matchstand; Texte ingame bearbeitbar, Standard-Farben über `config.yml` anpassbar |
|
||||
| 📋 Match-History | Letzte 50 Spiele dauerhaft gespeichert, abrufbar per `/fb history` |
|
||||
| 🔢 Warteschlange | Automatische Queue wenn Arena voll; nächster Spieler rückt nach |
|
||||
| 👀 Zuschauer-Modus | Sichtbarer Zuschauer-Bereich außerhalb des Feldes mit BossBar und Scoreboard |
|
||||
@@ -164,11 +166,93 @@ Alle Pflichtfelder müssen **§a✔** zeigen. Erst dann ist die Arena spielberei
|
||||
| `/fb setgk <arena> <spieler>` | Torwart eines laufenden Spiels manuell neu zuweisen |
|
||||
| `/fb dropball <arena>` | Schiedsrichterball – Ball neutral spawnen, beide Teams dürfen spielen |
|
||||
| `/fb debug <arena>` | Tor-/Feld-Regionen, Ball-Position und Aus-Seite debuggen |
|
||||
| `/fb stats reset <spieler\|all>` | Einzelne Spieler-Stats oder alle Statistiken zurücksetzen |
|
||||
| `/fb hologram set <arena> goals\|wins\|match` | Hologramm für eine Arena an deiner aktuellen Position erstellen |
|
||||
| `/fb hologram remove` | Nächstes Hologramm im 5-Block-Radius entfernen |
|
||||
| `/fb hologram delete <arena> goals\|wins\|match` | Hologramm einer Arena gezielt löschen |
|
||||
| `/fb hologram text <arena> goals\|wins\|match <zeile> <text>` | Einzelne Hologramm-Zeilen bearbeiten |
|
||||
| `/fb hologram textpreview <arena> goals\|wins\|match` | Aktuell gespeicherte Hologramm-Zeilen anzeigen |
|
||||
| `/fb hologram textreset <arena> goals\|wins\|match` | Standard-Text eines Hologramms wiederherstellen |
|
||||
| `/fb hologram list` | Alle Hologramme als `arena → typ` anzeigen |
|
||||
| `/fb hologram reload` | Hologramme sowie Hologramm-Farben aus `holograms.yml`/`config.yml` neu laden |
|
||||
|
||||
Alle Commands unterstützen **Tab-Completion** für Arena-Namen und Optionen.
|
||||
|
||||
---
|
||||
|
||||
## 🪄 Hologramme
|
||||
|
||||
Hologramme kannst du **direkt ingame** setzen: Stelle dich an die gewünschte Position und nutze den Befehl.
|
||||
|
||||
```
|
||||
/fb hologram set <arena> goals → Top-10 Torschützen
|
||||
/fb hologram set <arena> wins → Top-10 Gewinner
|
||||
/fb hologram set <arena> match → Großes Live-Hologramm (nur Spielstand + Zeit/Nachspielzeit)
|
||||
```
|
||||
|
||||
Beispiel für deine Arena:
|
||||
|
||||
```
|
||||
/fb hologram set allianz goals
|
||||
/fb hologram set allianz wins
|
||||
/fb hologram set allianz match
|
||||
```
|
||||
|
||||
Intern speichert das Plugin diese Hologramme getrennt pro Arena und Typ, also z. B. `allianz_goals`, `allianz_wins` und `allianz_match`.
|
||||
|
||||
Weitere Verwaltung:
|
||||
|
||||
```
|
||||
/fb hologram remove
|
||||
/fb hologram delete <arena> goals|wins|match
|
||||
/fb hologram text <arena> goals|wins|match <zeile> <text>
|
||||
/fb hologram textpreview <arena> goals|wins|match
|
||||
/fb hologram textreset <arena> goals|wins|match
|
||||
/fb hologram list
|
||||
/fb hologram reload
|
||||
```
|
||||
|
||||
**Hinweise:**
|
||||
- `<arena>` ist der Arena-Name, nicht die Hologramm-ID.
|
||||
- `match` zeigt bewusst nur Spielstand + Zeit und aktualisiert sich automatisch bei Toren, Zeitlauf und Nachspielzeit.
|
||||
- `goals`/`wins` lassen sich per Rechtsklick direkt am Hologramm umschalten.
|
||||
- Farben und Formatierungen für einzelne Zeilen kannst du über `&`-Codes im `text`-Befehl setzen, z. B. `&6&lTitel`, `&cRot`, `&oKursiv`.
|
||||
- Standard-Farbschema (Titel, Werte, Trennlinie, Match-Header usw.) stellst du zentral in `config.yml` unter `holograms:` ein und übernimmst es mit `/fb hologram reload`.
|
||||
- `text` bearbeitet immer genau eine Zeile. Fehlende Zeilen werden bei Bedarf automatisch ergänzt.
|
||||
- Mit `textreset` wird nur das gewählte Hologramm auf den Standardtext zurückgesetzt.
|
||||
|
||||
### Hologramm-Platzhalter
|
||||
|
||||
Für `goals` und `wins` stehen in bearbeitbaren Texten diese Platzhalter zur Verfügung:
|
||||
|
||||
```
|
||||
{title} → Standard-Überschrift des Hologramms
|
||||
{separator} → Trennlinie
|
||||
{entries} → Dynamische Top-10-Liste
|
||||
{toggle} → Hinweis zum Umschalten per Rechtsklick
|
||||
```
|
||||
|
||||
Für `match` stehen diese Platzhalter zur Verfügung:
|
||||
|
||||
```
|
||||
{header} → Live-Match Überschrift
|
||||
{separator} → Trennlinie
|
||||
{phase} → Aktuelle Spielphase
|
||||
{score} → Aktueller Spielstand
|
||||
{time} → Spielzeit oder Nachspielzeit
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
|
||||
```
|
||||
/fb hologram text allianz goals 1 &6&l⚽ Allianz Topscorer
|
||||
/fb hologram text allianz goals 2 &8{separator}
|
||||
/fb hologram text allianz goals 3 {entries}
|
||||
/fb hologram text allianz goals 4 &7{toggle}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Spielablauf
|
||||
|
||||
```
|
||||
@@ -392,6 +476,8 @@ Das Schild wird automatisch formatiert und mit Live-Status aktualisiert:
|
||||
```
|
||||
/fb stats → Eigene Statistiken
|
||||
/fb stats Notch → Statistiken von "Notch"
|
||||
/fb stats reset Notch → Statistiken von "Notch" zurücksetzen (Admin)
|
||||
/fb stats reset all → Alle Statistiken zurücksetzen (Admin)
|
||||
/fb top goals → Top 10 Torschützen
|
||||
/fb top wins → Top 10 nach Siegen (inkl. Siegquote)
|
||||
/fb top kicks → Top 10 nach Anzahl Schüssen
|
||||
@@ -412,7 +498,7 @@ Das Schild wird automatisch formatiert und mit Live-Status aktualisiert:
|
||||
| 📊 Gespielte Spiele | Gesamtanzahl Spiele |
|
||||
| 📈 Siegquote | Siege / Spiele × 100 % |
|
||||
|
||||
Alle Daten werden in `plugins/Fussball/stats.yml` dauerhaft gespeichert.
|
||||
Alle Daten werden in `plugins/Fussball/stats.yml` dauerhaft gespeichert und können über `/fb stats reset <spieler|all>` gezielt zurückgesetzt werden.
|
||||
|
||||
---
|
||||
|
||||
@@ -443,7 +529,7 @@ Wenn PlaceholderAPI installiert ist, sind folgende Platzhalter verfügbar:
|
||||
|
||||
| Permission | Beschreibung | Standard |
|
||||
|---|---|---|
|
||||
| `fussball.admin` | Alle Admin-Commands (create, delete, setup, stop, setgk, dropball, debug, Schilder, Global-Chat) | OP |
|
||||
| `fussball.admin` | Alle Admin-Commands inklusive Stats-Reset, Hologramm-Verwaltung, Schilder und Global-Chat | OP |
|
||||
| `fussball.play` | Spielen, Zuschauen, Stats anzeigen | Alle |
|
||||
|
||||
---
|
||||
@@ -494,6 +580,21 @@ gameplay:
|
||||
atmosphere:
|
||||
enabled: true
|
||||
goal-fireworks: 5 # Feuerwerke pro Tor (0 = deaktiviert)
|
||||
|
||||
holograms:
|
||||
goals-title-color: "&6&l" # Titel-Farbe Top-Tore
|
||||
goals-value-color: "&4" # Werte-Farbe Tore
|
||||
wins-title-color: "&2&l" # Titel-Farbe Top-Siege
|
||||
wins-value-color: "&2" # Werte-Farbe Siege
|
||||
name-color: "&0" # Spielername-Farbe
|
||||
label-color: "&8" # Labels (Tore/Siege/Trenner)
|
||||
separator-color: "&8&m" # Trennlinien-Stil
|
||||
toggle-color: "&8&o" # Umschalt-Hinweis
|
||||
match-header-color: "&e&l" # Match-Header
|
||||
match-score-red: "&c&l" # Score Rot
|
||||
match-score-blue: "&9&l" # Score Blau
|
||||
match-time-color: "&e" # Normale Spielzeit
|
||||
match-injury-color: "&c" # Nachspielzeit
|
||||
```
|
||||
|
||||
Alle `messages`-Einträge sind ebenfalls vollständig anpassbar. Platzhalter: `{player}`, `{team}`, `{score}`, `{time}`, `{reason}`, `{n}`, `{max}`.
|
||||
@@ -509,7 +610,7 @@ plugins/Fussball/
|
||||
├── signs.yml → Gespeicherte Join-/Zuschauer-Schilder (auto-generiert)
|
||||
├── stats.yml → Spieler-Statistiken (auto-generiert)
|
||||
├── matchhistory.yml → Letzte 50 Spiele (auto-generiert)
|
||||
└── holograms.yml → Statistik-Hologramme (auto-generiert)
|
||||
└── holograms.yml → Statistik-Hologramme inkl. benutzerdefinierter Texte (auto-generiert)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -519,6 +620,7 @@ plugins/Fussball/
|
||||
- **Feldgröße:** Mindestens **30×20 Blöcke** empfohlen (größer = mehr Spaß)
|
||||
- **Tore:** Ca. **5 Blöcke breit, 3 Blöcke hoch** – mindestens **2 Blöcke breit** da der Ball sonst stecken bleiben kann
|
||||
- **Tortiefe:** Mindestens **2 Blöcke tief** damit Tor-Erkennung sauber funktioniert
|
||||
- **Tor-Beacons (optional):** Platziere je einen Beacon nahe jedem Tor. Das Plugin setzt automatisch rotes/blaues Glas darüber und tauscht die Farben nach der Halbzeit beim Seitenwechsel.
|
||||
- **`fieldmin/fieldmax` immer setzen!** Sonst gibt es weder Aus-Erkennung noch Spieler-Feldgrenzenkontrolle
|
||||
- **`center` genau in die Feldmitte setzen** – wird für den Anstoß-Kreis verwendet
|
||||
- **`ballspawn` genau in die Mitte** – er ist gleichzeitig der Anstoßpunkt und der Spawn nach einem Tor
|
||||
@@ -549,6 +651,4 @@ Nach jedem Spiel wird automatisch ein Matchbericht ausgegeben:
|
||||
|
||||
---
|
||||
|
||||
**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten**
|
||||
|
||||
Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt.
|
||||
*Erstellt mit ❤️ von M_Viper — viel Spaß beim Fußballspielen! 🏆*
|
||||
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>de.fussball</groupId>
|
||||
<artifactId>Fussball</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.3</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>Fussball</name>
|
||||
<description>Ein vollständiges Fußball-Minigame für Minecraft</description>
|
||||
|
||||
Binary file not shown.
@@ -11,6 +11,7 @@ import de.fussball.plugin.hologram.HologramManager;
|
||||
import de.fussball.plugin.stats.MatchHistory;
|
||||
import de.fussball.plugin.stats.StatsManager;
|
||||
import de.fussball.plugin.utils.MessageUtil;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.*;
|
||||
@@ -81,6 +82,36 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
|
||||
case "stats" -> {
|
||||
if (args.length >= 2 && args[1].equalsIgnoreCase("reset")) {
|
||||
if (!sender.hasPermission("fussball.admin")) { sender.sendMessage(MessageUtil.error("Keine Berechtigung!")); return true; }
|
||||
if (args.length < 3) { sender.sendMessage(MessageUtil.error("Benutze: /fb stats reset <spieler|all>")); return true; }
|
||||
|
||||
if (args[2].equalsIgnoreCase("all") || args[2].equalsIgnoreCase("*")) {
|
||||
int removed = plugin.getStatsManager().resetAllStats();
|
||||
plugin.getHologramManager().refreshAll();
|
||||
sender.sendMessage(MessageUtil.success("Alle Statistiken zurückgesetzt! §7(" + removed + " Einträge)"));
|
||||
return true;
|
||||
}
|
||||
|
||||
Player onlineTarget = Bukkit.getPlayerExact(args[2]);
|
||||
UUID targetUuid = onlineTarget != null
|
||||
? onlineTarget.getUniqueId()
|
||||
: plugin.getStatsManager().findPlayerUuidByName(args[2]);
|
||||
if (targetUuid == null) {
|
||||
sender.sendMessage(MessageUtil.error("Keine gespeicherten Statistiken für §e" + args[2] + " §cgefunden!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!plugin.getStatsManager().resetStats(targetUuid)) {
|
||||
sender.sendMessage(MessageUtil.error("Statistiken von §e" + args[2] + " §ckonnten nicht zurückgesetzt werden."));
|
||||
return true;
|
||||
}
|
||||
|
||||
plugin.getHologramManager().refreshAll();
|
||||
sender.sendMessage(MessageUtil.success("Statistiken von §e" + args[2] + " §azurückgesetzt!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
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; }
|
||||
@@ -281,9 +312,9 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
|
||||
// ── Hologramm-Verwaltung ─────────────────────────────────────────
|
||||
// /fb hologram set <id> goals|wins|match – Hologramm erstellen
|
||||
// /fb hologram set <arena> goals|wins|match – Hologramm erstellen/verschieben
|
||||
// /fb hologram remove – Nächstes Hologramm (< 5 Blöcke) entfernen
|
||||
// /fb hologram delete <id> – Hologramm nach ID löschen
|
||||
// /fb hologram delete <arena> goals|wins|match – Hologramm gezielt löschen
|
||||
// /fb hologram reload – Alle Hologramme neu spawnen
|
||||
// /fb hologram list – Alle Hologramme anzeigen
|
||||
case "hologram", "holo" -> {
|
||||
@@ -291,9 +322,12 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
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|match §7– Hologramm setzen");
|
||||
player.sendMessage("§e/fb hologram set <arena> goals|wins|match §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 delete <arena> goals|wins|match §7– Gezielt löschen");
|
||||
player.sendMessage("§e/fb hologram text <arena> goals|wins|match <zeile> <text> §7– Textzeile anpassen");
|
||||
player.sendMessage("§e/fb hologram textpreview <arena> goals|wins|match §7– Aktuelles Textlayout ansehen");
|
||||
player.sendMessage("§e/fb hologram textreset <arena> goals|wins|match §7– Standardtext wiederherstellen");
|
||||
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");
|
||||
@@ -303,22 +337,26 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
switch (args[1].toLowerCase()) {
|
||||
case "set" -> {
|
||||
if (args.length < 4) {
|
||||
player.sendMessage(MessageUtil.error("Benutze: /fb hologram set <id> goals|wins|match"));
|
||||
player.sendMessage(MessageUtil.error("Benutze: /fb hologram set <arena> goals|wins|match"));
|
||||
return true;
|
||||
}
|
||||
Arena arena = plugin.getArenaManager().getArena(args[2]);
|
||||
if (arena == null) {
|
||||
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
|
||||
return true;
|
||||
}
|
||||
String id = args[2];
|
||||
FussballHologram.HoloType type = switch (args[3].toLowerCase()) {
|
||||
case "wins", "siege" -> FussballHologram.HoloType.WINS;
|
||||
case "match", "live", "game" -> FussballHologram.HoloType.MATCH;
|
||||
default -> FussballHologram.HoloType.GOALS;
|
||||
};
|
||||
plugin.getHologramManager().createHologram(id, player.getLocation(), type);
|
||||
String holoLabel = switch (type) {
|
||||
case WINS -> "Top-10-Siege";
|
||||
case MATCH -> "Live-Match";
|
||||
default -> "Top-10-Tore";
|
||||
};
|
||||
player.sendMessage(MessageUtil.success("§e" + id + " §a(" + holoLabel + ") Hologramm gesetzt!"));
|
||||
String id = buildHologramId(arena.getName(), type);
|
||||
plugin.getHologramManager().removeHologram(id);
|
||||
if (!plugin.getHologramManager().createHologram(id, player.getLocation(), type)) {
|
||||
player.sendMessage(MessageUtil.error("Konnte das Hologramm für Arena §e" + arena.getName() + " §cund Typ §e" + hologramTypeName(type) + " §cnicht setzen."));
|
||||
return true;
|
||||
}
|
||||
player.sendMessage(MessageUtil.success("Hologramm §e" + hologramTypeName(type) + " §afür Arena §e" + arena.getName() + " §agesetzt!"));
|
||||
if (type == FussballHologram.HoloType.MATCH) {
|
||||
player.sendMessage("§7§oLive-Match-Hologramm aktualisiert sich automatisch bei Toren und Nachspielzeit.");
|
||||
} else {
|
||||
@@ -334,13 +372,124 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
}
|
||||
case "delete" -> {
|
||||
if (args.length < 3) { player.sendMessage(MessageUtil.error("Benutze: /fb hologram delete <id>")); return true; }
|
||||
if (args.length >= 4) {
|
||||
Arena arena = plugin.getArenaManager().getArena(args[2]);
|
||||
if (arena == null) {
|
||||
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
|
||||
return true;
|
||||
}
|
||||
FussballHologram.HoloType type = switch (args[3].toLowerCase()) {
|
||||
case "wins", "siege" -> FussballHologram.HoloType.WINS;
|
||||
case "match", "live", "game" -> FussballHologram.HoloType.MATCH;
|
||||
default -> FussballHologram.HoloType.GOALS;
|
||||
};
|
||||
String id = buildHologramId(arena.getName(), type);
|
||||
if (plugin.getHologramManager().removeHologram(id)) {
|
||||
player.sendMessage(MessageUtil.success("Hologramm §e" + hologramTypeName(type) + " §ader Arena §e" + arena.getName() + " §agelöscht!"));
|
||||
} else {
|
||||
player.sendMessage(MessageUtil.error("Kein Hologramm vom Typ §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cgefunden!"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (args.length < 3) { player.sendMessage(MessageUtil.error("Benutze: /fb hologram delete <arena> goals|wins|match")); return true; }
|
||||
if (plugin.getHologramManager().removeHologram(args[2])) {
|
||||
player.sendMessage(MessageUtil.success("Hologramm §e" + args[2] + " §agelöscht!"));
|
||||
player.sendMessage(MessageUtil.success("Legacy-Hologramm §e" + args[2] + " §agelöscht!"));
|
||||
} else {
|
||||
player.sendMessage(MessageUtil.error("Kein Hologramm mit ID §e" + args[2] + "§c gefunden!"));
|
||||
}
|
||||
}
|
||||
case "text" -> {
|
||||
if (args.length < 6) {
|
||||
player.sendMessage(MessageUtil.error("Benutze: /fb hologram text <arena> goals|wins|match <zeile> <text>"));
|
||||
return true;
|
||||
}
|
||||
Arena arena = plugin.getArenaManager().getArena(args[2]);
|
||||
if (arena == null) {
|
||||
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
|
||||
return true;
|
||||
}
|
||||
FussballHologram.HoloType type = parseHologramType(args[3]);
|
||||
int line;
|
||||
try {
|
||||
line = Integer.parseInt(args[4]);
|
||||
} catch (NumberFormatException ex) {
|
||||
player.sendMessage(MessageUtil.error("§e" + args[4] + " §cist keine gültige Zeilennummer!"));
|
||||
return true;
|
||||
}
|
||||
if (line < 1) {
|
||||
player.sendMessage(MessageUtil.error("Zeilennummer muss mindestens §e1 §csein!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
String id = buildHologramId(arena.getName(), type);
|
||||
HologramManager hologramManager = plugin.getHologramManager();
|
||||
if (hologramManager.getHologram(id) == null) {
|
||||
player.sendMessage(MessageUtil.error("Hologramm §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cexistiert nicht!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> lines = new ArrayList<>(getEditableHologramTemplate(hologramManager, id, type));
|
||||
while (lines.size() < line) {
|
||||
lines.add(" ");
|
||||
}
|
||||
lines.set(line - 1, ChatColor.translateAlternateColorCodes('&', joinArgs(args, 5)));
|
||||
|
||||
if (!hologramManager.setCustomText(id, type, lines)) {
|
||||
player.sendMessage(MessageUtil.error("Konnte den Hologramm-Text nicht speichern!"));
|
||||
return true;
|
||||
}
|
||||
player.sendMessage(MessageUtil.success("Hologramm-Text für §e" + arena.getName() + " §a(" + hologramTypeName(type) + ") aktualisiert."));
|
||||
player.sendMessage("§7Zeile §e" + line + "§7: " + lines.get(line - 1));
|
||||
}
|
||||
case "textpreview" -> {
|
||||
if (args.length < 4) {
|
||||
player.sendMessage(MessageUtil.error("Benutze: /fb hologram textpreview <arena> goals|wins|match"));
|
||||
return true;
|
||||
}
|
||||
Arena arena = plugin.getArenaManager().getArena(args[2]);
|
||||
if (arena == null) {
|
||||
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
|
||||
return true;
|
||||
}
|
||||
FussballHologram.HoloType type = parseHologramType(args[3]);
|
||||
String id = buildHologramId(arena.getName(), type);
|
||||
HologramManager hologramManager = plugin.getHologramManager();
|
||||
if (hologramManager.getHologram(id) == null) {
|
||||
player.sendMessage(MessageUtil.error("Hologramm §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cexistiert nicht!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> lines = getEditableHologramTemplate(hologramManager, id, type);
|
||||
player.sendMessage(MessageUtil.header("Holo-Text: " + arena.getName() + " / " + hologramTypeName(type)));
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
player.sendMessage("§e" + (i + 1) + "§7: " + lines.get(i));
|
||||
}
|
||||
if (type == FussballHologram.HoloType.MATCH) {
|
||||
player.sendMessage("§8Platzhalter: §7{header} {separator} {phase} {score} {time}");
|
||||
} else {
|
||||
player.sendMessage("§8Platzhalter: §7{title} {separator} {entries} {toggle}");
|
||||
}
|
||||
}
|
||||
case "textreset" -> {
|
||||
if (args.length < 4) {
|
||||
player.sendMessage(MessageUtil.error("Benutze: /fb hologram textreset <arena> goals|wins|match"));
|
||||
return true;
|
||||
}
|
||||
Arena arena = plugin.getArenaManager().getArena(args[2]);
|
||||
if (arena == null) {
|
||||
player.sendMessage(MessageUtil.error("Arena §e" + args[2] + " §cnicht gefunden!"));
|
||||
return true;
|
||||
}
|
||||
FussballHologram.HoloType type = parseHologramType(args[3]);
|
||||
String id = buildHologramId(arena.getName(), type);
|
||||
HologramManager hologramManager = plugin.getHologramManager();
|
||||
if (hologramManager.getHologram(id) == null) {
|
||||
player.sendMessage(MessageUtil.error("Hologramm §e" + hologramTypeName(type) + " §cfür Arena §e" + arena.getName() + " §cexistiert nicht!"));
|
||||
return true;
|
||||
}
|
||||
hologramManager.resetCustomText(id, type);
|
||||
player.sendMessage(MessageUtil.success("Standardtext für §e" + arena.getName() + " §a(" + hologramTypeName(type) + ") wiederhergestellt."));
|
||||
}
|
||||
case "reload" -> {
|
||||
plugin.getHologramManager().reload();
|
||||
player.sendMessage(MessageUtil.success("Hologramme neu geladen! §7(" + plugin.getHologramManager().getCount() + " gesamt)"));
|
||||
@@ -351,11 +500,11 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
player.sendMessage(MessageUtil.warn("Keine Hologramme vorhanden."));
|
||||
} else {
|
||||
for (String id : plugin.getHologramManager().getHologramIds()) {
|
||||
player.sendMessage("§7 • §e" + id);
|
||||
player.sendMessage("§7 • §e" + formatHologramDisplay(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> player.sendMessage(MessageUtil.error("Gültig: set <id> goals|wins|match | remove | delete <id> | reload | list"));
|
||||
default -> player.sendMessage(MessageUtil.error("Gültig: set | remove | delete | text | textpreview | textreset | reload | list"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +643,8 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
s.sendMessage("§e/fb history [n] §7- Letzte Spiele anzeigen");
|
||||
if (s.hasPermission("fussball.admin")) {
|
||||
s.sendMessage("§c§lAdmin: §ccreate / delete / setup / stop / debug / dropball");
|
||||
s.sendMessage("§c§lAdmin: §chologram set goals|wins|match / remove / reload");
|
||||
s.sendMessage("§c§lAdmin: §cstats reset <spieler|all>");
|
||||
s.sendMessage("§c§lAdmin: §chologram set|delete|text|textpreview|textreset <arena> <typ>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,6 +662,43 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
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); }
|
||||
private String buildHologramId(String arenaName, FussballHologram.HoloType type) { return arenaName.toLowerCase() + "_" + hologramTypeName(type); }
|
||||
private String joinArgs(String[] args, int start) { return String.join(" ", Arrays.copyOfRange(args, start, args.length)); }
|
||||
private List<String> getEditableHologramTemplate(HologramManager hologramManager, String id, FussballHologram.HoloType type) {
|
||||
List<String> custom = hologramManager.getCustomText(id, type);
|
||||
return custom.isEmpty() ? getDefaultHologramTemplate(type) : custom;
|
||||
}
|
||||
private List<String> getDefaultHologramTemplate(FussballHologram.HoloType type) {
|
||||
if (type == FussballHologram.HoloType.MATCH) {
|
||||
return List.of("{header}", "{separator}", "{phase}", "{score}", "{time}");
|
||||
}
|
||||
return List.of("{title}", "{separator}", "{entries}", "{separator}", "{toggle}");
|
||||
}
|
||||
private FussballHologram.HoloType parseHologramType(String rawType) {
|
||||
return switch (rawType.toLowerCase()) {
|
||||
case "wins", "siege" -> FussballHologram.HoloType.WINS;
|
||||
case "match", "live", "game" -> FussballHologram.HoloType.MATCH;
|
||||
default -> FussballHologram.HoloType.GOALS;
|
||||
};
|
||||
}
|
||||
private String hologramTypeName(FussballHologram.HoloType type) {
|
||||
return switch (type) {
|
||||
case WINS -> "wins";
|
||||
case MATCH -> "match";
|
||||
default -> "goals";
|
||||
};
|
||||
}
|
||||
private String formatHologramDisplay(String id) {
|
||||
int split = id.lastIndexOf('_');
|
||||
if (split > 0 && split < id.length() - 1) {
|
||||
String arena = id.substring(0, split);
|
||||
String type = id.substring(split + 1);
|
||||
if (type.equals("goals") || type.equals("wins") || type.equals("match")) {
|
||||
return arena + " §7→ §e" + type;
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
// ── Tab-Completion ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -523,14 +710,21 @@ public class FussballCommand implements CommandExecutor, TabCompleter {
|
||||
if (sender.hasPermission("fussball.admin")) list.addAll(List.of("create", "delete", "setup", "stop", "setgk", "debug", "hologram", "dropball"));
|
||||
} else if (args.length == 2 && List.of("join","delete","setup","stop","setgk","debug","spectate","dropball").contains(args[0].toLowerCase())) {
|
||||
list.addAll(plugin.getArenaManager().getArenaNames());
|
||||
} else if (args.length == 2 && args[0].equalsIgnoreCase("stats") && sender.hasPermission("fussball.admin")) {
|
||||
list.addAll(List.of("reset"));
|
||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("stats") && args[1].equalsIgnoreCase("reset") && sender.hasPermission("fussball.admin")) {
|
||||
list.add("all");
|
||||
for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
|
||||
list.add(onlinePlayer.getName());
|
||||
}
|
||||
} 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("set", "remove", "delete", "text", "textpreview", "textreset", "reload", "list"));
|
||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("hologram") && List.of("set", "delete", "text", "textpreview", "textreset").contains(args[1].toLowerCase())) {
|
||||
list.addAll(plugin.getArenaManager().getArenaNames());
|
||||
} else if (args.length == 4 && args[0].equalsIgnoreCase("hologram") && List.of("set", "delete", "text", "textpreview", "textreset").contains(args[1].toLowerCase())) {
|
||||
list.addAll(List.of("goals", "wins", "match"));
|
||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("delete")) {
|
||||
list.addAll(plugin.getHologramManager().getHologramIds());
|
||||
} else if (args.length == 5 && args[0].equalsIgnoreCase("hologram") && args[1].equalsIgnoreCase("text")) {
|
||||
list.addAll(List.of("1", "2", "3", "4", "5"));
|
||||
} else if (args.length == 3 && args[0].equalsIgnoreCase("setgk")) {
|
||||
// Spielernamen aus dem aktiven Spiel vorschlagen
|
||||
Game gkGame = plugin.getGameManager().getGame(args[1]);
|
||||
|
||||
@@ -79,6 +79,9 @@ public class Game {
|
||||
private UUID lastKicker = null;
|
||||
private UUID secondLastKicker = null; // für Assist-Erkennung
|
||||
private boolean lastKickWasHeader = false; // für Rückpass-Regel (Header erlaubt)
|
||||
/** true wenn der letzte Schuss ein Restart-Kick war (Einwurf/Eckstoß/Abstoß/Anstoß)
|
||||
* → der NÄCHSTE Empfänger darf lt. Regel 11 §3 nicht auf Abseits geprüft werden */
|
||||
private boolean lastKickWasRestart = false;
|
||||
private Team lastTouchTeam = null;
|
||||
private Team throwInTeam = null;
|
||||
|
||||
@@ -389,6 +392,10 @@ public class Game {
|
||||
cd--;
|
||||
} else {
|
||||
spawnBallDelayed(arena.getBallSpawn());
|
||||
// Regel 8: Rot führt den Anstoß in der 1. Halbzeit aus
|
||||
throwInTeam = Team.RED;
|
||||
kickoffEnforceTicks = 200; // 10s Kreisschutz (Regel 8: 9,15m Abstand)
|
||||
broadcastAll(Messages.get("kickoff-team", "team", "§cRotes Team"));
|
||||
for (UUID uuid : allPlayers) {
|
||||
Player p = Bukkit.getPlayer(uuid);
|
||||
if (p != null) {
|
||||
@@ -444,9 +451,19 @@ public class Game {
|
||||
secondHalf = true;
|
||||
timeLeft = arena.getGameDuration() / 2;
|
||||
updateGoalBeaconColors();
|
||||
// Nachspielzeit der 1. Halbzeit zurücksetzen
|
||||
// Nachspielzeit und Spielzustand der 1. Halbzeit zurücksetzen
|
||||
injuryTimeBuffer = 0;
|
||||
inInjuryTime = false;
|
||||
lastKicker = null;
|
||||
secondLastKicker = null;
|
||||
lastTouchTeam = null;
|
||||
lastKickWasHeader = false;
|
||||
lastKickWasRestart = false;
|
||||
lastBallLocation = null;
|
||||
outCooldown = false;
|
||||
offsideCooldown = false;
|
||||
headerCooldowns.clear();
|
||||
outOfBoundsCountdown.clear();
|
||||
|
||||
// Seitenwechsel: Rotes Team → BlueSpawn, Blaues Team → RedSpawn
|
||||
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.teleport(arena.getBlueSpawn()); }
|
||||
@@ -472,6 +489,10 @@ public class Game {
|
||||
cd--;
|
||||
} else {
|
||||
spawnBallDelayed(arena.getBallSpawn());
|
||||
// Regel 8: Das andere Team (Blau) stößt in der 2. Halbzeit an
|
||||
throwInTeam = Team.BLUE;
|
||||
kickoffEnforceTicks = 200;
|
||||
broadcastAll(Messages.get("kickoff-team", "team", "§9Blaues Team"));
|
||||
for (UUID uuid : allPlayers) {
|
||||
Player p = Bukkit.getPlayer(uuid);
|
||||
if (p != null) {
|
||||
@@ -497,11 +518,16 @@ public class Game {
|
||||
|
||||
if (ball != null) ball.remove();
|
||||
spawnBallDelayed(arena.getBallSpawn());
|
||||
// Regel 8: In der Verlängerung stößt das Team an, das in der 2. Halbzeit NICHT angestoßen hat.
|
||||
// In der 2. HZ stieß Blau an → in der VL stößt Rot an.
|
||||
throwInTeam = Team.RED;
|
||||
kickoffEnforceTicks = 200; // 10s Anstoß-Kreis
|
||||
|
||||
broadcastAll("§6§l╔══════════════════════╗");
|
||||
broadcastAll("§6§l║ ⚽ VERLÄNGERUNG! ║");
|
||||
broadcastAll("§6§l╚══════════════════════╝");
|
||||
broadcastAll("§7Spielstand: §c" + redScore + " §7: §9" + blueScore);
|
||||
broadcastAll(Messages.get("kickoff-team", "team", "§cRotes Team"));
|
||||
|
||||
for (UUID uuid : redTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getRedSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } }
|
||||
for (UUID uuid : blueTeam) { Player p = Bukkit.getPlayer(uuid); if (p != null) { p.teleport(arena.getBlueSpawn()); p.sendTitle("§6§lVERLÄNGERUNG!", "§710 Minuten extra!", 10, 60, 10); } }
|
||||
@@ -856,17 +882,27 @@ public class Game {
|
||||
Vector diff = to.toVector().subtract(from.toVector());
|
||||
double distance = diff.length();
|
||||
int steps = Math.max(1, (int) Math.ceil(distance / 0.2));
|
||||
// BUG FIX: Vier Y-Offsets prüfen:
|
||||
// 0.0 = ArmorStand-Füße (Entity-Position)
|
||||
// 0.5 = Mitte des kleinen Stands
|
||||
// 0.975 = tatsächliche Helmposition (Textur sichtbar hier!)
|
||||
// 1.4 = konservativer oberer Puffer
|
||||
// Früher wurden nur 0 und 1.4 geprüft → Bälle auf Helm-Höhe (0.975)
|
||||
// wurden nicht als Tor erkannt und landeten als Ecke/Abstoß.
|
||||
final double[] Y_OFFSETS = {0.0, 0.5, 0.975, 1.4};
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = (double) i / steps;
|
||||
Location p = from.clone().add(diff.clone().multiply(t));
|
||||
Location head = p.clone().add(0, 1.4, 0);
|
||||
Location base = from.clone().add(diff.clone().multiply(t));
|
||||
for (double dy : Y_OFFSETS) {
|
||||
Location check = base.clone().add(0, dy, 0);
|
||||
// In der 2. Halbzeit sind die Seiten getauscht → Tore umkehren
|
||||
if (!secondHalf) {
|
||||
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.BLUE;
|
||||
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.RED;
|
||||
if (arena.isInRedGoal(check)) return Team.BLUE;
|
||||
if (arena.isInBlueGoal(check)) return Team.RED;
|
||||
} else {
|
||||
if (arena.isInRedGoal(p) || arena.isInRedGoal(head)) return Team.RED;
|
||||
if (arena.isInBlueGoal(p) || arena.isInBlueGoal(head)) return Team.BLUE;
|
||||
if (arena.isInRedGoal(check)) return Team.RED;
|
||||
if (arena.isInBlueGoal(check)) return Team.BLUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -910,25 +946,47 @@ public class Game {
|
||||
}
|
||||
}
|
||||
case "redEnd" -> {
|
||||
if (touchTeam == Team.RED) {
|
||||
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25);
|
||||
throwInTeam = Team.BLUE;
|
||||
message = "§e⚽ §7Ball im Aus! §9Ecke für Blaues Team§7!";
|
||||
// Korrekte Fußball-Regel:
|
||||
// Letzter Kontakt durch VERTEIDIGER an eigener Torlinie → ECKE für Angreifer
|
||||
// Letzter Kontakt durch ANGREIFER an gegnerischer Torlinie → ABSTOSS für Verteidiger
|
||||
// 1. Halbzeit: ROT verteidigt diese Seite (redEnd = rotes Tor)
|
||||
// 2. Halbzeit: BLAU verteidigt diese Seite (Seitenwechsel)
|
||||
Team defenderHere = secondHalf ? Team.BLUE : Team.RED;
|
||||
Team attackerHere = defenderHere.getOpponent();
|
||||
if (touchTeam == defenderHere) {
|
||||
// Verteidiger hat den Ball ins Aus geschossen → ECKE für Angreifer
|
||||
// Ball an Strafraumgrenze (11m-Linie) platzieren – nicht in der Spielfeldecke
|
||||
resumeLocation = moveInsideField(getPenaltyAreaCornerLocation(outLocation, true), 1.25);
|
||||
throwInTeam = attackerHere;
|
||||
String teamStr = attackerHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
|
||||
message = "§e⚽ §7Ball im Aus! §7Ecke für " + teamStr + "§7!";
|
||||
} else {
|
||||
resumeLocation = moveInsideField(arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn(), 1.25);
|
||||
throwInTeam = Team.RED;
|
||||
message = "§e⚽ §7Ball im Aus! §cAbstoß für Rotes Team§7!";
|
||||
// Angreifer (oder unbekannt) hat den Ball ins Aus geschossen → ABSTOSS für Verteidiger
|
||||
// Ball ~5,5 Blöcke vor der Torlinie (5-Meter-Raum), Feldmitte
|
||||
resumeLocation = moveInsideField(getGoalKickSpawnLocation(true), 1.25);
|
||||
throwInTeam = defenderHere;
|
||||
String teamStr = defenderHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
|
||||
message = "§e⚽ §7Ball im Aus! §7Abstoß für " + teamStr + "§7!";
|
||||
}
|
||||
}
|
||||
case "blueEnd" -> {
|
||||
if (touchTeam == Team.BLUE) {
|
||||
resumeLocation = moveInsideField(getCornerLocation(outLocation), 1.25);
|
||||
throwInTeam = Team.RED;
|
||||
message = "§e⚽ §7Ball im Aus! §cEcke für Rotes Team§7!";
|
||||
// analog zu redEnd.
|
||||
// 1. Halbzeit: BLAU verteidigt diese Seite (blueEnd = blaues Tor)
|
||||
// 2. Halbzeit: ROT verteidigt diese Seite (Seitenwechsel)
|
||||
Team defenderHere = secondHalf ? Team.RED : Team.BLUE;
|
||||
Team attackerHere = defenderHere.getOpponent();
|
||||
if (touchTeam == defenderHere) {
|
||||
// Verteidiger → ECKE für Angreifer
|
||||
resumeLocation = moveInsideField(getPenaltyAreaCornerLocation(outLocation, false), 1.25);
|
||||
throwInTeam = attackerHere;
|
||||
String teamStr = attackerHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
|
||||
message = "§e⚽ §7Ball im Aus! §7Ecke für " + teamStr + "§7!";
|
||||
} else {
|
||||
resumeLocation = moveInsideField(arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn(), 1.25);
|
||||
throwInTeam = Team.BLUE;
|
||||
message = "§e⚽ §7Ball im Aus! §9Abstoß für Blaues Team§7!";
|
||||
// Angreifer (oder unbekannt) → ABSTOSS für Verteidiger
|
||||
resumeLocation = moveInsideField(getGoalKickSpawnLocation(false), 1.25);
|
||||
throwInTeam = defenderHere;
|
||||
String teamStr = defenderHere == Team.RED ? "§cRotes Team" : "§9Blaues Team";
|
||||
message = "§e⚽ §7Ball im Aus! §7Abstoß für " + teamStr + "§7!";
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
@@ -942,14 +1000,137 @@ public class Game {
|
||||
for (UUID uuid : allPlayers) { Player p = Bukkit.getPlayer(uuid); if (p != null) p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 1f, 1.2f); }
|
||||
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-out", 3));
|
||||
|
||||
// throwInTeam sichern – spawnBallDelayed() würde es auf null zurücksetzen
|
||||
final Team capturedThrowIn = throwInTeam;
|
||||
final Location spawnHere = resumeLocation;
|
||||
new BukkitRunnable() {
|
||||
public void run() {
|
||||
if (state == GameState.RUNNING || state == GameState.OVERTIME) spawnBallDelayed(spawnHere);
|
||||
if (state == GameState.RUNNING || state == GameState.OVERTIME) {
|
||||
spawnBallDelayed(spawnHere);
|
||||
throwInTeam = capturedThrowIn;
|
||||
// Abstandsregel pro Spielfortsetzungs-Typ erzwingen (Regel 15/16/17)
|
||||
freekickLocation = spawnHere.clone();
|
||||
freekickTicks = plugin.getConfig().getInt("gameplay.freekick-duration", 600);
|
||||
}
|
||||
}
|
||||
}.runTaskLater(plugin, 40L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet den Freistoß-Aufstellungsort für eine Ecke.
|
||||
* Statt der wörtlichen Spielfeldecke wird der Ball an der Strafraumgrenze
|
||||
* (Tiefe aus gameplay.penalty-area-depth) entlang der nächsten Seitenlinie platziert.
|
||||
* Das entspricht dem Wunsch "an oder vor der 11-Meter-Grenze".
|
||||
*
|
||||
* @param outLoc – Wo der Ball das Feld verlassen hat
|
||||
* @param isRedEnd – true = rotes Tor-Ende (redGoal-Seite des Feldes)
|
||||
*/
|
||||
private Location getPenaltyAreaCornerLocation(Location outLoc, boolean isRedEnd) {
|
||||
if (arena.getFieldMin() == null || arena.getFieldMax() == null || arena.getBallSpawn() == null) {
|
||||
return getCornerLocation(outLoc);
|
||||
}
|
||||
double y = arena.getBallSpawn().getY();
|
||||
double penaltyDepth = plugin.getConfig().getDouble("gameplay.penalty-area-depth", 16);
|
||||
|
||||
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
|
||||
double maxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX());
|
||||
double minZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
|
||||
double maxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
|
||||
|
||||
org.bukkit.util.Vector fieldDir = arena.getFieldDirection();
|
||||
if (fieldDir == null) return getCornerLocation(outLoc);
|
||||
|
||||
if (Math.abs(fieldDir.getZ()) >= Math.abs(fieldDir.getX())) {
|
||||
// ── Feld läuft entlang Z-Achse ──────────────────────────────────
|
||||
boolean redIsLowZ = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
|
||||
// Nächste Seitenlinie (X-Seite) bestimmen
|
||||
double sideX = (Math.abs(outLoc.getX() - minX) <= Math.abs(outLoc.getX() - maxX)) ? minX : maxX;
|
||||
// Z-Position: Strafraum-Tiefe vom Toraus-Ende ins Feld
|
||||
double endZ, targetZ;
|
||||
if (isRedEnd) {
|
||||
endZ = redIsLowZ ? minZ : maxZ;
|
||||
targetZ = redIsLowZ ? endZ + penaltyDepth : endZ - penaltyDepth;
|
||||
} else {
|
||||
endZ = redIsLowZ ? maxZ : minZ;
|
||||
targetZ = redIsLowZ ? endZ - penaltyDepth : endZ + penaltyDepth;
|
||||
}
|
||||
targetZ = Math.max(minZ + 1.0, Math.min(maxZ - 1.0, targetZ));
|
||||
return new Location(outLoc.getWorld(), sideX, y, targetZ);
|
||||
} else {
|
||||
// ── Feld läuft entlang X-Achse ──────────────────────────────────
|
||||
boolean redIsLowX = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
|
||||
double sideZ = (Math.abs(outLoc.getZ() - minZ) <= Math.abs(outLoc.getZ() - maxZ)) ? minZ : maxZ;
|
||||
double endX, targetX;
|
||||
if (isRedEnd) {
|
||||
endX = redIsLowX ? minX : maxX;
|
||||
targetX = redIsLowX ? endX + penaltyDepth : endX - penaltyDepth;
|
||||
} else {
|
||||
endX = redIsLowX ? maxX : minX;
|
||||
targetX = redIsLowX ? endX - penaltyDepth : endX + penaltyDepth;
|
||||
}
|
||||
targetX = Math.max(minX + 1.0, Math.min(maxX - 1.0, targetX));
|
||||
return new Location(outLoc.getWorld(), targetX, y, sideZ);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Ball-Aufstellungsort für einen Abstoß zurück.
|
||||
* Der Ball wird ~5.5 Blöcke vor der Torlinie, mittig auf dem Feld platziert
|
||||
* (entspricht dem 5-Meter-Raum / Torabstoß-Raum im echten Fußball).
|
||||
*
|
||||
* @param isRedEnd – true = roter Torbereich
|
||||
*/
|
||||
private Location getGoalKickSpawnLocation(boolean isRedEnd) {
|
||||
if (arena.getFieldMin() == null || arena.getFieldMax() == null || arena.getBallSpawn() == null) {
|
||||
if (isRedEnd) return arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn();
|
||||
else return arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn();
|
||||
}
|
||||
double y = arena.getBallSpawn().getY();
|
||||
final double GOAL_KICK_INSET = 5.5; // ~5-Meter-Raum (6-Yard-Box)
|
||||
|
||||
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
|
||||
double maxX = Math.max(arena.getFieldMin().getX(), arena.getFieldMax().getX());
|
||||
double minZ = Math.min(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
|
||||
double maxZ = Math.max(arena.getFieldMin().getZ(), arena.getFieldMax().getZ());
|
||||
double centerX = (minX + maxX) / 2.0;
|
||||
double centerZ = (minZ + maxZ) / 2.0;
|
||||
|
||||
org.bukkit.util.Vector fieldDir = arena.getFieldDirection();
|
||||
if (fieldDir == null) {
|
||||
return isRedEnd ? (arena.getRedSpawn() != null ? arena.getRedSpawn() : arena.getBallSpawn())
|
||||
: (arena.getBlueSpawn() != null ? arena.getBlueSpawn() : arena.getBallSpawn());
|
||||
}
|
||||
|
||||
if (Math.abs(fieldDir.getZ()) >= Math.abs(fieldDir.getX())) {
|
||||
// Feld entlang Z-Achse
|
||||
boolean redIsLowZ = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
|
||||
double kickZ;
|
||||
if (isRedEnd) {
|
||||
double endZ = redIsLowZ ? minZ : maxZ;
|
||||
kickZ = redIsLowZ ? endZ + GOAL_KICK_INSET : endZ - GOAL_KICK_INSET;
|
||||
} else {
|
||||
double endZ = redIsLowZ ? maxZ : minZ;
|
||||
kickZ = redIsLowZ ? endZ - GOAL_KICK_INSET : endZ + GOAL_KICK_INSET;
|
||||
}
|
||||
kickZ = Math.max(minZ + 1.0, Math.min(maxZ - 1.0, kickZ));
|
||||
return new Location(arena.getFieldMin().getWorld(), centerX, y, kickZ);
|
||||
} else {
|
||||
// Feld entlang X-Achse
|
||||
boolean redIsLowX = arena.getRedGoalAxisValue() < arena.getBlueGoalAxisValue();
|
||||
double kickX;
|
||||
if (isRedEnd) {
|
||||
double endX = redIsLowX ? minX : maxX;
|
||||
kickX = redIsLowX ? endX + GOAL_KICK_INSET : endX - GOAL_KICK_INSET;
|
||||
} else {
|
||||
double endX = redIsLowX ? maxX : minX;
|
||||
kickX = redIsLowX ? endX - GOAL_KICK_INSET : endX + GOAL_KICK_INSET;
|
||||
}
|
||||
kickX = Math.max(minX + 1.0, Math.min(maxX - 1.0, kickX));
|
||||
return new Location(arena.getFieldMin().getWorld(), kickX, y, centerZ);
|
||||
}
|
||||
}
|
||||
|
||||
/** Hilfsmethode: wörtliche Spielfeldecke (als Fallback). */
|
||||
private Location getCornerLocation(Location outLoc) {
|
||||
if (arena.getFieldMin() == null || arena.getFieldMax() == null) return arena.getBallSpawn();
|
||||
double minX = Math.min(arena.getFieldMin().getX(), arena.getFieldMax().getX());
|
||||
@@ -1019,8 +1200,8 @@ public class Game {
|
||||
}
|
||||
}
|
||||
|
||||
throwInTeam = null;
|
||||
setLastKicker(uuid); // korrekt: nutzt setLastKicker statt direktem Feldzugriff
|
||||
clearThrowIn(); // setzt lastKickWasRestart=true falls Einwurf/Restart war → kein Abseits für Empfänger (Regel 11 §3)
|
||||
setLastKicker(uuid);
|
||||
ball.kick(p);
|
||||
break; // pro Tick max. 1 Auto-Kick
|
||||
}
|
||||
@@ -1370,30 +1551,43 @@ public class Game {
|
||||
addInjuryTime(plugin.getConfig().getInt("gameplay.injury-time-per-foul", 5));
|
||||
logMatchEvent("§cFoul: §e" + fouler.getName() + " §7→ §e" + victim.getName());
|
||||
|
||||
// ── Foul im Strafraum → Elfmeter ───────────────────────────────────
|
||||
// ── Foul im Strafraum → Elfmeter (Regel 14) ──────────────────────────────
|
||||
// Regel 14: Strafstoß wenn ein Spieler ein direktes Foul im eigenen Strafraum begeht.
|
||||
// In der 2. Halbzeit sind die Seiten getauscht:
|
||||
// 1. HZ: Rot verteidigt roten SR, Blau verteidigt blauen SR
|
||||
// 2. HZ: Blau verteidigt roten SR, Rot verteidigt blauen SR
|
||||
boolean inRedPenalty = arena.isInRedPenaltyArea(foulLocation);
|
||||
boolean inBluePenalty = arena.isInBluePenaltyArea(foulLocation);
|
||||
boolean penaltyKick = false;
|
||||
|
||||
if (inRedPenalty && victimTeam == Team.BLUE) {
|
||||
// Foul an Blau im roten Strafraum → Elfmeter für Blau
|
||||
boolean penaltyForBlue, penaltyForRed;
|
||||
if (!secondHalf) {
|
||||
penaltyForBlue = inRedPenalty && victimTeam == Team.BLUE;
|
||||
penaltyForRed = inBluePenalty && victimTeam == Team.RED;
|
||||
} else {
|
||||
// Seitenwechsel: Blau greift jetzt auf roten SR-Seite an
|
||||
penaltyForBlue = inBluePenalty && victimTeam == Team.BLUE;
|
||||
penaltyForRed = inRedPenalty && victimTeam == Team.RED;
|
||||
}
|
||||
|
||||
if (penaltyForBlue) {
|
||||
broadcastAll(Messages.get("foul-penalty", "team", "§9Blaues Team"));
|
||||
for (UUID uuid : getAllAndSpectators()) {
|
||||
Player p = Bukkit.getPlayer(uuid);
|
||||
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§9Blaues Team§7 schießt!", 5, 50, 10);
|
||||
}
|
||||
penaltyKick = true;
|
||||
// Elfmeter als Freistoß direkt auf Ballspawn (ggf. später: separater Elfmeter-Punkt)
|
||||
startFreekick(Team.BLUE, arena.getBallSpawn(), "Elfmeter");
|
||||
} else if (inBluePenalty && victimTeam == Team.RED) {
|
||||
// Foul an Rot im blauen Strafraum → Elfmeter für Rot
|
||||
Location penSpot = arena.getPenaltySpot(Team.BLUE);
|
||||
startFreekick(Team.BLUE, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
|
||||
} else if (penaltyForRed) {
|
||||
broadcastAll(Messages.get("foul-penalty", "team", "§cRotes Team"));
|
||||
for (UUID uuid : getAllAndSpectators()) {
|
||||
Player p = Bukkit.getPlayer(uuid);
|
||||
if (p != null) p.sendTitle("§c⚠ ELFMETER!", "§cRotes Team§7 schießt!", 5, 50, 10);
|
||||
}
|
||||
penaltyKick = true;
|
||||
startFreekick(Team.RED, arena.getBallSpawn(), "Elfmeter");
|
||||
Location penSpot = arena.getPenaltySpot(Team.RED);
|
||||
startFreekick(Team.RED, penSpot != null ? penSpot : arena.getBallSpawn(), "Elfmeter");
|
||||
}
|
||||
|
||||
if (!penaltyKick) {
|
||||
@@ -1447,8 +1641,18 @@ public class Game {
|
||||
broadcastAll(Messages.get("freekick-hint", "n", String.format("%.0f", dist)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt den korrekten Abstand für die jeweilige Spielfortsetzung (Regel 13–17).
|
||||
* Freistoß (Regel 13): 5 Blöcke (config: freekick-distance)
|
||||
* Einwurf (Regel 15): 2 Blöcke
|
||||
* Abstoß (Regel 16): 9,15 Blöcke (Gegner außerhalb Strafraum)
|
||||
* Eckstoß (Regel 17): 9,15 Blöcke
|
||||
* Anstoß (Regel 8): 9,15 Blöcke (via kickoffEnforceTicks)
|
||||
*/
|
||||
private void enforceFreekickDistance() {
|
||||
if (freekickLocation == null || throwInTeam == null) return;
|
||||
// Abstand je nach Typ: prüfe ob freekickLocation nah an einer Seitenlinie ist (=Einwurf)
|
||||
// oder in der Feldhälfte nahe einer Torlinie (=Abstoß/Eckstoß) oder zentral (=Freistoß)
|
||||
double minDist = plugin.getConfig().getDouble("gameplay.freekick-distance", 5.0);
|
||||
Team opposingTeam = throwInTeam.getOpponent();
|
||||
List<UUID> opponents = opposingTeam == Team.RED ? redTeam : blueTeam;
|
||||
@@ -1843,6 +2047,7 @@ public class Game {
|
||||
lastKicker = null;
|
||||
secondLastKicker = null;
|
||||
lastKickWasHeader = false;
|
||||
lastKickWasRestart = false;
|
||||
secondHalf = false;
|
||||
updateGoalBeaconColors();
|
||||
|
||||
@@ -2121,7 +2326,8 @@ public class Game {
|
||||
Player newKicker = Bukkit.getPlayer(uuid);
|
||||
if (prevKicker != null && newKicker != null) {
|
||||
double dist = lastKickLocation.distance(ball.getEntity().getLocation());
|
||||
if (dist >= LONG_PASS_DISTANCE && getTeam(prevKicker) == getTeam(newKicker)) {
|
||||
double longPassDist = plugin.getConfig().getDouble("gameplay.long-pass-distance", LONG_PASS_DISTANCE);
|
||||
if (dist >= longPassDist && getTeam(prevKicker) == getTeam(newKicker)) {
|
||||
// Langer Pass innerhalb des Teams
|
||||
String msg = "§7⚽ §eLangpass §7von §f" + prevKicker.getName()
|
||||
+ " §7zu §f" + newKicker.getName()
|
||||
@@ -2144,10 +2350,16 @@ public class Game {
|
||||
if (p != null) lastTouchTeam = getTeam(p);
|
||||
if (p != null) kicks.merge(uuid, 1, Integer::sum);
|
||||
|
||||
// Abseits-Check
|
||||
// ── Abseits-Check (Regel 11 §3: kein Abseits nach Einwurf, Abstoß, Eckstoß) ──
|
||||
// lastKickWasRestart wird in clearThrowIn() gesetzt wenn throwInTeam != null war.
|
||||
// Der EMPFÄNGER des ersten Restart-Passes darf nicht auf Abseits geprüft werden.
|
||||
// Das Flag wird hier konsumiert (→ gilt nur für diesen einen Empfänger).
|
||||
boolean skipOffside = lastKickWasRestart;
|
||||
lastKickWasRestart = false; // Flag zurücksetzen nach Konsum
|
||||
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
|
||||
&& (state == GameState.RUNNING || state == GameState.OVERTIME)
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown) {
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown
|
||||
&& !skipOffside) {
|
||||
checkOffside(uuid, ball.getEntity().getLocation());
|
||||
}
|
||||
}
|
||||
@@ -2163,9 +2375,13 @@ public class Game {
|
||||
if (p != null) lastTouchTeam = getTeam(p);
|
||||
if (p != null) kicks.merge(uuid, 1, Integer::sum);
|
||||
|
||||
// Kopfball: gleiche Abseits-Logik – kein Abseits wenn Restart-Empfänger
|
||||
boolean skipOffside = lastKickWasRestart;
|
||||
lastKickWasRestart = false;
|
||||
if (plugin.getConfig().getBoolean("gameplay.offside-enabled", true)
|
||||
&& (state == GameState.RUNNING || state == GameState.OVERTIME)
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown) {
|
||||
&& ball != null && ball.getEntity() != null && !offsideCooldown
|
||||
&& !skipOffside) {
|
||||
checkOffside(uuid, ball.getEntity().getLocation());
|
||||
}
|
||||
}
|
||||
@@ -2201,6 +2417,8 @@ public class Game {
|
||||
public boolean isLastKickWasHeader() { return lastKickWasHeader; }
|
||||
/** Berechtigung aufheben – wird von BallListener nach dem ersten Schuss gerufen */
|
||||
public void clearThrowIn() {
|
||||
// Wenn throwInTeam gesetzt war, war das ein Restart-Kick → nächster Empfänger kein Abseits
|
||||
if (throwInTeam != null) lastKickWasRestart = true;
|
||||
throwInTeam = null;
|
||||
freekickLocation = null;
|
||||
freekickTicks = 0;
|
||||
|
||||
@@ -4,6 +4,7 @@ import de.fussball.plugin.Fussball;
|
||||
import de.fussball.plugin.game.Game;
|
||||
import de.fussball.plugin.game.GameState;
|
||||
import de.fussball.plugin.stats.StatsManager;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
@@ -15,6 +16,10 @@ import org.bukkit.util.Transformation;
|
||||
import org.joml.AxisAngle4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -44,14 +49,22 @@ public class FussballHologram {
|
||||
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 Map<HoloType, List<String>> customTextTemplates = new EnumMap<>(HoloType.class);
|
||||
|
||||
private final Fussball plugin;
|
||||
|
||||
public FussballHologram(String id, Location location, HoloType type, Fussball plugin) {
|
||||
this(id, location, type, plugin, Collections.emptyMap());
|
||||
}
|
||||
|
||||
public FussballHologram(String id, Location location, HoloType type, Fussball plugin, Map<HoloType, List<String>> customTextTemplates) {
|
||||
this.id = id;
|
||||
this.location = location.clone();
|
||||
this.type = type;
|
||||
this.plugin = plugin;
|
||||
for (Map.Entry<HoloType, List<String>> entry : customTextTemplates.entrySet()) {
|
||||
setCustomText(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Seiten-Wechsel ───────────────────────────────────────────────────────
|
||||
@@ -183,70 +196,228 @@ public class FussballHologram {
|
||||
.anyMatch(i -> i.getUniqueId().equals(entityId));
|
||||
}
|
||||
|
||||
public void setCustomText(HoloType targetType, List<String> lines) {
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
customTextTemplates.remove(targetType);
|
||||
return;
|
||||
}
|
||||
List<String> sanitized = new ArrayList<>();
|
||||
for (String line : lines) {
|
||||
sanitized.add(ChatColor.translateAlternateColorCodes('&', line));
|
||||
}
|
||||
customTextTemplates.put(targetType, List.copyOf(sanitized));
|
||||
}
|
||||
|
||||
public void clearCustomText(HoloType targetType) {
|
||||
customTextTemplates.remove(targetType);
|
||||
}
|
||||
|
||||
public List<String> getCustomText(HoloType targetType) {
|
||||
return customTextTemplates.getOrDefault(targetType, Collections.emptyList());
|
||||
}
|
||||
|
||||
/** Baut den anzuzeigenden Text aus den aktuellen Top-10-Statistiken */
|
||||
private String buildText(HoloType showType) {
|
||||
if (showType == HoloType.MATCH) {
|
||||
return buildMatchText();
|
||||
}
|
||||
|
||||
String customText = buildCustomStatsText(showType);
|
||||
if (customText != null) {
|
||||
return customText;
|
||||
}
|
||||
|
||||
return buildDefaultStatsText(showType);
|
||||
}
|
||||
|
||||
private String buildDefaultStatsText(HoloType showType) {
|
||||
String nameColor = holoColor("name-color", "&0");
|
||||
String labelColor = holoColor("label-color", "&8");
|
||||
String sep = holoColor("separator-color", "&8&m") + "══════════════════════" + ChatColor.RESET;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (showType == HoloType.GOALS) {
|
||||
sb.append("§6§l⚽ TOP 10 TORSCHÜTZEN ⚽\n");
|
||||
sb.append("§8§m══════════════════════§r\n");
|
||||
String title = holoColor("goals-title-color", "&6&l") + "⚽ TOP 10 TORSCHÜTZEN ⚽";
|
||||
String valColor = holoColor("goals-value-color", "&4");
|
||||
String toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Siege anzeigen]";
|
||||
sb.append(title).append("\n");
|
||||
sb.append(sep).append("\n");
|
||||
var list = plugin.getStatsManager().getTopScorers(10);
|
||||
if (list.isEmpty()) {
|
||||
sb.append("§8Noch keine Statistiken vorhanden.");
|
||||
sb.append(labelColor).append("Noch 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");
|
||||
.append(" ").append(nameColor).append(s.name)
|
||||
.append(" ").append(valColor).append(s.goals).append(" ").append(labelColor).append("Tore");
|
||||
if (i < list.size() - 1) sb.append("\n");
|
||||
}
|
||||
}
|
||||
sb.append("\n§8§m══════════════════════§r");
|
||||
sb.append("\n§8§o[Rechtsklick → Siege anzeigen]");
|
||||
sb.append("\n").append(sep);
|
||||
sb.append("\n").append(toggle);
|
||||
|
||||
} else {
|
||||
sb.append("§2§l🏆 TOP 10 GEWINNER 🏆\n");
|
||||
sb.append("§8§m══════════════════════§r\n");
|
||||
String title = holoColor("wins-title-color", "&2&l") + "🏆 TOP 10 GEWINNER 🏆";
|
||||
String valColor = holoColor("wins-value-color", "&2");
|
||||
String toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Tore anzeigen]";
|
||||
sb.append(title).append("\n");
|
||||
sb.append(sep).append("\n");
|
||||
var list = plugin.getStatsManager().getTopWins(10);
|
||||
if (list.isEmpty()) {
|
||||
sb.append("§8Noch keine Statistiken vorhanden.");
|
||||
sb.append(labelColor).append("Noch 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("%)");
|
||||
.append(" ").append(nameColor).append(s.name)
|
||||
.append(" ").append(valColor).append(s.wins).append(" ").append(labelColor).append("Siege")
|
||||
.append(" ").append(labelColor).append("(").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]");
|
||||
sb.append("\n").append(sep);
|
||||
sb.append("\n").append(toggle);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildMatchText() {
|
||||
Game game = findRelevantGame();
|
||||
if (game == null) {
|
||||
return "§e§lLive Match\n§8§m────────────────\n§7Kein Spiel aktiv\n§8- : -\n§8--:--";
|
||||
private String buildCustomStatsText(HoloType showType) {
|
||||
List<String> template = customTextTemplates.get(showType);
|
||||
if (template == null || template.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String header = "§e§lLive Match";
|
||||
String separator = "§8§m────────────────";
|
||||
String phase = buildPhaseLabel(game);
|
||||
String score = "§c§l" + game.getRedScore() + " §r§7: §9§l" + game.getBlueScore();
|
||||
String timeLabel = game.isInInjuryTime()
|
||||
? "§c+" + formatInjury(game.getInjuryTimeBuffer()) + " §7(Nachspielzeit)"
|
||||
: "§e" + formatMainTime(game.getTimeLeft());
|
||||
String sep = holoColor("separator-color", "&8&m") + "══════════════════════" + ChatColor.RESET;
|
||||
String title, toggle;
|
||||
if (showType == HoloType.GOALS) {
|
||||
title = holoColor("goals-title-color", "&6&l") + "⚽ TOP 10 TORSCHÜTZEN ⚽";
|
||||
toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Siege anzeigen]";
|
||||
} else {
|
||||
title = holoColor("wins-title-color", "&2&l") + "🏆 TOP 10 GEWINNER 🏆";
|
||||
toggle = holoColor("toggle-color", "&8&o") + "[Rechtsklick → Tore anzeigen]";
|
||||
}
|
||||
String entries = buildStatsEntries(showType);
|
||||
|
||||
return header + "\n" + separator + "\n" + phase + "\n" + score + "\n" + timeLabel;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < template.size(); i++) {
|
||||
String line = template.get(i)
|
||||
.replace("{title}", title)
|
||||
.replace("{separator}", sep)
|
||||
.replace("{entries}", entries)
|
||||
.replace("{toggle}", toggle);
|
||||
sb.append(line);
|
||||
if (i < template.size() - 1) {
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildStatsEntries(HoloType showType) {
|
||||
String nameColor = holoColor("name-color", "&0");
|
||||
String labelColor = holoColor("label-color", "&8");
|
||||
StringBuilder entries = new StringBuilder();
|
||||
if (showType == HoloType.GOALS) {
|
||||
String valColor = holoColor("goals-value-color", "&4");
|
||||
var list = plugin.getStatsManager().getTopScorers(10);
|
||||
if (list.isEmpty()) {
|
||||
return labelColor + "Noch keine Statistiken vorhanden.";
|
||||
}
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
StatsManager.PlayerStats s = list.get(i).getValue();
|
||||
entries.append(medal(i + 1))
|
||||
.append(" ").append(nameColor).append(s.name)
|
||||
.append(" ").append(valColor).append(s.goals).append(" ").append(labelColor).append("Tore");
|
||||
if (i < list.size() - 1) {
|
||||
entries.append("\n");
|
||||
}
|
||||
}
|
||||
return entries.toString();
|
||||
}
|
||||
|
||||
String valColor = holoColor("wins-value-color", "&2");
|
||||
var list = plugin.getStatsManager().getTopWins(10);
|
||||
if (list.isEmpty()) {
|
||||
return labelColor + "Noch keine Statistiken vorhanden.";
|
||||
}
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
StatsManager.PlayerStats s = list.get(i).getValue();
|
||||
entries.append(medal(i + 1))
|
||||
.append(" ").append(nameColor).append(s.name)
|
||||
.append(" ").append(valColor).append(s.wins).append(" ").append(labelColor).append("Siege")
|
||||
.append(" ").append(labelColor).append("(").append(String.format("%.0f", s.getWinRate())).append("%)");
|
||||
if (i < list.size() - 1) {
|
||||
entries.append("\n");
|
||||
}
|
||||
}
|
||||
return entries.toString();
|
||||
}
|
||||
|
||||
private String buildMatchText() {
|
||||
String customText = buildCustomMatchText();
|
||||
if (customText != null) {
|
||||
return customText;
|
||||
}
|
||||
|
||||
Game game = findRelevantGame();
|
||||
String header = holoColor("match-header-color", "&e&l") + "Live Match";
|
||||
String sep = holoColor("separator-color", "&8&m") + "────────────────";
|
||||
if (game == null) {
|
||||
String lc = holoColor("label-color", "&8");
|
||||
return header + "\n" + sep + "\n§7Kein Spiel aktiv\n" + lc + "- : -\n" + lc + "--:--";
|
||||
}
|
||||
|
||||
String phase = buildPhaseLabel(game);
|
||||
String score = holoColor("match-score-red", "&c&l") + game.getRedScore()
|
||||
+ " §r§7: "
|
||||
+ holoColor("match-score-blue", "&9&l") + game.getBlueScore();
|
||||
String timeLabel = game.isInInjuryTime()
|
||||
? holoColor("match-injury-color", "&c") + "+" + formatInjury(game.getInjuryTimeBuffer()) + " §7(Nachspielzeit)"
|
||||
: holoColor("match-time-color", "&e") + formatMainTime(game.getTimeLeft());
|
||||
|
||||
return header + "\n" + sep + "\n" + phase + "\n" + score + "\n" + timeLabel;
|
||||
}
|
||||
|
||||
private String buildCustomMatchText() {
|
||||
List<String> template = customTextTemplates.get(HoloType.MATCH);
|
||||
if (template == null || template.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Game game = findRelevantGame();
|
||||
String header = holoColor("match-header-color", "&e&l") + "Live Match";
|
||||
String separator = holoColor("separator-color", "&8&m") + "────────────────";
|
||||
String phase = game == null ? "§7Kein Spiel aktiv" : buildPhaseLabel(game);
|
||||
String score, time;
|
||||
if (game == null) {
|
||||
String lc = holoColor("label-color", "&8");
|
||||
score = lc + "- : -";
|
||||
time = lc + "--:--";
|
||||
} else {
|
||||
score = holoColor("match-score-red", "&c&l") + game.getRedScore()
|
||||
+ " §r§7: "
|
||||
+ holoColor("match-score-blue", "&9&l") + game.getBlueScore();
|
||||
time = game.isInInjuryTime()
|
||||
? holoColor("match-injury-color", "&c") + "+" + formatInjury(game.getInjuryTimeBuffer()) + " §7(Nachspielzeit)"
|
||||
: holoColor("match-time-color", "&e") + formatMainTime(game.getTimeLeft());
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < template.size(); i++) {
|
||||
String line = template.get(i)
|
||||
.replace("{header}", header)
|
||||
.replace("{separator}", separator)
|
||||
.replace("{phase}", phase)
|
||||
.replace("{score}", score)
|
||||
.replace("{time}", time);
|
||||
sb.append(line);
|
||||
if (i < template.size() - 1) {
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildPhaseLabel(Game game) {
|
||||
@@ -317,6 +488,11 @@ public class FussballHologram {
|
||||
return "+" + safe + "s";
|
||||
}
|
||||
|
||||
private String holoColor(String key, String def) {
|
||||
String val = plugin.getConfig().getString("holograms." + key, def);
|
||||
return ChatColor.translateAlternateColorCodes('&', val != null ? val : def);
|
||||
}
|
||||
|
||||
private String medal(int rank) {
|
||||
return switch (rank) {
|
||||
case 1 -> "§6§l#1"; // Gold bleibt – hebt sich gut ab
|
||||
|
||||
@@ -21,6 +21,10 @@ import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -115,7 +119,7 @@ public class HologramManager implements Listener {
|
||||
type = FussballHologram.HoloType.GOALS;
|
||||
}
|
||||
|
||||
holograms.put(id, new FussballHologram(id, loc, type, plugin));
|
||||
holograms.put(id, new FussballHologram(id, loc, type, plugin, loadCustomTextTemplates(path)));
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[Hologram] " + holograms.size() + " Hologramme geladen.");
|
||||
@@ -123,10 +127,12 @@ public class HologramManager implements Listener {
|
||||
|
||||
/**
|
||||
* Erstellt ein neues Hologramm und speichert es in holograms.yml.
|
||||
* Falls die ID bereits existiert, wird das alte sauber entfernt.
|
||||
* Falls die ID bereits existiert, wird kein Hologramm ersetzt.
|
||||
*/
|
||||
public boolean createHologram(String id, Location loc, FussballHologram.HoloType type) {
|
||||
if (holograms.containsKey(id)) removeHologram(id);
|
||||
if (holograms.containsKey(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String path = "holograms." + id;
|
||||
holoConfig.set(path + ".world", loc.getWorld().getName());
|
||||
@@ -187,6 +193,7 @@ public class HologramManager implements Listener {
|
||||
|
||||
/** Alle Hologramme neu laden (z.B. nach /fb hologram reload) */
|
||||
public void reload() {
|
||||
plugin.reloadConfig();
|
||||
loadConfig();
|
||||
loadHolograms();
|
||||
// Für alle Online-Spieler sofort rendern
|
||||
@@ -195,6 +202,51 @@ public class HologramManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public FussballHologram getHologram(String id) {
|
||||
return holograms.get(id);
|
||||
}
|
||||
|
||||
public boolean setCustomText(String id, FussballHologram.HoloType type, List<String> lines) {
|
||||
FussballHologram holo = holograms.get(id);
|
||||
if (holo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> sanitized = new ArrayList<>(lines);
|
||||
holo.setCustomText(type, sanitized);
|
||||
holoConfig.set(getTextPath(id, type), sanitized);
|
||||
saveConfig();
|
||||
rerenderHologram(holo);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean resetCustomText(String id, FussballHologram.HoloType type) {
|
||||
FussballHologram holo = holograms.get(id);
|
||||
if (holo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
holo.clearCustomText(type);
|
||||
holoConfig.set(getTextPath(id, type), null);
|
||||
saveConfig();
|
||||
rerenderHologram(holo);
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<String> getCustomText(String id, FussballHologram.HoloType type) {
|
||||
FussballHologram holo = holograms.get(id);
|
||||
if (holo == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return holo.getCustomText(type);
|
||||
}
|
||||
|
||||
public void refreshAll() {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
holograms.values().forEach(h -> h.renderForPlayer(player));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render-Task ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -294,4 +346,25 @@ public class HologramManager implements Listener {
|
||||
|
||||
/** @return Set aller Hologramm-IDs (für Tab-Completion) */
|
||||
public Set<String> getHologramIds() { return holograms.keySet(); }
|
||||
|
||||
private Map<FussballHologram.HoloType, List<String>> loadCustomTextTemplates(String path) {
|
||||
Map<FussballHologram.HoloType, List<String>> templates = new EnumMap<>(FussballHologram.HoloType.class);
|
||||
for (FussballHologram.HoloType holoType : FussballHologram.HoloType.values()) {
|
||||
List<String> lines = holoConfig.getStringList(path + ".text." + holoType.name().toLowerCase());
|
||||
if (!lines.isEmpty()) {
|
||||
templates.put(holoType, lines);
|
||||
}
|
||||
}
|
||||
return templates;
|
||||
}
|
||||
|
||||
private String getTextPath(String id, FussballHologram.HoloType type) {
|
||||
return "holograms." + id + ".text." + type.name().toLowerCase();
|
||||
}
|
||||
|
||||
private void rerenderHologram(FussballHologram holo) {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
holo.renderForPlayer(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,34 @@ public class StatsManager {
|
||||
save();
|
||||
}
|
||||
|
||||
public boolean resetStats(UUID uuid) {
|
||||
boolean removed = cache.remove(uuid) != null;
|
||||
if (!removed) {
|
||||
return false;
|
||||
}
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public int resetAllStats() {
|
||||
int count = cache.size();
|
||||
if (count == 0) {
|
||||
return 0;
|
||||
}
|
||||
cache.clear();
|
||||
save();
|
||||
return count;
|
||||
}
|
||||
|
||||
public UUID findPlayerUuidByName(String playerName) {
|
||||
for (Map.Entry<UUID, PlayerStats> entry : cache.entrySet()) {
|
||||
if (entry.getValue().name != null && entry.getValue().name.equalsIgnoreCase(playerName)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 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());
|
||||
|
||||
@@ -17,6 +17,29 @@ import java.util.function.Consumer;
|
||||
* new UpdateChecker(this, RESOURCE_ID).getVersion(version -> { ... });
|
||||
*/
|
||||
public class UpdateChecker {
|
||||
/**
|
||||
* Vergleicht zwei Versionsnummern (z.B. "1.0.3" und "1.0.2").
|
||||
* Gibt >0 zurück, wenn v1 > v2, <0 wenn v1 < v2, 0 wenn gleich.
|
||||
*/
|
||||
public static int compareVersions(String v1, String v2) {
|
||||
String[] parts1 = v1.replace("v", "").split("\\.");
|
||||
String[] parts2 = v2.replace("v", "").split("\\.");
|
||||
int len = Math.max(parts1.length, parts2.length);
|
||||
for (int i = 0; i < len; i++) {
|
||||
int n1 = i < parts1.length ? parseIntSafe(parts1[i]) : 0;
|
||||
int n2 = i < parts2.length ? parseIntSafe(parts2[i]) : 0;
|
||||
if (n1 != n2) return Integer.compare(n1, n2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int parseIntSafe(String s) {
|
||||
try {
|
||||
return Integer.parseInt(s.replaceAll("[^0-9]", ""));
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final int resourceId;
|
||||
|
||||
@@ -61,6 +61,24 @@ atmosphere:
|
||||
enabled: true
|
||||
goal-fireworks: 5 # Anzahl Feuerwerke bei einem Tor (0 = deaktiviert)
|
||||
|
||||
# ── Hologramm-Farben ────────────────────────────────────────────────────────
|
||||
# Verwende &-Codes (z.B. &6 = Gold, &c = Rot, &9 = Blau, &l = Fett, &o = Kursiv)
|
||||
# Änderungen werden nach /fb hologram reload wirksam.
|
||||
holograms:
|
||||
goals-title-color: "&6&l" # Titel Tore-Hologramm (Standard: Gold + Fett)
|
||||
goals-value-color: "&4" # Tor-Anzahl in der Liste (Standard: Dunkelrot)
|
||||
wins-title-color: "&2&l" # Titel Siege-Hologramm (Standard: Dunkelgrün + Fett)
|
||||
wins-value-color: "&2" # Siege-Anzahl in der Liste (Standard: Dunkelgrün)
|
||||
name-color: "&0" # Spielername in der Liste (Standard: Schwarz)
|
||||
label-color: "&8" # Beschriftungen (Tore/Siege) (Standard: Dunkelgrau)
|
||||
separator-color: "&8&m" # Trennlinie (Standard: Durchgestrichen)
|
||||
toggle-color: "&8&o" # Umschalte-Hinweis (Standard: Kursiv Dunkelgrau)
|
||||
match-header-color: "&e&l" # Match-Header (Standard: Gelb + Fett)
|
||||
match-score-red: "&c&l" # Rot-Team Spielstand (Standard: Rot + Fett)
|
||||
match-score-blue: "&9&l" # Blau-Team Spielstand (Standard: Blau + Fett)
|
||||
match-time-color: "&e" # Spielzeit (Standard: Gelb)
|
||||
match-injury-color: "&c" # Nachspielzeit (Standard: Rot)
|
||||
|
||||
# ── Nachrichten (alle editierbar) ─────────────────────────────────────────────
|
||||
# Verfügbare Platzhalter je nach Kontext:
|
||||
# {player} = Spielername
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Fussball
|
||||
version: 1.0.1
|
||||
version: 1.0.3
|
||||
main: de.fussball.plugin.Fussball
|
||||
api-version: 1.21
|
||||
author: M_Viper
|
||||
@@ -19,5 +19,5 @@ permissions:
|
||||
commands:
|
||||
fussball:
|
||||
description: Hauptbefehl des Fußball-Plugins
|
||||
usage: /fussball <join|leave|spectate|team|list|stats|top|history|create|delete|setup|stop|setgk|dropball|debug|hologram>
|
||||
usage: "/fussball <join|leave|spectate|team|list|stats|top|history|create|delete|setup|stop|setgk|dropball|debug|hologram> | Admin: stats reset, hologram text/textpreview/textreset"
|
||||
aliases: [fb, soccer]
|
||||
Reference in New Issue
Block a user