Upload via GUI (52 Dateien)

This commit is contained in:
2026-11-12 16:59:18 +00:00
parent 208ec12e0c
commit 6dd60de671
37 changed files with 7789 additions and 559 deletions
+326 -4
View File
@@ -13,6 +13,8 @@ Es hat **drei Kalender** und ein **Geburtstags-Geschenk**:
Du musst **nichts programmieren**. Plugin hineinkopieren, Server starten, fertig.
Alles Weitere (Belohnungen, Texte, Zeiten) kannst du in normalen Textdateien ändern.
Zusätzlich bringt das Plugin eine **ganze Weihnachtswelt** mit: Schnee, Santa-Event, Cracker, Magic Tree, Schneemänner, Plätzchen, Countdown, Wichteln, Adventskranz, Schneeball-Spiel, Deko-Menü, Login-Serie, Quiz, Wunschzettel und einen Weihnachtsmarkt mit Portalen. Details stehen in [Kapitel 18](#18-weihnachts-module).
---
## Inhaltsverzeichnis
@@ -34,6 +36,7 @@ Alles Weitere (Belohnungen, Texte, Zeiten) kannst du in normalen Textdateien än
15. [Plugin aktualisieren](#15-plugin-aktualisieren)
16. [Probleme und ihre Lösung](#16-probleme-und-ihre-lösung)
17. [Häufige Fragen](#17-häufige-fragen)
18. [Weihnachts-Module](#18-weihnachts-module)
---
@@ -41,10 +44,11 @@ Alles Weitere (Belohnungen, Texte, Zeiten) kannst du in normalen Textdateien än
- Einen Minecraft-Server mit **Spigot** oder **Paper** (oder einem Fork davon) ab **Version 1.20**.
- **Java 21** auf dem Server. Ältere Java-Versionen starten das Plugin nicht.
- Optional: das Plugin **PlaceholderAPI**, wenn du Werte des Kalenders auf Scoreboards oder Hologrammen anzeigen willst.
- Optional: das Plugin **PlaceholderAPI**, wenn du Werte des Kalenders und der Weihnachts-Module auf Scoreboards oder Hologrammen anzeigen willst.
- Optional: **WorldGuard**, wenn Schnee nur in bestimmten Regionen fallen soll.
- Optional: eine **MySQL-Datenbank**, wenn mehrere Server (z. B. mit BungeeCord) sich die Daten teilen sollen. Für einen einzelnen Server brauchst du das **nicht**.
> Hinweis: Das Plugin wurde für Minecraft 1.20 gebaut. Auf neueren Versionen sollte es laufen, teste es aber vor dem Einsatz auf deinem Live-Server kurz aus.
> Hinweis: Das Plugin wird gegen die Spigot-API 1.21.10 gebaut und läuft auf Paper 26.2 und Minecraft 26.3. Für Santa als Spielermodell brauchst du Minecraft 1.21.9 oder neuer. Teste das Plugin vor dem Einsatz auf deinem Live-Server kurz aus und mach vorher eine Sicherung.
---
@@ -53,7 +57,7 @@ Alles Weitere (Belohnungen, Texte, Zeiten) kannst du in normalen Textdateien än
1. **Server ausschalten** (oder einfach weitermachen, wenn du später neu startest).
2. Die Datei **`adventskalender-1.0.3.jar`** in den Ordner **`plugins`** deines Servers kopieren.
3. **Server starten.**
4. Das Plugin legt automatisch den Ordner **`plugins/Adventskalender`** an. Darin liegen jetzt die Dateien `config.yml`, `messages_de.yml` und `messages_en.yml`.
4. Das Plugin legt automatisch den Ordner **`plugins/Adventskalender`** an. Darin liegen jetzt die Dateien `config.yml`, `messages_de.yml`, `messages_en.yml`, `christmas.yml` und `christmas_messages_de.yml` (bzw. `_en`).
5. **Fertig.** Im Spiel `/ak` eingeben und der Kalender geht auf.
Wenn du in der Konsole die Meldung **„Adventskalender erfolgreich gestartet!“** siehst, hat alles geklappt.
@@ -140,6 +144,8 @@ Rechte vergibst du mit einem Rechte-Plugin (z. B. LuckPerms).
| `adventskalender.birthday` | Geburtstag eintragen und Geschenk abholen | **alle Spieler** |
| `adventskalender.admin` | Alle Admin-Befehle und Update-Hinweis beim Beitritt | nur Operatoren |
Die Rechte der Weihnachts-Module (`xmas.use`, `xmas.admin`, `christmasplus.*`) stehen in [Kapitel 18](#rechte-der-weihnachts-module).
---
## 6. Die Dateien des Plugins
@@ -151,7 +157,10 @@ Alles liegt in `plugins/Adventskalender/`:
| `config.yml` | **Einstellungen und Belohnungen.** Hier änderst du, was es zu gewinnen gibt und wann die Kalender laufen. |
| `messages_de.yml` | Alle **deutschen Texte** (Titel, Nachrichten, Namen der Geschenke) |
| `messages_en.yml` | Alle **englischen Texte** |
| `christmas.yml` | **Einstellungen der Weihnachts-Module** (Schnee, Santa, Baum, Quiz, Markt, Portale und mehr) |
| `christmas_messages_de.yml` / `christmas_messages_en.yml` | **Texte der Weihnachts-Module** |
| `data.yml` | Hier speichert das Plugin, **wer was geöffnet hat** und die Geburtstage. Diese Datei **nicht löschen**, sonst fangen alle von vorne an. |
| `trees.yml`, `wichteln.yml`, `wishes.yml`, `portals.yml` u. a. | Daten der Weihnachts-Module (siehe [Kapitel 18](#gut-zu-wissen)). Nicht löschen. |
### So bearbeitest du die Dateien richtig
@@ -476,6 +485,10 @@ Schreibe den Namen des Kalenders vor den Platzhalter:
| `%ak_birthday%` | Eingetragenes Datum (z. B. `15.03.`) oder `-` |
| `%ak_birthday_active%` | „Ja“, wenn der Spieler gerade sein Geschenk abholen kann, sonst „Nein“ |
### Weihnachts-Module
Countdown, Advent, Bäume, Schneeball-Punkte, Login-Serie und Wichtel-Partner haben eigene Platzhalter mit dem Präfix `xmas`, zum Beispiel `%xmas_days_left%`. Die ganze Liste steht in [Kapitel 18](#platzhalter-für-die-weihnachts-module).
---
## 15. Plugin aktualisieren
@@ -492,12 +505,14 @@ Deine **Daten** (`data.yml`) und deine **Einstellungen** bleiben erhalten.
Deine alte `config.yml` und die Sprachdateien werden bei einem Update **nicht überschrieben**. Neue Funktionen laufen trotzdem mit den Standardwerten, du siehst die neuen Einstellungen aber nicht in der Datei. Willst du sie sehen und ändern:
1. Server ausschalten.
2. `config.yml` umbenennen, z. B. in `config_alt.yml`. (Dasselbe mit den `messages_...yml`-Dateien, falls nötig.)
2. `config.yml` umbenennen, z. B. in `config_alt.yml`. (Dasselbe mit den `messages_...yml`-Dateien und, falls nötig, mit `christmas.yml` und `christmas_messages_...yml`.)
3. Server starten. Das Plugin erstellt eine frische, vollständige Datei.
4. Deine Änderungen aus `config_alt.yml` in die neue Datei übertragen.
Die `data.yml` bleibt dabei **immer unangetastet**.
> **Beim Update auf 1.0.3:** Die Dateien `christmas.yml` und `christmas_messages_...yml` werden beim ersten Start neu angelegt. Fehlende Einstellungen und Texte ergänzt das Plugin später selbst mit den Standardwerten. Texte, die du **geändert** hast, bleiben erhalten. Neue Wörter oder Korrekturen in den Standardtexten siehst du nur, wenn du die Textdatei umbenennst und neu erzeugen lässt.
### Update-Hinweis
Erscheint eine neue Version auf SpigotMC, schreibt das Plugin eine Warnung in die Konsole. Admins bekommen beim Einloggen eine klickbare Nachricht mit dem Download-Link.
@@ -554,4 +569,311 @@ Dann haben alle Spieler wieder alle Türchen und Geburtstage sind weg. Spiele vo
---
## 18. Weihnachts-Module
Ab Version 1.0.3 bringt das Plugin zusätzlich viele Weihnachts-Funktionen mit. Du brauchst dafür **keine weiteren Plugins** (kein Christmas+, kein X-Mas, kein Lib's Disguises).
**Wichtig zu wissen:**
- Alle Einstellungen stehen in **`plugins/Adventskalender/christmas.yml`**.
- Alle Texte stehen in **`christmas_messages_de.yml`** (Deutsch) und **`christmas_messages_en.yml`** (Englisch). Welche benutzt wird, hängt von `general.language` in der `config.yml` ab.
- Mit `/ak admin reload` oder `/xmas reload` lädst du alles neu, ohne den Server neu zu starten.
- Befehle: **`/christmas`** (Kurzformen `/xmas`, `/ch`, `/chr`, `/christmasplus`) und **`/cracker`**.
- Items schreibst du überall so: `MATERIAL:Anzahl`, zum Beispiel `DIAMOND:2`. Zusätzlich gehen `cookie:<id>:<Anzahl>`, `cracker:<id>:<Anzahl>`, `crystal:<Anzahl>` und `present:<Anzahl>` (Geschenk-Kopf).
- Mit `/xmas help` zeigt dir das Plugin im Spiel alle Befehle.
| Funktion | Kurz erklärt |
|---|---|
| [Schnee](#schnee) | Schneefall-Partikel, jeder schaltet ihn selbst um |
| [Santa-Event](#santa-claus-event) | Santa fliegt ein, landet und wirft Geschenke |
| [Cracker](#cracker) | Items, die man öffnet und eine Belohnung bekommt |
| [Magic Tree](#magic-tree) | Ein Baum, der mit Items wächst und Geschenke gibt |
| [Schneemänner](#schneemänner) | Deko-Schneemänner und selbst gebaute, die Geschenke geben |
| [Plätzchen](#plätzchen-und-lebkuchen) | Essbare Items mit Effekten |
| [Countdown](#countdown-bis-heiligabend) | Bossbar und Nachricht „Noch X Tage“ |
| [Wichteln](#wichteln) | Zufällig ziehen, Geschenke verteilen |
| [Adventskranz](#adventskranz) | Vier Kerzen, die an den Adventssonntagen angehen |
| [Schneeball-Spiel](#schneeball-spiel) | Arena mit Punkten und Belohnungen |
| [Deko-Menü](#deko-menü) | Deko per Klick aufstellen |
| [Login-Serie](#login-serie) | Belohnungen für tägliches Einloggen |
| [Weihnachtsgruß](#weihnachtsgruß-und-neujahr) | Chat-Gruß und Feuerwerk an besonderen Tagen |
| [Kamin-Socken](#kamin-socken) | Socke aufhängen, am 25.12. liegt etwas darin |
| [Nikolausmütze](#nikolausmütze) | Rote Mütze als Item |
| [Quiz](#weihnachtsquiz) | Frage im Chat, der Schnellste gewinnt |
| [Wunschzettel](#wunschzettel) | Spieler wünschen sich etwas, Admins erfüllen es |
| [Weihnachtsmarkt und Portale](#weihnachtsmarkt-und-portale) | Händler-Stände und Portale dorthin |
### Schnee
- `/xmas snow` schaltet den Schnee **für dich selbst** an oder aus.
- Einstellungen unter `snow:`: Stärke (`level` 1 bis 5), Welten sperren (`disable-in-worlds`), Standard an/aus (`default-on`).
- Mit `enable-only-in-regions: true` und einer Liste unter `regions:` gibt es Schnee nur in bestimmten **WorldGuard**-Regionen. Dafür muss WorldGuard installiert sein.
- Bei vielen Spielern kannst du `display-task-interval` erhöhen (z. B. 10), dann gibt es weniger Last.
### Santa-Claus-Event
| Befehl | Was er macht |
|---|---|
| `/xmas santa start [Zeit] [Welt x y z]` | Santa an deiner Stelle oder an den Koordinaten starten |
| `/xmas santa start [Zeit] random` | Santa an einem zufälligen, festgelegten Ort starten |
| `/xmas santa stop` | Santa beenden |
| `/xmas santa teleport` | Zu Santa teleportieren |
Zeit geht als `300`, `30s`, `5m` oder `1h`. Ohne Angabe gilt `santa.duration`.
- **Anflug:** Santa kommt mit Schlitten und zwei Rentieren (Rudolph, Blitzen) eingeflogen und wirft Geschenke ab, bevor er landet. Abschalten mit `sleigh.enable: false`.
- **Melodie:** Wenn Santa kommt, spielt eine Weihnachtsmelodie mit Glocken (Notenblock-Klänge, keine Dateien nötig). Einstellbar unter `santa.melody` (`enable`, `volume`, `tempo`, `everyone`).
- **Geschenke:** `drop-interval` (Sekunden), `drop-chance` (in %), `radius` und die Liste `gifts`. Cracker gibst du als `cracker:cracker:1` an.
- **Spazieren:** Mit `walk-radius` läuft Santa herum und hinterlässt eine Schneespur (`snow-trail`), die wieder verschwindet.
- **Aussehen:** Ab **Minecraft 1.21.9** ist Santa ein Spielermodell, davor ein Rüstungsständer. Einen eigenen Skin trägst du bei `skin-url` (Textur-Adresse von `textures.minecraft.net`) oder `skin-player` (Spielername) ein. Mit `model: ARMOR_STAND` erzwingst du den Rüstungsständer.
- **Zufällige Orte:** Unter `locations:` trägst du Welt und Koordinaten ein (siehe Beispiel in der Datei).
### Cracker
| Befehl | Recht |
|---|---|
| `/cracker list` | `christmasplus.cracker.list` |
| `/cracker get [id] [Anzahl]` | `christmasplus.cracker.get` (mehr als 1 zusätzlich `christmasplus.cracker.get.multiple`) |
| `/cracker give <Spieler> [id] [Anzahl]` | `christmasplus.cracker.give` |
Rechtsklick öffnet einen Cracker. Die Belohnungen stehen unter `crackers.<id>.rewards`. Jeder Eintrag hat eine `chance` (Gewicht) und `item`, `command` (Befehl, `%player%` wird ersetzt) oder `permission`.
```yaml
crackers:
cracker:
material: FIREWORK_STAR
name: "&c&lWeihnachts-Cracker"
rewards:
- { chance: 40, item: "COOKIE:8" }
- { chance: 10, command: "give %player% diamond 3" }
```
### Magic Tree
So funktioniert der Baum:
1. **Kristall besorgen:** Du bekommst ihn mit `/xmas give <Spieler> [Anzahl]`, oder du stellst ihn in der **Werkbank** her: **Diamanten oben, links, rechts und unten, in der Mitte ein Smaragd**. Abschalten mit `magic-tree.crystal-recipe: false`.
2. **Pflanzen:** Rechtsklick mit dem Kristall auf einen **Setzling**.
3. **Füttern:** Wirf die Items, die der Baum braucht, **neben den Baum**. Er nimmt sie auf (nur vom Besitzer). Mit Rechtsklick auf den Baum siehst du, was noch fehlt.
4. **Wachsen:** Sind alle Items da, kommt eine Meldung und ein Ring leuchtet am Boden. Rechtsklick auf den Baum, dann wächst er eine Stufe.
5. **Stufen:** Setzling, kleiner Baum, Baum, Magic Tree. Items und Wartezeiten stehen unter `magic-tree.levels`.
6. **Geschenke:** Ab dem kleinen Baum erscheinen Geschenk-Köpfe rund um den Baum. Rechtsklick oder Abbauen öffnet sie.
Standard-Kosten:
| Von → nach | Items |
|---|---|
| Setzling → kleiner Baum | 1 Diamant, 10 Redstone, 1 Enderperle |
| Kleiner Baum → Baum | 3 Diamanten, 5 Goldbarren, 10 Blaze-Pulver, 30 Schneebälle |
| Baum → Magic Tree | 5 Diamanten, 3 Smaragde, 8 Goldnuggets, 16 Glowstone-Staub |
Weitere Dinge:
- **Abbauen:** Den Stamm **zweimal** abbauen. Dabei geht der Fortschritt verloren.
- **Schutz:** Bäume und Geschenke sind vor Kolben, Explosionen, Feuer und Laubzerfall geschützt.
- **Limit:** `tree-limit` Bäume pro Spieler.
- **Glück:** Mit `luck.enabled: true` kann ein Geschenk leer sein (`luck.chance` in %).
- **Eigene Geschenke:** `/xmas addhand` fügt das Item in deiner Hand hinzu (gespeichert in `gifts.yml`). `/xmas gifts` lässt sofort Geschenke unter allen Bäumen erscheinen.
- **Event beenden:** `/xmas end` oder automatisch am Datum `holiday-ends.date`. Danach geben Bäume keine Geschenke mehr, Spieler können sie abbauen und bekommen ihre Items zurück (`resource-back`).
- **Partikel:** Spirale, leuchtende Ringe, Lichter, fallende Schneeflocken und ein Stern. Die Stärke stellst du mit `magic-tree.particles.intensity` ein (0,2 bis 5,0, Standard 2,0), abschalten mit `particles.enable: false`.
### Schneemänner
- **Deko-Schneemann (Admin):** `/xmas snowman spawn [stand|walk] [Name]` stellt einen unverwundbaren Schneemann auf: `stand` = steht still, `walk` = läuft im Umkreis um seinen Startpunkt herum (`snowman.deco.walk-radius`, Standard 8 Blöcke). Ohne Angabe gilt `snowman.deco.walk` (Standard `false` = stehend). `/xmas snowman remove` entfernt den nächsten. Die Schnee-Spur laufender Schneemänner verschwindet nach `snowman.trail-fade-seconds` Sekunden wieder (`snowman.trail`: `fade`, `none` oder `keep`).
- **Selbst gebaute:** Wer einen Schneemann baut (zwei Schneeblöcke und ein Kürbis), bekommt einen Namen wie „Steves Schneemann“. Er wirft alle `gift-interval` Sekunden ein kleines Geschenk ab. Es zählen die ältesten `max-per-player` Schneemänner eines Spielers.
- Einstellungen unter `snowman:`.
### Plätzchen und Lebkuchen
Drei Sorten sind eingebaut: `zimtstern` (Tempo), `lebkuchen` (Regeneration) und `bratapfel` (Absorption).
- `/xmas cookie list` zeigt alle Sorten, `/xmas cookie give <Spieler> <id> [Anzahl]` verteilt sie.
- Als Belohnung schreibst du `cookie:zimtstern:2` in Cracker, Santa-Geschenke, Kalender-Geschenke und so weiter.
- Eigene Sorten legst du unter `cookies:` an. Effekte im Format `"effekt:Sekunden:Stufe"`, zum Beispiel `"speed:30:1"`.
### Countdown bis Heiligabend
- Beim Join erscheint „Noch X Tage bis Heiligabend“, dazu eine **Bossbar**. `/xmas countdown` schaltet die Leiste für dich um.
- Einstellungen unter `countdown:`: Zieldatum (`target`, Monat-Tag), ab wie vielen Tagen vorher die Anzeige beginnt (`show-days-before`, Standard 60), Bossbar und Join-Nachricht.
- Für Scoreboards und Hologramme gibt es Platzhalter, siehe [Platzhalter](#platzhalter-für-die-weihnachts-module).
### Wichteln
| Befehl | Wer | Was |
|---|---|---|
| `/xmas wichteln open` | Admin | Wichteln eröffnen, alle werden informiert |
| `/xmas wichteln join` / `leave` | Spieler | Mitmachen / austreten |
| `/xmas wichteln draw` | Admin | Zufällig ziehen (mindestens 3 Teilnehmer, niemand zieht sich selbst) |
| `/xmas wichteln who` | Spieler | Wen du beschenkst |
| `/xmas wichteln gift` | Spieler | Item in der Hand als Geschenk ablegen (ein anderes Item ersetzt es) |
| `/xmas wichteln deliver` | Admin | Alle Geschenke verteilen |
| `/xmas wichteln status` | alle | Phase und Teilnehmerzahl |
| `/xmas wichteln reset` | Admin | Alles zurücksetzen |
Wer beim Verteilen offline ist, bekommt das Geschenk beim nächsten Join. Mit `wichteln.reveal-giver: true` sieht der Empfänger, von wem es kommt.
### Adventskranz
- `/xmas wreath create` baut einen Kranz an der Stelle, auf die du schaust. `/xmas wreath remove` entfernt ihn.
- An den **vier Adventssonntagen** gehen die Kerzen nacheinander an (die dritte ist rosa), mit einer Meldung an alle. Bis zum Datum `wreath.burn-until` (Standard 26.12.) brennen sie.
- Der Kranz ist vor Abbauen geschützt (außer für Admins).
### Schneeball-Spiel
1. Stell dich in die Mitte der Fläche und gib `/xmas snowball arena <Radius>` ein.
2. `/xmas snowball start [Zeit]` startet eine Runde. `/xmas snowball stop` beendet sie.
3. In der Arena bekommt jeder **nur während einer laufenden Runde** immer mindestens 16 Schneebälle (`refill-amount`).
4. Wer einen anderen Spieler in der Arena trifft, bekommt Punkte (`points-per-hit`). Die Punkte stehen in der Actionbar.
5. Am Ende bekommen die **Top 3** ihre Belohnung (`snowball.rewards`). `/xmas snowball top` zeigt die Rangliste.
### Deko-Menü
`/xmas deco` (Admin) öffnet ein Menü: Geschenk, Zuckerstange, Laternenpfahl, Weihnachtsbaum, Kranz, Schneemann, Lichterkette und Schneehaufen. Ein Klick stellt die Deko dort auf, wohin du schaust. `/xmas deco undo` macht die letzte rückgängig.
### Login-Serie
Wer an mehreren Tagen hintereinander einloggt, bekommt steigende Belohnungen. Die Serie zählt im Zeitraum `from` bis `until` (Standard 1.12. bis 31.12.). Nach `cycle` Tagen (Standard 7) beginnen die Belohnungen von vorn. Die Belohnungen stehen unter `login-streak.rewards`.
### Weihnachtsgruß und Neujahr
Am 24.12., 25.12., 31.12. und 1.1. gibt es einmal pro Tag einen Gruß im Chat. Wer an diesem Tag joint, bekommt ihn ebenfalls. Dazu gibt es auf Wunsch ein kleines **Feuerwerk** über dem Spieler (ohne Schaden). Einstellungen unter `greetings:`: Welcher Tag welchen Text bekommt, `join-fireworks`, `fireworks-on-broadcast`.
### Kamin-Socken
- `/xmas stocking` hängt ab dem 1.12. (`hang-from`) eine Socke auf.
- Ab dem 25.12. (`open-on`) bekommt jeder mit aufgehängter Socke automatisch sein Geschenk, oder er holt es mit demselben Befehl ab. Bis `until` (31.12.).
- Die Belohnungen stehen unter `stocking.rewards`.
- **Socke an die Wand hängen:** `/xmas stocking place` hängt eine rote Socke an die Wand, auf die du schaust (höchstens `stocking.max-placed-per-player`, Standard 3). `/xmas stocking remove` entfernt die nächste eigene.
- **Santas Socken-Flug:** Am Weihnachtstag und dem Tag danach (`stocking-flight.from` / `until`, Standard 24.12. bis 25.12.) fliegt Santa von aufgestellter Socke zu aufgestellter Socke und legt unter jeder ein Geschenk ab. Das Geschenk öffnest du mit Rechtsklick; standardmäßig nur der Besitzer der Socke (`owner-only`). Der Besitzer bekommt eine Nachricht mit den Koordinaten, wenn er online ist.
- Einstellungen unter `stocking-flight`: `enable` (an/aus), `interval-minutes` (Abstand zwischen Flügen), `max-stockings-per-flight`, `speed`, `rewards` (leer = `stocking.rewards`).
- Admins können den Flug mit `/xmas stocking flight` sofort testen (auch außerhalb des Zeitraums). Es fliegen nur Socken in geladenen Chunks.
### Nikolausmütze
Eine rote Mütze als Item. Beim Join (in der Weihnachtszeit) erscheint ein **anklickbares Angebot** im Chat. `/xmas hat` gibt sie dir direkt. Mit `santa-hat.auto-equip: true` wird sie automatisch aufgesetzt. Zeitraum: `santa-hat.from` bis `until`.
### Weihnachtsquiz
- `/xmas quiz start [Nr]` stellt eine Frage im Chat (ohne Nummer eine zufällige). Wer **zuerst richtig antwortet**, gewinnt die Belohnung. Die Antwort tippt man einfach in den Chat und sie ist dann für andere nicht sichtbar.
- `/xmas quiz skip` beendet eine laufende Frage, `/xmas quiz count` zeigt die Zahl der Fragen.
- Groß- und Kleinschreibung und Umlaute (ä oder ae) sind egal.
- Mit `quiz.auto.enable: true` kommt alle `interval-minutes` Minuten automatisch eine Frage, wenn mindestens `min-players` Spieler online sind.
- Eigene Fragen trägst du unter `quiz.questions` ein:
```yaml
quiz:
questions:
- { q: "Wie viele Kerzen hat ein Adventskranz?", a: ["4", "vier"] }
- { q: "Frage?", a: ["Antwort"], reward: [ { item: "DIAMOND:1" } ] }
```
### Wunschzettel
- **Spieler:** `/xmas wish <Text>` schreibt einen Wunsch (maximal `max-open-per-player` offene, `max-length` Zeichen). `/xmas wish list` zeigt sie, `/xmas wish clear` löscht sie.
- **Admins:** `/xmas wishes` öffnet das Menü mit allen offenen Wünschen.
- **Linksklick** mit einem Item in der Hand: Wunsch erfüllt, das Item geht als Geschenk an den Spieler.
- **Linksklick ohne Item:** Es passiert nichts, du bekommst einen Hinweis.
- **Shift + Linksklick:** Wunsch ohne Geschenk erfüllen.
- **Rechtsklick:** Wunsch löschen.
- Ist der Spieler offline, bekommt er Geschenk und Meldung beim nächsten Join. Admins werden bei einem neuen Wunsch benachrichtigt.
### Weihnachtsmarkt und Portale
**Marktplatz und Stände (Admin):**
| Befehl | Was er macht |
|---|---|
| `/xmas market set` | **Marktplatz** an deiner Stelle festlegen (Ziel der Portale) |
| `/xmas market unset` | Marktplatz aufheben, Portale führen dann nirgendwohin |
| `/xmas market tp` | Zum Marktplatz teleportieren |
| `/xmas market spawn <id>` | Marktstand aufstellen (mitgeliefert: `plaetzchen`, `deko`) |
| `/xmas market remove` | Nächsten Stand entfernen |
| `/xmas market list` | Stand-IDs anzeigen |
Ein Stand ist ein Händler (Villager), bei dem Spieler Items tauschen. Die Angebote stehen unter `market.stalls.<id>.trades`. Nach einer Änderung lädst du mit `/xmas reload` neu.
```yaml
market:
stalls:
plaetzchen:
name: "&6Plätzchen-Stand"
profession: BUTCHER
trades:
- { give: "cookie:zimtstern:2", price: ["EMERALD:1"] }
```
**Portale (Spieler):** Sie funktionieren **nur, wenn ein Admin den Marktplatz festgelegt hat**.
- `/xmas portal create`: Schau auf den Boden und gib den Befehl ein. Das Plugin baut einen Torbogen aus **Schneeblöcken** (10 Stück, werden verbraucht, Admins zahlen nichts). Er braucht 5 Blöcke Breite und Höhe sowie festen Boden.
- Pro Spieler ist ein Portal erlaubt (`portal.limit-per-player`).
- Wer in der Öffnung steht und `teleport-delay-seconds` (Standard 3) wartet, landet auf dem Markt. Danach gilt eine Pause (`cooldown-seconds`).
- `/xmas market back` oder der Klick-Text im Chat führt zurück vor das Portal.
- `/xmas portal remove` baut das Portal ab (der Besitzer bekommt die Schneeblöcke zurück), `/xmas portal info` zeigt Kosten und Anzahl.
- Portale sind vor Abbauen, Kolben und Explosionen geschützt.
### Zusätze für den Adventskalender
Unter `rewards.<Tag>` in der `config.yml` sind zusätzlich möglich: `commands`, `permissions`, `multiple` (mehrere Geschenke), `random` (Zufallsgeschenk mit Gewichtung), `cracker`, `crystal` und `groups` (extra Geschenke für Gruppen wie Donor, über ein Recht).
```yaml
rewards:
3:
material: DIAMOND
amount: 2
multiple: ["COOKIE:8", "GOLD_INGOT:3"]
random:
- { chance: 50, item: "EMERALD:2" }
- { chance: 50, command: "give %player% apple 5" }
groups:
donor:
permission: "christmasplus.donor"
multiple: ["DIAMOND:3"]
```
- `/ac test` schaltet für dich den Testmodus um (alle Tage öffnen), Recht `christmasplus.adventcalendar.test`.
- `/ac reset <Spieler> <Tag|all>` setzt zurück, Recht `christmasplus.adventcalendar.reset`.
- Mit `calendar-extras.restrict-past-days: true` sind vergangene Tage nur mit `christmasplus.adventcalendar.claimpast` öffenbar.
- Mit `calendar-extras.broadcast-claim: true` sehen alle, wer ein Türchen geöffnet hat.
- Für `permissions` als Belohnung trägst du in der `christmas.yml` bei `permission-command` einen Befehl ein, z. B. für LuckPerms `lp user %player% permission set %permission% true`.
### Platzhalter für die Weihnachts-Module
Mit **PlaceholderAPI** (Präfix `xmas`):
| Platzhalter | Zeigt |
|---|---|
| `%xmas_days_left%` | Tage bis Heiligabend |
| `%xmas_countdown%` | Der Countdown-Text |
| `%xmas_advent%` | Aktueller Advent (0 bis 4) |
| `%xmas_trees%` | Magic Trees des Spielers |
| `%xmas_snowball_points%` | Punkte im Schneeball-Spiel |
| `%xmas_snowball_rank%` | Platz im Schneeball-Spiel |
| `%xmas_streak%` | Aktuelle Login-Serie |
| `%xmas_streak_best%` | Beste Login-Serie |
| `%xmas_wichteln%` | Name des Wichtel-Partners |
### Rechte der Weihnachts-Module
| Recht | Wofür | Standard |
|---|---|---|
| `xmas.use` | Bäume pflanzen, Portale bauen, Wichteln, Wunschzettel | alle |
| `xmas.admin` | Alle Admin-Befehle der Weihnachts-Module | Operatoren |
| `christmasplus.christmas.snow` | `/xmas snow` | alle |
| `christmasplus.christmas.santa` | Santa-Befehle | Operatoren |
| `christmasplus.christmas.reload` | `/xmas reload` | Operatoren |
| `christmasplus.cracker.list` / `get` / `get.multiple` / `give` | Cracker-Befehle | Operatoren |
| `christmasplus.adventcalendar.test` | `/ac test` | Operatoren |
| `christmasplus.adventcalendar.reset` | `/ac reset` | Operatoren |
| `christmasplus.adventcalendar.claimpast` | Vergangene Tage öffnen | alle |
### Gut zu wissen
- Die Weihnachts-Module nutzen eigene Daten-Dateien im Plugin-Ordner (`trees.yml`, `gifts.yml`, `wichteln.yml`, `wreaths.yml`, `snowball.yml`, `streaks.yml`, `stockings.yml`, `wishes.yml`, `market.yml`, `portals.yml`, `greetings.yml`). Diese nicht löschen, wenn die Daten erhalten bleiben sollen.
- **Bäume, Wichteln und Co. werden lokal gespeichert**, nicht in MySQL. Mehrere Server teilen sich diese Daten nicht.
- Das Plugin ist gegen die Spigot-API **1.21.10** gebaut. Auf Servern unter 1.20 wurde es nicht getestet.
- Das Spielermodell für Santa braucht Minecraft **1.21.9** oder neuer, sonst nutzt das Plugin einen Rüstungsständer.
- Quizfragen, Cracker- und Plätzchen-Namen stehen in der `christmas.yml` auf Deutsch und lassen sich dort ändern.
---
*Autor: M_Viper • Frohe Weihnachten, schaurig-schönes Halloween, frohe Ostern und alles Gute zum Geburtstag! 🎄🎃🐰🎂*
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

@@ -16,8 +16,25 @@ public class AdventCommand implements CommandExecutor, TabCompleter {
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
Adventskalender plugin = Adventskalender.getInstance();
// Christmas+-Kurzbefehle: /ac test und /ac reset <Spieler> <Tag|all>
boolean elevated = false;
if (args.length == 1 && args[0].equalsIgnoreCase("test")
&& sender instanceof Player tp && tp.hasPermission("christmasplus.adventcalendar.test")) {
boolean on = de.mviper.adventskalender.christmas.CalendarPerks.toggle(tp);
var xm = de.mviper.adventskalender.christmas.ChristmasManager.get();
tp.sendMessage(xm != null ? xm.msg(on ? "calendar-test-on" : "calendar-test-off") : (on ? "Testmodus an" : "Testmodus aus"));
return true;
}
if (args.length == 3 && args[0].equalsIgnoreCase("reset")
&& sender.hasPermission("christmasplus.adventcalendar.reset")) {
args = args[2].equalsIgnoreCase("all")
? new String[]{"admin", "reset", args[1]}
: new String[]{"admin", "resetday", args[1], args[2]};
elevated = true;
}
if (args.length > 0 && args[0].equalsIgnoreCase("admin")) {
if (!sender.hasPermission("adventskalender.admin")) {
if (!elevated && !sender.hasPermission("adventskalender.admin")) {
sender.sendMessage(LanguageManager.getMessage("messages.no_permission"));
return true;
}
@@ -29,6 +46,7 @@ public class AdventCommand implements CommandExecutor, TabCompleter {
LanguageManager.setup();
DataHandler.setup();
AdventInventory.initCache();
de.mviper.adventskalender.christmas.ChristmasManager.reload();
sender.sendMessage(LanguageManager.getMessage("messages.admin.reload_success"));
return true;
}
@@ -95,12 +95,23 @@ public class AdventListener implements Listener {
Adventskalender plugin = Adventskalender.getInstance();
if (calendar.currentDay() < day) {
boolean tester = de.mviper.adventskalender.christmas.CalendarPerks.isTester(player);
if (calendar.currentDay() < day && !tester) {
player.sendMessage(LanguageManager.getMessage("messages.day_not_available", calendar));
player.playSound(player.getLocation(), Sound.BLOCK_CHEST_LOCKED, 1f, 1f);
return;
}
// Christmas+: vergangene Tage nur mit Recht oeffnen (optional)
var xm = de.mviper.adventskalender.christmas.ChristmasManager.get();
if (xm != null && !tester && calendar.currentDay() > day
&& xm.cfg().getBoolean("calendar-extras.restrict-past-days", false)
&& !player.hasPermission("christmasplus.adventcalendar.claimpast")) {
player.sendMessage(xm.msg("past-day-restricted"));
player.playSound(player.getLocation(), Sound.BLOCK_CHEST_LOCKED, 1f, 1f);
return;
}
// Belohnung vorab prüfen: Bei Konfigurationsfehlern wird der Tag nicht verbraucht
ItemStack reward = buildReward(calendar, day);
if (reward == null) {
@@ -158,6 +169,11 @@ public class AdventListener implements Listener {
if (RewardUtil.give(player, reward)) {
player.sendMessage(LanguageManager.getMessage("messages.inventory_full", calendar));
}
de.mviper.adventskalender.christmas.ChristmasExtras.give(player, calendar.rewardsPath() + "." + day);
var xm = de.mviper.adventskalender.christmas.ChristmasManager.get();
if (xm != null && xm.cfg().getBoolean("calendar-extras.broadcast-claim", false)) {
Bukkit.broadcastMessage(xm.msg("calendar-claimed-broadcast", "%player%", player.getName(), "%day%", String.valueOf(day)));
}
player.sendMessage(LanguageManager.getMessage("messages.reward_received", calendar).replace("%day%", String.valueOf(day)));
}
}
@@ -50,11 +50,14 @@ public final class Adventskalender extends JavaPlugin {
new AdventCalendarExpansion().register();
}
de.mviper.adventskalender.christmas.ChristmasManager.enable(this);
getLogger().info("Adventskalender erfolgreich gestartet!");
}
@Override
public void onDisable() {
de.mviper.adventskalender.christmas.ChristmasManager.disable();
MySQLManager.close();
}
@@ -0,0 +1,27 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.entity.Player;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/** Persoenlicher Testmodus (/ac test): erlaubt einem Spieler, jeden Tag zu oeffnen. */
public final class CalendarPerks {
private static final Set<UUID> TESTERS = ConcurrentHashMap.newKeySet();
private CalendarPerks() {}
public static boolean isTester(Player p) {
return TESTERS.contains(p.getUniqueId()) && p.hasPermission("christmasplus.adventcalendar.test");
}
/** @return true, wenn der Testmodus jetzt an ist */
public static boolean toggle(Player p) {
if (!TESTERS.remove(p.getUniqueId())) {
TESTERS.add(p.getUniqueId());
return true;
}
return false;
}
}
@@ -0,0 +1,529 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.RewardUtil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** Verarbeitet /christmas und /cracker. */
public final class ChristmasCommand implements CommandExecutor, TabCompleter {
private final ChristmasManager mgr;
ChristmasCommand(ChristmasManager mgr) { this.mgr = mgr; }
@Override
public boolean onCommand(CommandSender s, Command cmd, String label, String[] a) {
if (cmd.getName().equalsIgnoreCase("cracker")) return cracker(s, a);
if (a.length == 0) {
s.sendMessage(mgr.msg("info", "%status%", mgr.raw(mgr.trees().isEnded() ? "status-ended" : "status-active"),
"%trees%", String.valueOf(mgr.trees().treeCount())));
return true;
}
switch (a[0].toLowerCase()) {
case "help" -> mgr.rawList("help").forEach(s::sendMessage);
case "snow" -> snow(s);
case "santa" -> santa(s, a);
case "snowman" -> snowman(s, a);
case "countdown" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
s.sendMessage(mgr.msg(mgr.countdown().toggle(p) ? "countdown-on" : "countdown-off"));
if (!mgr.countdown().visible()) {
s.sendMessage(mgr.msg("countdown-not-yet", "%days%", String.valueOf(mgr.countdown().daysLeft()),
"%in%", String.valueOf(mgr.countdown().daysUntilVisible())));
} else {
s.sendMessage(mgr.prefixed(mgr.countdown().text()));
}
}
case "cookie" -> cookie(s, a);
case "wichteln" -> wichteln(s, a);
case "wreath" -> wreath(s, a);
case "snowball" -> snowball(s, a);
case "deco" -> deco(s, a);
case "stocking" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
String sub = a.length > 1 ? a[1].toLowerCase() : "";
switch (sub) {
case "place" -> mgr.stockings().place(p);
case "remove" -> mgr.stockings().removeNearest(p);
case "flight" -> {
if (!admin(s)) return true;
s.sendMessage(mgr.msg(mgr.stockings().flight().launch(true) ? "stocking-flight-ok" : "stocking-flight-none"));
}
default -> mgr.stockings().use(p);
}
}
case "hat" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
mgr.hats().give(p);
}
case "quiz" -> quiz(s, a);
case "wish" -> wish(s, a);
case "wishes" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
if (!admin(s)) return true;
mgr.wishes().openMenu(p, 0);
}
case "market" -> market(s, a);
case "portal" -> portal(s, a);
case "give" -> giveCrystal(s, a);
case "gifts" -> {
if (!admin(s)) return true;
mgr.trees().spawnPresentsEverywhere();
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw("gifts-spawned")));
}
case "addhand" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
if (!admin(s)) return true;
s.sendMessage(mgr.msg(mgr.trees().addGift(p.getInventory().getItemInMainHand()) ? "addhand-ok" : "addhand-empty"));
}
case "end" -> {
if (!admin(s)) return true;
mgr.trees().setEnded(true);
s.sendMessage(mgr.msg("end-set"));
}
case "reload" -> {
if (!s.hasPermission("christmasplus.christmas.reload") && !s.hasPermission("xmas.admin")) {
s.sendMessage(mgr.msg("no-permission"));
return true;
}
ChristmasManager.reload();
s.sendMessage(mgr.msg("reloaded"));
}
default -> s.sendMessage(mgr.msg("usage"));
}
return true;
}
// ---------- Plaetzchen ----------
private void cookie(CommandSender s, String[] a) {
if (!admin(s)) return;
CookieManager cm = mgr.cookies();
if (a.length >= 2 && a[1].equalsIgnoreCase("list")) {
s.sendMessage(mgr.prefixed(String.join(", ", cm.ids())));
return;
}
if (a.length < 4 || !a[1].equalsIgnoreCase("give")) { s.sendMessage(mgr.msg("cookie-usage")); return; }
Player t = Bukkit.getPlayerExact(a[2]);
if (t == null) { s.sendMessage(mgr.msg("player-offline")); return; }
int amount = a.length > 4 ? Math.max(1, Math.min(64, ChristmasManager.toInt(a[4], 1))) : 1;
ItemStack it = cm.create(a[3], amount);
if (it == null) { s.sendMessage(mgr.msg("cookie-unknown", "%id%", a[3])); return; }
RewardUtil.give(t, it);
t.sendMessage(mgr.msg("cookie-given", "%amount%", String.valueOf(amount), "%id%", a[3]));
}
// ---------- Wichteln ----------
private void wichteln(CommandSender s, String[] a) {
WichtelnManager w = mgr.wichteln();
String sub = a.length > 1 ? a[1].toLowerCase() : "status";
boolean adminCmd = sub.equals("open") || sub.equals("draw") || sub.equals("deliver") || sub.equals("reset");
if (adminCmd && !admin(s)) return;
if (!adminCmd && !perm(s, "xmas.use")) return;
switch (sub) {
case "open" -> {
if (!w.open()) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
Bukkit.broadcastMessage(mgr.msg("wi-open"));
}
case "draw" -> {
if (w.state() != WichtelnManager.State.OPEN) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
if (!w.draw()) { s.sendMessage(mgr.msg("wi-too-few")); return; }
Bukkit.broadcastMessage(mgr.msg("wi-drawn"));
}
case "deliver" -> {
int n = w.deliver();
if (n < 0) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
Bukkit.broadcastMessage(mgr.msg("wi-delivered", "%count%", String.valueOf(n)));
}
case "reset" -> { w.reset(); s.sendMessage(mgr.msg("wi-reset")); }
case "join" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (w.state() != WichtelnManager.State.OPEN) { s.sendMessage(mgr.msg("wi-wrong-state")); return; }
boolean already = w.joined(p.getUniqueId());
w.join(p);
s.sendMessage(mgr.msg(already ? "wi-already" : "wi-joined"));
}
case "leave" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
s.sendMessage(mgr.msg(w.leave(p) ? "wi-left" : "wi-not-joined"));
}
case "who" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
String name = w.targetName(p.getUniqueId());
s.sendMessage(name.isEmpty() ? mgr.msg("wi-not-joined") : mgr.msg("wi-who", "%name%", name));
}
case "gift" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
String key = w.setGift(p);
s.sendMessage(mgr.msg(key, "%name%", w.targetName(p.getUniqueId())));
}
default -> s.sendMessage(mgr.msg("wi-status", "%state%", w.state().name(),
"%count%", String.valueOf(w.participantCount()), "%gifts%", String.valueOf(w.giftCount())));
}
}
// ---------- Adventskranz ----------
private void wreath(CommandSender s, String[] a) {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (!admin(s)) return;
org.bukkit.block.Block target = p.getTargetBlockExact(8);
if (target == null) { s.sendMessage(mgr.msg("deco-look")); return; }
String sub = a.length > 1 ? a[1].toLowerCase() : "";
if (sub.equals("create")) {
mgr.wreaths().create(target);
s.sendMessage(mgr.msg("wreath-created"));
} else if (sub.equals("remove")) {
s.sendMessage(mgr.msg(mgr.wreaths().removeNear(target) ? "wreath-removed" : "wreath-none"));
} else {
s.sendMessage(mgr.msg("wreath-usage"));
}
}
// ---------- Schneeball-Spiel ----------
private void snowball(CommandSender s, String[] a) {
SnowballGame g = mgr.snowball();
String sub = a.length > 1 ? a[1].toLowerCase() : "top";
if (sub.equals("top")) {
List<String> lines = g.topLines(5);
if (lines.isEmpty()) s.sendMessage(mgr.msg("sb-no-scores")); else lines.forEach(s::sendMessage);
return;
}
if (!admin(s)) return;
switch (sub) {
case "arena" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
int r = a.length > 2 ? ChristmasManager.toInt(a[2], 15) : 15;
g.setArena(p.getLocation(), r);
s.sendMessage(mgr.msg("sb-arena-set", "%radius%", String.valueOf(Math.max(3, r))));
}
case "start" -> {
if (!g.hasArena()) { s.sendMessage(mgr.msg("sb-no-arena")); return; }
int time = mgr.cfg().getInt("snowball.duration", 180);
if (a.length > 2 && ChristmasManager.parseDuration(a[2]) > 0) time = ChristmasManager.parseDuration(a[2]);
if (!g.start(time)) s.sendMessage(mgr.msg("sb-already"));
}
case "stop" -> {
if (!g.isActive()) { s.sendMessage(mgr.msg("sb-not-active")); return; }
g.stop(true);
}
default -> s.sendMessage(mgr.msg("sb-usage"));
}
}
// ---------- Deko ----------
private void deco(CommandSender s, String[] a) {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (!admin(s)) return;
if (a.length > 1 && a[1].equalsIgnoreCase("undo")) mgr.deco().undoLast(p);
else mgr.deco().open(p);
}
// ---------- Quiz ----------
private void quiz(CommandSender s, String[] a) {
if (!admin(s)) return;
QuizManager q = mgr.quiz();
String sub = a.length > 1 ? a[1].toLowerCase() : "";
switch (sub) {
case "start" -> {
int idx = a.length > 2 ? ChristmasManager.toInt(a[2], 0) - 1 : -1;
if (q.isActive()) { s.sendMessage(mgr.msg("quiz-already")); return; }
if (!q.ask(idx)) s.sendMessage(mgr.msg("quiz-none"));
}
case "skip", "stop" -> s.sendMessage(mgr.msg(q.skip() ? "quiz-stopped" : "quiz-not-active"));
case "count" -> s.sendMessage(mgr.prefixed(String.valueOf(q.questionCount())));
default -> s.sendMessage(mgr.msg("quiz-usage"));
}
}
// ---------- Wunschzettel ----------
private void wish(CommandSender s, String[] a) {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (!perm(s, "xmas.use")) return;
if (a.length < 2) { s.sendMessage(mgr.msg("wish-usage")); return; }
if (a[1].equalsIgnoreCase("list")) { mgr.wishes().list(p); return; }
if (a[1].equalsIgnoreCase("clear")) {
s.sendMessage(mgr.msg("wish-cleared", "%count%", String.valueOf(mgr.wishes().clearOwn(p))));
return;
}
String text = String.join(" ", Arrays.copyOfRange(a, 1, a.length));
s.sendMessage(mgr.msg(mgr.wishes().add(p, text), "%max%", String.valueOf(mgr.cfg().getInt("wishes.max-length", 120))));
}
// ---------- Marktstaende ----------
private void market(CommandSender s, String[] a) {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
String sub = a.length > 1 ? a[1].toLowerCase() : "";
if (sub.equals("back")) { // fuer alle Spieler
s.sendMessage(mgr.msg(mgr.portals().back(p)));
return;
}
if (!admin(s)) return;
switch (sub) {
case "list" -> s.sendMessage(mgr.prefixed(String.join(", ", mgr.market().stallIds())));
case "remove" -> s.sendMessage(mgr.msg(mgr.market().removeNearest(p, 6) ? "market-removed" : "market-none"));
case "set" -> {
mgr.market().setSpawn(p.getLocation());
s.sendMessage(mgr.msg("market-set"));
}
case "unset" -> {
mgr.market().clearSpawn();
s.sendMessage(mgr.msg("market-unset"));
}
case "tp" -> {
Location dest = mgr.market().spawn();
if (dest == null) { s.sendMessage(mgr.msg("portal-no-market")); return; }
p.teleport(dest);
}
case "spawn" -> {
if (a.length < 3) { s.sendMessage(mgr.msg("market-usage")); return; }
if (!mgr.market().spawn(p.getLocation(), a[2])) { s.sendMessage(mgr.msg("market-unknown", "%id%", a[2])); return; }
s.sendMessage(mgr.msg("market-spawned"));
}
default -> s.sendMessage(mgr.msg("market-usage"));
}
}
// ---------- Portale ----------
private void portal(CommandSender s, String[] a) {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (!perm(s, "xmas.use")) return;
String sub = a.length > 1 ? a[1].toLowerCase() : "";
org.bukkit.block.Block target = p.getTargetBlockExact(6);
switch (sub) {
case "create" -> {
if (target == null) { s.sendMessage(mgr.msg("deco-look")); return; }
s.sendMessage(mgr.msg(mgr.portals().create(p, target), "%cost%",
String.join(", ", mgr.cfg().getStringList("portal.cost")),
"%limit%", String.valueOf(mgr.cfg().getInt("portal.limit-per-player", 1))));
}
case "remove" -> {
if (target == null) { s.sendMessage(mgr.msg("portal-none")); return; }
s.sendMessage(mgr.msg(mgr.portals().remove(p, target)));
}
case "info" -> s.sendMessage(mgr.msg("portal-info", "%cost%", String.join(", ", mgr.cfg().getStringList("portal.cost")),
"%count%", String.valueOf(mgr.portals().countOf(p.getUniqueId())),
"%limit%", String.valueOf(mgr.cfg().getInt("portal.limit-per-player", 1))));
default -> s.sendMessage(mgr.msg("portal-usage"));
}
}
private void snowman(CommandSender s, String[] a) {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (!admin(s)) return;
if (a.length < 2) { s.sendMessage(mgr.msg("snowman-usage")); return; }
switch (a[1].toLowerCase()) {
case "spawn" -> {
int from = 2;
Boolean walk = null;
if (a.length > 2) {
String m = a[2].toLowerCase();
if (m.equals("walk") || m.equals("laufen")) { walk = true; from = 3; }
else if (m.equals("stand") || m.equals("stehen")) { walk = false; from = 3; }
}
String name = a.length > from ? String.join(" ", Arrays.copyOfRange(a, from, a.length)) : "";
mgr.snowmen().spawnDeco(p.getLocation(), name, walk);
s.sendMessage(mgr.msg("snowman-spawned"));
}
case "remove" -> s.sendMessage(mgr.msg(mgr.snowmen().removeNearestDeco(p, 6) ? "snowman-removed" : "snowman-none"));
default -> s.sendMessage(mgr.msg("snowman-usage"));
}
}
private boolean admin(CommandSender s) {
return perm(s, "xmas.admin");
}
private void giveCrystal(CommandSender s, String[] a) {
if (!admin(s)) return;
if (a.length < 2) { s.sendMessage(mgr.msg("usage")); return; }
Player t = Bukkit.getPlayerExact(a[1]);
if (t == null) { s.sendMessage(mgr.msg("player-offline")); return; }
int amount = 1;
if (a.length > 2 && a[2].matches("\\d+")) amount = Math.max(1, Math.min(64, Integer.parseInt(a[2])));
RewardUtil.give(t, mgr.trees().crystal(amount));
t.sendMessage(mgr.msg("crystal-given", "%amount%", String.valueOf(amount)));
if (s != t) s.sendMessage(mgr.msg("crystal-gave", "%player%", t.getName(), "%amount%", String.valueOf(amount)));
}
private boolean perm(CommandSender s, String p) {
if (s.hasPermission(p)) return true;
s.sendMessage(mgr.msg("no-permission"));
return false;
}
// ---------- Snow ----------
private void snow(CommandSender s) {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (!perm(s, "christmasplus.christmas.snow")) return;
if (!mgr.cfg().getBoolean("snow.enable", true)) { s.sendMessage(mgr.msg("snow-disabled-global")); return; }
s.sendMessage(mgr.msg(mgr.toggleSnow(p) ? "snow-on" : "snow-off"));
}
// ---------- Santa ----------
private void santa(CommandSender s, String[] a) {
if (!perm(s, "christmasplus.christmas.santa")) return;
if (a.length < 2) { s.sendMessage(mgr.msg("santa-usage")); return; }
SantaEvent santa = mgr.santa();
switch (a[1].toLowerCase()) {
case "stop" -> {
if (!santa.isActive()) s.sendMessage(mgr.msg("santa-not-active"));
else santa.stop(true);
}
case "teleport" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return; }
if (!santa.isActive()) { s.sendMessage(mgr.msg("santa-not-active")); return; }
p.teleport(santa.getLocation());
p.sendMessage(mgr.msg("santa-teleported"));
}
case "start" -> {
if (santa.isActive()) { s.sendMessage(mgr.msg("santa-already")); return; }
int time = mgr.cfg().getInt("santa.duration", 300);
int i = 2;
if (a.length > i && ChristmasManager.parseDuration(a[i]) > 0) time = ChristmasManager.parseDuration(a[i++]);
Location where = null;
if (a.length > i && a[i].equalsIgnoreCase("random")) {
where = santa.randomLocation();
if (where == null) { s.sendMessage(mgr.msg("santa-no-locations")); return; }
} else if (a.length >= i + 4) {
World w = Bukkit.getWorld(a[i]);
try {
if (w != null) where = new Location(w, Double.parseDouble(a[i + 1]),
Double.parseDouble(a[i + 2]), Double.parseDouble(a[i + 3]));
} catch (NumberFormatException ignored) { }
} else if (s instanceof Player p) {
where = p.getLocation();
}
if (where == null) { s.sendMessage(mgr.msg("santa-usage")); return; }
santa.start(time, where);
s.sendMessage(mgr.msg("santa-started-sender", "%duration%", SantaEvent.formatDuration(time)));
}
default -> s.sendMessage(mgr.msg("santa-usage"));
}
}
// ---------- Cracker ----------
private boolean cracker(CommandSender s, String[] a) {
CrackerManager cm = mgr.crackers();
if (a.length == 0) { s.sendMessage(mgr.msg("cracker-usage")); return true; }
switch (a[0].toLowerCase()) {
case "list" -> {
if (!perm(s, "christmasplus.cracker.list")) return true;
s.sendMessage(mgr.prefixed(String.join(", ", cm.ids())));
}
case "get" -> {
if (!(s instanceof Player p)) { s.sendMessage(mgr.msg("player-only")); return true; }
if (!perm(s, "christmasplus.cracker.get")) return true;
String id = cm.firstId();
int amount = 1;
for (int i = 1; i < a.length; i++) {
if (a[i].matches("\\d+")) amount = Integer.parseInt(a[i]); else id = a[i];
}
if (amount > 1 && !perm(s, "christmasplus.cracker.get.multiple")) return true;
giveCracker(s, p, id, amount);
}
case "give" -> {
if (!perm(s, "christmasplus.cracker.give")) return true;
if (a.length < 2) { s.sendMessage(mgr.msg("cracker-usage")); return true; }
Player t = Bukkit.getPlayerExact(a[1]);
if (t == null) { s.sendMessage(mgr.msg("player-not-found")); return true; }
String id = cm.firstId();
int amount = 1;
for (int i = 2; i < a.length; i++) {
if (a[i].matches("\\d+")) amount = Integer.parseInt(a[i]); else id = a[i];
}
giveCracker(s, t, id, amount);
}
default -> s.sendMessage(mgr.msg("cracker-usage"));
}
return true;
}
private void giveCracker(CommandSender s, Player target, String id, int amount) {
amount = Math.max(1, Math.min(amount, 64));
ItemStack it = id == null ? null : mgr.crackers().create(id, amount);
if (it == null) { s.sendMessage(mgr.msg("cracker-unknown", "%id%", String.valueOf(id))); return; }
RewardUtil.give(target, it);
target.sendMessage(mgr.msg("cracker-given", "%amount%", String.valueOf(amount), "%id%", id));
if (s != target) s.sendMessage(mgr.msg("cracker-gave", "%player%", target.getName(),
"%amount%", String.valueOf(amount), "%id%", id));
}
// ---------- Tab ----------
@Override
public List<String> onTabComplete(CommandSender s, Command cmd, String label, String[] a) {
List<String> out = new ArrayList<>();
if (cmd.getName().equalsIgnoreCase("cracker")) {
if (a.length == 1) out.addAll(Arrays.asList("list", "get", "give"));
else if (a[0].equalsIgnoreCase("give") && a.length == 2)
Bukkit.getOnlinePlayers().forEach(p -> out.add(p.getName()));
else out.addAll(mgr.crackers().ids());
} else if (a.length == 1) {
out.addAll(Arrays.asList("help", "snow", "santa", "give", "gifts", "addhand", "snowman", "countdown", "cookie", "wichteln", "wreath", "snowball", "deco", "stocking", "hat", "quiz", "wish", "wishes", "market", "portal", "end", "reload"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("santa")) {
out.addAll(Arrays.asList("start", "stop", "teleport"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("stocking")) {
out.addAll(Arrays.asList("place", "remove", "flight"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("snowman")) {
out.addAll(Arrays.asList("spawn", "remove"));
} else if (a.length == 3 && a[0].equalsIgnoreCase("snowman") && a[1].equalsIgnoreCase("spawn")) {
out.addAll(Arrays.asList("stand", "walk"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("cookie")) {
out.addAll(Arrays.asList("give", "list"));
} else if (a.length == 4 && a[0].equalsIgnoreCase("cookie")) {
out.addAll(mgr.cookies().ids());
} else if (a.length == 3 && a[0].equalsIgnoreCase("cookie")) {
Bukkit.getOnlinePlayers().forEach(p -> out.add(p.getName()));
} else if (a.length == 2 && a[0].equalsIgnoreCase("wichteln")) {
out.addAll(Arrays.asList("join", "leave", "who", "gift", "status", "open", "draw", "deliver", "reset"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("wreath")) {
out.addAll(Arrays.asList("create", "remove"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("snowball")) {
out.addAll(Arrays.asList("top", "arena", "start", "stop"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("deco")) {
out.add("undo");
} else if (a.length == 2 && a[0].equalsIgnoreCase("portal")) {
out.addAll(Arrays.asList("create", "remove", "info"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("quiz")) {
out.addAll(Arrays.asList("start", "skip", "count"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("wish")) {
out.addAll(Arrays.asList("list", "clear"));
} else if (a.length == 2 && a[0].equalsIgnoreCase("market")) {
out.addAll(Arrays.asList("spawn", "remove", "list", "set", "unset", "tp", "back"));
} else if (a.length == 3 && a[0].equalsIgnoreCase("market") && a[1].equalsIgnoreCase("spawn")) {
out.addAll(mgr.market().stallIds());
} else if (a.length == 2 && a[0].equalsIgnoreCase("give")) {
Bukkit.getOnlinePlayers().forEach(p -> out.add(p.getName()));
} else {
return Collections.emptyList();
}
String last = a[a.length - 1].toLowerCase();
out.removeIf(x -> !x.toLowerCase().startsWith(last));
return out;
}
}
@@ -0,0 +1,123 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.Adventskalender;
import de.mviper.adventskalender.RewardUtil;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.Map;
/**
* Zusatz-Belohnungen fuer Kalendertage (Christmas+ Funktionen). Im Abschnitt rewards.&lt;Tag&gt; der config.yml moeglich:
* <pre>
* commands: ["give %player% apple 3"]
* permissions: ["essentials.fly"] # wird ueber calendar-extras.permission-command vergeben
* multiple: ["DIAMOND:2", "COOKIE:8"] # mehrere Geschenke am Tag
* random: # zufaelliges Zusatzgeschenk (chance = Gewicht)
* - { chance: 50, item: "GOLD_INGOT:4" }
* - { chance: 50, command: "say Hallo %player%" }
* cracker: "cracker:2" # Cracker-ID und Anzahl
* crystal: 1 # Weihnachts-Kristall
* groups: # Extra-Geschenke nur fuer bestimmte Gruppen (z.B. Donor)
* donor:
* permission: "christmasplus.donor"
* material: EMERALD # optional, wie das normale Geschenk
* amount: 3
* multiple: ["DIAMOND:1"] # und alle Schluessel von oben
* </pre>
*/
public final class ChristmasExtras {
private ChristmasExtras() {}
public static void give(Player p, String path) {
ConfigurationSection s = Adventskalender.getInstance().getConfig().getConfigurationSection(path);
if (s == null) return;
apply(p, s);
ConfigurationSection groups = s.getConfigurationSection("groups");
if (groups == null) return;
for (String name : groups.getKeys(false)) {
ConfigurationSection g = groups.getConfigurationSection(name);
if (g == null) continue;
String perm = g.getString("permission", "");
if (!perm.isBlank() && !p.hasPermission(perm)) continue;
if (g.contains("material")) {
ItemStack it = RewardUtil.build(path + ".groups." + name, "Gruppe " + name);
if (it != null) RewardUtil.give(p, it);
}
apply(p, g);
}
}
/** Wendet commands, permissions, multiple, random, cracker und crystal eines Abschnitts an. */
public static void apply(Player p, ConfigurationSection s) {
for (String cmd : s.getStringList("commands")) run(p, cmd);
for (String perm : s.getStringList("permissions")) grantPermission(p, perm);
for (String spec : s.getStringList("multiple")) {
ItemStack it = ChristmasManager.parseItem(spec);
if (it != null) RewardUtil.give(p, it);
}
List<Map<?, ?>> random = s.getMapList("random");
double sum = 0;
for (Map<?, ?> r : random) sum += weight(r);
if (sum > 0) {
double roll = ChristmasManager.RANDOM.nextDouble() * sum;
for (Map<?, ?> r : random) {
roll -= weight(r);
if (roll > 0) continue;
applyMap(p, r);
break;
}
}
ChristmasManager mgr = ChristmasManager.get();
if (mgr == null) return;
String cracker = s.getString("cracker");
if (cracker != null && !cracker.isBlank()) {
String[] parts = cracker.split(":");
int amount = 1;
if (parts.length > 1) {
try { amount = Integer.parseInt(parts[1].trim()); } catch (NumberFormatException ignored) { }
}
ItemStack it = mgr.crackers().create(parts[0].trim(), amount);
if (it != null) RewardUtil.give(p, it);
}
int crystal = s.getInt("crystal", 0);
if (crystal > 0) RewardUtil.give(p, mgr.trees().crystal(crystal));
}
/** Eintrag mit den Schluesseln item, command, permission. */
public static void applyMap(Player p, Map<?, ?> r) {
if (r.get("item") != null) {
ItemStack it = ChristmasManager.parseItem(String.valueOf(r.get("item")));
if (it != null) RewardUtil.give(p, it);
}
if (r.get("command") != null) run(p, String.valueOf(r.get("command")));
if (r.get("permission") != null) grantPermission(p, String.valueOf(r.get("permission")));
}
public static void grantPermission(Player p, String permission) {
ChristmasManager mgr = ChristmasManager.get();
String template = mgr == null ? "" : mgr.cfg().getString("permission-command", "");
if (template == null || template.isBlank()) {
Adventskalender.getInstance().getLogger().warning(
"Permission '" + permission + "' soll vergeben werden, aber 'permission-command' ist in christmas.yml leer.");
return;
}
run(p, template.replace("%permission%", permission));
}
private static void run(Player p, String cmd) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("%player%", p.getName()));
}
private static double weight(Map<?, ?> r) {
Object c = r.get("chance");
return c instanceof Number n ? Math.max(0, n.doubleValue()) : 1;
}
}
@@ -0,0 +1,262 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Tag;
import org.bukkit.block.Block;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.entity.Snowball;
import org.bukkit.entity.Snowman;
import org.bukkit.event.entity.ProjectileHitEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.block.Action;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerShearEntityEvent;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockBurnEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockPistonExtendEvent;
import org.bukkit.event.block.BlockPistonRetractEvent;
import org.bukkit.event.block.LeavesDecayEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.world.StructureGrowEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
public final class ChristmasListener implements Listener {
private final ChristmasManager mgr;
ChristmasListener(ChristmasManager mgr) { this.mgr = mgr; }
@EventHandler(priority = EventPriority.HIGH)
public void onInteract(PlayerInteractEvent e) {
if (e.getHand() != EquipmentSlot.HAND) return;
Action a = e.getAction();
Player p = e.getPlayer();
MagicTreeManager trees = mgr.trees();
// Cracker oeffnen
if (a == Action.RIGHT_CLICK_AIR || a == Action.RIGHT_CLICK_BLOCK) {
ItemStack inHand = e.getItem();
String id = mgr.crackers().idOf(inHand);
if (id != null) {
e.setCancelled(true);
inHand.setAmount(inHand.getAmount() - 1);
mgr.crackers().open(p, id);
return;
}
}
if (a != Action.RIGHT_CLICK_BLOCK || e.getClickedBlock() == null) return;
Block b = e.getClickedBlock();
// Geschenk unter einer Socke oeffnen
if (mgr.stockings().isPresentBlock(b)) {
e.setCancelled(true);
mgr.stockings().openPresent(p, b);
return;
}
// Geschenk oeffnen
if (trees.isPresentBlock(b)) {
e.setCancelled(true);
trees.openPresent(p, trees.treeOfPresent(b), b);
return;
}
// Kristall auf Setzling
if (trees.isCrystal(e.getItem())) {
e.setCancelled(true);
if (Tag.SAPLINGS.isTagged(b.getType()) && !trees.isTreeBlock(b)) {
ItemStack inHand = e.getItem();
if (!p.hasPermission("xmas.use")) { p.sendMessage(mgr.msg("no-permission")); return; }
int before = trees.treeCount();
trees.plant(p, b);
if (trees.treeCount() > before) inHand.setAmount(inHand.getAmount() - 1);
}
return;
}
// Baum: Rechtsklick = wachsen / Info
if (trees.isTreeBlock(b)) {
e.setCancelled(true);
MagicTreeManager.Tree t = trees.treeOf(b);
if (p.isSneaking()) trees.info(p, t);
else trees.click(p, t);
}
}
@EventHandler(priority = EventPriority.HIGH)
public void onBreak(BlockBreakEvent e) {
MagicTreeManager trees = mgr.trees();
Block b = e.getBlock();
if (mgr.stockings().isPresentBlock(b)) {
e.setCancelled(true);
mgr.stockings().openPresent(e.getPlayer(), b);
return;
}
if (trees.isPresentBlock(b)) {
e.setCancelled(true);
trees.openPresent(e.getPlayer(), trees.treeOfPresent(b), b);
return;
}
if (trees.isTreeBlock(b)) {
e.setCancelled(true);
trees.breakAttempt(e.getPlayer(), trees.treeOf(b), b);
return;
}
if ((mgr.wreaths().isProtected(b) || mgr.portals().isProtected(b)) && !e.getPlayer().hasPermission("xmas.admin")) {
e.setCancelled(true);
}
}
@EventHandler
public void onDrop(PlayerDropItemEvent e) {
Item item = e.getItemDrop();
Player p = e.getPlayer();
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (item.isValid() && p.isOnline()) mgr.trees().absorb(p, item);
}, 40L);
}
// ---------- Schutz der Baeume und Geschenke ----------
@EventHandler(ignoreCancelled = true)
public void onPistonExtend(BlockPistonExtendEvent e) {
for (Block b : e.getBlocks()) if (mgr.isProtected(b)) { e.setCancelled(true); return; }
}
@EventHandler(ignoreCancelled = true)
public void onPistonRetract(BlockPistonRetractEvent e) {
for (Block b : e.getBlocks()) if (mgr.isProtected(b)) { e.setCancelled(true); return; }
}
@EventHandler(ignoreCancelled = true)
public void onExplode(EntityExplodeEvent e) {
e.blockList().removeIf(b -> mgr.isProtected(b));
}
@EventHandler(ignoreCancelled = true)
public void onBlockExplode(BlockExplodeEvent e) {
e.blockList().removeIf(b -> mgr.isProtected(b));
}
@EventHandler(ignoreCancelled = true)
public void onBurn(BlockBurnEvent e) {
if (mgr.isProtected(e.getBlock())) e.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onDecay(LeavesDecayEvent e) {
if (mgr.isProtected(e.getBlock())) e.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onStructureGrow(StructureGrowEvent e) {
if (mgr.isProtected(e.getLocation().getBlock())) e.setCancelled(true);
}
// ---------- Schneemaenner ----------
@EventHandler(ignoreCancelled = true)
public void onSnowmanBuilt(CreatureSpawnEvent e) {
if (e.getSpawnReason() == CreatureSpawnEvent.SpawnReason.BUILD_SNOWMAN && e.getEntity() instanceof Snowman s) {
// Eine Tick spaeter, damit der Schneemann sicher existiert
Bukkit.getScheduler().runTask(mgr.plugin(), () -> { if (s.isValid()) mgr.snowmen().onBuilt(s); });
}
}
@EventHandler(ignoreCancelled = true)
public void onShear(PlayerShearEntityEvent e) {
if (mgr.snowmen().isDeco(e.getEntity())) e.setCancelled(true);
}
// ---------- Join, Plaetzchen, Schneeball, Deko-Menue ----------
@EventHandler
public void onJoin(PlayerJoinEvent e) {
Player p = e.getPlayer();
mgr.countdown().onJoin(p);
mgr.streaks().onJoin(p);
mgr.greetings().onJoin(p);
mgr.hats().onJoin(p);
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (!p.isOnline()) return;
mgr.stockings().tryDeliver(p, false);
mgr.wishes().deliver(p);
}, 140L);
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> { if (p.isOnline()) mgr.wichteln().onJoin(p); }, 100L);
}
@EventHandler(ignoreCancelled = true)
public void onConsume(PlayerItemConsumeEvent e) {
String id = mgr.cookies().idOf(e.getItem());
if (id != null) mgr.cookies().eat(e.getPlayer(), id);
}
@EventHandler
public void onSnowballHit(ProjectileHitEvent e) {
if (e.getEntity() instanceof Snowball sb && sb.getShooter() instanceof Player shooter
&& e.getHitEntity() instanceof Player victim) {
mgr.snowball().onHit(shooter, victim);
}
}
@EventHandler
public void onMenuClick(InventoryClickEvent e) {
if (!(e.getInventory().getHolder() instanceof DecoManager.Menu)) return;
e.setCancelled(true);
if (e.getWhoClicked() instanceof Player p && e.getClickedInventory() == e.getInventory()
&& p.hasPermission("xmas.admin")) {
mgr.deco().click(p, e.getRawSlot());
}
}
@EventHandler
public void onMenuDrag(InventoryDragEvent e) {
if (e.getInventory().getHolder() instanceof DecoManager.Menu) e.setCancelled(true);
}
// ---------- Quiz, Feuerwerk, Wunschzettel-Menue ----------
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.LOWEST)
public void onChat(AsyncPlayerChatEvent e) {
if (mgr.quiz().onChat(e.getPlayer(), e.getMessage())) e.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onSnowmanTrail(org.bukkit.event.block.EntityBlockFormEvent e) {
mgr.snowmen().onSnowForm(e);
}
@EventHandler(ignoreCancelled = true)
public void onFireworkDamage(EntityDamageByEntityEvent e) {
if (mgr.greetings().isOurs(e.getDamager())) e.setCancelled(true);
}
@EventHandler
public void onWishMenuClick(InventoryClickEvent e) {
if (!(e.getInventory().getHolder() instanceof WishManager.Menu menu)) return;
e.setCancelled(true);
if (e.getWhoClicked() instanceof Player p && e.getClickedInventory() == e.getInventory()
&& p.hasPermission("xmas.admin")) {
mgr.wishes().click(p, menu, e.getRawSlot(), e.isRightClick(), e.isShiftClick());
}
}
@EventHandler
public void onWishMenuDrag(InventoryDragEvent e) {
if (e.getInventory().getHolder() instanceof WishManager.Menu) e.setCancelled(true);
}
}
@@ -0,0 +1,344 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.Adventskalender;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.util.List;
import java.util.Random;
/** Zentrale Verwaltung der Weihnachts-Module: Schnee, Santa, Cracker, Magic Tree. */
public final class ChristmasManager {
private static ChristmasManager instance;
public static final Random RANDOM = new Random();
private final Adventskalender plugin;
private YamlConfiguration cfg;
private final NamespacedKey snowOffKey;
private BukkitTask snowTask;
private SantaEvent santa;
private CrackerManager crackers;
private MagicTreeManager trees;
private SnowmanManager snowmen;
private CookieManager cookies;
private CountdownManager countdown;
private WichtelnManager wichteln;
private WreathManager wreaths;
private SnowballGame snowball;
private DecoManager deco;
private LoginStreakManager streaks;
private GreetingManager greetings;
private StockingManager stockings;
private HatManager hats;
private QuizManager quiz;
private WishManager wishes;
private MarketManager market;
private PortalManager portals;
private YamlConfiguration msgs;
private ChristmasManager(Adventskalender plugin) {
this.plugin = plugin;
this.snowOffKey = new NamespacedKey(plugin, "snow_off");
}
public static void enable(Adventskalender plugin) {
instance = new ChristmasManager(plugin);
instance.loadConfig();
instance.santa = new SantaEvent(instance);
instance.crackers = new CrackerManager(instance);
instance.trees = new MagicTreeManager(instance);
instance.snowmen = new SnowmanManager(instance);
instance.loadMessages();
instance.cookies = new CookieManager(instance);
instance.countdown = new CountdownManager(instance);
instance.wichteln = new WichtelnManager(instance);
instance.wreaths = new WreathManager(instance);
instance.snowball = new SnowballGame(instance);
instance.deco = new DecoManager(instance);
instance.streaks = new LoginStreakManager(instance);
instance.greetings = new GreetingManager(instance);
instance.stockings = new StockingManager(instance);
instance.hats = new HatManager(instance);
instance.quiz = new QuizManager(instance);
instance.wishes = new WishManager(instance);
instance.market = new MarketManager(instance);
instance.portals = new PortalManager(instance);
instance.crackers.reload();
instance.trees.load();
instance.snowmen.start();
instance.countdown.start();
instance.wichteln.load();
instance.wreaths.load();
instance.snowball.load();
instance.streaks.load();
instance.greetings.start();
instance.stockings.load();
instance.quiz.start();
instance.wishes.load();
instance.market.load();
instance.portals.load();
instance.startSnow();
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
try {
new XmasExpansion(instance).register();
} catch (Throwable t) {
plugin.getLogger().warning("PlaceholderAPI-Platzhalter (xmas) konnten nicht registriert werden: " + t);
}
}
ChristmasCommand cmd = new ChristmasCommand(instance);
plugin.getCommand("christmas").setExecutor(cmd);
plugin.getCommand("christmas").setTabCompleter(cmd);
plugin.getCommand("cracker").setExecutor(cmd);
plugin.getCommand("cracker").setTabCompleter(cmd);
plugin.getServer().getPluginManager().registerEvents(new ChristmasListener(instance), plugin);
}
public static void disable() {
if (instance == null) return;
instance.santa.stop(false);
instance.trees.save();
instance.trees.shutdown();
instance.snowmen.stop();
instance.countdown.stop();
instance.wichteln.save();
instance.wreaths.stop();
instance.snowball.shutdown();
instance.streaks.save();
instance.greetings.stop();
instance.stockings.stop();
instance.quiz.stop();
instance.portals.stop();
if (instance.snowTask != null) instance.snowTask.cancel();
instance = null;
}
public static void reload() {
if (instance == null) return;
instance.loadConfig();
instance.loadMessages();
instance.crackers.reload();
instance.countdown.start();
instance.quiz.start();
instance.market.refreshAll();
instance.startSnow();
}
public static ChristmasManager get() { return instance; }
private void loadConfig() {
File f = new File(plugin.getDataFolder(), "christmas.yml");
if (!f.exists()) plugin.saveResource("christmas.yml", false);
cfg = YamlConfiguration.loadConfiguration(f);
try (java.io.InputStream in = plugin.getResource("christmas.yml")) {
if (in != null) {
cfg.setDefaults(YamlConfiguration.loadConfiguration(
new java.io.InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8)));
}
} catch (java.io.IOException ignored) { }
}
public Adventskalender plugin() { return plugin; }
public YamlConfiguration cfg() { return cfg; }
public SantaEvent santa() { return santa; }
public CrackerManager crackers() { return crackers; }
public MagicTreeManager trees() { return trees; }
public SnowmanManager snowmen() { return snowmen; }
public CookieManager cookies() { return cookies; }
public CountdownManager countdown() { return countdown; }
public WichtelnManager wichteln() { return wichteln; }
public WreathManager wreaths() { return wreaths; }
public SnowballGame snowball() { return snowball; }
public DecoManager deco() { return deco; }
public LoginStreakManager streaks() { return streaks; }
public GreetingManager greetings() { return greetings; }
public StockingManager stockings() { return stockings; }
public HatManager hats() { return hats; }
public QuizManager quiz() { return quiz; }
public WishManager wishes() { return wishes; }
public MarketManager market() { return market; }
public PortalManager portals() { return portals; }
/** Baum-, Geschenk- und Kranzbloecke, die vor Zerstoerung geschuetzt sind. */
public boolean isProtected(org.bukkit.block.Block b) {
return trees.isProtected(b) || wreaths.isProtected(b) || portals.isProtected(b) || stockings.isProtected(b);
}
// ---------- Nachrichten ----------
/** Laedt die Sprachdatei (christmas_messages_de.yml / _en.yml) passend zu general.language. */
private void loadMessages() {
String lang = plugin.getConfig().getString("general.language", "de").toLowerCase();
if (!lang.matches("[a-z_]+")) lang = "de";
String res = "christmas_messages_" + lang + ".yml";
if (plugin.getResource(res) == null) res = "christmas_messages_de.yml";
File f = new File(plugin.getDataFolder(), res);
if (!f.exists()) plugin.saveResource(res, false);
msgs = YamlConfiguration.loadConfiguration(f);
try (java.io.InputStream in = plugin.getResource(res)) {
if (in != null) {
msgs.setDefaults(YamlConfiguration.loadConfiguration(
new java.io.InputStreamReader(in, java.nio.charset.StandardCharsets.UTF_8)));
}
} catch (java.io.IOException ignored) { }
}
public String raw(String key) {
// Reihenfolge: Sprachdatei, alte Texte aus christmas.yml, sonst Fehlermeldung
String text = msgs == null ? null : msgs.getString("messages." + key);
if (text == null) text = cfg.getString("messages." + key);
return ChatColor.translateAlternateColorCodes('&', text != null ? text : "&cMissing: " + key);
}
public List<String> rawList(String key) {
List<String> l = msgs == null ? List.of() : msgs.getStringList("messages." + key);
if (l.isEmpty()) l = cfg.getStringList("messages." + key);
List<String> out = new java.util.ArrayList<>();
for (String x : l) out.add(ChatColor.translateAlternateColorCodes('&', x));
return out;
}
public String msg(String key, String... replacements) {
String s = raw(key);
for (int i = 0; i + 1 < replacements.length; i += 2) s = s.replace(replacements[i], replacements[i + 1]);
return raw("prefix") + s;
}
public String prefixed(String text) {
return raw("prefix") + text;
}
/** "300", "30s", "5m", "1h" -> Sekunden; -1 bei ungueltiger Eingabe. */
public static int parseDuration(String s) {
if (s == null || s.isBlank()) return -1;
s = s.trim().toLowerCase();
int mult = 1;
char last = s.charAt(s.length() - 1);
if (last == 's' || last == 'm' || last == 'h') {
mult = last == 'm' ? 60 : last == 'h' ? 3600 : 1;
s = s.substring(0, s.length() - 1);
}
try {
return Integer.parseInt(s) * mult;
} catch (NumberFormatException e) {
return -1;
}
}
public static String color(String s) {
return ChatColor.translateAlternateColorCodes('&', s);
}
// ---------- Item-Hilfen ----------
/**
* "DIAMOND:2" -> ItemStack, null wenn ungueltig. Zusaetzlich: "cracker:id:n", "cookie:id:n", "crystal:n".
*/
public static ItemStack parseItem(String spec) {
if (spec == null) return null;
String[] p = spec.trim().split(":");
String head = p[0].trim().toLowerCase();
ChristmasManager m = instance;
if (m != null) {
if ((head.equals("cracker") || head.equals("cookie")) && p.length > 1) {
int a = p.length > 2 ? toInt(p[2], 1) : 1;
return head.equals("cracker") ? m.crackers.create(p[1].trim(), a) : m.cookies.create(p[1].trim(), a);
}
if (head.equals("crystal")) return m.trees.crystal(p.length > 1 ? toInt(p[1], 1) : 1);
if (head.equals("present")) return m.presentHead(p.length > 1 ? toInt(p[1], 1) : 1);
}
Material mat = Material.matchMaterial(p[0].trim());
if (mat == null || !mat.isItem() || mat.isAir()) return null;
return new ItemStack(mat, Math.max(1, p.length > 1 ? toInt(p[1], 1) : 1));
}
/** Geschenk-Kopf (Textur aus magic-tree.presents) als Item. */
ItemStack presentHead(int amount) {
ItemStack it = new ItemStack(Material.PLAYER_HEAD, Math.max(1, amount));
List<String> skins = cfg.getStringList("magic-tree.presents");
if (skins.isEmpty()) return it;
org.bukkit.inventory.meta.SkullMeta meta = (org.bukkit.inventory.meta.SkullMeta) it.getItemMeta();
if (meta == null) return it;
String url = skins.get(RANDOM.nextInt(skins.size()));
try {
org.bukkit.profile.PlayerProfile prof = Bukkit.createPlayerProfile(java.util.UUID.nameUUIDFromBytes(url.getBytes()), "present");
org.bukkit.profile.PlayerTextures tex = prof.getTextures();
tex.setSkin(new java.net.URL(url));
prof.setTextures(tex);
meta.setOwnerProfile(prof);
meta.setDisplayName(raw("present-name"));
it.setItemMeta(meta);
} catch (java.net.MalformedURLException ignored) { }
return it;
}
static int toInt(String s, int def) {
try { return Integer.parseInt(s.trim()); } catch (NumberFormatException e) { return def; }
}
public static boolean hasAll(Player p, List<String> cost) {
for (String c : cost) {
ItemStack it = parseItem(c);
if (it != null && !p.getInventory().containsAtLeast(new ItemStack(it.getType()), it.getAmount())) return false;
}
return true;
}
public static void takeAll(Player p, List<String> cost) {
for (String c : cost) {
ItemStack it = parseItem(c);
if (it != null) p.getInventory().removeItem(it);
}
}
// ---------- Schnee ----------
public boolean isSnowEnabledFor(Player p) {
Byte off = p.getPersistentDataContainer().get(snowOffKey, PersistentDataType.BYTE);
if (off != null) return off == 0;
return cfg.getBoolean("snow.default-on", true);
}
public boolean toggleSnow(Player p) {
boolean now = !isSnowEnabledFor(p);
p.getPersistentDataContainer().set(snowOffKey, PersistentDataType.BYTE, (byte) (now ? 0 : 1));
return now;
}
private void startSnow() {
if (snowTask != null) snowTask.cancel();
snowTask = null;
if (!cfg.getBoolean("snow.enable", true)) return;
int interval = Math.max(1, cfg.getInt("snow.display-task-interval", 2));
int level = Math.max(1, Math.min(5, cfg.getInt("snow.level", 3)));
List<String> blocked = cfg.getStringList("snow.disable-in-worlds");
boolean regionsOnly = cfg.getBoolean("snow.enable-only-in-regions", false)
&& Bukkit.getPluginManager().getPlugin("WorldGuard") != null;
List<String> regions = cfg.getStringList("snow.regions");
snowTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
for (Player p : Bukkit.getOnlinePlayers()) {
World w = p.getWorld();
if (blocked.contains(w.getName()) || !isSnowEnabledFor(p)) continue;
var loc = p.getLocation();
if (regionsOnly && !WorldGuardHook.inAnyRegion(loc, regions)) continue;
for (int i = 0; i < level * 4; i++) {
double x = loc.getX() + (RANDOM.nextDouble() - 0.5) * 20;
double z = loc.getZ() + (RANDOM.nextDouble() - 0.5) * 20;
double y = loc.getY() + 2 + RANDOM.nextDouble() * 7;
p.spawnParticle(Particle.SNOWFLAKE, x, y, z, 0, 0, -0.15, 0, 1);
}
}
}, 20L, interval);
}
}
@@ -0,0 +1,75 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/** Plaetzchen und Lebkuchen: Essbare Items mit Traenke-Effekten. */
public final class CookieManager {
private final ChristmasManager mgr;
private final NamespacedKey key;
CookieManager(ChristmasManager mgr) {
this.mgr = mgr;
this.key = new NamespacedKey(mgr.plugin(), "cookie_id");
}
public Set<String> ids() {
ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies");
return s == null ? new LinkedHashSet<>() : s.getKeys(false);
}
public ItemStack create(String id, int amount) {
ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies." + id);
if (s == null) return null;
Material m = Material.matchMaterial(s.getString("material", "COOKIE"));
if (m == null || !m.isItem() || !m.isEdible()) m = Material.COOKIE;
ItemStack it = new ItemStack(m, Math.max(1, amount));
ItemMeta meta = it.getItemMeta();
if (meta == null) return it;
meta.setDisplayName(ChristmasManager.color(s.getString("name", "&6" + id)));
List<String> lore = new ArrayList<>();
for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l));
meta.setLore(lore);
meta.setEnchantmentGlintOverride(true);
meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, id);
it.setItemMeta(meta);
return it;
}
public String idOf(ItemStack it) {
if (it == null || !it.hasItemMeta()) return null;
return it.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING);
}
/** Wendet die Effekte eines gegessenen Plaetzchens an. */
public void eat(Player p, String id) {
for (String spec : mgr.cfg().getStringList("cookies." + id + ".effects")) {
String[] parts = spec.split(":");
PotionEffectType type = effect(parts[0].trim());
if (type == null) continue;
int seconds = parts.length > 1 ? ChristmasManager.toInt(parts[1], 30) : 30;
int amp = parts.length > 2 ? ChristmasManager.toInt(parts[2], 0) : 0;
p.addPotionEffect(new PotionEffect(type, seconds * 20, Math.max(0, amp)));
}
}
@SuppressWarnings("deprecation")
private static PotionEffectType effect(String name) {
String n = name.toLowerCase();
PotionEffectType t = Registry.EFFECT.get(NamespacedKey.minecraft(n));
return t != null ? t : PotionEffectType.getByName(name.toUpperCase());
}
}
@@ -0,0 +1,123 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.Adventskalender;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.scheduler.BukkitTask;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/** Countdown bis Heiligabend: Nachricht beim Join, Bossbar und Platzhalter. */
public final class CountdownManager {
private final ChristmasManager mgr;
private final NamespacedKey offKey;
private BossBar bar;
private BukkitTask task;
CountdownManager(ChristmasManager mgr) {
this.mgr = mgr;
this.offKey = new NamespacedKey(mgr.plugin(), "countdown_off");
}
void start() {
stop();
if (!mgr.cfg().getBoolean("countdown.enable", true)) return;
bar = Bukkit.createBossBar("", BarColor.RED, BarStyle.SEGMENTED_10);
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::update, 40L, 20L * 30);
}
void stop() {
if (task != null) task.cancel();
task = null;
if (bar != null) { bar.removeAll(); bar = null; }
}
// ---------- Berechnung ----------
private LocalDate today() {
return LocalDate.now(Adventskalender.getInstance().getZone());
}
private LocalDate target() {
String t = mgr.cfg().getString("countdown.target", "12-24");
String[] p = t.split("-");
int month = 12, day = 24;
try {
month = Integer.parseInt(p[0].trim());
day = Integer.parseInt(p[1].trim());
} catch (Exception ignored) { }
LocalDate today = today();
LocalDate thisYear;
try {
thisYear = LocalDate.of(today.getYear(), month, day);
} catch (Exception e) {
thisYear = LocalDate.of(today.getYear(), 12, 24);
}
return today.isAfter(thisYear) ? thisYear.plusYears(1) : thisYear;
}
/** Tage bis zum Zieldatum (0 = heute). */
public int daysLeft() {
return (int) ChronoUnit.DAYS.between(today(), target());
}
/** Liegt der Countdown im Anzeigefenster (show-days-before Tage vorher)? */
public boolean visible() {
return mgr.cfg().getBoolean("countdown.enable", true)
&& daysLeft() <= mgr.cfg().getInt("countdown.show-days-before", 60);
}
/** Tage, bis die Anzeige beginnt (0 = schon sichtbar). */
public int daysUntilVisible() {
return Math.max(0, daysLeft() - mgr.cfg().getInt("countdown.show-days-before", 60));
}
public String text() {
int d = daysLeft();
if (d == 0) return mgr.raw("countdown-today");
return mgr.raw(d == 1 ? "countdown-bossbar-one" : "countdown-bossbar").replace("%days%", String.valueOf(d));
}
// ---------- Anzeige ----------
private boolean hiddenFor(Player p) {
Byte b = p.getPersistentDataContainer().get(offKey, PersistentDataType.BYTE);
return b != null && b == 1;
}
/** Schaltet die Bossbar fuer den Spieler um; true = jetzt sichtbar. */
public boolean toggle(Player p) {
boolean nowHidden = !hiddenFor(p);
p.getPersistentDataContainer().set(offKey, PersistentDataType.BYTE, (byte) (nowHidden ? 1 : 0));
update();
return !nowHidden;
}
private void update() {
if (bar == null) return;
boolean show = visible() && mgr.cfg().getBoolean("countdown.bossbar", true);
if (!show) { bar.removeAll(); return; }
int show_days = Math.max(1, mgr.cfg().getInt("countdown.show-days-before", 60));
bar.setTitle(text());
bar.setProgress(Math.max(0, Math.min(1, 1.0 - daysLeft() / (double) show_days)));
for (Player p : Bukkit.getOnlinePlayers()) {
if (hiddenFor(p)) bar.removePlayer(p); else bar.addPlayer(p);
}
}
public void onJoin(Player p) {
if (!visible()) return;
if (bar != null && mgr.cfg().getBoolean("countdown.bossbar", true) && !hiddenFor(p)) bar.addPlayer(p);
if (mgr.cfg().getBoolean("countdown.join-message", true)) {
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (p.isOnline()) p.sendMessage(mgr.prefixed(text()));
}, 60L);
}
}
}
@@ -0,0 +1,91 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.RewardUtil;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Weihnachts-Cracker: Items, die per Rechtsklick geoeffnet werden und eine zufaellige Belohnung geben. */
public final class CrackerManager {
private final ChristmasManager mgr;
private final NamespacedKey key;
CrackerManager(ChristmasManager mgr) {
this.mgr = mgr;
this.key = new NamespacedKey(mgr.plugin(), "cracker_id");
}
void reload() { /* Konfiguration wird bei Bedarf direkt gelesen */ }
public Set<String> ids() {
ConfigurationSection s = mgr.cfg().getConfigurationSection("crackers");
return s == null ? new LinkedHashSet<>() : s.getKeys(false);
}
public String firstId() {
return ids().stream().findFirst().orElse(null);
}
public ItemStack create(String id, int amount) {
ConfigurationSection s = mgr.cfg().getConfigurationSection("crackers." + id);
if (s == null) return null;
Material m = Material.matchMaterial(s.getString("material", "FIREWORK_STAR"));
if (m == null || !m.isItem()) m = Material.FIREWORK_STAR;
ItemStack it = new ItemStack(m, Math.max(1, amount));
ItemMeta meta = it.getItemMeta();
if (meta == null) return it;
meta.setDisplayName(ChristmasManager.color(s.getString("name", "&cCracker")));
List<String> lore = new ArrayList<>();
for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l));
meta.setLore(lore);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES);
meta.addEnchant(Enchantment.LURE, 1, true);
meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, id);
it.setItemMeta(meta);
return it;
}
/** Gibt die Cracker-ID des Items zurueck oder null. */
public String idOf(ItemStack it) {
if (it == null || !it.hasItemMeta()) return null;
return it.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING);
}
/** Oeffnet einen Cracker (verbraucht das Item NICHT, das macht der Aufrufer). */
public void open(Player p, String id) {
List<Map<?, ?>> rewards = mgr.cfg().getMapList("crackers." + id + ".rewards");
double sum = 0;
for (Map<?, ?> r : rewards) sum += weight(r);
if (sum <= 0) return;
double roll = ChristmasManager.RANDOM.nextDouble() * sum;
for (Map<?, ?> r : rewards) {
roll -= weight(r);
if (roll > 0) continue;
ChristmasExtras.applyMap(p, r);
break;
}
p.sendMessage(mgr.msg("cracker-opened"));
p.getWorld().spawnParticle(Particle.END_ROD, p.getLocation().add(0, 1.5, 0), 40, 0.4, 0.4, 0.4, 0.1);
p.getWorld().playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1f, 1f);
}
private static double weight(Map<?, ?> r) {
Object c = r.get("chance");
return c instanceof Number n ? Math.max(0, n.doubleValue()) : 1;
}
}
@@ -0,0 +1,217 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.Skull;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Candle;
import org.bukkit.block.data.type.Leaves;
import org.bukkit.block.data.type.Snow;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
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.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Weihnachtsmarkt-Deko: Ein Menue, aus dem Admins Deko per Klick aufstellen und wieder rueckgaengig machen. */
public final class DecoManager {
public static final class Menu implements InventoryHolder {
private Inventory inv;
@Override public Inventory getInventory() { return inv; }
}
public enum Type {
PRESENT(Material.CHEST, 10), CANDY_CANE(Material.RED_CONCRETE, 11),
LANTERN_POST(Material.LANTERN, 12), TREE(Material.SPRUCE_SAPLING, 13), WREATH(Material.RED_CANDLE, 14),
SNOWMAN(Material.CARVED_PUMPKIN, 15), LIGHTS(Material.SEA_LANTERN, 16), SNOW_PILE(Material.SNOW_BLOCK, 21);
final Material icon;
final int slot;
Type(Material icon, int slot) { this.icon = icon; this.slot = slot; }
}
private final ChristmasManager mgr;
private final Map<UUID, Deque<Runnable>> undo = new HashMap<>();
DecoManager(ChristmasManager mgr) { this.mgr = mgr; }
// ---------- Menue ----------
public void open(Player p) {
Menu holder = new Menu();
holder.inv = Bukkit.createInventory(holder, 27, mgr.raw("deco-menu-title"));
for (Type t : Type.values()) {
ItemStack it = new ItemStack(t.icon);
ItemMeta meta = it.getItemMeta();
if (meta != null) {
meta.setDisplayName(mgr.raw("deco-" + t.name().toLowerCase()));
meta.setLore(List.of(mgr.raw("deco-click-hint")));
it.setItemMeta(meta);
}
holder.inv.setItem(t.slot, it);
}
p.openInventory(holder.inv);
}
public void click(Player p, int slot) {
for (Type t : Type.values()) {
if (t.slot == slot) {
p.closeInventory();
place(p, t);
return;
}
}
}
// ---------- Platzieren ----------
private final class Session {
final List<BlockState> before = new ArrayList<>();
final List<Entity> entities = new ArrayList<>();
void set(Block b, BlockData data) {
before.add(b.getState());
b.setBlockData(data, false);
}
void set(Block b, Material m) { set(b, m.createBlockData()); }
boolean free(Block b) {
return b.getType().isAir() || (b.isPassable() && !b.isLiquid());
}
}
public void place(Player p, Type type) {
Block target = p.getTargetBlockExact(8);
if (target == null) { p.sendMessage(mgr.msg("deco-look")); return; }
Block base = target.getRelative(BlockFace.UP);
Session s = new Session();
BlockFace facing = p.getFacing();
switch (type) {
case PRESENT -> present(s, base);
case CANDY_CANE -> candyCane(s, base, facing);
case LANTERN_POST -> {
for (int i = 0; i < 3; i++) if (s.free(base.getRelative(0, i, 0))) s.set(base.getRelative(0, i, 0), Material.OAK_FENCE);
Block top = base.getRelative(0, 3, 0);
if (s.free(top)) s.set(top, Material.LANTERN);
}
case TREE -> {
Map<Vector, BlockData> st = mgr.trees().structure(MagicTreeManager.Level.TREE);
for (var e : st.entrySet()) {
Vector v = e.getKey();
Block b = base.getRelative(v.getBlockX(), v.getBlockY(), v.getBlockZ());
if (s.free(b)) s.set(b, e.getValue());
}
}
case WREATH -> wreath(s, base);
case SNOWMAN -> {
mgr.snowmen().spawnDeco(base.getLocation().add(0.5, 0, 0.5), "");
for (Entity e : base.getWorld().getNearbyEntities(base.getLocation().add(0.5, 0.5, 0.5), 1, 1, 1)) {
if (e instanceof org.bukkit.entity.Snowman sm && mgr.snowmen().isDeco(sm)) s.entities.add(sm);
}
}
case LIGHTS -> {
Material[] mats = {Material.SEA_LANTERN, Material.SHROOMLIGHT, Material.GLOWSTONE, Material.SHROOMLIGHT};
BlockFace f = facing == BlockFace.UP || facing == BlockFace.DOWN ? BlockFace.NORTH : facing;
for (int i = 0; i < 7; i++) {
Block b = base.getRelative(f, i);
if (s.free(b)) s.set(b, mats[i % mats.length]);
}
}
case SNOW_PILE -> {
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
Block b = base.getRelative(dx, 0, dz);
if (!s.free(b)) continue;
Snow data = (Snow) Material.SNOW.createBlockData();
data.setLayers(dx == 0 && dz == 0 ? 3 : (Math.abs(dx) + Math.abs(dz) == 1 ? 2 : 1));
s.set(b, data);
}
}
}
}
undo.computeIfAbsent(p.getUniqueId(), k -> new ArrayDeque<>()).push(() -> {
for (int i = s.before.size() - 1; i >= 0; i--) s.before.get(i).update(true, false);
for (Entity e : s.entities) if (e.isValid()) e.remove();
});
Deque<Runnable> stack = undo.get(p.getUniqueId());
while (stack.size() > 30) stack.removeLast();
p.sendMessage(mgr.msg("deco-placed"));
}
public void undoLast(Player p) {
Deque<Runnable> stack = undo.get(p.getUniqueId());
if (stack == null || stack.isEmpty()) { p.sendMessage(mgr.msg("deco-nothing-undo")); return; }
stack.pop().run();
p.sendMessage(mgr.msg("deco-undone"));
}
// ---------- Formen ----------
private void present(Session s, Block base) {
List<String> skins = mgr.cfg().getStringList("magic-tree.presents");
if (!s.free(base) || skins.isEmpty()) return;
s.set(base, Material.PLAYER_HEAD);
if (base.getState() instanceof Skull sk) {
String url = skins.get(ChristmasManager.RANDOM.nextInt(skins.size()));
try {
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(url.getBytes()), "present");
PlayerTextures tex = prof.getTextures();
tex.setSkin(new URL(url));
prof.setTextures(tex);
sk.setOwnerProfile(prof);
sk.update(false, false);
} catch (MalformedURLException ignored) { }
}
}
private void candyCane(Session s, Block base, BlockFace facing) {
BlockFace f = facing == BlockFace.UP || facing == BlockFace.DOWN ? BlockFace.NORTH : facing;
for (int i = 0; i < 4; i++) {
Block b = base.getRelative(0, i, 0);
if (s.free(b)) s.set(b, i % 2 == 0 ? Material.RED_CONCRETE : Material.WHITE_CONCRETE);
}
Block hook = base.getRelative(0, 3, 0).getRelative(f);
if (s.free(hook)) s.set(hook, Material.RED_CONCRETE);
Block hook2 = hook.getRelative(BlockFace.DOWN);
if (s.free(hook2)) s.set(hook2, Material.WHITE_CONCRETE);
}
private void wreath(Session s, Block base) {
Leaves leaves = (Leaves) Material.SPRUCE_LEAVES.createBlockData();
leaves.setPersistent(true);
int[][] ring = {{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}};
for (int[] r : ring) {
Block b = base.getRelative(r[0], 0, r[1]);
if (s.free(b)) s.set(b, leaves);
}
int[][] candles = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
for (int[] c : candles) {
Block b = base.getRelative(c[0], 1, c[1]);
if (!s.free(b)) continue;
Candle data = (Candle) Material.RED_CANDLE.createBlockData();
data.setLit(true);
s.set(b, data);
}
}
}
@@ -0,0 +1,97 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.FireworkEffect;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Firework;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.FireworkMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
/** Weihnachts- und Neujahrsgruss: Chat-Nachricht an bestimmten Tagen, auf Wunsch mit Feuerwerk beim Join. */
public final class GreetingManager {
private final ChristmasManager mgr;
private final File file;
private final NamespacedKey fireworkKey;
private String lastDate = "";
private BukkitTask task;
GreetingManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "greetings.yml");
this.fireworkKey = new NamespacedKey(mgr.plugin(), "xmas_firework");
}
void start() {
stop();
if (file.exists()) lastDate = YamlConfiguration.loadConfiguration(file).getString("last", "");
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::check, 200L, 20L * 30);
}
void stop() {
if (task != null) task.cancel();
task = null;
}
private LocalDate today() { return LocalDate.now(mgr.plugin().getZone()); }
/** Nachrichten-Schluessel fuer heute oder null. */
private String keyForToday() {
if (!mgr.cfg().getBoolean("greetings.enable", true)) return null;
LocalDate d = today();
String md = String.format("%02d-%02d", d.getMonthValue(), d.getDayOfMonth());
return mgr.cfg().getString("greetings.days." + md);
}
/** Einmal pro Tag an alle Online-Spieler. */
private void check() {
String key = keyForToday();
if (key == null || today().toString().equals(lastDate)) return;
if (Bukkit.getOnlinePlayers().isEmpty()) return;
lastDate = today().toString();
YamlConfiguration y = new YamlConfiguration();
y.set("last", lastDate);
try { y.save(file); } catch (IOException ignored) { }
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw(key)));
if (mgr.cfg().getBoolean("greetings.fireworks-on-broadcast", true)) {
for (Player p : Bukkit.getOnlinePlayers()) fireworks(p);
}
}
public void onJoin(Player p) {
String key = keyForToday();
if (key == null) return;
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (!p.isOnline()) return;
p.sendMessage(mgr.prefixed(mgr.raw(key)));
if (mgr.cfg().getBoolean("greetings.join-fireworks", true)) fireworks(p);
}, 80L);
}
/** Ein kleines Feuerwerk hoch ueber dem Spieler (kein Schaden). */
public void fireworks(Player p) {
Location at = p.getLocation().add(0, 8, 0);
Firework fw = p.getWorld().spawn(at, Firework.class, f -> {
FireworkMeta meta = f.getFireworkMeta();
meta.addEffect(FireworkEffect.builder().with(FireworkEffect.Type.BALL_LARGE)
.withColor(Color.RED, Color.WHITE).withFade(Color.GREEN).flicker(true).trail(true).build());
f.setFireworkMeta(meta);
f.getPersistentDataContainer().set(fireworkKey, PersistentDataType.BYTE, (byte) 1);
});
fw.detonate();
}
public boolean isOurs(Entity e) {
return e.getPersistentDataContainer().has(fireworkKey, PersistentDataType.BYTE);
}
}
@@ -0,0 +1,100 @@
package de.mviper.adventskalender.christmas;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.persistence.PersistentDataType;
import java.time.LocalDate;
import java.util.List;
/** Nikolausmuetze: Rotes Kopf-Item, das beim Join angeboten (oder direkt aufgesetzt) wird. */
public final class HatManager {
private final ChristmasManager mgr;
private final NamespacedKey hatKey;
HatManager(ChristmasManager mgr) {
this.mgr = mgr;
this.hatKey = new NamespacedKey(mgr.plugin(), "santa_hat");
}
public boolean inSeason() {
if (!mgr.cfg().getBoolean("santa-hat.enable", true)) return false;
LocalDate today = LocalDate.now(mgr.plugin().getZone());
try {
String[] from = mgr.cfg().getString("santa-hat.from", "12-01").split("-");
String[] until = mgr.cfg().getString("santa-hat.until", "12-31").split("-");
LocalDate a = LocalDate.of(today.getYear(), Integer.parseInt(from[0].trim()), Integer.parseInt(from[1].trim()));
LocalDate b = LocalDate.of(today.getYear(), Integer.parseInt(until[0].trim()), Integer.parseInt(until[1].trim()));
return !today.isBefore(a) && !today.isAfter(b);
} catch (Exception e) {
return true;
}
}
public ItemStack create() {
ItemStack it = new ItemStack(Material.LEATHER_HELMET);
LeatherArmorMeta meta = (LeatherArmorMeta) it.getItemMeta();
if (meta != null) {
meta.setColor(Color.fromRGB(200, 25, 30));
meta.setDisplayName(mgr.raw("hat-name"));
meta.setLore(mgr.rawList("hat-lore"));
meta.setUnbreakable(true);
meta.addItemFlags(org.bukkit.inventory.ItemFlag.HIDE_UNBREAKABLE, org.bukkit.inventory.ItemFlag.HIDE_ATTRIBUTES,
org.bukkit.inventory.ItemFlag.HIDE_DYE);
meta.getPersistentDataContainer().set(hatKey, PersistentDataType.BYTE, (byte) 1);
it.setItemMeta(meta);
}
return it;
}
private boolean isHat(ItemStack it) {
return it != null && it.hasItemMeta()
&& it.getItemMeta().getPersistentDataContainer().has(hatKey, PersistentDataType.BYTE);
}
private boolean owns(Player p) {
for (ItemStack it : p.getInventory().getContents()) if (isHat(it)) return true;
for (ItemStack it : p.getInventory().getArmorContents()) if (isHat(it)) return true;
return false;
}
/** /xmas hat und Klick auf das Angebot. */
public void give(Player p) {
if (!inSeason()) { p.sendMessage(mgr.msg("hat-season")); return; }
if (owns(p)) { p.sendMessage(mgr.msg("hat-have")); return; }
ItemStack hat = create();
if (p.getInventory().getHelmet() == null || p.getInventory().getHelmet().getType().isAir()) {
p.getInventory().setHelmet(hat);
} else {
de.mviper.adventskalender.RewardUtil.give(p, hat);
}
p.sendMessage(mgr.msg("hat-given"));
}
public void onJoin(Player p) {
if (!inSeason() || owns(p)) return;
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (!p.isOnline() || owns(p)) return;
if (mgr.cfg().getBoolean("santa-hat.auto-equip", false)) {
give(p);
return;
}
if (!mgr.cfg().getBoolean("santa-hat.offer-on-join", true)) return;
TextComponent msg = new TextComponent(mgr.prefixed(mgr.raw("hat-offer")) + " ");
TextComponent click = new TextComponent(mgr.raw("hat-offer-click"));
click.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/xmas hat"));
click.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(mgr.raw("hat-offer-hover"))));
msg.addExtra(click);
p.spigot().sendMessage(msg);
}, 120L);
}
}
@@ -0,0 +1,109 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/** Login-Serie: Wer mehrere Tage hintereinander online kommt, bekommt steigende Belohnungen. */
public final class LoginStreakManager {
private static final class Data {
LocalDate last;
int streak;
int best;
}
private final ChristmasManager mgr;
private final File file;
private final Map<UUID, Data> data = new HashMap<>();
LoginStreakManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "streaks.yml");
}
public int streak(UUID id) {
Data d = data.get(id);
return d == null ? 0 : d.streak;
}
public int best(UUID id) {
Data d = data.get(id);
return d == null ? 0 : d.best;
}
void load() {
data.clear();
if (!file.exists()) return;
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
ConfigurationSection s = y.getConfigurationSection("players");
if (s == null) return;
for (String k : s.getKeys(false)) {
try {
Data d = new Data();
String last = s.getString(k + ".last");
d.last = last == null ? null : LocalDate.parse(last);
d.streak = s.getInt(k + ".streak");
d.best = s.getInt(k + ".best");
data.put(UUID.fromString(k), d);
} catch (Exception ignored) { }
}
}
void save() {
YamlConfiguration y = new YamlConfiguration();
for (var e : data.entrySet()) {
String p = "players." + e.getKey();
y.set(p + ".last", e.getValue().last == null ? null : e.getValue().last.toString());
y.set(p + ".streak", e.getValue().streak);
y.set(p + ".best", e.getValue().best);
}
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("streaks.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
private boolean inWindow(LocalDate today) {
try {
String[] from = mgr.cfg().getString("login-streak.from", "12-01").split("-");
String[] until = mgr.cfg().getString("login-streak.until", "12-31").split("-");
LocalDate a = LocalDate.of(today.getYear(), Integer.parseInt(from[0].trim()), Integer.parseInt(from[1].trim()));
LocalDate b = LocalDate.of(today.getYear(), Integer.parseInt(until[0].trim()), Integer.parseInt(until[1].trim()));
return !today.isBefore(a) && !today.isAfter(b);
} catch (Exception e) {
return true;
}
}
public void onJoin(Player p) {
if (!mgr.cfg().getBoolean("login-streak.enable", true)) return;
LocalDate today = LocalDate.now(mgr.plugin().getZone());
if (!inWindow(today)) return;
Data d = data.computeIfAbsent(p.getUniqueId(), k -> new Data());
if (today.equals(d.last)) return; // heute schon belohnt
d.streak = (d.last != null && d.last.plusDays(1).equals(today)) ? d.streak + 1 : 1;
d.best = Math.max(d.best, d.streak);
d.last = today;
save();
int cycle = Math.max(1, mgr.cfg().getInt("login-streak.cycle", 7));
int day = ((d.streak - 1) % cycle) + 1;
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (!p.isOnline()) return;
for (Map<?, ?> reward : mgr.cfg().getMapList("login-streak.rewards." + day)) {
ChristmasExtras.applyMap(p, reward);
}
p.sendMessage(mgr.msg("streak-reward", "%day%", String.valueOf(d.streak), "%cycle%", String.valueOf(day)));
}, 100L);
}
}
@@ -0,0 +1,812 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.RewardUtil;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.Skull;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Leaves;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.profile.PlayerTextures;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/** Magic Tree: Kristall auf Setzling, Items neben den Baum werfen, Rechtsklick = wachsen, Geschenk-Koepfe. */
public final class MagicTreeManager {
public enum Level {
SAPLING("sapling", 1), SMALL_TREE("small_tree", 2), TREE("tree", 3), MAGIC_TREE("magic_tree", 4);
final String key;
final int radius;
Level(String key, int radius) { this.key = key; this.radius = radius; }
Level next() { return ordinal() + 1 < values().length ? values()[ordinal() + 1] : null; }
static Level parse(String s) {
try { return valueOf(s.toUpperCase()); } catch (Exception e) { return SAPLING; }
}
}
public static final class Tree {
final UUID id = UUID.randomUUID();
final UUID owner;
String ownerName;
final String world;
final int x, y, z;
Level level = Level.SAPLING;
final Map<Material, Integer> req = new EnumMap<>(Material.class);
final Map<Material, Integer> spent = new EnumMap<>(Material.class);
final Set<String> blocks = new HashSet<>();
final Set<String> presents = new HashSet<>();
boolean indexed;
boolean readyAnnounced;
long counter;
Tree(UUID owner, String ownerName, String world, int x, int y, int z) {
this.owner = owner; this.ownerName = ownerName; this.world = world;
this.x = x; this.y = y; this.z = z;
}
public UUID owner() { return owner; }
public String ownerName() { return ownerName; }
public Level level() { return level; }
Location loc() {
World w = Bukkit.getWorld(world);
return w == null ? null : new Location(w, x, y, z);
}
}
private final ChristmasManager mgr;
private final List<Tree> trees = new ArrayList<>();
private final Map<String, Tree> blockMap = new HashMap<>();
private final Map<String, Tree> presentMap = new HashMap<>();
private final Map<UUID, Long> destroyers = new HashMap<>();
private final File file;
private final File giftFile;
private final NamespacedKey crystalKey;
private final List<ItemStack> customGifts = new ArrayList<>();
private boolean endedFlag;
private boolean endAnnounced;
private BukkitTask task;
private BukkitTask fxTask;
private int fxTick;
MagicTreeManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "trees.yml");
this.giftFile = new File(mgr.plugin().getDataFolder(), "gifts.yml");
this.crystalKey = new NamespacedKey(mgr.plugin(), "xmas_crystal");
}
// ================= Status =================
public boolean enabled() { return mgr.cfg().getBoolean("magic-tree.enable", true); }
public int treeCount() { return trees.size(); }
public long countOf(UUID owner) {
return trees.stream().filter(t -> t.owner.equals(owner)).count();
}
public boolean isEnded() {
if (endedFlag) return true;
if (!mgr.cfg().getBoolean("magic-tree.holiday-ends.enabled", true)) return false;
try {
LocalDateTime end = LocalDateTime.parse(mgr.cfg().getString("magic-tree.holiday-ends.date", ""),
DateTimeFormatter.ofPattern("dd-MM-yyyy HH-mm-ss"));
return LocalDateTime.now().isAfter(end);
} catch (DateTimeParseException e) {
return false;
}
}
public void setEnded(boolean ended) {
endedFlag = ended;
save();
if (ended) announceEnd();
}
private void announceEnd() {
if (endAnnounced) return;
endAnnounced = true;
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw("timeout")));
}
// ================= Keys =================
private static String key(Block b) {
return b.getWorld().getName() + ";" + b.getX() + ";" + b.getY() + ";" + b.getZ();
}
public boolean isProtected(Block b) {
String k = key(b);
return blockMap.containsKey(k) || presentMap.containsKey(k);
}
public boolean isTreeBlock(Block b) { return blockMap.containsKey(key(b)); }
public boolean isPresentBlock(Block b) { return presentMap.containsKey(key(b)); }
public Tree treeOf(Block b) { return blockMap.get(key(b)); }
public Tree treeOfPresent(Block b) { return presentMap.get(key(b)); }
// ================= Kristall =================
public ItemStack crystal(int amount) {
ConfigurationSection s = mgr.cfg().getConfigurationSection("crystal");
Material m = Material.matchMaterial(s == null ? "NETHER_STAR" : s.getString("material", "NETHER_STAR"));
if (m == null || !m.isItem()) m = Material.NETHER_STAR;
ItemStack it = new ItemStack(m, Math.max(1, amount));
ItemMeta meta = it.getItemMeta();
if (meta != null) {
meta.setDisplayName(ChristmasManager.color(s == null ? "&bWeihnachts-Kristall" : s.getString("name", "&bWeihnachts-Kristall")));
List<String> lore = new ArrayList<>();
if (s != null) for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l));
meta.setLore(lore);
meta.getPersistentDataContainer().set(crystalKey, PersistentDataType.BYTE, (byte) 1);
it.setItemMeta(meta);
}
return it;
}
public boolean isCrystal(ItemStack it) {
return it != null && it.hasItemMeta()
&& it.getItemMeta().getPersistentDataContainer().has(crystalKey, PersistentDataType.BYTE);
}
// ================= Persistenz =================
void load() {
trees.clear();
blockMap.clear();
presentMap.clear();
customGifts.clear();
if (giftFile.exists()) {
YamlConfiguration g = YamlConfiguration.loadConfiguration(giftFile);
for (Object o : g.getList("items", new ArrayList<>())) if (o instanceof ItemStack i) customGifts.add(i);
}
if (file.exists()) {
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
endedFlag = y.getBoolean("ended", false);
ConfigurationSection s = y.getConfigurationSection("trees");
if (s != null) {
for (String k : s.getKeys(false)) {
ConfigurationSection t = s.getConfigurationSection(k);
if (t == null || !t.isString("level")) continue;
try {
Tree tr = new Tree(UUID.fromString(t.getString("owner", "")), t.getString("owner-name", "?"),
t.getString("world", "world"), t.getInt("x"), t.getInt("y"), t.getInt("z"));
tr.level = Level.parse(t.getString("level", "SAPLING"));
readMap(t.getConfigurationSection("req"), tr.req);
readMap(t.getConfigurationSection("spent"), tr.spent);
tr.counter = t.getLong("counter", 0);
for (String p : t.getStringList("presents")) {
tr.presents.add(p);
presentMap.put(p, tr);
}
trees.add(tr);
} catch (IllegalArgumentException ignored) { }
}
}
}
if (task != null) task.cancel();
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 20L);
registerRecipe();
if (fxTask != null) fxTask.cancel();
fxTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::fx, 60L, 3L);
}
// ---------- Rezept: Diamanten im Kreuz, Smaragd in der Mitte ----------
private NamespacedKey recipeKey() { return new NamespacedKey(mgr.plugin(), "xmas_crystal"); }
void registerRecipe() {
unregisterRecipe();
if (!mgr.cfg().getBoolean("magic-tree.crystal-recipe", true)) return;
ShapedRecipe r = new ShapedRecipe(recipeKey(), crystal(1));
r.shape(" D ", "DED", " D ");
r.setIngredient('D', Material.DIAMOND);
r.setIngredient('E', Material.EMERALD);
Bukkit.addRecipe(r);
}
void unregisterRecipe() {
Bukkit.removeRecipe(recipeKey());
}
private static void readMap(ConfigurationSection s, Map<Material, Integer> out) {
if (s == null) return;
for (String k : s.getKeys(false)) {
Material m = Material.matchMaterial(k);
if (m != null && s.getInt(k) > 0) out.put(m, s.getInt(k));
}
}
void save() {
YamlConfiguration y = new YamlConfiguration();
y.set("ended", endedFlag);
for (Tree t : trees) {
String p = "trees." + t.id;
y.set(p + ".owner", t.owner.toString());
y.set(p + ".owner-name", t.ownerName);
y.set(p + ".world", t.world);
y.set(p + ".x", t.x);
y.set(p + ".y", t.y);
y.set(p + ".z", t.z);
y.set(p + ".level", t.level.name());
y.set(p + ".counter", t.counter);
for (var e : t.req.entrySet()) y.set(p + ".req." + e.getKey().name(), e.getValue());
for (var e : t.spent.entrySet()) y.set(p + ".spent." + e.getKey().name(), e.getValue());
y.set(p + ".presents", new ArrayList<>(t.presents));
}
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("trees.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
private void saveGifts() {
YamlConfiguration g = new YamlConfiguration();
g.set("items", customGifts);
try {
g.save(giftFile);
} catch (IOException e) {
mgr.plugin().getLogger().warning("gifts.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
void shutdown() {
if (task != null) task.cancel();
task = null;
if (fxTask != null) fxTask.cancel();
fxTask = null;
unregisterRecipe();
}
public boolean addGift(ItemStack hand) {
if (hand == null || hand.getType().isAir()) return false;
customGifts.add(hand.clone());
saveGifts();
return true;
}
// ================= Struktur =================
Map<Vector, BlockData> structure(Level lv) {
Map<Vector, BlockData> m = new LinkedHashMap<>();
if (lv == Level.SAPLING) {
m.put(new Vector(0, 0, 0), Material.SPRUCE_SAPLING.createBlockData());
return m;
}
int trunk;
int[] radii;
switch (lv) {
case SMALL_TREE -> { trunk = 2; radii = new int[]{2, 1, 0}; }
case TREE -> { trunk = 3; radii = new int[]{3, 2, 2, 1, 1, 0}; }
default -> { trunk = 3; radii = new int[]{4, 3, 3, 2, 2, 1, 1, 0}; }
}
Leaves leaves = (Leaves) Material.SPRUCE_LEAVES.createBlockData();
leaves.setPersistent(true);
Material[] swag = {Material.RED_CONCRETE, Material.GOLD_BLOCK, Material.SEA_LANTERN, Material.BLUE_CONCRETE};
for (int y = 0; y < trunk; y++) m.put(new Vector(0, y, 0), Material.SPRUCE_LOG.createBlockData());
for (int i = 0; i < radii.length; i++) {
int r = radii[i];
int y = trunk + i;
if (r == 0) {
m.put(new Vector(0, y, 0), Material.GLOWSTONE.createBlockData());
continue;
}
for (int dx = -r; dx <= r; dx++) {
for (int dz = -r; dz <= r; dz++) {
int d2 = dx * dx + dz * dz;
if (d2 > r * r + 1) continue;
if (dx == 0 && dz == 0) { m.put(new Vector(0, y, 0), Material.SPRUCE_LOG.createBlockData()); continue; }
boolean edge = d2 >= r * r;
if (lv != Level.SMALL_TREE && edge && Math.floorMod(dx * 7 + dz * 13 + y * 5, 5) == 0) {
m.put(new Vector(dx, y, dz), swag[Math.floorMod(dx + dz + y, swag.length)].createBlockData());
} else {
m.put(new Vector(dx, y, dz), leaves);
}
}
}
}
return m;
}
private boolean canGrow(Tree t, Level lv) {
Location base = t.loc();
if (base == null) return false;
for (Vector v : structure(lv).keySet()) {
Block b = base.getWorld().getBlockAt(t.x + v.getBlockX(), t.y + v.getBlockY(), t.z + v.getBlockZ());
if (b.getType().isAir() || t.blocks.contains(key(b)) || (b.isPassable() && !b.isLiquid())) continue;
return false;
}
return true;
}
private void clearBlocks(Tree t) {
World w = Bukkit.getWorld(t.world);
for (String k : t.blocks) {
blockMap.remove(k);
if (w == null) continue;
String[] p = k.split(";");
w.getBlockAt(Integer.parseInt(p[1]), Integer.parseInt(p[2]), Integer.parseInt(p[3])).setType(Material.AIR, false);
}
t.blocks.clear();
}
private void build(Tree t) {
World w = Bukkit.getWorld(t.world);
if (w == null) return;
clearBlocks(t);
for (var e : structure(t.level).entrySet()) {
Vector v = e.getKey();
Block b = w.getBlockAt(t.x + v.getBlockX(), t.y + v.getBlockY(), t.z + v.getBlockZ());
b.setBlockData(e.getValue(), false);
t.blocks.add(key(b));
blockMap.put(key(b), t);
}
t.indexed = true;
}
/** Merkt sich nach dem Laden, welche Bloecke zum Baum gehoeren (ohne etwas zu platzieren). */
private void index(Tree t) {
World w = Bukkit.getWorld(t.world);
if (w == null || !w.isChunkLoaded(t.x >> 4, t.z >> 4)) return;
t.blocks.clear();
for (var e : structure(t.level).entrySet()) {
Vector v = e.getKey();
Block b = w.getBlockAt(t.x + v.getBlockX(), t.y + v.getBlockY(), t.z + v.getBlockZ());
if (b.getType() == e.getValue().getMaterial()) {
t.blocks.add(key(b));
blockMap.put(key(b), t);
}
}
t.indexed = true;
}
private int height(Level lv) {
int max = 0;
for (Vector v : structure(lv).keySet()) max = Math.max(max, v.getBlockY());
return max;
}
// ================= Aktionen =================
private void loadRequirements(Tree t) {
t.req.clear();
ConfigurationSection s = mgr.cfg().getConfigurationSection("magic-tree.levels." + t.level.key + ".lvlup");
if (s == null) return;
for (String k : s.getKeys(false)) {
Material m = Material.matchMaterial(k);
if (m != null && s.getInt(k) > 0) t.req.put(m, s.getInt(k));
}
t.readyAnnounced = false;
}
public void plant(Player p, Block sapling) {
if (!enabled() || isEnded()) { p.sendMessage(mgr.msg("tree.tree-disabled")); return; }
int limit = mgr.cfg().getInt("magic-tree.tree-limit", 3);
if (!p.hasPermission("xmas.admin") && trees.stream().filter(t -> t.owner.equals(p.getUniqueId())).count() >= limit) {
p.sendMessage(mgr.msg("tree.tree-limit"));
return;
}
Tree t = new Tree(p.getUniqueId(), p.getName(), sapling.getWorld().getName(),
sapling.getX(), sapling.getY(), sapling.getZ());
loadRequirements(t);
t.counter = cooldown(t.level);
trees.add(t);
build(t);
save();
p.getWorld().spawnParticle(Particle.END_ROD, sapling.getLocation().add(0.5, 0.5, 0.5), 30, 0.4, 0.4, 0.4, 0.05);
p.playSound(p.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, 1.2f);
p.sendMessage(mgr.msg("tree.tree-planted"));
}
private String reqText(Tree t) {
List<String> parts = new ArrayList<>();
for (var e : t.req.entrySet()) parts.add(e.getValue() + "x " + e.getKey().name());
return String.join(", ", parts);
}
/** Rechtsklick auf einen Baumblock. */
public void click(Player p, Tree t) {
if (!t.owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) {
p.sendMessage(mgr.msg("tree.not-owner"));
return;
}
if (isEnded()) {
p.sendMessage(mgr.msg("tree.tree-disabled"));
return;
}
Level next = t.level.next();
if (next == null) { p.sendMessage(mgr.msg("tree.grow-lvl-max")); return; }
if (!t.req.isEmpty()) {
p.sendMessage(mgr.raw("tree.grow-req-list-title"));
p.sendMessage("§f" + reqText(t));
return;
}
if (!canGrow(t, next)) { p.sendMessage(mgr.msg("tree.grow-not-enough-place")); return; }
t.level = next;
build(t);
loadRequirements(t);
t.counter = cooldown(t.level);
save();
Location top = t.loc().add(0.5, height(t.level) + 1, 0.5);
t.loc().getWorld().spawnParticle(Particle.END_ROD, top, 60, 1, 1, 1, 0.1);
t.loc().getWorld().playSound(top, Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f);
if (t.level.next() == null) p.sendMessage(mgr.msg("tree.grow-lvl-max"));
}
/** Geworfenes Item nahe am Baum aufnehmen. */
public void absorb(Player p, Item item) {
ItemStack stack = item.getItemStack();
for (Tree t : trees) {
if (!t.owner.equals(p.getUniqueId())) continue;
Location base = t.loc();
if (base == null || !base.getWorld().equals(item.getWorld())) continue;
Location il = item.getLocation();
double dx = il.getX() - (t.x + 0.5), dz = il.getZ() - (t.z + 0.5), dy = il.getY() - t.y;
if (dx * dx + dz * dz > Math.pow(t.level.radius + 3, 2) || dy < -2 || dy > height(t.level) + 4) continue;
Integer need = t.req.get(stack.getType());
if (need == null || need <= 0) continue;
int take = Math.min(need, stack.getAmount());
if (take >= need) t.req.remove(stack.getType()); else t.req.put(stack.getType(), need - take);
t.spent.merge(stack.getType(), take, Integer::sum);
if (stack.getAmount() - take <= 0) item.remove();
else { stack.setAmount(stack.getAmount() - take); item.setItemStack(stack); }
il.getWorld().spawnParticle(Particle.END_ROD, il, 12, 0.2, 0.2, 0.2, 0.05);
p.sendMessage(mgr.msg("tree.grow-fed", "%amount%", String.valueOf(take), "%item%", stack.getType().name()));
if (t.req.isEmpty() && t.level.next() != null && !t.readyAnnounced) {
t.readyAnnounced = true;
p.sendMessage(mgr.msg("tree.grow-lvl-ready"));
}
save();
return;
}
}
/** Abbauversuch auf einen Baumblock. */
public void breakAttempt(Player p, Tree t, Block b) {
boolean trunkLike = b.getType() == Material.SPRUCE_LOG || Tag_isSapling(b.getType());
if (!trunkLike) {
p.sendMessage(mgr.msg(t.owner.equals(p.getUniqueId()) ? "tree.destroy-leaves-tut" : "tree.destroy-leaves-santa"));
return;
}
if (!t.owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) {
p.sendMessage(mgr.msg("tree.destroy-fail-owner"));
return;
}
long now = System.currentTimeMillis();
Long last = destroyers.get(p.getUniqueId());
if (last == null || now - last > 10_000) {
destroyers.put(p.getUniqueId(), now);
p.sendMessage(mgr.msg(t.level == Level.SAPLING ? "tree.destroy-sapling" : "tree.destroy-warning"));
return;
}
destroyers.remove(p.getUniqueId());
destroy(p, t);
}
private static boolean Tag_isSapling(Material m) {
return org.bukkit.Tag.SAPLINGS.isTagged(m);
}
private void destroy(Player p, Tree t) {
boolean refund = isEnded() && mgr.cfg().getBoolean("magic-tree.holiday-ends.resource-back", true);
removeTree(t);
if (refund) {
for (var e : t.spent.entrySet()) RewardUtil.give(p, new ItemStack(e.getKey(), e.getValue()));
p.sendMessage(mgr.msg("tree.destroy-refund"));
} else {
p.sendMessage(mgr.msg("tree.destroy-complete"));
}
}
private void removeTree(Tree t) {
World w = Bukkit.getWorld(t.world);
for (String k : t.presents) {
presentMap.remove(k);
if (w == null) continue;
String[] pp = k.split(";");
w.getBlockAt(Integer.parseInt(pp[1]), Integer.parseInt(pp[2]), Integer.parseInt(pp[3])).setType(Material.AIR, false);
}
t.presents.clear();
clearBlocks(t);
trees.remove(t);
save();
}
public void info(Player p, Tree t) {
p.sendMessage(mgr.msg("tree.info", "%owner%", t.ownerName, "%level%", t.level.name()));
}
// ================= Geschenke =================
private long cooldown(Level lv) {
return mgr.cfg().getLong("magic-tree.levels." + lv.key + ".gift-cooldown", -1);
}
public int spawnPresentsEverywhere() {
int n = 0;
for (Tree t : new ArrayList<>(trees)) if (t.level != Level.SAPLING && spawnPresent(t, true)) n++;
return n;
}
private boolean spawnPresent(Tree t, boolean ignoreCap) {
Location base = t.loc();
if (base == null || !base.getWorld().isChunkLoaded(t.x >> 4, t.z >> 4)) return false;
int cap = mgr.cfg().getInt("magic-tree.max-presents-per-tree", 5);
if (!ignoreCap && t.presents.size() >= cap) return false;
List<String> skins = mgr.cfg().getStringList("magic-tree.presents");
if (skins.isEmpty()) return false;
World w = base.getWorld();
for (int attempt = 0; attempt < 12; attempt++) {
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
double dist = t.level.radius + 1 + ChristmasManager.RANDOM.nextInt(2);
int bx = t.x + (int) Math.round(Math.cos(ang) * dist);
int bz = t.z + (int) Math.round(Math.sin(ang) * dist);
for (int by = t.y + 2; by >= t.y - 3; by--) {
Block b = w.getBlockAt(bx, by, bz);
if (b.getType().isAir() && w.getBlockAt(bx, by - 1, bz).getType().isSolid() && !isProtected(b)) {
setSkin(b, skins.get(ChristmasManager.RANDOM.nextInt(skins.size())));
t.presents.add(key(b));
presentMap.put(key(b), t);
w.spawnParticle(Particle.SNOWFLAKE, b.getLocation().add(0.5, 0.8, 0.5), 15, 0.3, 0.3, 0.3, 0.02);
return true;
}
}
}
return false;
}
void setSkin(Block b, String url) {
b.setType(Material.PLAYER_HEAD, false);
if (!(b.getState() instanceof Skull sk)) return;
try {
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(url.getBytes()), "present");
PlayerTextures tex = prof.getTextures();
tex.setSkin(new URL(url));
prof.setTextures(tex);
sk.setOwnerProfile(prof);
sk.update(false, false);
} catch (MalformedURLException ignored) { }
}
public void openPresent(Player p, Tree t, Block b) {
String k = key(b);
presentMap.remove(k);
t.presents.remove(k);
b.setType(Material.AIR, false);
b.getWorld().spawnParticle(Particle.END_ROD, b.getLocation().add(0.5, 0.5, 0.5), 20, 0.3, 0.3, 0.3, 0.05);
b.getWorld().playSound(b.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.4f);
save();
if (mgr.cfg().getBoolean("magic-tree.luck.enabled", false)
&& ChristmasManager.RANDOM.nextInt(100) >= mgr.cfg().getInt("magic-tree.luck.chance", 75)) {
p.sendMessage(mgr.msg("gift.unluck-message"));
return;
}
ItemStack gift = randomGift();
if (gift != null) {
RewardUtil.give(p, gift);
p.sendMessage(mgr.msg("gift.opened"));
}
}
private ItemStack randomGift() {
List<String> mats = mgr.cfg().getStringList("magic-tree.gifts");
int total = mats.size() + customGifts.size();
if (total == 0) return null;
int i = ChristmasManager.RANDOM.nextInt(total);
if (i < mats.size()) return ChristmasManager.parseItem(mats.get(i));
return customGifts.get(i - mats.size()).clone();
}
// ================= Partikel-Show =================
private static final Particle DUST = lookup("DUST", "REDSTONE");
private static final Color[] SPIRAL = {Color.RED, Color.fromRGB(255, 200, 50), Color.LIME};
private static Particle lookup(String... names) {
for (String n : names) {
try { return Particle.valueOf(n); } catch (IllegalArgumentException ignored) { }
}
return null;
}
private void dust(World w, double x, double y, double z, Color c, float size) {
if (DUST == null) return;
w.spawnParticle(DUST, x, y, z, 1, 0, 0, 0, 0, new Particle.DustOptions(c, size));
}
private boolean playerNear(Location l, double dist) {
for (Player p : l.getWorld().getPlayers()) {
if (p.getLocation().distanceSquared(l) <= dist * dist) return true;
}
return false;
}
/** Leuchtende Spirale, Lichter, Stern an der Spitze und Glitzer ueber Geschenken. */
private void fx() {
if (!enabled() || isEnded() || !mgr.cfg().getBoolean("magic-tree.particles.enable", true)) return;
fxTick++;
double intensity = Math.max(0.2, Math.min(5.0, mgr.cfg().getDouble("magic-tree.particles.intensity", 2.0)));
for (Tree t : trees) {
Location base = t.loc();
if (base == null || !base.getWorld().isChunkLoaded(t.x >> 4, t.z >> 4) || !playerNear(base, 40)) continue;
World w = base.getWorld();
double cx = t.x + 0.5, cz = t.z + 0.5;
int h = height(t.level);
if (t.level == Level.SAPLING) {
if (fxTick % 4 == 0) {
double a = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
w.spawnParticle(Particle.END_ROD, cx + Math.cos(a) * 0.6, t.y + 0.3 + ChristmasManager.RANDOM.nextDouble() * 0.6,
cz + Math.sin(a) * 0.6, 1, 0, 0, 0, 0.002);
}
continue;
}
double yStart = t.y + 2.2;
double yEnd = t.y + h + 0.5;
int strands = t.level.ordinal() + 1; // 2, 3, 4 Straenge je nach Stufe
int trail = Math.max(3, (int) Math.round(5 * intensity)); // Punkte pro Strang (laengerer Schweif)
// Spirale in Rot, Gold und Gruen mit leuchtendem Schweif, die nach oben wandert
for (int sIdx = 0; sIdx < strands; sIdx++) {
double f0 = ((fxTick * 0.012) + sIdx / (double) strands) % 1.0;
double phase0 = fxTick * 0.30 + sIdx * (Math.PI * 2 / strands);
Color c = SPIRAL[(sIdx + fxTick / 8) % SPIRAL.length];
for (int k = 0; k < trail; k++) {
double f = Math.max(0, f0 - k * 0.012);
double phase = phase0 - k * 0.22;
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.35);
dust(w, cx + Math.cos(phase) * rad, yStart + f * (yEnd - yStart), cz + Math.sin(phase) * rad,
c, k == 0 ? 1.5f : 1.1f);
}
}
// Leuchtende Ringe, die ueber den Baum pulsieren
if (fxTick % 6 == 0) {
int rings = 3;
int pointsPerRing = Math.max(8, (int) Math.round(10 * intensity));
for (int r = 0; r < rings; r++) {
double f = ((fxTick * 0.01) + r / (double) rings) % 1.0;
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.45);
double y = yStart + f * (yEnd - yStart);
Color c = SPIRAL[(r + fxTick / 12) % SPIRAL.length];
for (int i = 0; i < pointsPerRing; i++) {
double a = i * Math.PI * 2 / pointsPerRing;
dust(w, cx + Math.cos(a) * rad, y, cz + Math.sin(a) * rad, c, 0.9f);
}
}
}
// Funkelnde Lichter auf der Baumoberflaeche
int twinkles = Math.max(1, (int) Math.round(3 * intensity));
for (int i = 0; i < twinkles; i++) {
double f = ChristmasManager.RANDOM.nextDouble();
double a = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.2);
double x = cx + Math.cos(a) * rad, y = yStart + f * (yEnd - yStart), z = cz + Math.sin(a) * rad;
if (ChristmasManager.RANDOM.nextInt(3) == 0) {
dust(w, x, y, z, Color.fromRGB(255, 240, 170), 1.3f);
} else {
w.spawnParticle(Particle.END_ROD, x, y, z, 1, 0, 0, 0, 0.001);
}
}
// Schneeflocken, die von den Aesten nach unten fallen
int flakes = Math.max(1, (int) Math.round(2 * intensity));
for (int i = 0; i < flakes; i++) {
double f = ChristmasManager.RANDOM.nextDouble() * 0.8;
double a = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
double rad = Math.max(0.3, t.level.radius * (1 - f) + 0.3);
w.spawnParticle(Particle.SNOWFLAKE, cx + Math.cos(a) * rad, yStart + f * (yEnd - yStart), cz + Math.sin(a) * rad,
0, 0, -1, 0, 0.12);
}
// Stern an der Spitze
w.spawnParticle(Particle.END_ROD, cx, t.y + h + 1.3, cz, Math.max(2, (int) Math.round(3 * intensity)), 0.2, 0.2, 0.2, 0.004);
if (fxTick % 12 == 0) {
// Strahlenkranz um den Stern
for (int i = 0; i < 8; i++) {
double a = i * Math.PI / 4 + fxTick * 0.05;
w.spawnParticle(Particle.END_ROD, cx + Math.cos(a) * 0.5, t.y + h + 1.3, cz + Math.sin(a) * 0.5, 1, 0, 0, 0, 0.01);
}
}
if (t.level == Level.MAGIC_TREE && fxTick % 20 == 0) {
w.spawnParticle(Particle.END_ROD, cx, t.y + h + 1.3, cz, (int) (24 * intensity), 0.1, 0.1, 0.1, 0.15);
w.spawnParticle(Particle.SNOWFLAKE, cx, t.y + h + 0.5, cz, (int) (20 * intensity), 1.6, 0.8, 1.6, 0.02);
}
// Bereit zum Wachsen: leuchtender Ring am Boden
if (t.req.isEmpty() && t.level.next() != null && fxTick % 10 == 0) {
for (int i = 0; i < 16; i++) {
double a = i * Math.PI * 2 / 16;
w.spawnParticle(Particle.END_ROD, cx + Math.cos(a) * (t.level.radius + 0.8), t.y + 0.2,
cz + Math.sin(a) * (t.level.radius + 0.8), 1, 0, 0, 0, 0.004);
}
}
// Glitzer ueber den Geschenken
if (fxTick % 8 == 0) {
for (String key : t.presents) {
String[] pp = key.split(";");
if (pp.length < 4) continue;
try {
dust(w, Integer.parseInt(pp[1]) + 0.5, Integer.parseInt(pp[2]) + 1.0, Integer.parseInt(pp[3]) + 0.5,
Color.fromRGB(255, 215, 0), 0.9f);
} catch (NumberFormatException ignored) { }
}
}
}
}
// ================= Tick =================
private void tick() {
if (!enabled()) return;
if (isEnded()) {
if (!endAnnounced && !trees.isEmpty()) announceEnd();
return;
}
for (Tree t : trees) {
Location base = t.loc();
if (base == null || !base.getWorld().isChunkLoaded(t.x >> 4, t.z >> 4)) continue;
if (!t.indexed) index(t);
Location top = base.clone().add(0.5, height(t.level) + 0.5, 0.5);
int r = t.level.radius;
base.getWorld().spawnParticle(Particle.SNOWFLAKE, top, 6, r, 1, r, 0.01);
if (t.req.isEmpty() && t.level.next() != null) {
base.getWorld().spawnParticle(Particle.END_ROD, top, 3, 0.3, 0.3, 0.3, 0.02);
}
long cd = cooldown(t.level);
if (cd > 0) {
t.counter--;
if (t.counter <= 0) {
spawnPresent(t, false);
t.counter = cd;
}
}
}
}
}
@@ -0,0 +1,68 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mannequin;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.profile.PlayerTextures;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID;
/**
* Spielermodell mit Skin ueber das Mannequin (Minecraft 1.21.9+), ohne Zusatz-Plugin.
* Diese Klasse darf nur geladen werden, wenn {@link #supported()} true ergibt.
*/
final class MannequinSupport {
private MannequinSupport() {}
static boolean supported() {
try {
EntityType.valueOf("MANNEQUIN");
Class.forName("org.bukkit.entity.Mannequin");
return true;
} catch (Throwable t) {
return false;
}
}
static LivingEntity spawn(Location loc, String name, String skinUrl, String skinPlayer) {
return loc.getWorld().spawn(loc, Mannequin.class, m -> {
m.setPersistent(false);
m.setInvulnerable(true);
m.setGravity(false);
m.setSilent(true);
m.setCustomName(name);
m.setCustomNameVisible(true);
// Diese Methoden heissen je nach Version anders oder fehlen: einzeln und fehlertolerant aufrufen
optional(m, "setImmovable", true);
optional(m, "setHideDescription", true);
applySkin(m, skinUrl, skinPlayer);
});
}
private static void optional(Object target, String method, boolean value) {
try {
target.getClass().getMethod(method, boolean.class).invoke(target, value);
} catch (Throwable ignored) { }
}
private static void applySkin(Mannequin m, String skinUrl, String skinPlayer) {
if (skinUrl != null && !skinUrl.isBlank()) {
try {
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(skinUrl.getBytes()), "santa");
PlayerTextures tex = prof.getTextures();
tex.setSkin(new URL(skinUrl));
prof.setTextures(tex);
m.setPlayerProfile(prof);
} catch (MalformedURLException ignored) { }
} else if (skinPlayer != null && !skinPlayer.isBlank()) {
PlayerProfile prof = Bukkit.createPlayerProfile(skinPlayer);
prof.update().thenAccept(updated -> Bukkit.getScheduler().runTask(
ChristmasManager.get().plugin(), () -> { if (m.isValid()) m.setPlayerProfile(updated); }));
}
}
}
@@ -0,0 +1,167 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.MerchantRecipe;
import org.bukkit.persistence.PersistentDataType;
import java.io.File;
import java.io.IOException;
import org.bukkit.configuration.file.YamlConfiguration;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Weihnachtsmarkt-Staende: Haendler (Villager), bei denen Spieler Plaetzchen und Deko gegen Items tauschen. */
public final class MarketManager {
private final ChristmasManager mgr;
private final NamespacedKey stallKey;
private final File spawnFile;
private Location spawn;
MarketManager(ChristmasManager mgr) {
this.mgr = mgr;
this.spawnFile = new File(mgr.plugin().getDataFolder(), "market.yml");
this.stallKey = new NamespacedKey(mgr.plugin(), "market_stall");
}
// ---------- Marktpunkt (Ziel der Portale) ----------
void load() {
spawn = null;
if (!spawnFile.exists()) return;
YamlConfiguration y = YamlConfiguration.loadConfiguration(spawnFile);
World w = Bukkit.getWorld(y.getString("spawn.world", ""));
if (w == null) return;
spawn = new Location(w, y.getDouble("spawn.x"), y.getDouble("spawn.y"), y.getDouble("spawn.z"),
(float) y.getDouble("spawn.yaw"), (float) y.getDouble("spawn.pitch"));
}
/** Ziel der Portale oder null, wenn noch nicht gesetzt. */
public Location spawn() {
if (spawn == null || spawn.getWorld() == null) load();
return spawn == null ? null : spawn.clone();
}
/** Hebt den Marktpunkt auf: Portale fuehren dann nirgendwohin, bis ein Admin ihn neu setzt. */
public void clearSpawn() {
spawn = null;
if (spawnFile.exists()) spawnFile.delete();
}
public void setSpawn(Location l) {
spawn = l.clone();
YamlConfiguration y = new YamlConfiguration();
y.set("spawn.world", l.getWorld().getName());
y.set("spawn.x", l.getX());
y.set("spawn.y", l.getY());
y.set("spawn.z", l.getZ());
y.set("spawn.yaw", l.getYaw());
y.set("spawn.pitch", l.getPitch());
try {
y.save(spawnFile);
} catch (IOException e) {
mgr.plugin().getLogger().warning("market.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
public Set<String> stallIds() {
ConfigurationSection s = mgr.cfg().getConfigurationSection("market.stalls");
return s == null ? new LinkedHashSet<>() : s.getKeys(false);
}
public boolean isStall(Entity e) {
return e.getPersistentDataContainer().has(stallKey, PersistentDataType.STRING);
}
public boolean spawn(Location loc, String id) {
ConfigurationSection s = mgr.cfg().getConfigurationSection("market.stalls." + id);
if (s == null) return false;
World w = loc.getWorld();
Villager v = w.spawn(loc, Villager.class, vill -> {
vill.setAI(false);
vill.setInvulnerable(true);
vill.setPersistent(true);
vill.setRemoveWhenFarAway(false);
vill.setSilent(true);
vill.setCustomName(ChristmasManager.color(s.getString("name", "&6Markt")));
vill.setCustomNameVisible(true);
vill.setVillagerType(Villager.Type.SNOW);
vill.setProfession(profession(s.getString("profession", "LIBRARIAN")));
vill.setVillagerLevel(5);
vill.getPersistentDataContainer().set(stallKey, PersistentDataType.STRING, id);
});
applyTrades(v, id);
return true;
}
private static Villager.Profession profession(String name) {
Villager.Profession p = Registry.VILLAGER_PROFESSION.get(NamespacedKey.minecraft(name.toLowerCase()));
return p != null ? p : Villager.Profession.LIBRARIAN;
}
private void applyTrades(Villager v, String id) {
List<MerchantRecipe> recipes = new ArrayList<>();
for (Map<?, ?> t : mgr.cfg().getMapList("market.stalls." + id + ".trades")) {
ItemStack give = ChristmasManager.parseItem(String.valueOf(t.get("give")));
if (give == null) {
mgr.plugin().getLogger().warning("Markt '" + id + "': ungueltiges Item '" + t.get("give") + "'");
continue;
}
MerchantRecipe r = new MerchantRecipe(give, 0, 100000, false, 0, 0f);
Object price = t.get("price");
int added = 0;
if (price instanceof List<?> list) {
for (Object o : list) {
ItemStack p = ChristmasManager.parseItem(String.valueOf(o));
if (p != null && added < 2) { r.addIngredient(p); added++; }
}
} else if (price != null) {
ItemStack p = ChristmasManager.parseItem(String.valueOf(price));
if (p != null) { r.addIngredient(p); added++; }
}
if (added == 0) continue;
// Rabatte durch Ruf/Nachfrage abschalten (nur auf Paper vorhanden, sonst ignorieren)
try {
r.getClass().getMethod("setIgnoreDiscounts", boolean.class).invoke(r, true);
} catch (Throwable ignored) { }
recipes.add(r);
}
v.setRecipes(recipes);
}
/** Wendet nach /xmas reload die Angebote auf alle geladenen Staende an. */
public void refreshAll() {
for (World w : Bukkit.getWorlds()) {
for (Villager v : w.getEntitiesByClass(Villager.class)) {
String id = v.getPersistentDataContainer().get(stallKey, PersistentDataType.STRING);
if (id != null && mgr.cfg().getConfigurationSection("market.stalls." + id) != null) applyTrades(v, id);
}
}
}
public boolean removeNearest(Player p, double radius) {
Villager best = null;
double bestDist = Double.MAX_VALUE;
for (Entity e : p.getNearbyEntities(radius, radius, radius)) {
if (e instanceof Villager v && isStall(v)) {
double d = v.getLocation().distanceSquared(p.getLocation());
if (d < bestDist) { bestDist = d; best = v; }
}
}
if (best == null) return false;
best.remove();
return true;
}
}
@@ -0,0 +1,340 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.RewardUtil;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Candle;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/** Portale zum Weihnachtsmarkt: Spieler bauen sie per Befehl, der Markt wird vom Admin gesetzt. */
public final class PortalManager {
private record Portal(UUID owner, String ownerName, String world, int x, int y, int z, boolean axisX) {}
private record Part(int u, int v, Material material) {}
private final ChristmasManager mgr;
private final File file;
private final List<Portal> portals = new ArrayList<>();
private final Set<String> blocks = new HashSet<>();
private final Map<UUID, Long> entering = new HashMap<>();
private final Map<UUID, Long> cooldown = new HashMap<>();
private final Map<UUID, Location> origin = new HashMap<>();
private BukkitTask task;
private int tickCount;
PortalManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "portals.yml");
}
// ---------- Form ----------
/** Rahmen aus Schneebloecken: zwei Pfosten (u=-2/+2), Sturz, Seelaterne in der Mitte, Schneeschicht und Kerzen oben. */
private static List<Part> parts() {
List<Part> list = new ArrayList<>();
for (int v = 0; v <= 3; v++) {
list.add(new Part(-2, v, Material.SNOW_BLOCK));
list.add(new Part(2, v, Material.SNOW_BLOCK));
}
for (int u = -1; u <= 1; u++) {
list.add(new Part(u, 3, u == 0 ? Material.SEA_LANTERN : Material.SNOW_BLOCK));
list.add(new Part(u, 4, Material.SNOW));
}
list.add(new Part(-2, 4, Material.RED_CANDLE));
list.add(new Part(2, 4, Material.RED_CANDLE));
return list;
}
private static int bx(Portal p, int u) { return p.axisX() ? p.x() + u : p.x(); }
private static int bz(Portal p, int u) { return p.axisX() ? p.z() : p.z() + u; }
private static String key(String world, int x, int y, int z) { return world + ";" + x + ";" + y + ";" + z; }
// ---------- Persistenz ----------
void load() {
portals.clear();
blocks.clear();
if (file.exists()) {
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
ConfigurationSection s = y.getConfigurationSection("portals");
if (s != null) {
for (String k : s.getKeys(false)) {
ConfigurationSection c = s.getConfigurationSection(k);
if (c == null) continue;
try {
Portal p = new Portal(UUID.fromString(c.getString("owner", "")), c.getString("owner-name", "?"),
c.getString("world", "world"), c.getInt("x"), c.getInt("y"), c.getInt("z"),
c.getBoolean("axis-x", true));
portals.add(p);
index(p);
} catch (IllegalArgumentException ignored) { }
}
}
}
stop();
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 5L);
}
void stop() {
if (task != null) task.cancel();
task = null;
}
private void save() {
YamlConfiguration y = new YamlConfiguration();
int i = 0;
for (Portal p : portals) {
String k = "portals." + (i++);
y.set(k + ".owner", p.owner().toString());
y.set(k + ".owner-name", p.ownerName());
y.set(k + ".world", p.world());
y.set(k + ".x", p.x());
y.set(k + ".y", p.y());
y.set(k + ".z", p.z());
y.set(k + ".axis-x", p.axisX());
}
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("portals.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
private void index(Portal p) {
for (Part part : parts()) blocks.add(key(p.world(), bx(p, part.u()), p.y() + part.v(), bz(p, part.u())));
}
public boolean isProtected(Block b) {
return blocks.contains(key(b.getWorld().getName(), b.getX(), b.getY(), b.getZ()));
}
public int countOf(UUID owner) {
int n = 0;
for (Portal p : portals) if (p.owner().equals(owner)) n++;
return n;
}
// ---------- Bauen und Entfernen ----------
private List<String> cost() { return mgr.cfg().getStringList("portal.cost"); }
/** @return Nachrichten-Schluessel */
public String create(Player p, Block ground) {
if (!mgr.cfg().getBoolean("portal.enable", true)) return "portal-disabled";
if (mgr.market().spawn() == null) return "portal-no-market";
List<String> worlds = mgr.cfg().getStringList("portal.allowed-worlds");
if (!worlds.isEmpty() && !worlds.contains(p.getWorld().getName())) return "portal-world";
int limit = Math.max(1, mgr.cfg().getInt("portal.limit-per-player", 1));
if (!p.hasPermission("xmas.admin") && countOf(p.getUniqueId()) >= limit) return "portal-limit";
BlockFace f = p.getFacing();
boolean axisX = f == BlockFace.NORTH || f == BlockFace.SOUTH;
Block base = ground.getRelative(BlockFace.UP);
Portal portal = new Portal(p.getUniqueId(), p.getName(), p.getWorld().getName(),
base.getX(), base.getY(), base.getZ(), axisX);
World w = p.getWorld();
// Platz pruefen: Rahmen und Oeffnung muessen frei sein
for (Part part : parts()) {
if (!free(w.getBlockAt(bx(portal, part.u()), portal.y() + part.v(), bz(portal, part.u())))) return "portal-no-space";
}
for (int u = -1; u <= 1; u++) {
for (int v = 0; v <= 2; v++) {
if (!free(w.getBlockAt(bx(portal, u), portal.y() + v, bz(portal, u)))) return "portal-no-space";
}
}
// Boden unter dem Rahmen muss fest sein
for (int u = -2; u <= 2; u++) {
if (!w.getBlockAt(bx(portal, u), portal.y() - 1, bz(portal, u)).getType().isSolid()) return "portal-no-space";
}
List<String> cost = cost();
if (!p.hasPermission("xmas.admin")) {
if (!ChristmasManager.hasAll(p, cost)) return "portal-need";
ChristmasManager.takeAll(p, cost);
}
for (Part part : parts()) {
Block b = w.getBlockAt(bx(portal, part.u()), portal.y() + part.v(), bz(portal, part.u()));
if (part.material() == Material.RED_CANDLE) {
Candle c = (Candle) Material.RED_CANDLE.createBlockData();
c.setLit(true);
b.setBlockData(c, false);
} else {
b.setType(part.material(), false);
}
}
portals.add(portal);
index(portal);
save();
w.spawnParticle(Particle.END_ROD, bx(portal, 0) + 0.5, portal.y() + 1.5, bz(portal, 0) + 0.5, 60, 1, 1, 1, 0.08);
w.playSound(p.getLocation(), Sound.BLOCK_BEACON_ACTIVATE, 1f, 1.3f);
return "portal-created";
}
private static boolean free(Block b) {
return b.getType().isAir() || (b.isPassable() && !b.isLiquid());
}
/** Entfernt das Portal, zu dem der Block gehoert. */
public String remove(Player p, Block target) {
Portal found = null;
for (Portal portal : portals) {
if (!portal.world().equals(target.getWorld().getName())) continue;
for (Part part : parts()) {
if (bx(portal, part.u()) == target.getX() && portal.y() + part.v() == target.getY()
&& bz(portal, part.u()) == target.getZ()) { found = portal; break; }
}
if (found != null) break;
}
if (found == null) return "portal-none";
if (!found.owner().equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) return "portal-not-owner";
World w = Bukkit.getWorld(found.world());
if (w != null) {
for (Part part : parts()) {
blocks.remove(key(found.world(), bx(found, part.u()), found.y() + part.v(), bz(found, part.u())));
w.getBlockAt(bx(found, part.u()), found.y() + part.v(), bz(found, part.u())).setType(Material.AIR, false);
}
}
portals.remove(found);
save();
if (mgr.cfg().getBoolean("portal.refund", true) && found.owner().equals(p.getUniqueId())) {
for (String c : cost()) {
ItemStack it = ChristmasManager.parseItem(c);
if (it != null) RewardUtil.give(p, it);
}
}
return "portal-removed";
}
// ---------- Ruecksprung ----------
public String back(Player p) {
Location l = origin.get(p.getUniqueId());
if (l == null) return "portal-back-none";
p.teleport(l);
return "portal-back-ok";
}
// ---------- Tick: Partikel und Teleport ----------
private void tick() {
tickCount++;
long now = System.currentTimeMillis();
long delay = Math.max(1, mgr.cfg().getInt("portal.teleport-delay-seconds", 3)) * 1000L;
long cd = Math.max(0, mgr.cfg().getInt("portal.cooldown-seconds", 10)) * 1000L;
Set<UUID> inside = new HashSet<>();
for (Portal portal : portals) {
World w = Bukkit.getWorld(portal.world());
if (w == null || !w.isChunkLoaded(portal.x() >> 4, portal.z() >> 4)) continue;
boolean near = false;
for (Player p : w.getPlayers()) {
if (p.getLocation().distanceSquared(new Location(w, portal.x(), portal.y(), portal.z())) <= 40 * 40) near = true;
if (p.getLocation().distanceSquared(new Location(w, portal.x(), portal.y(), portal.z())) > 6 * 6) continue;
if (!inOpening(portal, p.getLocation())) continue;
inside.add(p.getUniqueId());
if (cooldown.getOrDefault(p.getUniqueId(), 0L) > now) continue;
long start = entering.computeIfAbsent(p.getUniqueId(), k -> now);
long left = delay - (now - start);
if (left > 0) {
p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(
mgr.raw("portal-countdown").replace("%s%", String.valueOf((left + 999) / 1000))));
w.spawnParticle(Particle.END_ROD, p.getLocation().add(0, 1, 0), 3, 0.3, 0.6, 0.3, 0.02);
} else {
teleport(p, portal);
entering.remove(p.getUniqueId());
cooldown.put(p.getUniqueId(), now + cd);
}
}
if (!near) continue;
// Schimmernde Oeffnung
if (tickCount % 2 == 0) {
double cx = bx(portal, 0) + 0.5, cz = bz(portal, 0) + 0.5;
for (int i = 0; i < 12; i++) {
double u = (ChristmasManager.RANDOM.nextDouble() * 2 - 1) * 1.4;
double v = ChristmasManager.RANDOM.nextDouble() * 3;
double x = portal.axisX() ? cx + u : cx;
double z = portal.axisX() ? cz : cz + u;
Color c = ChristmasManager.RANDOM.nextBoolean() ? Color.fromRGB(120, 200, 255) : Color.WHITE;
spawnDust(w, x, portal.y() + v, z, c);
}
w.spawnParticle(Particle.SNOWFLAKE, cx, portal.y() + 1.5, cz, 6, 0.8, 1.2, 0.8, 0.01);
w.spawnParticle(Particle.END_ROD, cx, portal.y() + 1.5, cz, 2, 0.9, 1.3, 0.9, 0.01);
}
}
entering.keySet().removeIf(id -> !inside.contains(id));
}
private void spawnDust(World w, double x, double y, double z, Color c) {
Particle dust;
try { dust = Particle.valueOf("DUST"); } catch (IllegalArgumentException e) {
try { dust = Particle.valueOf("REDSTONE"); } catch (IllegalArgumentException ex) { return; }
}
w.spawnParticle(dust, x, y, z, 1, 0, 0, 0, 0, new Particle.DustOptions(c, 1.2f));
}
private boolean inOpening(Portal p, Location l) {
double x = l.getX(), y = l.getY(), z = l.getZ();
if (y < p.y() - 0.2 || y > p.y() + 3) return false;
if (p.axisX()) {
return x >= p.x() - 1 && x < p.x() + 2 && z >= p.z() - 0.4 && z < p.z() + 1.4;
}
return z >= p.z() - 1 && z < p.z() + 2 && x >= p.x() - 0.4 && x < p.x() + 1.4;
}
private void teleport(Player p, Portal portal) {
Location dest = mgr.market().spawn();
if (dest == null) { p.sendMessage(mgr.msg("portal-no-market")); return; }
// Rueckweg: etwas vor dem Portal, auf der Seite, von der der Spieler kam
double side = portal.axisX() ? Math.signum(p.getLocation().getZ() - (portal.z() + 0.5))
: Math.signum(p.getLocation().getX() - (portal.x() + 0.5));
if (side == 0) side = 1;
double ox = portal.axisX() ? portal.x() + 0.5 : portal.x() + 0.5 + side * 2.5;
double oz = portal.axisX() ? portal.z() + 0.5 + side * 2.5 : portal.z() + 0.5;
Location back = new Location(Bukkit.getWorld(portal.world()), ox, portal.y(), oz,
portal.axisX() ? (side > 0 ? 180f : 0f) : (side > 0 ? 90f : -90f), 0f);
origin.put(p.getUniqueId(), back);
p.getWorld().spawnParticle(Particle.END_ROD, p.getLocation().add(0, 1, 0), 30, 0.4, 0.8, 0.4, 0.05);
p.teleport(dest);
p.getWorld().playSound(dest, Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1.4f);
p.getWorld().spawnParticle(Particle.SNOWFLAKE, dest.clone().add(0, 1, 0), 40, 0.6, 0.8, 0.6, 0.03);
net.md_5.bungee.api.chat.TextComponent msg = new net.md_5.bungee.api.chat.TextComponent(
mgr.prefixed(mgr.raw("portal-teleported")) + " ");
net.md_5.bungee.api.chat.TextComponent click = new net.md_5.bungee.api.chat.TextComponent(mgr.raw("portal-back-click"));
click.setClickEvent(new net.md_5.bungee.api.chat.ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.RUN_COMMAND, "/xmas market back"));
msg.addExtra(click);
p.spigot().sendMessage(msg);
}
}
@@ -0,0 +1,127 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/** Weihnachtsquiz: Frage im Chat, die schnellste richtige Antwort gewinnt. */
public final class QuizManager {
private final ChristmasManager mgr;
private volatile boolean active;
private volatile List<String> answers = List.of();
private volatile List<Map<?, ?>> rewards = List.of();
private String shownAnswer = "";
private final AtomicBoolean answered = new AtomicBoolean(false);
private BukkitTask timeoutTask;
private BukkitTask autoTask;
private int lastIndex = -1;
QuizManager(ChristmasManager mgr) { this.mgr = mgr; }
public boolean isActive() { return active; }
void start() {
stop();
if (!mgr.cfg().getBoolean("quiz.auto.enable", false)) return;
long ticks = 20L * 60 * Math.max(1, mgr.cfg().getInt("quiz.auto.interval-minutes", 20));
autoTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
int min = mgr.cfg().getInt("quiz.auto.min-players", 2);
if (!active && Bukkit.getOnlinePlayers().size() >= min) ask(-1);
}, ticks, ticks);
}
void stop() {
if (autoTask != null) autoTask.cancel();
autoTask = null;
cancelRound();
}
private void cancelRound() {
active = false;
if (timeoutTask != null) timeoutTask.cancel();
timeoutTask = null;
}
public int questionCount() { return mgr.cfg().getMapList("quiz.questions").size(); }
/** Startet eine Frage (index -1 = zufaellig). false, wenn schon aktiv oder keine Fragen vorhanden. */
public boolean ask(int index) {
if (active) return false;
List<Map<?, ?>> questions = mgr.cfg().getMapList("quiz.questions");
if (questions.isEmpty()) return false;
int i = index;
if (i < 0 || i >= questions.size()) {
do {
i = ChristmasManager.RANDOM.nextInt(questions.size());
} while (questions.size() > 1 && i == lastIndex);
}
lastIndex = i;
Map<?, ?> q = questions.get(i);
List<String> ans = new ArrayList<>();
Object a = q.get("a");
if (a instanceof List<?> list) for (Object o : list) ans.add(normalize(String.valueOf(o)));
else if (a != null) ans.add(normalize(String.valueOf(a)));
if (ans.isEmpty()) return false;
List<Map<?, ?>> rw = new ArrayList<>();
Object r = q.get("reward");
if (r instanceof List<?> rl) for (Object o : rl) if (o instanceof Map<?, ?> m) rw.add(m);
if (rw.isEmpty()) rw = mgr.cfg().getMapList("quiz.default-reward");
String shown = a instanceof List<?> l && !l.isEmpty() ? String.valueOf(l.get(0)) : String.valueOf(a);
answers = ans;
rewards = rw;
shownAnswer = shown;
answered.set(false);
active = true;
int seconds = Math.max(5, mgr.cfg().getInt("quiz.answer-seconds", 30));
Bukkit.broadcastMessage(mgr.msg("quiz-question", "%question%", String.valueOf(q.get("q")),
"%time%", String.valueOf(seconds)));
timeoutTask = Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (!active) return;
cancelRound();
Bukkit.broadcastMessage(mgr.msg("quiz-timeout", "%answer%", shownAnswer));
}, seconds * 20L);
return true;
}
public boolean skip() {
if (!active) return false;
cancelRound();
Bukkit.broadcastMessage(mgr.msg("quiz-timeout", "%answer%", shownAnswer));
return true;
}
private static String normalize(String s) {
// Umlaute wie ae/oe/ue behandeln, damit "Türchen" und "Tuerchen" beide richtig sind
String lower = s.trim().toLowerCase().replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss");
String n = Normalizer.normalize(lower, Normalizer.Form.NFD).replaceAll("\\p{M}", "");
return n.replaceAll("[^a-z0-9 ]", "").replaceAll("\\s+", " ").trim();
}
/**
* Wird aus dem (asynchronen) Chat-Ereignis aufgerufen. true = richtige Antwort, Nachricht sollte unterdrueckt werden.
*/
public boolean onChat(Player p, String message) {
if (!active) return false;
if (!answers.contains(normalize(message))) return false;
if (!answered.compareAndSet(false, true)) return true; // jemand war schneller
Bukkit.getScheduler().runTask(mgr.plugin(), () -> win(p));
return true;
}
private void win(Player p) {
cancelRound();
Bukkit.broadcastMessage(mgr.msg("quiz-win", "%player%", p.getName(), "%answer%", shownAnswer));
for (Map<?, ?> reward : rewards) ChristmasExtras.applyMap(p, reward);
p.sendMessage(mgr.msg("quiz-reward"));
}
}
@@ -0,0 +1,429 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Horse;
import org.bukkit.entity.LivingEntity;
import org.bukkit.util.Vector;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.profile.PlayerTextures;
import org.bukkit.scheduler.BukkitTask;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Santa-Claus-Event: Ein Weihnachtsmann steht (oder laeuft) an einem Ort, hinterlaesst Schnee und wirft Geschenke ab. */
public final class SantaEvent {
private final ChristmasManager mgr;
private boolean active;
private Location origin;
private Location loc;
private LivingEntity stand;
private BossBar bar;
private BukkitTask task;
private int total;
private int left;
private final Map<Block, Long> trail = new HashMap<>();
private final SantaMelody melody;
private long lastDropMsg;
// Schlitten-Anflug
private boolean flying;
private BukkitTask flightTask;
private final List<LivingEntity> flightEntities = new java.util.ArrayList<>();
private ArmorStand sleighStand;
private int flightStep;
SantaEvent(ChristmasManager mgr) { this.mgr = mgr; this.melody = new SantaMelody(mgr); }
public boolean isActive() { return active; }
public Location getLocation() { return loc == null ? null : loc.clone(); }
/** Waehlt zufaellig einen konfigurierten Ort, null wenn keiner vorhanden. */
public Location randomLocation() {
List<Map<?, ?>> list = mgr.cfg().getMapList("santa.locations");
for (int tries = 0; tries < 10 && !list.isEmpty(); tries++) {
Map<?, ?> m = list.get(ChristmasManager.RANDOM.nextInt(list.size()));
World w = Bukkit.getWorld(String.valueOf(m.get("world")));
if (w == null) continue;
return new Location(w, num(m.get("x")), num(m.get("y")), num(m.get("z")));
}
return null;
}
private static double num(Object o) { return o instanceof Number n ? n.doubleValue() : 0; }
public static String formatDuration(int sec) {
if (sec % 3600 == 0 && sec >= 3600) return (sec / 3600) + "h";
if (sec % 60 == 0 && sec >= 60) return (sec / 60) + " min";
return sec + "s";
}
public void start(int seconds, Location where) {
if (active || where == null || where.getWorld() == null) return;
active = true;
origin = where.clone();
loc = where.clone();
total = left = Math.max(5, seconds);
stand = createModel(loc);
bar = Bukkit.createBossBar(barTitle(), BarColor.RED, BarStyle.SOLID);
if (mgr.cfg().getBoolean("santa.sleigh.enable", true)) beginFlight();
Bukkit.broadcastMessage(mgr.msg("santa-started",
"%x%", String.valueOf(loc.getBlockX()), "%y%", String.valueOf(loc.getBlockY()),
"%z%", String.valueOf(loc.getBlockZ()), "%world%", loc.getWorld().getName(),
"%duration%", formatDuration(total)));
lastDropMsg = 0;
melody.play(loc.getWorld());
startWalking();
int dropEvery = Math.max(1, mgr.cfg().getInt("santa.drop-interval", 3));
double chance = mgr.cfg().getDouble("santa.drop-chance", 100);
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
left--;
fadeTrail(false);
if (left <= 0) { stop(true); return; }
bar.setTitle(barTitle());
bar.setProgress(Math.max(0, Math.min(1, left / (double) total)));
for (Player p : Bukkit.getOnlinePlayers()) bar.addPlayer(p);
loc.getWorld().spawnParticle(Particle.SNOWFLAKE, loc.clone().add(0, 2, 0), 20, 1, 1, 1, 0.02);
if ((total - left) % dropEvery == 0 && ChristmasManager.RANDOM.nextDouble() * 100 < chance) dropGift();
}, 20L, 20L);
}
/** Erzeugt Santas Modell (Spielermodell oder Ruestungsstaender) an der Stelle. */
LivingEntity createModel(Location loc) {
LivingEntity stand = null;
String name = ChristmasManager.color(mgr.cfg().getString("santa.name", "&cSanta Claus"));
String skin = mgr.cfg().getString("santa.skin-url", "");
String model = mgr.cfg().getString("santa.model", "AUTO").toUpperCase();
String skinPlayer = mgr.cfg().getString("santa.skin-player", "");
boolean mannequin = !model.equals("ARMOR_STAND") && MannequinSupport.supported();
if (mannequin) {
try {
stand = MannequinSupport.spawn(loc, name, skin, skinPlayer);
if (skin.isBlank() && skinPlayer.isBlank()) {
// Ohne eigenen Skin: rote Lederruestung, damit er wie Santa aussieht
var eq = stand.getEquipment();
eq.setHelmet(dyed(Material.LEATHER_HELMET));
eq.setChestplate(dyed(Material.LEATHER_CHESTPLATE));
eq.setLeggings(dyed(Material.LEATHER_LEGGINGS));
eq.setBoots(dyed(Material.LEATHER_BOOTS));
}
} catch (Throwable t) {
mgr.plugin().getLogger().warning("Mannequin konnte nicht erzeugt werden, nutze Rüstungsständer: " + t);
if (stand != null) stand.remove();
stand = null;
}
}
if (stand == null) {
stand = loc.getWorld().spawn(loc, ArmorStand.class, s -> {
s.setPersistent(false);
s.setInvulnerable(true);
s.setGravity(false);
s.setArms(true);
s.setCustomName(name);
s.setCustomNameVisible(true);
ItemStack head = skin.isBlank() ? dyed(Material.LEATHER_HELMET) : skull(skin);
s.getEquipment().setHelmet(head);
s.getEquipment().setChestplate(dyed(Material.LEATHER_CHESTPLATE));
s.getEquipment().setLeggings(dyed(Material.LEATHER_LEGGINGS));
s.getEquipment().setBoots(dyed(Material.LEATHER_BOOTS));
});
}
return stand;
}
private String barTitle() {
return mgr.raw("santa-bossbar").replace("%time%", String.valueOf(left));
}
private static ItemStack dyed(Material m) {
ItemStack it = new ItemStack(m);
LeatherArmorMeta meta = (LeatherArmorMeta) it.getItemMeta();
if (meta != null) { meta.setColor(Color.RED); it.setItemMeta(meta); }
return it;
}
private static ItemStack skull(String url) {
ItemStack it = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) it.getItemMeta();
if (meta == null) return it;
try {
PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(url.getBytes()), "santa");
PlayerTextures tex = prof.getTextures();
tex.setSkin(new URL(url));
prof.setTextures(tex);
meta.setOwnerProfile(prof);
it.setItemMeta(meta);
} catch (MalformedURLException ignored) { }
return it;
}
// ---------- Laufen und Schneespur ----------
// Fluessiges Laufen: jeden Tick ein kleiner Schritt zu einem Zufallsziel, sanftes Drehen, kurze Pausen.
private BukkitTask walkTask;
private Location walkTarget;
private int walkPause;
private float walkYaw;
private Block lastStep;
private void startWalking() {
stopWalking();
if (mgr.cfg().getDouble("santa.walk-radius", 0) <= 0) return;
walkYaw = loc.getYaw();
walkTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::walkStep, 20L, 1L);
}
private void stopWalking() {
if (walkTask != null) walkTask.cancel();
walkTask = null;
walkTarget = null;
}
/** Sucht die Standhoehe an x/z (Fuss-Block frei, darunter fest, 2 Bloecke Platz); NaN = nicht begehbar. */
private double groundAt(World w, double x, double z, double fromY) {
int bx = (int) Math.floor(x), bz = (int) Math.floor(z);
for (int by = (int) Math.floor(fromY) + 1; by >= (int) Math.floor(fromY) - 3; by--) {
Block feet = w.getBlockAt(bx, by, bz);
if (!feet.getType().isSolid() && !feet.isLiquid()
&& w.getBlockAt(bx, by + 1, bz).getType().isAir()
&& w.getBlockAt(bx, by - 1, bz).getType().isSolid()) {
return by;
}
}
return Double.NaN;
}
private void walkStep() {
if (!active || flying || stand == null || stand.isDead()) return;
double radius = mgr.cfg().getDouble("santa.walk-radius", 0);
if (radius <= 0) return;
if (walkPause > 0) { walkPause--; return; }
World w = loc.getWorld();
if (walkTarget == null) {
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
double dist = 2 + ChristmasManager.RANDOM.nextDouble() * Math.max(1, radius - 1);
double tx = origin.getX() + Math.cos(ang) * Math.min(dist, radius);
double tz = origin.getZ() + Math.sin(ang) * Math.min(dist, radius);
double gy = groundAt(w, tx, tz, loc.getY());
if (Double.isNaN(gy)) { walkPause = 20; return; }
walkTarget = new Location(w, tx, gy, tz);
}
double dx = walkTarget.getX() - loc.getX(), dz = walkTarget.getZ() - loc.getZ();
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist < 0.25) {
walkTarget = null;
walkPause = 30 + ChristmasManager.RANDOM.nextInt(80); // stehen bleiben und umsehen
return;
}
// sanft zum Ziel drehen
float want = (float) Math.toDegrees(Math.atan2(-dx, dz));
float diff = ((want - walkYaw + 540f) % 360f) - 180f;
walkYaw += Math.max(-10f, Math.min(10f, diff));
if (Math.abs(diff) > 45f) return; // erst drehen, dann loslaufen
double speed = Math.max(0.02, mgr.cfg().getDouble("santa.walk-speed", 0.07));
double rad = Math.toRadians(walkYaw);
double nx = loc.getX() - Math.sin(rad) * speed;
double nz = loc.getZ() + Math.cos(rad) * speed;
double ox = nx - origin.getX(), oz = nz - origin.getZ();
double gy = (ox * ox + oz * oz > radius * radius) ? Double.NaN : groundAt(w, nx, nz, loc.getY());
if (Double.isNaN(gy) || Math.abs(gy - loc.getY()) > 1.01) {
walkTarget = null; // Hindernis oder Rand: neues Ziel nach kurzer Pause
walkPause = 10 + ChristmasManager.RANDOM.nextInt(20);
return;
}
double ny = loc.getY() + Math.max(-0.25, Math.min(0.25, gy - loc.getY()));
Location next = new Location(w, nx, ny, nz, walkYaw, 0f);
stand.teleport(next);
loc = next;
// Schritte: Geraeusch und kurze Schneespur
if (stand.getTicksLived() % 9 == 0) w.playSound(next, Sound.BLOCK_SNOW_STEP, 0.6f, 1f);
Block feet = w.getBlockAt(next.getBlockX(), (int) Math.floor(gy), next.getBlockZ());
if (!feet.equals(lastStep)) {
lastStep = feet;
if (mgr.cfg().getBoolean("santa.snow-trail.enabled", true) && feet.getType() == Material.AIR) {
feet.setType(Material.SNOW, false);
long fade = Math.max(1, mgr.cfg().getLong("santa.snow-trail.fade-seconds", 5)) * 1000L;
trail.put(feet, System.currentTimeMillis() + fade);
}
}
}
private void fadeTrail(boolean all) {
long now = System.currentTimeMillis();
Iterator<Map.Entry<Block, Long>> it = trail.entrySet().iterator();
while (it.hasNext()) {
var e = it.next();
if (all || e.getValue() <= now) {
if (e.getKey().getType() == Material.SNOW) e.getKey().setType(Material.AIR, false);
it.remove();
}
}
}
// ---------- Schlitten-Anflug ----------
private void beginFlight() {
int seconds = Math.max(5, Math.min(total - 2, mgr.cfg().getInt("santa.sleigh.flight-seconds", 15)));
double dist = Math.max(20, mgr.cfg().getDouble("santa.sleigh.distance", 70));
double height = Math.max(8, mgr.cfg().getDouble("santa.sleigh.height", 30));
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
Vector dir = new Vector(Math.cos(ang), 0, Math.sin(ang));
Location from = origin.clone().add(dir.clone().multiply(-dist));
from.setY(origin.getY() + height);
final int steps = seconds * 10;
flying = true;
flightStep = 0;
World w = origin.getWorld();
sleighStand = w.spawn(from, ArmorStand.class, s -> {
s.setPersistent(false);
s.setInvulnerable(true);
s.setGravity(false);
s.setVisible(false);
s.getEquipment().setHelmet(new ItemStack(Material.RED_SHULKER_BOX));
});
List<String> names = mgr.cfg().getStringList("santa.sleigh.reindeer-names");
for (int i = 0; i < 2; i++) {
final String n = names.size() > i ? names.get(i) : "Rudolph";
Horse horse = w.spawn(from, Horse.class, h -> {
h.setPersistent(false);
h.setInvulnerable(true);
h.setGravity(false);
h.setAI(false);
h.setSilent(true);
h.setColor(Horse.Color.BROWN);
h.setCustomName(ChristmasManager.color("&6" + n));
h.setCustomNameVisible(true);
});
flightEntities.add(horse);
}
flightTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
if (stand == null || !stand.isValid()) { endFlight(); return; }
double t = Math.min(1.0, flightStep / (double) steps);
// Sinkflug: erst waagerecht, dann weich landen
double eased = t * t * (3 - 2 * t);
Location pos = from.clone().add(origin.clone().subtract(from).toVector().multiply(t));
pos.setY(from.getY() + (origin.getY() - from.getY()) * eased);
pos.setDirection(dir);
loc = pos.clone();
stand.teleport(pos);
if (sleighStand != null) {
sleighStand.teleport(pos.clone().subtract(dir.clone().multiply(3)).subtract(0, 1.5, 0));
}
for (int i = 0; i < flightEntities.size(); i++) {
flightEntities.get(i).teleport(pos.clone().add(dir.clone().multiply(3 + i * 2.5)));
}
w.spawnParticle(Particle.END_ROD, pos.clone().subtract(dir.clone().multiply(4)), 6, 0.3, 0.3, 0.3, 0.02);
w.spawnParticle(Particle.SNOWFLAKE, pos.clone().subtract(dir.clone().multiply(2)), 10, 0.6, 0.4, 0.6, 0.02);
if (flightStep % 10 == 0) w.playSound(pos, Sound.BLOCK_AMETHYST_BLOCK_CHIME, 2f, 1.5f);
flightStep++;
if (flightStep > steps) endFlight();
}, 0L, 2L);
}
private void endFlight() {
if (flightTask != null) flightTask.cancel();
flightTask = null;
flying = false;
if (sleighStand != null) { sleighStand.remove(); sleighStand = null; }
for (LivingEntity e : flightEntities) e.remove();
flightEntities.clear();
if (active && stand != null && stand.isValid() && origin != null) {
loc = origin.clone();
stand.teleport(origin);
origin.getWorld().spawnParticle(Particle.CLOUD, origin.clone().add(0, 1, 0), 40, 1, 0.5, 1, 0.05);
}
}
// ---------- Geschenke ----------
private void dropGift() {
List<String> gifts = mgr.cfg().getStringList("santa.gifts");
if (gifts.isEmpty()) return;
String pick = gifts.get(ChristmasManager.RANDOM.nextInt(gifts.size()));
ItemStack it;
if (pick.toLowerCase().startsWith("cracker:")) {
String[] p = pick.split(":");
int amount = 1;
if (p.length > 2) {
try { amount = Integer.parseInt(p[2].trim()); } catch (NumberFormatException ignored) { }
}
it = mgr.crackers().create(p[1].trim(), amount);
} else {
it = ChristmasManager.parseItem(pick);
}
if (it == null) return;
double r = mgr.cfg().getDouble("santa.radius", 6);
Location at = loc.clone().add((ChristmasManager.RANDOM.nextDouble() - 0.5) * 2 * r, 3,
(ChristmasManager.RANDOM.nextDouble() - 0.5) * 2 * r);
loc.getWorld().dropItem(at, it);
loc.getWorld().spawnParticle(Particle.END_ROD, at, 10, 0.2, 0.2, 0.2, 0.02);
loc.getWorld().playSound(at, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.6f, 1.2f);
// Nachricht nur selten, sonst wird der Chat bei jedem Geschenk zugespammt
long every = mgr.cfg().getLong("santa.drop-message-interval", 60) * 1000L;
long now = System.currentTimeMillis();
if (every > 0 && now - lastDropMsg >= every) {
lastDropMsg = now;
for (Player p : loc.getWorld().getPlayers()) {
if (p.getLocation().distanceSquared(loc) <= 64 * 64) p.sendMessage(mgr.msg("santa-dropped"));
}
}
}
public void stop(boolean announce) {
if (!active) return;
active = false;
if (task != null) task.cancel();
task = null;
fadeTrail(true);
melody.stop();
stopWalking();
endFlightSilently();
if (stand != null) stand.remove();
stand = null;
if (bar != null) { bar.removeAll(); bar = null; }
if (announce) Bukkit.broadcastMessage(mgr.msg("santa-stopped"));
}
private void endFlightSilently() {
if (flightTask != null) flightTask.cancel();
flightTask = null;
flying = false;
if (sleighStand != null) { sleighStand.remove(); sleighStand = null; }
for (LivingEntity e : flightEntities) e.remove();
flightEntities.clear();
}
}
@@ -0,0 +1,50 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
/** Spielt beim Santa-Event eine Weihnachtsmelodie mit Glocken (Notenblock-Klaenge, keine Dateien noetig). */
final class SantaMelody {
// Melodie: {Halbtoene ueber C4 (-1 = Pause), Dauer in Viertelschlag-Achteln (1 = Achtel)}
private static final int[][] JINGLE = {
{4, 2}, {4, 2}, {4, 4}, {4, 2}, {4, 2}, {4, 4}, {4, 2}, {7, 2}, {0, 3}, {2, 1}, {4, 8},
{5, 2}, {5, 2}, {5, 3}, {5, 1}, {5, 2}, {4, 2}, {4, 2}, {4, 1}, {4, 1}, {4, 2}, {2, 2}, {2, 2}, {4, 2}, {2, 4}, {7, 4},
{4, 2}, {4, 2}, {4, 4}, {4, 2}, {4, 2}, {4, 4}, {4, 2}, {7, 2}, {0, 3}, {2, 1}, {4, 8},
{5, 2}, {5, 2}, {5, 3}, {5, 1}, {5, 2}, {4, 2}, {4, 2}, {4, 1}, {4, 1}, {7, 2}, {7, 2}, {5, 2}, {2, 2}, {0, 8},
};
private final ChristmasManager mgr;
private BukkitTask task;
SantaMelody(ChristmasManager mgr) { this.mgr = mgr; }
void play(World world) {
stop();
if (!mgr.cfg().getBoolean("santa.melody.enable", true)) return;
final float volume = (float) mgr.cfg().getDouble("santa.melody.volume", 1.0);
final boolean everyone = mgr.cfg().getBoolean("santa.melody.everyone", false);
final int tickPerUnit = Math.max(1, mgr.cfg().getInt("santa.melody.tempo", 3));
final int[] pos = {0};
final int[] wait = {0};
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
if (wait[0]-- > 0) return;
if (pos[0] >= JINGLE.length) { stop(); return; }
int[] n = JINGLE[pos[0]++];
float pitch = (float) Math.pow(2.0, (n[0] + 6 - 12) / 12.0); // C4 = Notenblock-Ton 6
for (Player p : everyone ? Bukkit.getOnlinePlayers() : world.getPlayers()) {
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BELL, SoundCategory.RECORDS, volume, pitch);
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, SoundCategory.RECORDS, volume * 0.5f, pitch);
}
wait[0] = n[1] * tickPerUnit - 1;
}, 10L, 1L);
}
void stop() {
if (task != null) task.cancel();
task = null;
}
}
@@ -0,0 +1,192 @@
package de.mviper.adventskalender.christmas;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Schneeball-Minispiel: In einer Arena zaehlen Treffer mit Schneebaellen, die Besten bekommen Belohnungen. */
public final class SnowballGame {
private final ChristmasManager mgr;
private final File file;
private String arenaWorld;
private double ax, ay, az;
private int radius;
private boolean active;
private int left;
private BukkitTask task;
private BukkitTask arenaTask;
private final Map<UUID, Integer> points = new HashMap<>();
private final Map<UUID, String> names = new HashMap<>();
SnowballGame(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "snowball.yml");
}
public boolean isActive() { return active; }
public boolean hasArena() { return arenaWorld != null && radius > 0; }
void load() {
startArenaTask();
if (!file.exists()) return;
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
arenaWorld = y.getString("arena.world");
ax = y.getDouble("arena.x");
ay = y.getDouble("arena.y");
az = y.getDouble("arena.z");
radius = y.getInt("arena.radius", 0);
}
public void setArena(Location center, int r) {
arenaWorld = center.getWorld().getName();
ax = center.getX();
ay = center.getY();
az = center.getZ();
radius = Math.max(3, r);
YamlConfiguration y = new YamlConfiguration();
y.set("arena.world", arenaWorld);
y.set("arena.x", ax);
y.set("arena.y", ay);
y.set("arena.z", az);
y.set("arena.radius", radius);
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("snowball.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
/** Laeuft immer, fuellt aber nur waehrend einer laufenden Runde die Schneebaelle der Spieler in der Arena auf. */
private void startArenaTask() {
if (arenaTask != null) arenaTask.cancel();
arenaTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
if (!hasArena()) return;
if (!active) return; // nur waehrend einer laufenden Runde
int refill = Math.max(1, mgr.cfg().getInt("snowball.refill-amount", 16));
for (Player p : Bukkit.getOnlinePlayers()) {
if (inArena(p)) refill(p, refill);
}
}, 20L, 20L);
}
void shutdown() {
stop(false);
if (arenaTask != null) arenaTask.cancel();
arenaTask = null;
}
public boolean inArena(Player p) {
if (!hasArena()) return false;
World w = p.getWorld();
if (!w.getName().equals(arenaWorld)) return false;
double dx = p.getLocation().getX() - ax, dz = p.getLocation().getZ() - az;
return dx * dx + dz * dz <= (double) radius * radius && Math.abs(p.getLocation().getY() - ay) <= 40;
}
// ---------- Ablauf ----------
public boolean start(int seconds) {
if (active || !hasArena()) return false;
points.clear();
names.clear();
active = true;
left = Math.max(10, seconds);
Bukkit.broadcastMessage(mgr.msg("sb-started", "%time%", SantaEvent.formatDuration(left),
"%world%", arenaWorld, "%x%", String.valueOf((int) ax), "%z%", String.valueOf((int) az)));
int refill = Math.max(1, mgr.cfg().getInt("snowball.refill-amount", 16));
for (Player p : Bukkit.getOnlinePlayers()) if (inArena(p)) refill(p, refill);
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
left--;
if (left <= 0) { stop(true); return; }
for (Player p : Bukkit.getOnlinePlayers()) {
if (!inArena(p)) continue;
String text = mgr.raw("sb-actionbar").replace("%points%", String.valueOf(points(p.getUniqueId())))
.replace("%rank%", String.valueOf(Math.max(1, rank(p.getUniqueId()))))
.replace("%time%", String.valueOf(left));
p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(text));
}
}, 20L, 20L);
return true;
}
private void refill(Player p, int amount) {
int have = 0;
for (ItemStack it : p.getInventory().getContents()) {
if (it != null && it.getType() == Material.SNOWBALL) have += it.getAmount();
}
if (have < amount) p.getInventory().addItem(new ItemStack(Material.SNOWBALL, amount - have));
}
public void stop(boolean announce) {
if (task != null) task.cancel();
task = null;
if (!active) return;
active = false;
if (!announce) return;
List<Map.Entry<UUID, Integer>> top = ranking();
Bukkit.broadcastMessage(mgr.msg("sb-ended"));
for (int i = 0; i < Math.min(3, top.size()); i++) {
var e = top.get(i);
Bukkit.broadcastMessage(mgr.prefixed(mgr.raw("sb-top-line").replace("%rank%", String.valueOf(i + 1))
.replace("%player%", names.getOrDefault(e.getKey(), "?"))
.replace("%points%", String.valueOf(e.getValue()))));
Player winner = Bukkit.getPlayer(e.getKey());
if (winner != null) {
for (Map<?, ?> reward : mgr.cfg().getMapList("snowball.rewards." + (i + 1))) {
ChristmasExtras.applyMap(winner, reward);
}
winner.sendMessage(mgr.msg("sb-reward", "%rank%", String.valueOf(i + 1)));
}
}
// Spieler bekommen ihre Schneebaelle nicht zurueck: einfach liegen lassen
}
// ---------- Punkte ----------
public void onHit(Player shooter, Player victim) {
if (!active || shooter.equals(victim) || !inArena(shooter) || !inArena(victim)) return;
points.merge(shooter.getUniqueId(), Math.max(1, mgr.cfg().getInt("snowball.points-per-hit", 1)), Integer::sum);
names.put(shooter.getUniqueId(), shooter.getName());
}
public int points(UUID id) { return points.getOrDefault(id, 0); }
private List<Map.Entry<UUID, Integer>> ranking() {
List<Map.Entry<UUID, Integer>> list = new ArrayList<>(points.entrySet());
list.sort((a, b) -> Integer.compare(b.getValue(), a.getValue()));
return list;
}
public int rank(UUID id) {
List<Map.Entry<UUID, Integer>> r = ranking();
for (int i = 0; i < r.size(); i++) if (r.get(i).getKey().equals(id)) return i + 1;
return 0;
}
public List<String> topLines(int n) {
List<String> out = new ArrayList<>();
List<Map.Entry<UUID, Integer>> r = ranking();
for (int i = 0; i < Math.min(n, r.size()); i++) {
out.add(mgr.prefixed(mgr.raw("sb-top-line").replace("%rank%", String.valueOf(i + 1))
.replace("%player%", names.getOrDefault(r.get(i).getKey(), "?"))
.replace("%points%", String.valueOf(r.get(i).getValue()))));
}
return out;
}
}
@@ -0,0 +1,262 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.RewardUtil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Snowman;
import org.bukkit.block.Block;
import org.bukkit.Material;
import org.bukkit.event.block.EntityBlockFormEvent;
import java.util.HashSet;
import java.util.Set;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.scheduler.BukkitTask;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Schneemaenner: Deko-Schneemaenner (per Befehl aufgestellt) und Spieler-Schneemaenner
* (normal gebaut), die gelegentlich ein Geschenk abwerfen.
*/
public final class SnowmanManager {
private static final String DECO = "deco";
private static final String PLAYER = "player";
private final ChristmasManager mgr;
private final NamespacedKey typeKey;
private final NamespacedKey ownerKey;
private final NamespacedKey homeKey;
private final NamespacedKey walkKey;
private final Map<UUID, Long> nextGift = new HashMap<>();
private BukkitTask task;
private final Set<Block> trail = new HashSet<>();
SnowmanManager(ChristmasManager mgr) {
this.mgr = mgr;
this.typeKey = new NamespacedKey(mgr.plugin(), "snowman_type");
this.ownerKey = new NamespacedKey(mgr.plugin(), "snowman_owner");
this.walkKey = new NamespacedKey(mgr.plugin(), "snowman_walk");
this.homeKey = new NamespacedKey(mgr.plugin(), "snowman_home");
}
void start() {
stop();
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 40L);
}
void stop() {
if (task != null) task.cancel();
task = null;
for (Block b : trail) if (b.getType() == Material.SNOW) b.setType(Material.AIR, false);
trail.clear();
}
/** Schnee-Spur der Schneemaenner: standardmaessig verschwindet sie nach einigen Sekunden wieder. */
public void onSnowForm(EntityBlockFormEvent e) {
if (!(e.getEntity() instanceof Snowman)) return;
if (e.getNewState().getType() != Material.SNOW) return;
String mode = mgr.cfg().getString("snowman.trail", "fade").toLowerCase();
if (mode.equals("keep")) return;
if (mode.equals("none")) { e.setCancelled(true); return; }
Block b = e.getBlock();
trail.add(b);
long ticks = Math.max(1, mgr.cfg().getLong("snowman.trail-fade-seconds", 8)) * 20L;
Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> {
if (trail.remove(b) && b.getType() == Material.SNOW) b.setType(Material.AIR, false);
}, ticks);
}
// ---------- Abfragen ----------
public boolean isDeco(Entity e) { return DECO.equals(type(e)); }
private String type(Entity e) {
return e.getPersistentDataContainer().get(typeKey, PersistentDataType.STRING);
}
private UUID owner(Entity e) {
String s = e.getPersistentDataContainer().get(ownerKey, PersistentDataType.STRING);
try { return s == null ? null : UUID.fromString(s); } catch (IllegalArgumentException ex) { return null; }
}
// ---------- Deko ----------
public void spawnDeco(Location loc, String name) { spawnDeco(loc, name, null); }
/** walk: null = Standard aus der Config, true = laufend, false = stehend. */
public void spawnDeco(Location loc, String name, Boolean walkMode) {
boolean hat = mgr.cfg().getBoolean("snowman.deco.hat", true);
boolean walk = walkMode != null ? walkMode : mgr.cfg().getBoolean("snowman.deco.walk", false);
loc.getWorld().spawn(loc, Snowman.class, s -> {
s.setAI(walk);
setHome(s, loc);
s.getPersistentDataContainer().set(walkKey, PersistentDataType.BYTE, (byte) (walk ? 1 : 0));
s.setInvulnerable(true);
s.setPersistent(true);
s.setRemoveWhenFarAway(false);
s.setSilent(true);
s.setDerp(!hat);
if (name != null && !name.isBlank()) {
s.setCustomName(ChristmasManager.color(name));
s.setCustomNameVisible(true);
}
s.getPersistentDataContainer().set(typeKey, PersistentDataType.STRING, DECO);
});
loc.getWorld().spawnParticle(Particle.SNOWFLAKE, loc.clone().add(0, 1, 0), 30, 0.4, 0.6, 0.4, 0.02);
}
private void setHome(Entity e, Location l) {
e.getPersistentDataContainer().set(homeKey, PersistentDataType.STRING,
l.getX() + ";" + l.getY() + ";" + l.getZ());
}
private Location home(Entity e) {
String h = e.getPersistentDataContainer().get(homeKey, PersistentDataType.STRING);
if (h == null) return null;
try {
String[] p = h.split(";");
return new Location(e.getWorld(), Double.parseDouble(p[0]), Double.parseDouble(p[1]), Double.parseDouble(p[2]));
} catch (Exception ex) { return null; }
}
/** Laufende Deko-Schneemaenner: frei herumlaufen, aber im Umkreis um den Startpunkt bleiben. */
private void walkDeco(Snowman s, boolean defaultWalk, double radius) {
Byte flag = s.getPersistentDataContainer().get(walkKey, PersistentDataType.BYTE);
boolean walk = flag != null ? flag == 1 : defaultWalk;
if (s.hasAI() != walk) s.setAI(walk);
Location home = home(s);
if (home == null) { setHome(s, s.getLocation()); return; }
if (!walk) {
if (s.getLocation().distanceSquared(home) > 1) s.teleport(home);
return;
}
double d = s.getLocation().distance(home);
if (d <= radius) return;
if (d > radius * 2.5 || !pathTo(s, home)) s.teleport(home);
}
/** Laeuft per Paper-Pathfinder zurueck (per Reflection, damit Spigot weiter geht). */
private boolean pathTo(Snowman s, Location target) {
try {
Object pf = s.getClass().getMethod("getPathfinder").invoke(s);
Object r = pf.getClass().getMethod("moveTo", Location.class, double.class)
.invoke(pf, target, 1.0D);
return r instanceof Boolean b && b;
} catch (Throwable t) {
return false;
}
}
/** Entfernt den naechsten Deko-Schneemann im Umkreis. */
public boolean removeNearestDeco(Player p, double radius) {
Snowman best = null;
double bestDist = Double.MAX_VALUE;
for (Entity e : p.getNearbyEntities(radius, radius, radius)) {
if (e instanceof Snowman s && isDeco(s)) {
double d = s.getLocation().distanceSquared(p.getLocation());
if (d < bestDist) { bestDist = d; best = s; }
}
}
if (best == null) return false;
best.remove();
return true;
}
// ---------- Spieler-Schneemaenner ----------
/** Wird aufgerufen, wenn ein Schneemann normal gebaut wurde. */
public void onBuilt(Snowman s) {
if (!mgr.cfg().getBoolean("snowman.player.enable", true)) return;
Player builder = null;
double best = 12 * 12;
for (Player p : s.getWorld().getPlayers()) {
double d = p.getLocation().distanceSquared(s.getLocation());
if (d < best) { best = d; builder = p; }
}
if (builder == null) return;
PersistentDataContainer pdc = s.getPersistentDataContainer();
pdc.set(typeKey, PersistentDataType.STRING, PLAYER);
pdc.set(ownerKey, PersistentDataType.STRING, builder.getUniqueId().toString());
String name = mgr.cfg().getString("snowman.player.name", "&f%player%s Schneemann");
s.setCustomName(ChristmasManager.color(name.replace("%player%", builder.getName())));
s.setCustomNameVisible(true);
s.setPersistent(true);
s.setRemoveWhenFarAway(false);
nextGift.put(s.getUniqueId(), System.currentTimeMillis() + interval());
int max = mgr.cfg().getInt("snowman.player.max-per-player", 3);
long count = playerSnowmen(builder.getUniqueId()).size();
builder.sendMessage(mgr.msg(count > max ? "snowman-limit" : "snowman-built", "%max%", String.valueOf(max)));
}
private long interval() {
return Math.max(10, mgr.cfg().getLong("snowman.player.gift-interval", 300)) * 1000L;
}
private List<Snowman> playerSnowmen(UUID owner) {
List<Snowman> list = new ArrayList<>();
for (World w : Bukkit.getWorlds()) {
for (Snowman s : w.getEntitiesByClass(Snowman.class)) {
if (PLAYER.equals(type(s)) && owner.equals(owner(s))) list.add(s);
}
}
return list;
}
// ---------- Tick ----------
private void tick() {
long now = System.currentTimeMillis();
boolean particles = mgr.cfg().getBoolean("snowman.deco.particles", true);
boolean walk = mgr.cfg().getBoolean("snowman.deco.walk", false);
double radius = Math.max(2, mgr.cfg().getDouble("snowman.deco.walk-radius", 8));
int max = mgr.cfg().getInt("snowman.player.max-per-player", 3);
List<String> gifts = mgr.cfg().getStringList("snowman.player.gifts");
Map<UUID, List<Snowman>> byOwner = new HashMap<>();
for (World w : Bukkit.getWorlds()) {
for (Snowman s : w.getEntitiesByClass(Snowman.class)) {
String t = type(s);
if (DECO.equals(t)) {
walkDeco(s, walk, radius);
if (particles) w.spawnParticle(Particle.SNOWFLAKE, s.getLocation().add(0, 1.2, 0), 4, 0.5, 0.7, 0.5, 0.01);
} else if (PLAYER.equals(t)) {
UUID o = owner(s);
if (o != null) byOwner.computeIfAbsent(o, k -> new ArrayList<>()).add(s);
}
}
}
for (List<Snowman> list : byOwner.values()) {
// Aelteste zuerst: nur die ersten "max" Schneemaenner geben Geschenke
list.sort(Comparator.comparingInt(Snowman::getTicksLived).reversed());
for (int i = 0; i < list.size() && i < max; i++) {
Snowman s = list.get(i);
long due = nextGift.computeIfAbsent(s.getUniqueId(), k -> now + interval());
if (now < due || gifts.isEmpty()) continue;
nextGift.put(s.getUniqueId(), now + interval());
ItemStack it = ChristmasManager.parseItem(gifts.get(ChristmasManager.RANDOM.nextInt(gifts.size())));
if (it == null) continue;
Location at = s.getLocation().add(0, 1.2, 0);
s.getWorld().dropItem(at, it);
s.getWorld().spawnParticle(Particle.SNOWFLAKE, at, 20, 0.4, 0.4, 0.4, 0.03);
s.getWorld().playSound(at, Sound.BLOCK_SNOW_PLACE, 1f, 1.2f);
}
}
nextGift.keySet().removeIf(id -> Bukkit.getEntity(id) == null && nextGift.size() > 500);
}
}
@@ -0,0 +1,274 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Weihnachtsflug: Am Weihnachtstag (und einen Tag danach) fliegt Santa von aufgestellter Socke zu
* aufgestellter Socke und legt ein Geschenk darunter ab.
*/
final class StockingFlight {
private final ChristmasManager mgr;
private final StockingManager stockings;
private BukkitTask scheduler;
private BukkitTask flightTask;
private LivingEntity santa;
private long lastFlight;
// Ablauf eines Fluges
private List<StockingManager.Placed> queue = new ArrayList<>();
private int index;
private int hover;
private boolean leaving;
private int leaveTicks;
private Location leaveTarget;
private float yaw;
private int tickCount;
StockingFlight(ChristmasManager mgr, StockingManager stockings) {
this.mgr = mgr;
this.stockings = stockings;
}
void start() {
if (scheduler != null) scheduler.cancel();
scheduler = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::check, 600L, 20L * 30);
}
void stopAll() {
if (scheduler != null) scheduler.cancel();
scheduler = null;
finish();
}
boolean running() { return flightTask != null; }
// ---------- Zeitplan ----------
private LocalDate date(String key, String def) {
LocalDate today = LocalDate.now(mgr.plugin().getZone());
String[] p = mgr.cfg().getString(key, def).split("-");
try {
return LocalDate.of(today.getYear(), Integer.parseInt(p[0].trim()), Integer.parseInt(p[1].trim()));
} catch (Exception e) {
return LocalDate.of(today.getYear(), 12, 24);
}
}
/** Liegt heute im Flug-Zeitraum (Standard 24.12. bis 25.12.)? */
boolean inWindow() {
LocalDate today = LocalDate.now(mgr.plugin().getZone());
return !today.isBefore(date("stocking-flight.from", "12-24")) && !today.isAfter(date("stocking-flight.until", "12-25"));
}
private void check() {
stockings.cleanup();
if (flightTask != null) return;
if (!mgr.cfg().getBoolean("stocking-flight.enable", true) || !mgr.cfg().getBoolean("stocking.enable", true)) return;
if (!inWindow() || Bukkit.getOnlinePlayers().isEmpty()) return;
long every = Math.max(1, mgr.cfg().getLong("stocking-flight.interval-minutes", 20)) * 60_000L;
if (System.currentTimeMillis() - lastFlight < every) return;
launch(false);
}
// ---------- Flug ----------
/** Startet einen Flug; force = auch ausserhalb des Zeitraums und trotz heutiger Lieferung (Admin-Test). */
boolean launch(boolean force) {
if (flightTask != null) return false;
String today = LocalDate.now(mgr.plugin().getZone()).toString();
// Socken in geladenen Chunks, nach Welt gruppiert; die Welt mit den meisten gewinnt
java.util.Map<String, List<StockingManager.Placed>> byWorld = new java.util.HashMap<>();
for (StockingManager.Placed pl : new ArrayList<>(stockings.placed().values())) {
World w = Bukkit.getWorld(pl.world);
if (w == null || !w.isChunkLoaded(pl.x >> 4, pl.z >> 4)) continue;
ItemFrame f = stockings.frameOf(pl);
if (f == null) continue;
if (!force && (today.equals(pl.lastDay) || pl.presentKey != null)) continue;
byWorld.computeIfAbsent(pl.world, k -> new ArrayList<>()).add(pl);
}
List<StockingManager.Placed> best = new ArrayList<>();
for (var l : byWorld.values()) if (l.size() > best.size()) best = l;
if (best.isEmpty()) return false;
// Reihenfolge: immer zur naechsten Socke
int max = Math.max(1, mgr.cfg().getInt("stocking-flight.max-stockings-per-flight", 12));
List<StockingManager.Placed> pool = new ArrayList<>(best);
queue = new ArrayList<>();
StockingManager.Placed cur = pool.remove(ChristmasManager.RANDOM.nextInt(pool.size()));
queue.add(cur);
while (!pool.isEmpty() && queue.size() < max) {
final StockingManager.Placed from = cur;
StockingManager.Placed next = pool.stream().min(java.util.Comparator.comparingDouble(
x -> dist2(from, x))).get();
pool.remove(next);
queue.add(next);
cur = next;
}
World w = Bukkit.getWorld(queue.get(0).world);
Location first = hoverPoint(queue.get(0));
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
Location spawn = first.clone().add(Math.cos(ang) * 45, 22, Math.sin(ang) * 45);
try {
santa = mgr.santa().createModel(spawn);
santa.setGravity(false);
santa.setInvulnerable(true);
santa.setSilent(true);
santa.setPersistent(false);
} catch (Throwable t) {
mgr.plugin().getLogger().warning("Santa-Flug konnte nicht gestartet werden: " + t);
finish();
return false;
}
lastFlight = System.currentTimeMillis();
index = 0;
hover = 0;
leaving = false;
tickCount = 0;
yaw = spawn.getYaw();
for (Player p : w.getPlayers()) p.sendMessage(mgr.msg("stocking-flight-started"));
w.playSound(spawn, Sound.BLOCK_AMETHYST_BLOCK_CHIME, SoundCategory.RECORDS, 4f, 1.4f);
flightTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 1L, 1L);
return true;
}
private double dist2(StockingManager.Placed a, StockingManager.Placed b) {
double dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
return dx * dx + dy * dy + dz * dz;
}
private Location hoverPoint(StockingManager.Placed pl) {
return new Location(Bukkit.getWorld(pl.world), pl.x + 0.5, pl.y + 2.2, pl.z + 0.5);
}
private void tick() {
if (santa == null || !santa.isValid()) { finish(); return; }
World w = santa.getWorld();
Location pos = santa.getLocation();
tickCount++;
// Schweif und Glocken
w.spawnParticle(Particle.END_ROD, pos.clone().add(0, 1, 0), 2, 0.3, 0.3, 0.3, 0.01);
w.spawnParticle(Particle.SNOWFLAKE, pos.clone().add(0, 0.8, 0), 4, 0.4, 0.4, 0.4, 0.02);
if (tickCount % 16 == 0) {
float pitch = (float) Math.pow(2.0, (ChristmasManager.RANDOM.nextInt(8) - 4) / 12.0);
w.playSound(pos, Sound.BLOCK_NOTE_BLOCK_BELL, SoundCategory.RECORDS, 2.5f, pitch);
}
if (leaving) {
leaveTicks--;
moveToward(pos, leaveTarget, 0.9);
if (leaveTicks <= 0) { finish(); }
return;
}
// Naechste noch vorhandene Socke suchen
while (index < queue.size() && stockings.frameOf(queue.get(index)) == null) index++;
if (index >= queue.size()) { startLeaving(pos); return; }
StockingManager.Placed pl = queue.get(index);
Location target = hoverPoint(pl);
if (hover == 0) {
double d = pos.distance(target);
double speed = Math.max(0.2, mgr.cfg().getDouble("stocking-flight.speed", 0.7));
moveToward(pos, target, Math.min(speed, Math.max(0.08, d * 0.12)));
if (d < 0.5) hover = 1;
return;
}
// Schweben und Geschenk ablegen
hover++;
if (hover == 14) placePresent(pl);
if (hover > 36) { hover = 0; index++; }
}
private void moveToward(Location pos, Location target, double step) {
double dx = target.getX() - pos.getX(), dy = target.getY() - pos.getY(), dz = target.getZ() - pos.getZ();
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len < 1e-6) return;
double f = Math.min(1.0, step / len);
double nx = pos.getX() + dx * f, ny = pos.getY() + dy * f, nz = pos.getZ() + dz * f;
double hx = target.getX() - pos.getX(), hz = target.getZ() - pos.getZ();
if (hx * hx + hz * hz > 0.25) {
float want = (float) Math.toDegrees(Math.atan2(-hx, hz));
float diff = ((want - yaw + 540f) % 360f) - 180f;
yaw += Math.max(-14f, Math.min(14f, diff));
}
santa.teleport(new Location(pos.getWorld(), nx, ny, nz, yaw, 0f));
}
private void startLeaving(Location pos) {
leaving = true;
leaveTicks = 70;
double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2;
leaveTarget = pos.clone().add(Math.cos(ang) * 60, 35, Math.sin(ang) * 60);
}
private void placePresent(StockingManager.Placed pl) {
World w = Bukkit.getWorld(pl.world);
if (w == null) return;
String today = LocalDate.now(mgr.plugin().getZone()).toString();
// Geschenk unter die Socke: freier Block darunter (bis zu 2 Bloecke tiefer)
Block frame = w.getBlockAt(pl.x, pl.y, pl.z);
Block spot = null;
for (int i = 1; i <= 2 && spot == null; i++) {
Block b = frame.getRelative(0, -i, 0);
if (b.getType().isAir()) spot = b;
else if (b.getType().isSolid()) break;
}
if (spot == null) return;
List<String> skins = mgr.cfg().getStringList("magic-tree.presents");
if (skins.isEmpty()) {
spot.setType(Material.CHEST, false);
} else {
mgr.trees().setSkin(spot, skins.get(ChristmasManager.RANDOM.nextInt(skins.size())));
}
stockings.registerPresent(pl, spot);
pl.lastDay = today;
stockings.save();
Location at = spot.getLocation().add(0.5, 0.6, 0.5);
w.spawnParticle(Particle.END_ROD, at, 25, 0.3, 0.3, 0.3, 0.05);
w.spawnParticle(Particle.HAPPY_VILLAGER, at, 12, 0.3, 0.3, 0.3, 0);
w.playSound(at, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.3f);
w.playSound(at, Sound.BLOCK_NOTE_BLOCK_CHIME, SoundCategory.RECORDS, 1.5f, 1.5f);
Player owner = Bukkit.getPlayer(pl.owner);
if (owner != null) {
owner.sendMessage(mgr.msg("stocking-present-placed",
"%x%", String.valueOf(spot.getX()), "%y%", String.valueOf(spot.getY()), "%z%", String.valueOf(spot.getZ())));
}
}
private void finish() {
if (flightTask != null) flightTask.cancel();
flightTask = null;
if (santa != null) {
try { santa.remove(); } catch (Throwable ignored) { }
santa = null;
}
queue = new ArrayList<>();
leaving = false;
}
/** Fuer Aufraeumen: ID der Socke zum Flug (nur zur Info). */
UUID currentSock() { return index < queue.size() ? queue.get(index).id : null; }
}
@@ -0,0 +1,344 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.ItemFrame;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.util.RayTraceResult;
import java.util.List;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/** Kamin-Geschenke: Socke aufhaengen, am 25.12. liegt etwas darin. */
public final class StockingManager {
private static final class Data {
int hungYear;
int claimedYear;
}
private final ChristmasManager mgr;
private final File file;
private final Map<UUID, Data> data = new HashMap<>();
private BukkitTask task;
/** Aufgestellte Socke (Item-Rahmen an der Wand). */
static final class Placed {
UUID id;
UUID owner;
String world;
int x, y, z;
String presentKey; // noch nicht abgeholtes Geschenk unter der Socke
String lastDay = ""; // Datum der letzten Lieferung
}
private final Map<UUID, Placed> placed = new HashMap<>();
private final Map<String, UUID> presents = new HashMap<>(); // Blockschluessel -> Besitzer
private final NamespacedKey sockKey;
private final StockingFlight flight;
StockingManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "stockings.yml");
this.sockKey = new NamespacedKey(mgr.plugin(), "stocking_frame");
this.flight = new StockingFlight(mgr, this);
}
StockingFlight flight() { return flight; }
void load() {
data.clear();
if (file.exists()) {
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
ConfigurationSection s = y.getConfigurationSection("players");
if (s != null) {
for (String k : s.getKeys(false)) {
try {
Data d = new Data();
d.hungYear = s.getInt(k + ".hung");
d.claimedYear = s.getInt(k + ".claimed");
data.put(UUID.fromString(k), d);
} catch (IllegalArgumentException ignored) { }
}
}
}
placed.clear();
presents.clear();
if (file.exists()) {
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
ConfigurationSection ps = y.getConfigurationSection("placed");
if (ps != null) {
for (String k : ps.getKeys(false)) {
try {
Placed pl = new Placed();
pl.id = UUID.fromString(k);
pl.owner = UUID.fromString(ps.getString(k + ".owner", ""));
pl.world = ps.getString(k + ".world", "");
pl.x = ps.getInt(k + ".x");
pl.y = ps.getInt(k + ".y");
pl.z = ps.getInt(k + ".z");
pl.presentKey = ps.getString(k + ".present");
pl.lastDay = ps.getString(k + ".last-day", "");
placed.put(pl.id, pl);
} catch (IllegalArgumentException ignored) { }
}
}
ConfigurationSection gs = y.getConfigurationSection("presents");
if (gs != null) {
for (String k : gs.getKeys(false)) {
try { presents.put(k.replace('|', ';'), UUID.fromString(gs.getString(k, ""))); }
catch (IllegalArgumentException ignored) { }
}
}
}
stop();
flight.start();
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), () -> {
for (Player p : Bukkit.getOnlinePlayers()) tryDeliver(p, false);
}, 400L, 20L * 60);
}
void stop() {
if (task != null) task.cancel();
task = null;
flight.stopAll();
}
void save() {
YamlConfiguration y = new YamlConfiguration();
for (var e : data.entrySet()) {
y.set("players." + e.getKey() + ".hung", e.getValue().hungYear);
y.set("players." + e.getKey() + ".claimed", e.getValue().claimedYear);
}
for (Placed pl : placed.values()) {
String b = "placed." + pl.id;
y.set(b + ".owner", pl.owner.toString());
y.set(b + ".world", pl.world);
y.set(b + ".x", pl.x);
y.set(b + ".y", pl.y);
y.set(b + ".z", pl.z);
y.set(b + ".present", pl.presentKey);
y.set(b + ".last-day", pl.lastDay);
}
for (var e : presents.entrySet()) y.set("presents." + e.getKey().replace(';', '|'), e.getValue().toString());
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("stockings.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
// ---------- Datumslogik ----------
private LocalDate date(String key, String def) {
LocalDate today = LocalDate.now(mgr.plugin().getZone());
String[] p = mgr.cfg().getString(key, def).split("-");
try {
return LocalDate.of(today.getYear(), Integer.parseInt(p[0].trim()), Integer.parseInt(p[1].trim()));
} catch (Exception e) {
return LocalDate.of(today.getYear(), 12, 25);
}
}
private LocalDate today() { return LocalDate.now(mgr.plugin().getZone()); }
// ---------- Aktionen ----------
/** /xmas stocking: aufhaengen oder (ab dem Tag) abholen. */
public void use(Player p) {
if (!mgr.cfg().getBoolean("stocking.enable", true)) return;
LocalDate today = today();
LocalDate from = date("stocking.hang-from", "12-01");
LocalDate open = date("stocking.open-on", "12-25");
int year = today.getYear();
Data d = data.computeIfAbsent(p.getUniqueId(), k -> new Data());
if (today.isBefore(open)) {
if (today.isBefore(from)) {
p.sendMessage(mgr.msg("stocking-too-early", "%from%", from.getDayOfMonth() + "." + from.getMonthValue() + "."));
return;
}
if (d.hungYear == year) { p.sendMessage(mgr.msg("stocking-already", "%date%", open.getDayOfMonth() + "." + open.getMonthValue() + ".")); return; }
d.hungYear = year;
save();
p.sendMessage(mgr.msg("stocking-hung", "%date%", open.getDayOfMonth() + "." + open.getMonthValue() + "."));
return;
}
if (d.hungYear != year) { p.sendMessage(mgr.msg("stocking-nothing")); return; }
if (d.claimedYear == year) { p.sendMessage(mgr.msg("stocking-claimed")); return; }
deliver(p, d);
}
/** Automatisch: nach dem Oeffnungstag bekommt jeder mit aufgehaengter Socke sein Geschenk. */
public void tryDeliver(Player p, boolean announceIfNone) {
if (!mgr.cfg().getBoolean("stocking.enable", true)) return;
LocalDate today = today();
if (today.isBefore(date("stocking.open-on", "12-25")) || today.isAfter(date("stocking.until", "12-31"))) return;
Data d = data.get(p.getUniqueId());
if (d == null || d.hungYear != today.getYear() || d.claimedYear == today.getYear()) return;
deliver(p, d);
}
private void deliver(Player p, Data d) {
d.claimedYear = today().getYear();
save();
for (var reward : mgr.cfg().getMapList("stocking.rewards")) ChristmasExtras.applyMap(p, reward);
p.sendMessage(mgr.msg("stocking-received"));
p.getWorld().spawnParticle(org.bukkit.Particle.SNOWFLAKE, p.getLocation().add(0, 1.5, 0), 30, 0.5, 0.5, 0.5, 0.03);
}
// ---------- Aufgestellte Socken ----------
Map<UUID, Placed> placed() { return placed; }
private static String key(Block b) {
return b.getWorld().getName() + ";" + b.getX() + ";" + b.getY() + ";" + b.getZ();
}
private ItemStack sockItem() {
ItemStack it = new ItemStack(Material.LEATHER_BOOTS);
if (it.getItemMeta() instanceof LeatherArmorMeta m) {
m.setColor(Color.RED);
m.setDisplayName(ChristmasManager.color("&cWeihnachtssocke"));
it.setItemMeta(m);
}
return it;
}
/** /xmas stocking place: haengt eine Socke an die Wand, auf die der Spieler schaut. */
public void place(Player p) {
if (!mgr.cfg().getBoolean("stocking.enable", true)) return;
int max = mgr.cfg().getInt("stocking.max-placed-per-player", 3);
long mine = placed.values().stream().filter(x -> x.owner.equals(p.getUniqueId())).count();
if (mine >= max && !p.hasPermission("xmas.admin")) {
p.sendMessage(mgr.msg("stocking-place-limit", "%max%", String.valueOf(max)));
return;
}
RayTraceResult r = p.rayTraceBlocks(5);
if (r == null || r.getHitBlock() == null || r.getHitBlockFace() == null) {
p.sendMessage(mgr.msg("stocking-place-usage"));
return;
}
BlockFace face = r.getHitBlockFace();
if (face == BlockFace.UP || face == BlockFace.DOWN) { p.sendMessage(mgr.msg("stocking-place-usage")); return; }
Block at = r.getHitBlock().getRelative(face);
if (!at.getType().isAir()) { p.sendMessage(mgr.msg("stocking-place-usage")); return; }
for (Placed o : placed.values()) {
if (o.world.equals(at.getWorld().getName()) && o.x == at.getX() && o.y == at.getY() && o.z == at.getZ()) {
p.sendMessage(mgr.msg("stocking-place-usage"));
return;
}
}
ItemFrame f = at.getWorld().spawn(at.getLocation(), ItemFrame.class, fr -> {
fr.setFacingDirection(face, true);
fr.setVisible(false);
fr.setItem(sockItem(), false);
fr.setFixed(true);
fr.setInvulnerable(true);
fr.setPersistent(true);
fr.getPersistentDataContainer().set(sockKey, PersistentDataType.STRING, p.getUniqueId().toString());
});
Placed pl = new Placed();
pl.id = f.getUniqueId();
pl.owner = p.getUniqueId();
pl.world = at.getWorld().getName();
pl.x = at.getX();
pl.y = at.getY();
pl.z = at.getZ();
placed.put(pl.id, pl);
save();
at.getWorld().spawnParticle(Particle.SNOWFLAKE, at.getLocation().add(0.5, 0.5, 0.5), 20, 0.3, 0.3, 0.3, 0.02);
p.sendMessage(mgr.msg("stocking-placed"));
}
/** /xmas stocking remove: entfernt die naechste eigene Socke (Admins: jede). */
public void removeNearest(Player p) {
ItemFrame best = null;
double bd = Double.MAX_VALUE;
for (Entity e : p.getNearbyEntities(6, 6, 6)) {
if (!(e instanceof ItemFrame f) || !placed.containsKey(f.getUniqueId())) continue;
Placed pl = placed.get(f.getUniqueId());
if (!pl.owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) continue;
double d = f.getLocation().distanceSquared(p.getLocation());
if (d < bd) { bd = d; best = f; }
}
if (best == null) { p.sendMessage(mgr.msg("stocking-none-near")); return; }
Placed pl = placed.remove(best.getUniqueId());
best.remove();
if (pl != null && pl.presentKey != null) presents.remove(pl.presentKey);
save();
p.sendMessage(mgr.msg("stocking-removed"));
}
/** Entfernt Eintraege, deren Rahmen es nicht mehr gibt (z.B. im Kreativmodus zerstoert). */
void cleanup() {
boolean changed = false;
for (var it = placed.values().iterator(); it.hasNext(); ) {
Placed pl = it.next();
World w = Bukkit.getWorld(pl.world);
if (w == null || !w.isChunkLoaded(pl.x >> 4, pl.z >> 4)) continue;
if (frameOf(pl) == null) { it.remove(); changed = true; }
}
if (changed) save();
}
/** Rahmen der Socke (null, wenn nicht geladen/vorhanden). */
ItemFrame frameOf(Placed pl) {
Entity e = Bukkit.getEntity(pl.id);
return e instanceof ItemFrame f && f.isValid() ? f : null;
}
// ---------- Geschenke unter der Socke ----------
public boolean isPresentBlock(Block b) { return presents.containsKey(key(b)); }
void registerPresent(Placed pl, Block b) {
String k = key(b);
presents.put(k, pl.owner);
pl.presentKey = k;
}
/** Spieler oeffnet ein Socken-Geschenk (Rechtsklick oder Abbauen). */
public void openPresent(Player p, Block b) {
String k = key(b);
UUID owner = presents.get(k);
if (owner == null) return;
if (mgr.cfg().getBoolean("stocking-flight.owner-only", true)
&& !owner.equals(p.getUniqueId()) && !p.hasPermission("xmas.admin")) {
p.sendMessage(mgr.msg("stocking-present-notyours"));
return;
}
presents.remove(k);
for (Placed pl : placed.values()) if (k.equals(pl.presentKey)) pl.presentKey = null;
b.setType(Material.AIR, false);
World w = b.getWorld();
w.spawnParticle(Particle.END_ROD, b.getLocation().add(0.5, 0.5, 0.5), 20, 0.3, 0.3, 0.3, 0.05);
w.playSound(b.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.4f);
List<Map<?, ?>> rewards = mgr.cfg().getMapList("stocking-flight.rewards");
if (rewards.isEmpty()) rewards = mgr.cfg().getMapList("stocking.rewards");
for (Map<?, ?> reward : rewards) ChristmasExtras.applyMap(p, reward);
p.sendMessage(mgr.msg("stocking-present-opened"));
save();
}
/** Geschenke unter Socken sind vor Zerstoerung geschuetzt. */
public boolean isProtected(Block b) { return isPresentBlock(b); }
}
@@ -0,0 +1,198 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.RewardUtil;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Wichteln: Anmelden, zufaellig ziehen, Geschenk ablegen, am Ende verteilen. */
public final class WichtelnManager {
public enum State { CLOSED, OPEN, DRAWN, DELIVERED }
private record Pending(String from, ItemStack item) {}
private final ChristmasManager mgr;
private final File file;
private State state = State.CLOSED;
private final Map<UUID, String> participants = new LinkedHashMap<>();
private final Map<UUID, UUID> assign = new LinkedHashMap<>();
private final Map<UUID, ItemStack> gifts = new LinkedHashMap<>();
private final Map<UUID, List<Pending>> pending = new LinkedHashMap<>();
WichtelnManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "wichteln.yml");
}
public State state() { return state; }
public int participantCount() { return participants.size(); }
public boolean joined(UUID id) { return participants.containsKey(id); }
public String targetName(UUID giver) {
UUID t = assign.get(giver);
return t == null ? "" : participants.getOrDefault(t, "");
}
// ---------- Phasen ----------
public boolean open() {
if (state != State.CLOSED && state != State.DELIVERED) return false;
reset();
state = State.OPEN;
save();
return true;
}
public boolean join(Player p) {
if (state != State.OPEN) return false;
participants.put(p.getUniqueId(), p.getName());
save();
return true;
}
public boolean leave(Player p) {
if (state != State.OPEN) return false;
boolean had = participants.remove(p.getUniqueId()) != null;
save();
return had;
}
/** Zieht zufaellig, sodass niemand sich selbst beschenkt. false bei zu wenigen Teilnehmern. */
public boolean draw() {
if (state != State.OPEN || participants.size() < 3) return false;
List<UUID> givers = new ArrayList<>(participants.keySet());
List<UUID> receivers = new ArrayList<>(givers);
boolean ok = false;
for (int attempt = 0; attempt < 2000 && !ok; attempt++) {
Collections.shuffle(receivers, ChristmasManager.RANDOM);
ok = true;
for (int i = 0; i < givers.size(); i++) {
if (givers.get(i).equals(receivers.get(i))) { ok = false; break; }
}
}
if (!ok) return false;
assign.clear();
for (int i = 0; i < givers.size(); i++) assign.put(givers.get(i), receivers.get(i));
state = State.DRAWN;
save();
for (UUID id : givers) {
Player p = Bukkit.getPlayer(id);
if (p != null) p.sendMessage(mgr.msg("wi-who", "%name%", targetName(id)));
}
return true;
}
/** Legt das Item in der Hand als Geschenk ab (ersetzt ein vorheriges, das zurueckgegeben wird). */
public String setGift(Player p) {
if (state != State.DRAWN) return "wi-wrong-state";
if (!assign.containsKey(p.getUniqueId())) return "wi-not-joined";
ItemStack hand = p.getInventory().getItemInMainHand();
if (hand == null || hand.getType().isAir()) return "wi-gift-empty";
ItemStack old = gifts.put(p.getUniqueId(), hand.clone());
p.getInventory().setItemInMainHand(null);
if (old != null) RewardUtil.give(p, old);
save();
return "wi-gift-saved";
}
public int giftCount() { return gifts.size(); }
/** Verteilt alle Geschenke; Offline-Spieler bekommen ihres beim naechsten Join. */
public int deliver() {
if (state != State.DRAWN) return -1;
boolean reveal = mgr.cfg().getBoolean("wichteln.reveal-giver", false);
int n = 0;
for (var e : gifts.entrySet()) {
UUID receiver = assign.get(e.getKey());
if (receiver == null) continue;
String from = reveal ? participants.getOrDefault(e.getKey(), "?") : "";
pending.computeIfAbsent(receiver, k -> new ArrayList<>()).add(new Pending(from, e.getValue()));
n++;
}
gifts.clear();
state = State.DELIVERED;
save();
for (Player p : Bukkit.getOnlinePlayers()) onJoin(p);
return n;
}
/** Uebergibt wartende Geschenke. */
public void onJoin(Player p) {
List<Pending> list = pending.remove(p.getUniqueId());
if (list == null) return;
for (Pending pe : list) {
RewardUtil.give(p, pe.item());
p.sendMessage(mgr.msg(pe.from().isEmpty() ? "wi-received" : "wi-received-from", "%from%", pe.from()));
}
save();
}
public void reset() {
participants.clear();
assign.clear();
gifts.clear();
pending.clear();
state = State.CLOSED;
save();
}
// ---------- Persistenz ----------
void load() {
if (!file.exists()) return;
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
try { state = State.valueOf(y.getString("state", "CLOSED")); } catch (IllegalArgumentException e) { state = State.CLOSED; }
participants.clear(); assign.clear(); gifts.clear(); pending.clear();
ConfigurationSection ps = y.getConfigurationSection("participants");
if (ps != null) for (String k : ps.getKeys(false)) participants.put(UUID.fromString(k), ps.getString(k, "?"));
ConfigurationSection as = y.getConfigurationSection("assign");
if (as != null) for (String k : as.getKeys(false)) assign.put(UUID.fromString(k), UUID.fromString(as.getString(k, "")));
ConfigurationSection gs = y.getConfigurationSection("gifts");
if (gs != null) for (String k : gs.getKeys(false)) { ItemStack it = gs.getItemStack(k); if (it != null) gifts.put(UUID.fromString(k), it); }
ConfigurationSection pe = y.getConfigurationSection("pending");
if (pe != null) {
for (String k : pe.getKeys(false)) {
List<Pending> list = new ArrayList<>();
for (Map<?, ?> m : pe.getMapList(k)) {
Object item = m.get("item");
if (item instanceof ItemStack is) list.add(new Pending(String.valueOf(m.get("from")), is));
}
pending.put(UUID.fromString(k), list);
}
}
}
void save() {
YamlConfiguration y = new YamlConfiguration();
y.set("state", state.name());
participants.forEach((k, v) -> y.set("participants." + k, v));
assign.forEach((k, v) -> y.set("assign." + k, v.toString()));
gifts.forEach((k, v) -> y.set("gifts." + k, v));
pending.forEach((k, list) -> {
List<Map<String, Object>> maps = new ArrayList<>();
for (Pending p : list) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("from", p.from());
m.put("item", p.item());
maps.add(m);
}
y.set("pending." + k, maps);
});
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("wichteln.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
}
@@ -0,0 +1,270 @@
package de.mviper.adventskalender.christmas;
import de.mviper.adventskalender.RewardUtil;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Wunschzettel: Spieler schreiben Wuensche, Admins sehen sie in einem Menue und erfuellen sie. */
public final class WishManager {
private static final class Wish {
int id;
UUID owner;
String name;
String text;
long created;
boolean done;
boolean notified;
ItemStack gift;
}
public static final class Menu implements InventoryHolder {
Inventory inv;
int page;
final Map<Integer, Integer> slotToId = new LinkedHashMap<>();
@Override public Inventory getInventory() { return inv; }
}
private static final int PAGE_SIZE = 45;
private final ChristmasManager mgr;
private final File file;
private final Map<Integer, Wish> wishes = new LinkedHashMap<>();
private int nextId = 1;
WishManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "wishes.yml");
}
// ---------- Persistenz ----------
void load() {
wishes.clear();
if (!file.exists()) return;
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
nextId = y.getInt("next-id", 1);
ConfigurationSection s = y.getConfigurationSection("wishes");
if (s == null) return;
for (String k : s.getKeys(false)) {
try {
Wish w = new Wish();
w.id = Integer.parseInt(k);
w.owner = UUID.fromString(s.getString(k + ".owner", ""));
w.name = s.getString(k + ".name", "?");
w.text = s.getString(k + ".text", "");
w.created = s.getLong(k + ".created");
w.done = s.getBoolean(k + ".done");
w.notified = s.getBoolean(k + ".notified");
w.gift = s.getItemStack(k + ".gift");
wishes.put(w.id, w);
} catch (IllegalArgumentException ignored) { }
}
}
void save() {
YamlConfiguration y = new YamlConfiguration();
y.set("next-id", nextId);
for (Wish w : wishes.values()) {
String p = "wishes." + w.id;
y.set(p + ".owner", w.owner.toString());
y.set(p + ".name", w.name);
y.set(p + ".text", w.text);
y.set(p + ".created", w.created);
y.set(p + ".done", w.done);
y.set(p + ".notified", w.notified);
if (w.gift != null) y.set(p + ".gift", w.gift);
}
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("wishes.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
// ---------- Spieler ----------
private List<Wish> openOf(UUID owner) {
List<Wish> list = new ArrayList<>();
for (Wish w : wishes.values()) if (!w.done && w.owner.equals(owner)) list.add(w);
return list;
}
/** @return Nachrichten-Schluessel */
public String add(Player p, String text) {
text = text.trim();
int max = Math.max(10, mgr.cfg().getInt("wishes.max-length", 120));
if (text.isEmpty()) return "wish-usage";
if (text.length() > max) return "wish-too-long";
if (openOf(p.getUniqueId()).size() >= Math.max(1, mgr.cfg().getInt("wishes.max-open-per-player", 3))) return "wish-limit";
Wish w = new Wish();
w.id = nextId++;
w.owner = p.getUniqueId();
w.name = p.getName();
w.text = text.replace('&', '+'); // keine Farbcodes von Spielern
w.created = System.currentTimeMillis();
wishes.put(w.id, w);
save();
for (Player admin : Bukkit.getOnlinePlayers()) {
if (admin.hasPermission("xmas.admin")) admin.sendMessage(mgr.msg("wish-admin-new", "%player%", p.getName()));
}
return "wish-saved";
}
public void list(Player p) {
List<Wish> open = openOf(p.getUniqueId());
if (open.isEmpty()) { p.sendMessage(mgr.msg("wish-empty")); return; }
p.sendMessage(mgr.msg("wish-list-header"));
for (Wish w : open) p.sendMessage(mgr.raw("wish-list-line").replace("%text%", w.text));
}
public int clearOwn(Player p) {
List<Wish> open = openOf(p.getUniqueId());
for (Wish w : open) wishes.remove(w.id);
if (!open.isEmpty()) save();
return open.size();
}
/** Erfuellte Wuensche zustellen (beim Join oder sofort). */
public void deliver(Player p) {
boolean changed = false;
List<Wish> done = new ArrayList<>();
for (Wish w : wishes.values()) if (w.done && !w.notified && w.owner.equals(p.getUniqueId())) done.add(w);
for (Wish w : done) {
if (w.gift != null) RewardUtil.give(p, w.gift);
p.sendMessage(mgr.msg(w.gift != null ? "wish-fulfilled-gift" : "wish-fulfilled", "%wish%", w.text));
wishes.remove(w.id);
changed = true;
}
if (changed) save();
}
// ---------- Admin-Menue ----------
private List<Wish> allOpen() {
List<Wish> list = new ArrayList<>();
for (Wish w : wishes.values()) if (!w.done) list.add(w);
return list;
}
public void openMenu(Player admin, int page) {
List<Wish> open = allOpen();
int pages = Math.max(1, (open.size() + PAGE_SIZE - 1) / PAGE_SIZE);
page = Math.max(0, Math.min(page, pages - 1));
Menu holder = new Menu();
holder.page = page;
holder.inv = Bukkit.createInventory(holder, 54, mgr.raw("wish-menu-title") + " (" + (page + 1) + "/" + pages + ")");
SimpleDateFormat fmt = new SimpleDateFormat("dd.MM. HH:mm");
for (int i = 0; i < PAGE_SIZE; i++) {
int idx = page * PAGE_SIZE + i;
if (idx >= open.size()) break;
Wish w = open.get(idx);
ItemStack it = new ItemStack(Material.PAPER);
ItemMeta meta = it.getItemMeta();
if (meta != null) {
meta.setDisplayName(mgr.raw("wish-menu-item-name").replace("%player%", w.name));
List<String> lore = new ArrayList<>();
for (String line : wrap(w.text, 34)) lore.add("§f" + line);
lore.add("§8" + fmt.format(new Date(w.created)));
lore.add(" ");
lore.add(mgr.raw("wish-menu-hint-left"));
lore.add(mgr.raw("wish-menu-hint-shift"));
lore.add(mgr.raw("wish-menu-hint-right"));
meta.setLore(lore);
it.setItemMeta(meta);
}
holder.inv.setItem(i, it);
holder.slotToId.put(i, w.id);
}
if (page > 0) holder.inv.setItem(45, named(Material.ARROW, mgr.raw("wish-page-prev")));
if (page < pages - 1) holder.inv.setItem(53, named(Material.ARROW, mgr.raw("wish-page-next")));
if (open.isEmpty()) holder.inv.setItem(22, named(Material.BARRIER, mgr.raw("wish-menu-empty")));
admin.openInventory(holder.inv);
}
private static ItemStack named(Material m, String name) {
ItemStack it = new ItemStack(m);
ItemMeta meta = it.getItemMeta();
if (meta != null) { meta.setDisplayName(name); it.setItemMeta(meta); }
return it;
}
private static List<String> wrap(String text, int width) {
List<String> lines = new ArrayList<>();
StringBuilder cur = new StringBuilder();
for (String word : text.split(" ")) {
if (cur.length() + word.length() + 1 > width && cur.length() > 0) {
lines.add(cur.toString());
cur = new StringBuilder();
}
if (cur.length() > 0) cur.append(' ');
cur.append(word);
}
if (cur.length() > 0) lines.add(cur.toString());
return lines;
}
public void click(Player admin, Menu menu, int slot, boolean right, boolean shift) {
if (slot == 45 && menu.page > 0) { openMenu(admin, menu.page - 1); return; }
if (slot == 53) { openMenu(admin, menu.page + 1); return; }
Integer id = menu.slotToId.get(slot);
if (id == null) return;
Wish w = wishes.get(id);
if (w == null || w.done) { openMenu(admin, menu.page); return; }
if (right) {
wishes.remove(id);
save();
admin.sendMessage(mgr.msg("wish-admin-deleted", "%player%", w.name));
openMenu(admin, menu.page);
return;
}
ItemStack hand = admin.getInventory().getItemInMainHand();
boolean hasItem = hand != null && !hand.getType().isAir();
if (!hasItem && !shift) {
// Kein Item in der Hand: nicht stillschweigend ohne Geschenk erfuellen
admin.sendMessage(mgr.msg("wish-admin-hold"));
return;
}
String itemText = "";
if (hasItem && !shift) {
w.gift = hand.clone();
itemText = hand.getAmount() + "x " + hand.getType().name();
admin.getInventory().setItemInMainHand(null);
}
w.done = true;
w.notified = false;
save();
Player owner = Bukkit.getPlayer(w.owner);
if (owner != null) {
deliver(owner);
admin.sendMessage(w.gift != null
? mgr.msg("wish-admin-done-gift", "%player%", w.name, "%item%", itemText)
: mgr.msg("wish-admin-done", "%player%", w.name));
} else {
admin.sendMessage(mgr.msg("wish-admin-done-offline", "%player%", w.name));
}
openMenu(admin, menu.page);
}
}
@@ -0,0 +1,22 @@
package de.mviper.adventskalender.christmas;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import org.bukkit.Location;
import java.util.List;
/** Nur laden, wenn WorldGuard installiert ist (sonst NoClassDefFoundError). */
final class WorldGuardHook {
private WorldGuardHook() {}
static boolean inAnyRegion(Location loc, List<String> names) {
var set = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery()
.getApplicableRegions(BukkitAdapter.adapt(loc));
for (ProtectedRegion r : set.getRegions()) {
for (String n : names) if (r.getId().equalsIgnoreCase(n)) return true;
}
return false;
}
}
@@ -0,0 +1,199 @@
package de.mviper.adventskalender.christmas;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Candle;
import org.bukkit.block.data.type.Leaves;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Adventskranz: Vier Kerzen, die an den vier Adventssonntagen nacheinander angehen. */
public final class WreathManager {
private record Wreath(String world, int x, int y, int z) {}
// Kerzen: 1., 2., 4. Advent violett, 3. Advent rosa
private static final int[][] CANDLE_OFFSETS = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
private static final Material[] CANDLES = {Material.PURPLE_CANDLE, Material.PURPLE_CANDLE,
Material.PINK_CANDLE, Material.PURPLE_CANDLE};
private final ChristmasManager mgr;
private final File file;
private final List<Wreath> wreaths = new ArrayList<>();
private final Set<String> blocks = new HashSet<>();
private String lastAnnounced = "";
private BukkitTask task;
WreathManager(ChristmasManager mgr) {
this.mgr = mgr;
this.file = new File(mgr.plugin().getDataFolder(), "wreaths.yml");
}
// ---------- Adventsberechnung ----------
/** Aktueller Advent 0-4 (0 = vor dem 1. Advent oder nach dem Brenn-Ende). */
public static int adventNumber(LocalDate today, ChristmasManager mgr) {
LocalDate fourth = LocalDate.of(today.getYear(), 12, 24);
while (fourth.getDayOfWeek() != DayOfWeek.SUNDAY) fourth = fourth.minusDays(1);
LocalDate burnUntil = LocalDate.of(today.getYear(), 12, 26);
String until = mgr.cfg().getString("wreath.burn-until", "12-26");
try {
String[] p = until.split("-");
burnUntil = LocalDate.of(today.getYear(), Integer.parseInt(p[0].trim()), Integer.parseInt(p[1].trim()));
} catch (Exception ignored) { }
if (today.isAfter(burnUntil)) return 0;
int n = 0;
for (int i = 0; i < 4; i++) {
if (!today.isBefore(fourth.minusWeeks(3 - i))) n = i + 1;
}
return n;
}
private int currentAdvent() {
return adventNumber(LocalDate.now(mgr.plugin().getZone()), mgr);
}
// ---------- Verwaltung ----------
private static String key(Block b) {
return b.getWorld().getName() + ";" + b.getX() + ";" + b.getY() + ";" + b.getZ();
}
public boolean isProtected(Block b) { return blocks.contains(key(b)); }
void load() {
wreaths.clear();
blocks.clear();
if (file.exists()) {
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
lastAnnounced = y.getString("announced", "");
ConfigurationSection s = y.getConfigurationSection("wreaths");
if (s != null) {
for (String k : s.getKeys(false)) {
ConfigurationSection w = s.getConfigurationSection(k);
if (w == null) continue;
Wreath wr = new Wreath(w.getString("world", "world"), w.getInt("x"), w.getInt("y"), w.getInt("z"));
wreaths.add(wr);
index(wr);
}
}
}
stop();
task = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::tick, 100L, 20L * 30);
}
void stop() {
if (task != null) task.cancel();
task = null;
}
private void save() {
YamlConfiguration y = new YamlConfiguration();
y.set("announced", lastAnnounced);
int i = 0;
for (Wreath w : wreaths) {
y.set("wreaths." + i + ".world", w.world());
y.set("wreaths." + i + ".x", w.x());
y.set("wreaths." + i + ".y", w.y());
y.set("wreaths." + i + ".z", w.z());
i++;
}
try {
y.save(file);
} catch (IOException e) {
mgr.plugin().getLogger().warning("wreaths.yml konnte nicht gespeichert werden: " + e.getMessage());
}
}
private void index(Wreath w) {
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
blocks.add(w.world() + ";" + (w.x() + dx) + ";" + w.y() + ";" + (w.z() + dz));
blocks.add(w.world() + ";" + (w.x() + dx) + ";" + (w.y() + 1) + ";" + (w.z() + dz));
}
}
}
/** Baut einen Kranz auf dem Block unter dem Ziel; Mitte des Kranzes = ground.getRelative(UP). */
public boolean create(Block ground) {
Block center = ground.getRelative(0, 1, 0);
World w = center.getWorld();
Leaves leaves = (Leaves) Material.SPRUCE_LEAVES.createBlockData();
leaves.setPersistent(true);
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (dx == 0 && dz == 0) continue;
Block b = w.getBlockAt(center.getX() + dx, center.getY(), center.getZ() + dz);
b.setBlockData(leaves, false);
}
}
// Mitte: Moosteppich-aehnlich als Dekoration
w.getBlockAt(center.getX(), center.getY(), center.getZ()).setType(Material.RED_CARPET, false);
Wreath wr = new Wreath(w.getName(), center.getX(), center.getY(), center.getZ());
wreaths.add(wr);
index(wr);
save();
applyCandles(wr, currentAdvent());
return true;
}
public boolean removeNear(Block b) {
for (Wreath w : new ArrayList<>(wreaths)) {
if (!w.world().equals(b.getWorld().getName())) continue;
if (Math.abs(w.x() - b.getX()) <= 2 && Math.abs(w.z() - b.getZ()) <= 2 && Math.abs(w.y() - b.getY()) <= 3) {
World world = b.getWorld();
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
for (int dy = 0; dy <= 1; dy++) {
blocks.remove(w.world() + ";" + (w.x() + dx) + ";" + (w.y() + dy) + ";" + (w.z() + dz));
world.getBlockAt(w.x() + dx, w.y() + dy, w.z() + dz).setType(Material.AIR, false);
}
}
}
wreaths.remove(w);
save();
return true;
}
}
return false;
}
// ---------- Kerzen ----------
private void applyCandles(Wreath w, int advent) {
World world = Bukkit.getWorld(w.world());
if (world == null || !world.isChunkLoaded(w.x() >> 4, w.z() >> 4)) return;
for (int i = 0; i < 4; i++) {
Block b = world.getBlockAt(w.x() + CANDLE_OFFSETS[i][0], w.y() + 1, w.z() + CANDLE_OFFSETS[i][1]);
Candle data = (Candle) CANDLES[i].createBlockData();
data.setLit(i < advent);
data.setCandles(1);
b.setBlockData(data, false);
}
}
private void tick() {
int advent = currentAdvent();
for (Wreath w : wreaths) applyCandles(w, advent);
if (advent > 0 && !wreaths.isEmpty() && mgr.cfg().getBoolean("wreath.announce", true)) {
String id = LocalDate.now(mgr.plugin().getZone()).getYear() + "-" + advent;
if (!id.equals(lastAnnounced)) {
lastAnnounced = id;
save();
Bukkit.broadcastMessage(mgr.msg("wreath-advent", "%n%", String.valueOf(advent)));
}
}
}
}
@@ -0,0 +1,50 @@
package de.mviper.adventskalender.christmas;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
/**
* PlaceholderAPI-Platzhalter mit dem Praefix xmas. Nur laden, wenn PlaceholderAPI installiert ist.
* <pre>
* %xmas_days_left% Tage bis Heiligabend
* %xmas_countdown% Text des Countdowns
* %xmas_advent% aktueller Advent (0-4)
* %xmas_trees% Magic Trees des Spielers
* %xmas_snowball_points% Punkte im Schneeballspiel
* %xmas_snowball_rank% Platz im Schneeballspiel
* %xmas_streak% aktuelle Login-Serie
* %xmas_streak_best% beste Login-Serie
* %xmas_wichteln% Name des Wichtel-Partners (falls gezogen)
* </pre>
*/
final class XmasExpansion extends PlaceholderExpansion {
private final ChristmasManager mgr;
XmasExpansion(ChristmasManager mgr) { this.mgr = mgr; }
@Override public String getIdentifier() { return "xmas"; }
@Override public String getAuthor() { return "M_Viper"; }
@Override public String getVersion() { return mgr.plugin().getDescription().getVersion(); }
@Override public boolean persist() { return true; }
@Override
public String onRequest(OfflinePlayer p, String params) {
switch (params.toLowerCase()) {
case "days_left": return String.valueOf(mgr.countdown().daysLeft());
case "countdown": return org.bukkit.ChatColor.stripColor(mgr.countdown().text());
case "advent": return String.valueOf(WreathManager.adventNumber(
java.time.LocalDate.now(mgr.plugin().getZone()), mgr));
default: break;
}
if (p == null) return "";
switch (params.toLowerCase()) {
case "trees": return String.valueOf(mgr.trees().countOf(p.getUniqueId()));
case "snowball_points": return String.valueOf(mgr.snowball().points(p.getUniqueId()));
case "snowball_rank": return String.valueOf(mgr.snowball().rank(p.getUniqueId()));
case "streak": return String.valueOf(mgr.streaks().streak(p.getUniqueId()));
case "streak_best": return String.valueOf(mgr.streaks().best(p.getUniqueId()));
case "wichteln": return mgr.wichteln().targetName(p.getUniqueId());
default: return null;
}
}
}
+419
View File
@@ -0,0 +1,419 @@
# =============================================================================
# Texte stehen in christmas_messages_de.yml / christmas_messages_en.yml
# (Sprache folgt general.language aus der config.yml).
#
# Weihnachts-Module (ersetzen Christmas+ und das X-Mas "Magic Tree" Plugin)
# Befehle: /christmas (alias /xmas) und /cracker
# Items: "MATERIAL:Anzahl", z.B. "DIAMOND:2"
# =============================================================================
crystal:
material: NETHER_STAR
name: "&bWeihnachts-Kristall"
lore:
- "&7Konzentrierter Weihnachtsgeist"
- "&7Rechtsklick auf einen Setzling,"
- "&7um ihn mit Magie zu füllen!"
# Befehl, mit dem Permissions als Belohnung vergeben werden (Kalender, Cracker).
# Beispiel LuckPerms: "lp user %player% permission set %permission% true"
permission-command: ""
# --- Schnee ---------------------------------------------------------------
snow:
enable: true
# Stärke 1-5
level: 3
# Ticks zwischen zwei Partikel-Wellen (höher = weniger Last)
display-task-interval: 2
default-on: true
disable-in-worlds: []
# Nur in diesen WorldGuard-Regionen (nur wenn WorldGuard installiert ist). Leer = überall.
enable-only-in-regions: false
regions: []
# --- Schneemänner --------------------------------------------------------
snowman:
# Deko-Schneemänner (/xmas snowman spawn [stand|walk] [Name]): stehend oder herumlaufend, unverwundbar
deco:
particles: true
# Kürbis-Kopf behalten
hat: true
# Standard beim Aufstellen ohne Angabe: true = laufend, false = stehend
# (/xmas snowman spawn stand|walk [Name] überschreibt das pro Schneemann)
walk: false
# Umkreis in Blöcken
walk-radius: 8
# Schnee-Spur der laufenden Schneemänner: fade = verschwindet wieder, none = keine Spur, keep = bleibt liegen
trail: fade
trail-fade-seconds: 8
# Normal gebaute Schneemänner (Schneeblöcke + Kürbis) geben ab und zu ein Geschenk
player:
enable: true
max-per-player: 3
name: "&f%player%s Schneemann"
# Sekunden zwischen zwei Geschenken
gift-interval: 300
gifts:
- "SNOWBALL:8"
- "COOKIE:3"
- "COAL:1"
- "GOLD_NUGGET:2"
# --- Santa-Event ----------------------------------------------------------
santa:
# Weihnachtsmelodie mit Glocken, wenn Santa kommt
melody:
enable: true
volume: 1.0
# Ticks pro Achtelnote (kleiner = schneller)
tempo: 3
# false = nur Spieler in Santas Welt, true = alle Spieler
everyone: false
duration: 300
drop-interval: 3
# Chat-Nachricht "Santa hat ein Geschenk fallen lassen" höchstens alle X Sekunden (0 = nie)
drop-message-interval: 60
# Chance (in %) pro Drop-Intervall, dass wirklich ein Geschenk fällt
drop-chance: 100
radius: 6
name: "&c&lSanta Claus"
# AUTO = Spielermodell (Mannequin, ab Minecraft 1.21.9) wenn möglich, sonst Rüstungsständer.
# ARMOR_STAND erzwingt den Rüstungsständer.
model: AUTO
# Skin: Textur-URL (textures.minecraft.net) ODER Spielername. Leer = Standard-Skin mit roter Lederrüstung.
skin-url: "https://textures.minecraft.net/texture/b2aad465030fedcd6e5a255ac228c357f731343bdf159ea78a61f4146b27289d"
skin-player: ""
# Santa läuft in diesem Radius herum (0 = steht still)
walk-radius: 8
# Lauftempo in Blöcken pro Tick (0.07 = gemütlicher Spaziergang)
walk-speed: 0.07
# Anflug mit Schlitten und zwei Rentieren, bevor Santa landet
sleigh:
enable: true
flight-seconds: 15
distance: 70
height: 30
reindeer-names: ["Rudolph", "Blitzen"]
snow-trail:
enabled: true
fade-seconds: 5
gifts:
- "DIAMOND:1"
- "EMERALD:2"
- "COOKIE:8"
- "CAKE:1"
- "GOLD_INGOT:3"
- "COAL:5"
# Cracker als Santa-Geschenk: "cracker:<id>:<Anzahl>" als Eintrag in gifts, z.B. "cracker:cracker:1"
locations: []
# - world: world
# x: 0.5
# y: 80
# z: 0.5
# --- Cracker --------------------------------------------------------------
crackers:
cracker:
material: FIREWORK_STAR
name: "&c&lWeihnachts-Cracker"
lore:
- "&7Rechtsklick zum Öffnen!"
rewards:
- { chance: 40, item: "COOKIE:8" }
- { chance: 30, item: "GOLD_INGOT:3" }
- { chance: 20, item: "DIAMOND:1" }
- { chance: 10, item: "EMERALD:5" }
# Beispiel Befehl: - { chance: 5, command: "give %player% diamond 3" }
# --- Magic Tree -----------------------------------------------------------
magic-tree:
enable: true
# Max. Bäume pro Spieler
tree-limit: 3
# Das Event endet automatisch an diesem Datum (Format dd-MM-yyyy HH-mm-ss)
holiday-ends:
enabled: true
date: "10-01-2027 00-00-00"
# Beim Abbau nach Eventende alle ausgegebenen Ressourcen zurückgeben
resource-back: true
# Chance, dass ein Geschenk Inhalt hat
luck:
enabled: false
chance: 75
# Weihnachts-Kristall in der Werkbank herstellbar:
# Diamanten oben/links/rechts/unten, Smaragd in der Mitte
crystal-recipe: true
# Partikel-Show am Baum (Spirale, Lichter, Stern, Glitzer über Geschenken)
particles:
enable: true
# 0.2 (dezent) bis 5.0 (sehr viel). Standard 2.0
intensity: 2.0
max-presents-per-tree: 5
# Geschenk-Köpfe (Texturen-URLs von textures.minecraft.net)
presents:
- "http://textures.minecraft.net/texture/21bc9d42b0041e8f95cb9b26628fdaf50cd0e36f7bb9d6b3a4d2af3949da97d6"
- "http://textures.minecraft.net/texture/2b1ec7dc753061ca174424ea45cf9490b39cd5dcca477d138a603e6be755ec72"
# Weitere Geschenke per /xmas addhand (gespeichert in gifts.yml)
gifts:
- DIAMOND
- EMERALD
- IRON_SWORD
- IRON_HELMET
- IRON_CHESTPLATE
- IRON_BOOTS
- CAKE
- COOKIE
- BLAZE_ROD
- DIAMOND_SWORD
- IRON_INGOT
- GOLD_INGOT
- SPONGE
- DIAMOND_HOE
- NAME_TAG
# gift-cooldown in Sekunden (-1 = keine Geschenke), lvlup = Items, die zum nächsten Level nötig sind
levels:
sapling:
gift-cooldown: -1
lvlup:
DIAMOND: 1
REDSTONE: 10
ENDER_PEARL: 1
small_tree:
gift-cooldown: 300
lvlup:
DIAMOND: 3
GOLD_INGOT: 5
BLAZE_POWDER: 10
SNOWBALL: 30
tree:
gift-cooldown: 180
lvlup:
DIAMOND: 5
EMERALD: 3
GOLD_NUGGET: 8
GLOWSTONE_DUST: 16
magic_tree:
gift-cooldown: 120
lvlup: {}
# --- Kalender-Zusatz (Christmas+ Funktionen) -------------------------------
# Wirken auf den Adventskalender (config.yml -> rewards.<Tag>)
calendar-extras:
# Beim Abholen eines Tages allen Spielern eine Meldung zeigen
broadcast-claim: false
# true: Nur der heutige Tag ist öffnbar; vergangene Tage nur mit Recht christmasplus.adventcalendar.claimpast
restrict-past-days: false
# --- Countdown bis Heiligabend ------------------------------------------------
countdown:
enable: true
# Zieldatum Monat-Tag
target: "12-24"
# Ab wie vielen Tagen vorher wird der Countdown angezeigt
show-days-before: 60
bossbar: true
join-message: true
# --- Plätzchen und Lebkuchen --------------------------------------------------
# Verteilen mit /xmas cookie give <Spieler> <id> oder als Item "cookie:<id>:<Anzahl>"
# in Belohnungen, Crackern, Santa-Geschenken usw.
# effects: "EFFEKT:Sekunden:Stufe" (Stufe 0 = I)
cookies:
zimtstern:
material: COOKIE
name: "&6Zimtstern"
lore:
- "&7Knusprig und zimtig."
- "&7Gibt kurz Tempo."
effects: ["speed:30:1"]
lebkuchen:
material: BREAD
name: "&cLebkuchen"
lore:
- "&7Frisch aus dem Ofen."
- "&7Regeneriert Leben."
effects: ["regeneration:10:1"]
bratapfel:
material: APPLE
name: "&eBratapfel"
lore:
- "&7Warm und süß."
- "&7Gibt Absorption."
effects: ["absorption:60:1"]
# --- Wichteln ----------------------------------------------------------------
wichteln:
# Beim Verteilen den Schenkenden verraten?
reveal-giver: false
# --- Adventskranz ------------------------------------------------------------
wreath:
# Meldung an alle am Adventssonntag
announce: true
# Bis zu diesem Tag (Monat-Tag) brennen die Kerzen
burn-until: "12-26"
# --- Schneeball-Minispiel ----------------------------------------------------
snowball:
duration: 180
points-per-hit: 1
# So viele Schneebälle haben Spieler in der Arena immer mindestens
refill-amount: 16
# Belohnungen für Platz 1 bis 3 (item / command / permission, %player% wird ersetzt)
rewards:
1:
- { item: "DIAMOND:3" }
2:
- { item: "EMERALD:5" }
3:
- { item: "GOLD_INGOT:5" }
# --- Login-Serie -------------------------------------------------------------
login-streak:
enable: true
# Zeitraum (Monat-Tag), in dem die Serie zählt
from: "12-01"
until: "12-31"
# Nach so vielen Tagen beginnen die Belohnungen von vorn
cycle: 7
rewards:
1:
- { item: "COOKIE:5" }
2:
- { item: "cookie:zimtstern:2" }
3:
- { item: "GOLD_INGOT:3" }
4:
- { item: "cookie:lebkuchen:2" }
5:
- { item: "EMERALD:2" }
6:
- { item: "cracker:cracker:1" }
7:
- { item: "DIAMOND:2" }
# --- Weihnachts- und Neujahrsgruß ---------------------------------------------
# Einmal pro Tag Chat-Gruß an alle (und beim Join), Schlüssel verweisen auf die Sprachdatei.
greetings:
enable: true
join-fireworks: true
fireworks-on-broadcast: true
days:
"12-24": greeting-christmas-eve
"12-25": plugin-enabled
"12-31": greeting-new-years-eve
"01-01": final-wish
# --- Kamin-Geschenke (Socke aufhängen) -----------------------------------------
stocking:
enable: true
# Socke aufhängen ab / Geschenk ab / Abholen möglich bis (Monat-Tag)
hang-from: "12-01"
open-on: "12-25"
until: "12-31"
# Wie viele Socken ein Spieler per /xmas stocking place aufstellen darf
max-placed-per-player: 3
rewards:
- { item: "COOKIE:6" }
- { item: "cookie:zimtstern:2" }
- { item: "GOLD_INGOT:2" }
- { item: "present:1" }
# --- Santas Socken-Flug ---------------------------------------------------------
# Am Weihnachtstag (und dem Tag danach) fliegt Santa von aufgestellter Socke zu aufgestellter
# Socke (/xmas stocking place) und legt unter jeder ein Geschenk ab.
stocking-flight:
enable: true
# Zeitraum (Monat-Tag, einschließlich): Weihnachtstag und ein Tag danach
from: "12-24"
until: "12-25"
# Mindestabstand zwischen zwei Flügen in Minuten
interval-minutes: 20
# Wie viele Socken pro Flug beliefert werden
max-stockings-per-flight: 12
# Flugtempo in Blöcken pro Tick
speed: 0.7
# Nur der Besitzer der Socke darf das Geschenk öffnen
owner-only: true
# Inhalt des Geschenks (leer = die Belohnungen von stocking.rewards)
rewards: []
# --- Nikolausmütze ------------------------------------------------------------
santa-hat:
enable: true
from: "12-01"
until: "12-31"
# Beim Join anklickbares Angebot im Chat
offer-on-join: true
# Statt Angebot direkt aufsetzen
auto-equip: false
# --- Weihnachtsquiz ------------------------------------------------------------
quiz:
answer-seconds: 30
# Automatisch alle X Minuten eine Frage (nur wenn genug Spieler online sind)
auto:
enable: false
interval-minutes: 20
min-players: 2
default-reward:
- { item: "EMERALD:2" }
# a = erlaubte Antworten (Groß-/Kleinschreibung und Sonderzeichen egal), reward optional
questions:
- { q: "Wie viele Kerzen hat ein Adventskranz?", a: ["4", "vier"] }
- { q: "Welches Tier zieht den Schlitten des Weihnachtsmanns?", a: ["Rentier", "Rentiere"] }
- { q: "Wie heißt das Rentier mit der roten Nase?", a: ["Rudolph", "Rudolf"] }
- { q: "Aus wie vielen Türchen besteht ein klassischer Adventskalender?", a: ["24", "vierundzwanzig"] }
- { q: "In welchem Monat ist Weihnachten?", a: ["Dezember"] }
- { q: "Welcher Baum wird traditionell zu Weihnachten geschmückt?", a: ["Tannenbaum", "Tanne", "Weihnachtsbaum", "Fichte"] }
- { q: "Wie heißt der Weihnachtsmann auf Englisch?", a: ["Santa Claus", "Santa"] }
- { q: "Wie viele Adventssonntage gibt es?", a: ["4", "vier"] }
- { q: "Wie viele Schneeblöcke braucht man für einen Schneegolem?", a: ["2", "zwei"] }
- { q: "Welcher Mob wirft Schneebälle auf Monster?", a: ["Schneegolem", "Schneemann", "Snow Golem"] }
# --- Wunschzettel --------------------------------------------------------------
wishes:
max-open-per-player: 3
max-length: 120
# --- Weihnachtsmarkt-Stände ---------------------------------------------------
# Aufstellen mit /xmas market spawn <id>. give: wie bei allen Items, auch cookie:/cracker:/crystal:/present:.
# price: bis zu 2 Items. Nach /xmas reload werden die Angebote aktualisiert.
market:
stalls:
plaetzchen:
name: "&6Plätzchen-Stand"
profession: BUTCHER
trades:
- { give: "cookie:zimtstern:2", price: ["EMERALD:1"] }
- { give: "cookie:lebkuchen:2", price: ["EMERALD:1"] }
- { give: "cookie:bratapfel:1", price: ["GOLD_INGOT:2"] }
- { give: "COOKIE:8", price: ["WHEAT:8"] }
deko:
name: "&2Deko-Stand"
profession: LIBRARIAN
trades:
- { give: "present:1", price: ["EMERALD:3"] }
- { give: "SPRUCE_SAPLING:4", price: ["GOLD_INGOT:1"] }
- { give: "RED_CANDLE:4", price: ["IRON_INGOT:2"] }
- { give: "LANTERN:2", price: ["IRON_INGOT:3"] }
- { give: "SNOWBALL:16", price: ["COAL:1"] }
- { give: "crystal:1", price: ["DIAMOND:5", "EMERALD:5"] }
# --- Portale zum Weihnachtsmarkt -----------------------------------------------
# Portale funktionieren NUR, wenn ein Admin/OP den Marktplatz mit /xmas market set festgelegt hat
# (aufheben mit /xmas market unset). Spieler bauen sich mit /xmas portal create (auf den Boden
# schauen) ein Portal aus Schneebloecken, das dorthin fuehrt.
portal:
enable: true
# Kosten fuer ein Portal: Das Portal besteht aus 10 Schneebloecken (Admins bauen kostenlos)
cost: ["SNOW_BLOCK:10"]
# Beim Entfernen durch den Besitzer die Kosten zurueckgeben
refund: true
limit-per-player: 1
# Wie lange man im Portal stehen muss
teleport-delay-seconds: 3
cooldown-seconds: 10
# Leer = ueberall erlaubt, sonst nur in diesen Welten
allowed-worlds: []
@@ -0,0 +1,275 @@
messages:
prefix: "&c[Christmas] &f"
no-permission: "&cDazu hast du keine Berechtigung."
player-only: "&cDieser Befehl geht nur im Spiel."
player-offline: "&cSpieler nicht gefunden."
usage: "&cNutzung: /christmas <help|snow|santa|give|gifts|addhand|snowman|countdown|cookie|wichteln|wreath|snowball|deco|stocking|hat|quiz|wish|wishes|market|portal|end|reload>"
help:
- "&2/xmas &7- Version und Status"
- "&2/xmas snow &7- Schnee für dich an/aus"
- "&2/xmas countdown &7- Countdown-Leiste an/aus"
- "&2/xmas santa start [Zeit] [Welt x y z | random] | stop | teleport"
- "&2/xmas give <Name> [Anzahl] &7- Weihnachts-Kristall geben"
- "&2/xmas gifts &7- Geschenke unter alle Bäume"
- "&2/xmas addhand &7- Item in der Hand als Geschenk hinzufügen"
- "&2/xmas snowman spawn [stand|walk] [Name] | remove &7- Deko-Schneemann (stehend/laufend)"
- "&2/xmas cookie list | give <Spieler> <id> [Anzahl]"
- "&2/xmas wichteln join|leave|who|gift|status &7- Wichteln"
- "&2/xmas wichteln open|draw|deliver|reset &7- (Admin)"
- "&2/xmas wreath create|remove &7- Adventskranz"
- "&2/xmas snowball arena [Radius] | start [Zeit] | stop | top"
- "&2/xmas deco [undo] &7- Deko-Menü"
- "&2/xmas stocking [place | remove | flight] &7- Socke aufhängen / abholen / an die Wand hängen"
- "&2/xmas hat &7- Nikolausmütze"
- "&2/xmas wish <Text> | list | clear &7- Wunschzettel"
- "&2/xmas wishes &7- Wunschzettel ansehen (Admin)"
- "&2/xmas quiz start [Nr] | skip &7- Weihnachtsquiz (Admin)"
- "&2/xmas market spawn <id> | remove | list &7- Marktstände (Admin)"
- "&2/xmas portal create|remove|info &7- Portal zum Weihnachtsmarkt"
- "&2/xmas market set|unset|tp|back &7- Marktpunkt (Admin) / Rückweg"
- "&2/xmas end &7- Event beenden"
- "&2/xmas reload &7- Konfiguration neu laden"
- "&2/cracker list | get [id] [Anzahl] | give <Spieler> [id] [Anzahl]"
info: "&aX-Mas Modul aktiv. Status: &e%status% &7- Bäume: &e%trees%"
status-active: "AKTIV"
status-ended: "BEENDET"
reloaded: "&aChristmas-Konfiguration neu geladen."
calendar-test-on: "&aKalender-Testmodus &leingeschaltet&a: Du kannst jeden Tag öffnen."
calendar-test-off: "&7Kalender-Testmodus ausgeschaltet."
past-day-restricted: "&cVergangene Tage kannst du nicht mehr öffnen."
plugin-enabled: "&aFrohe Weihnachten und ein gutes neues Jahr!"
timeout: "&cDie Feiertage sind vorbei! Bis nächstes Jahr!"
final-wish: "&6Alles Gute zum neuen Jahr, meine Freunde!"
end-set: "&7Event beendet. Spieler können ihre Bäume abbauen und bekommen Ressourcen zurück."
gifts-spawned: "&cHo! &2Ho! &cHo! &3Zeit, die Geschenke zu prüfen!"
addhand-ok: "&aItem als Geschenk hinzugefügt."
addhand-empty: "&cDu hältst kein Item in der Hand."
crystal-given: "&aDu hast &e%amount%x Weihnachts-Kristall &aerhalten."
crystal-gave: "&a%player% hat %amount% Kristall(e) bekommen."
# Schnee
snow-on: "&aSchnee ist für dich &leingeschaltet&a."
snow-off: "&7Schnee ist für dich &causgeschaltet&7."
snow-disabled-global: "&cSchnee ist auf dem Server deaktiviert."
# Schneemänner
snowman-usage: "&cNutzung: /christmas snowman spawn [stand|walk] [Name] | remove"
snowman-spawned: "&aDeko-Schneemann aufgestellt."
snowman-removed: "&7Deko-Schneemann entfernt."
snowman-none: "&cKein Deko-Schneemann in 6 Blöcken Umkreis."
snowman-built: "&aDein Schneemann gibt dir ab und zu ein Geschenk! (maximal %max% Schneemänner)"
snowman-limit: "&cDu hast schon %max% Schneemänner, dieser gibt keine Geschenke."
# Santa
santa-started: "&aDas Santa-Claus-Event hat begonnen! Santa ist in &e%world% &abei &ex=%x%, y=%y%, z=%z%&a und wartet &e%duration%&a. (/christmas santa teleport)"
santa-started-sender: "&aDu hast das Santa-Claus-Event für &e%duration% &agestartet."
santa-dropped: "&aSanta hat gerade ein Geschenk fallen lassen!"
calendar-claimed-broadcast: "&e%player% &ahat das Geschenk von Kalendertag &e%day% &aabgeholt."
calendar-claimed-self: "&aDu hast das Geschenk von Tag &e%day% &aabgeholt."
santa-stopped: "&7Der Weihnachtsmann ist wieder weitergezogen."
santa-already: "&cDer Weihnachtsmann ist bereits aktiv."
santa-not-active: "&cDer Weihnachtsmann ist gerade nicht aktiv."
santa-no-locations: "&cEs sind keine Santa-Orte in der christmas.yml hinterlegt."
santa-usage: "&cNutzung: /christmas santa start [Sekunden] [Welt x y z | random] | stop | teleport"
santa-bossbar: "&cSanta Claus &f- noch &e%time%s"
santa-teleported: "&aDu wurdest zu Santa teleportiert."
# Cracker
cracker-given: "&aDu hast &e%amount%x %id% &aerhalten."
cracker-gave: "&a%player% hat %amount%x %id% bekommen."
cracker-unknown: "&cUnbekannter Cracker: %id%"
cracker-opened: "&6Knall! &fDu hast einen Cracker geöffnet."
cracker-usage: "&cNutzung: /cracker list | get [id] [Anzahl] | give <Spieler> [id] [Anzahl]"
# Magic Tree
tree:
grow-lvl-ready: "&aWusch! Rechtsklick auf den Baum, damit er wächst!"
grow-req-list-title: "&eBenötigt (Items neben den Baum werfen):"
grow-not-enough-place: "&cHier ist nicht genug Platz zum Wachsen."
grow-lvl-max: "&6Dieser Baum hat sein maximales Level erreicht!"
grow-fed: "&7Der Baum hat &e%amount%x %item% &7aufgenommen."
tree-limit: "&cSei nicht gierig! (Baum-Limit erreicht)"
tree-planted: "&aDein Magic Tree wurde gepflanzt! Wirf die benötigten Items neben den Baum."
tree-disabled: "&cMagic Trees sind beendet/deaktiviert."
destroy-sapling: "&cDieses Wunder kommt nicht wieder. Wirklich abbauen? Nochmal abbauen zum Bestätigen."
destroy-leaves-santa: "&cDer Weihnachtsmann sieht alles."
destroy-leaves-tut: "&7Um den Baum zu zerstören, fälle den Stamm."
destroy-warning: "&cBist du sicher? Dein Fortschritt geht verloren, keine Ressourcen zurück. Nochmal abbauen zum Bestätigen."
destroy-fail-owner: "&cDas ist nicht dein Baum!"
destroy-complete: "&4Du bist ein MONSTER!"
destroy-refund: "&aDer Baum wurde abgebaut, deine Ressourcen kommen zurück."
not-owner: "&cDas ist nicht dein Baum."
info: "&aMagic Tree von &e%owner%&a, Level: &e%level%"
gift:
unluck-message: "&7Vielleicht hat der Weihnachtsmann dich auf die schwarze Liste gesetzt. Nächstes Mal klappt es bestimmt!"
opened: "&aDu hast ein Geschenk geöffnet!"
# Countdown
countdown-bossbar: "&cNoch &e%days% &cTage bis Heiligabend"
countdown-bossbar-one: "&cNoch &e1 &cTag bis Heiligabend"
countdown-not-yet: "&7Noch &e%days% &7Tage bis Heiligabend. Die Anzeige startet in &e%in% &7Tag(en)."
countdown-today: "&aHeute ist Heiligabend! Frohe Weihnachten!"
countdown-on: "&aCountdown-Leiste eingeschaltet."
countdown-off: "&7Countdown-Leiste ausgeschaltet."
# Plätzchen
cookie-usage: "&cNutzung: /xmas cookie list | give <Spieler> <id> [Anzahl]"
cookie-unknown: "&cUnbekanntes Plätzchen: %id%"
cookie-given: "&aDu hast &e%amount%x %id% &aerhalten."
# Wichteln
wi-open: "&aDas Wichteln ist eröffnet! Mit &e/xmas wichteln join &amachst du mit."
wi-joined: "&aDu bist beim Wichteln dabei!"
wi-already: "&7Du bist schon angemeldet."
wi-left: "&7Du bist aus dem Wichteln ausgetreten."
wi-not-joined: "&cDu nimmst nicht am Wichteln teil."
wi-wrong-state: "&cDas geht gerade nicht (falsche Phase)."
wi-too-few: "&cEs braucht mindestens 3 Teilnehmer."
wi-drawn: "&aGezogen! Jeder hat einen Wichtel-Partner. Schau mit &e/xmas wichteln who &anach, wen du beschenkst."
wi-who: "&aDu beschenkst: &e%name%"
wi-gift-empty: "&cNimm das Geschenk in die Hand."
wi-gift-saved: "&aDein Geschenk für &e%name% &aist gespeichert. (Mit einem anderen Item in der Hand kannst du es ersetzen.)"
wi-delivered: "&6Die Wichtel-Geschenke wurden verteilt! (&e%count% &6Geschenke)"
wi-received: "&aDu hast ein Wichtel-Geschenk bekommen!"
wi-received-from: "&aDu hast ein Wichtel-Geschenk von &e%from% &abekommen!"
wi-reset: "&7Wichteln zurückgesetzt."
wi-status: "&aWichteln: &e%state% &7- Teilnehmer: &e%count% &7- Geschenke: &e%gifts%"
# Adventskranz
wreath-created: "&aAdventskranz gebaut."
wreath-removed: "&7Adventskranz entfernt."
wreath-none: "&cKein Adventskranz in der Nähe."
wreath-usage: "&cNutzung: /xmas wreath create | remove (schau auf den Boden)"
wreath-advent: "&6Heute ist der &e%n%. Advent&6! Am Adventskranz brennen jetzt &e%n% &6Kerzen."
# Schneeball-Spiel
sb-usage: "&cNutzung: /xmas snowball arena [Radius] | start [Zeit] | stop | top"
sb-arena-set: "&aSchneeball-Arena gesetzt (Radius &e%radius%&a)."
sb-no-arena: "&cEs ist noch keine Arena gesetzt (/xmas snowball arena)."
sb-already: "&cDas Schneeball-Spiel läuft schon."
sb-not-active: "&cDas Schneeball-Spiel läuft nicht."
sb-started: "&b&lSchneeballschlacht! &7Für &e%time% &7in der Arena (&e%world% %x% %z%&7). Wirf Schneebälle auf andere Spieler!"
sb-ended: "&b&lSchneeballschlacht beendet! &7Die Besten:"
sb-top-line: "&e%rank%. &f%player% &7- &a%points% Punkte"
sb-no-scores: "&7Noch keine Punkte."
sb-actionbar: "&fPunkte: &a%points% &7| Platz &e%rank% &7| noch &c%time%s"
sb-reward: "&aGlückwunsch zu Platz %rank%! Deine Belohnung wurde ausgegeben."
# Deko
deco-menu-title: "Weihnachts-Deko"
deco-click-hint: "&7Klicken, dann wird sie dort aufgestellt, wo du hinschaust."
deco-present: "&aGeschenk"
deco-candy_cane: "&cZuckerstange"
deco-lantern_post: "&eLaternenpfahl"
deco-tree: "&2Weihnachtsbaum"
deco-wreath: "&4Kranz"
deco-snowman: "&fSchneemann"
deco-lights: "&6Lichterkette"
deco-snow_pile: "&bSchneehaufen"
deco-placed: "&aDeko aufgestellt. &7/xmas deco undo macht es rückgängig."
deco-undone: "&7Letzte Deko entfernt."
deco-nothing-undo: "&cNichts zum Rückgängigmachen."
deco-look: "&cSchau auf einen Block."
# Login-Serie
streak-reward: "&6Login-Serie: Tag &e%day% &6in Folge! Belohnung (Stufe &e%cycle%&6) erhalten."
# Grüße
greeting-christmas-eve: "&6Heute ist Heiligabend! &fWir wünschen allen einen schönen Abend."
greeting-new-years-eve: "&6Heute ist Silvester! &fRutscht gut ins neue Jahr."
present-name: "&aGeschenk"
# Socken
stocking-hung: "&aDeine Socke hängt am Kamin! Am Morgen des &e%date% &aliegt etwas darin."
stocking-already: "&7Deine Socke hängt schon. Am &e%date% &7liegt etwas darin."
stocking-too-early: "&cDu kannst deine Socke erst ab dem &e%from% &caufhängen."
stocking-nothing: "&cDu hast dieses Jahr keine Socke aufgehängt."
stocking-claimed: "&7Du hast deine Socke schon geleert."
stocking-received: "&aIn deiner Socke lag etwas für dich! Frohe Weihnachten!"
stocking-placed: "&aDeine Socke hängt jetzt an der Wand! Am Weihnachtstag fliegt Santa vorbei und legt ein Geschenk darunter."
stocking-place-usage: "&cSchau auf eine freie Wand (Seitenfläche eines Blocks), um die Socke aufzuhängen."
stocking-place-limit: "&cDu kannst höchstens &e%max% &cSocken aufstellen."
stocking-removed: "&7Socke entfernt."
stocking-none-near: "&cIn der Nähe hängt keine Socke von dir."
stocking-present-placed: "&aSanta hat ein Geschenk unter deine Socke gelegt! (x=%x%, y=%y%, z=%z%)"
stocking-present-notyours: "&cDieses Geschenk gehört jemand anderem."
stocking-present-opened: "&aDu hast dein Socken-Geschenk geöffnet. Frohe Weihnachten!"
stocking-flight-started: "&aHo ho ho! Santa fliegt von Socke zu Socke und bringt Geschenke!"
stocking-flight-ok: "&aSantas Socken-Flug wurde gestartet."
stocking-flight-none: "&cKein Flug möglich: Keine aufgestellten Socken in geladenen Chunks (oder ein Flug läuft schon)."
# Nikolausmütze
hat-name: "&cNikolausmütze"
hat-lore:
- "&7Ho ho ho!"
hat-offer: "&aMöchtest du eine Nikolausmütze?"
hat-offer-click: "&e[Hier klicken]"
hat-offer-hover: "&7Gibt dir eine Nikolausmütze"
hat-given: "&aHo ho ho! Du hast eine Nikolausmütze bekommen."
hat-have: "&7Du hast schon eine Nikolausmütze."
hat-season: "&cDie Nikolausmütze gibt es nur in der Weihnachtszeit."
# Quiz
quiz-question: "&6&lWeihnachtsquiz! &f%question% &7(%time%s)"
quiz-win: "&a%player% &fhat es zuerst gewusst: &e%answer%"
quiz-timeout: "&7Niemand wusste es. Die Antwort war: &e%answer%"
quiz-reward: "&aDeine Belohnung wurde ausgegeben."
quiz-usage: "&cNutzung: /xmas quiz start [Nr] | skip | count"
quiz-already: "&cEs läuft schon eine Frage."
quiz-none: "&cEs sind keine Quizfragen in der christmas.yml."
quiz-stopped: "&7Frage beendet."
quiz-not-active: "&cGerade läuft keine Frage."
# Wunschzettel
wish-usage: "&cNutzung: /xmas wish <Text> | list | clear"
wish-saved: "&aDein Wunsch steht auf dem Wunschzettel! Vielleicht erfüllt ihn der Weihnachtsmann."
wish-limit: "&cDu hast schon genug offene Wünsche. (/xmas wish list, /xmas wish clear)"
wish-too-long: "&cDer Wunsch ist zu lang (maximal %max% Zeichen)."
wish-empty: "&7Du hast keine offenen Wünsche."
wish-list-header: "&aDeine offenen Wünsche:"
wish-list-line: "&7- &f%text%"
wish-cleared: "&7%count% Wunsch/Wünsche gelöscht."
wish-fulfilled: "&aDein Wunsch wurde erfüllt: &e%wish%"
wish-fulfilled-gift: "&aDein Wunsch wurde erfüllt, ein Geschenk liegt im Inventar: &e%wish%"
wish-admin-new: "&6Neuer Wunsch von &e%player%&6. Ansehen mit /xmas wishes"
wish-admin-done: "&aWunsch von %player% erfüllt (ohne Geschenk)."
wish-admin-done-gift: "&aWunsch von %player% erfüllt. Geschenk: &e%item%"
wish-admin-done-offline: "&7%player% ist offline. Der Wunsch ist erfüllt, das Geschenk wird beim nächsten Join übergeben."
wish-admin-hold: "&cHalte das Geschenk in der Hand und klicke dann links. (Shift+Linksklick erfüllt den Wunsch ohne Geschenk.)"
wish-admin-deleted: "&7Wunsch von %player% gelöscht."
wish-menu-title: "Wunschzettel"
wish-menu-item-name: "&e%player%"
wish-menu-hint-left: "&aLinksklick: erfüllen &7(Item in der Hand wird verschenkt)"
wish-menu-hint-shift: "&eShift+Linksklick: ohne Geschenk erfüllen"
wish-menu-hint-right: "&cRechtsklick: löschen"
wish-page-prev: "&7Vorherige Seite"
wish-page-next: "&7Nächste Seite"
wish-menu-empty: "&7Keine offenen Wünsche"
# Marktstände
market-usage: "&cNutzung: /xmas market spawn <id> | remove | list"
market-unknown: "&cUnbekannter Stand: %id%"
market-spawned: "&aMarktstand aufgestellt."
market-removed: "&7Marktstand entfernt."
market-none: "&cKein Marktstand in 6 Blöcken Umkreis."
# Portale und Weihnachtsmarkt
market-unset: "&7Marktpunkt aufgehoben. Portale funktionieren erst wieder, wenn ein Admin ihn neu setzt."
market-set: "&aWeihnachtsmarkt-Punkt gesetzt. Portale führen jetzt hierher."
portal-usage: "&cNutzung: /xmas portal create | remove | info (schau auf den Boden bzw. aufs Portal)"
portal-created: "&aDein Portal zum Weihnachtsmarkt steht! Geh hinein und warte kurz."
portal-removed: "&7Portal entfernt."
portal-none: "&cDu schaust kein Portal an."
portal-not-owner: "&cDas ist nicht dein Portal."
portal-limit: "&cDu hast schon %limit% Portal(e)."
portal-need: "&cFür das Portal brauchst du Schneeblöcke: &f%cost%"
portal-no-space: "&cHier ist nicht genug Platz (5 Blöcke breit, 5 hoch, fester Boden)."
portal-no-market: "&cDer Weihnachtsmarkt wurde noch nicht von einem Admin festgelegt. Portale funktionieren erst danach."
portal-world: "&cIn dieser Welt kannst du kein Portal bauen."
portal-disabled: "&cPortale sind deaktiviert."
portal-countdown: "&bPortal: &f%s% s"
portal-teleported: "&aWillkommen auf dem Weihnachtsmarkt!"
portal-back-click: "&e[Zurück zum Portal]"
portal-back-ok: "&7Du bist zurück am Portal."
portal-back-none: "&cEs ist kein Rückweg gespeichert."
portal-info: "&aPortal-Kosten: &f%cost% &7- Deine Portale: &e%count%/%limit%"
@@ -0,0 +1,276 @@
messages:
prefix: "&c[Christmas] &f"
no-permission: "&cYou don't have permission."
player-only: "&cThis command is for players only."
player-offline: "&cPlayer not found."
player-not-found: "&cPlayer not found."
usage: "&cUsage: /christmas <help|snow|santa|give|gifts|addhand|snowman|countdown|cookie|wichteln|wreath|snowball|deco|stocking|hat|quiz|wish|wishes|market|portal|end|reload>"
help:
- "&2/xmas &7- version and status"
- "&2/xmas snow &7- toggle snow for yourself"
- "&2/xmas countdown &7- toggle the countdown bar"
- "&2/xmas santa start [time] [world x y z | random] | stop | teleport"
- "&2/xmas give <name> [amount] &7- give a Christmas crystal"
- "&2/xmas gifts &7- spawn presents under every tree"
- "&2/xmas addhand &7- add the held item as a gift"
- "&2/xmas snowman spawn [stand|walk] [name] | remove &7- decorative snowman (standing/walking)"
- "&2/xmas cookie list | give <player> <id> [amount]"
- "&2/xmas wichteln join|leave|who|gift|status &7- secret santa"
- "&2/xmas wichteln open|draw|deliver|reset &7- (admin)"
- "&2/xmas wreath create|remove &7- advent wreath"
- "&2/xmas snowball arena [radius] | start [time] | stop | top"
- "&2/xmas deco [undo] &7- decoration menu"
- "&2/xmas stocking [place | remove | flight] &7- hang up / open your stocking / place it on a wall"
- "&2/xmas hat &7- Santa hat"
- "&2/xmas wish <text> | list | clear &7- wish list"
- "&2/xmas wishes &7- view wishes (admin)"
- "&2/xmas quiz start [no.] | skip &7- Christmas quiz (admin)"
- "&2/xmas market spawn <id> | remove | list &7- market stalls (admin)"
- "&2/xmas portal create|remove|info &7- portal to the Christmas market"
- "&2/xmas market set|unset|tp|back &7- market point (admin) / way back"
- "&2/xmas end &7- end the event"
- "&2/xmas reload &7- reload configuration"
- "&2/cracker list | get [id] [amount] | give <player> [id] [amount]"
info: "&aX-Mas module active. Status: &e%status% &7- Trees: &e%trees%"
status-active: "ACTIVE"
status-ended: "ENDED"
reloaded: "&aChristmas configuration reloaded."
past-day-restricted: "&cYou can't open past days any more."
calendar-test-on: "&aCalendar test mode &eenabled&a: you can open any day."
calendar-test-off: "&7Calendar test mode disabled."
calendar-claimed-broadcast: "&e%player% &aclaimed the gift of calendar day &e%day%&a."
calendar-claimed-self: "&aYou claimed the gift of day &e%day%&a."
plugin-enabled: "&aMerry Christmas and a happy new year!"
timeout: "&cThe holidays are over! See you next year!"
final-wish: "&6Happy New Year, my friends!"
end-set: "&7Event ended. Players can take down their trees and get their resources back."
gifts-spawned: "&cHo! &2Ho! &cHo! &3Time to check the presents!"
addhand-ok: "&aItem added as a gift."
addhand-empty: "&cYou aren't holding an item."
crystal-given: "&aYou received &e%amount%x Christmas crystal&a."
crystal-gave: "&a%player% received %amount% crystal(s)."
# Snow
snow-on: "&aSnow is now &eenabled &afor you."
snow-off: "&7Snow is now &cdisabled &7for you."
snow-disabled-global: "&cSnow is disabled on this server."
# Snowmen
snowman-usage: "&cUsage: /christmas snowman spawn [stand|walk] [name] | remove"
snowman-spawned: "&aDecorative snowman placed."
snowman-removed: "&7Decorative snowman removed."
snowman-none: "&cNo decorative snowman within 6 blocks."
snowman-built: "&aYour snowman will give you a gift now and then! (max. %max% snowmen)"
snowman-limit: "&cYou already have %max% snowmen, this one won't give gifts."
# Santa
santa-started: "&aThe Santa Claus event has started! Santa is in &e%world% &aat &ex=%x%, y=%y%, z=%z% &aand waits &e%duration%&a. (/christmas santa teleport)"
santa-started-sender: "&aYou started the Santa Claus event for &e%duration%&a."
santa-dropped: "&aSanta has just dropped a gift!"
santa-stopped: "&7Santa has moved on."
santa-already: "&cSanta is already active."
santa-not-active: "&cSanta is not active right now."
santa-no-locations: "&cNo Santa locations are set in christmas.yml."
santa-usage: "&cUsage: /christmas santa start [time] [world x y z | random] | stop | teleport"
santa-bossbar: "&cSanta Claus &f- &e%time%s left"
santa-teleported: "&aYou were teleported to Santa."
# Crackers
cracker-given: "&aYou received &e%amount%x %id%&a."
cracker-gave: "&a%player% received %amount%x %id%."
cracker-unknown: "&cUnknown cracker: %id%"
cracker-opened: "&6Bang! &fYou opened a cracker."
cracker-usage: "&cUsage: /cracker list | get [id] [amount] | give <player> [id] [amount]"
# Magic tree
tree:
grow-lvl-ready: "&aWoosh! Right-click the tree to let it grow!"
grow-req-list-title: "&eRequired (throw the items next to the tree):"
grow-not-enough-place: "&cNot enough space to grow here."
grow-lvl-max: "&6This tree has reached its maximum level!"
grow-fed: "&7The tree absorbed &e%amount%x %item%&7."
tree-limit: "&cDon't be greedy! (tree limit reached)"
tree-planted: "&aYour Magic Tree was planted! Throw the required items next to it."
tree-disabled: "&cMagic Trees are ended/disabled."
destroy-sapling: "&cThis miracle won't come back. Really destroy it? Break it again to confirm."
destroy-leaves-santa: "&cSanta sees everything."
destroy-leaves-tut: "&7To destroy the tree, cut down the trunk."
destroy-warning: "&cAre you sure? Your progress will be lost, no resources back. Break it again to confirm."
destroy-fail-owner: "&cThis is not your tree!"
destroy-complete: "&4You monster!"
destroy-refund: "&aThe tree was taken down, your resources were returned."
not-owner: "&cThis is not your tree."
info: "&aMagic Tree of &e%owner%&a, level: &e%level%"
gift:
unluck-message: "&7Maybe Santa put you on his naughty list. Better luck next time!"
opened: "&aYou opened a gift!"
# Countdown
countdown-bossbar: "&cOnly &e%days% &cdays until Christmas Eve"
countdown-bossbar-one: "&cOnly &e1 &cday until Christmas Eve"
countdown-not-yet: "&7%days% days until Christmas Eve. The display starts in &e%in% &7day(s)."
countdown-today: "&aIt's Christmas Eve! Merry Christmas!"
countdown-on: "&aCountdown bar enabled."
countdown-off: "&7Countdown bar disabled."
# Cookies
cookie-usage: "&cUsage: /xmas cookie list | give <player> <id> [amount]"
cookie-unknown: "&cUnknown cookie: %id%"
cookie-given: "&aYou received &e%amount%x %id%&a."
# Secret Santa
wi-open: "&aSecret Santa is open! Join with &e/xmas wichteln join&a."
wi-joined: "&aYou joined the Secret Santa!"
wi-already: "&7You are already signed up."
wi-left: "&7You left the Secret Santa."
wi-not-joined: "&cYou are not part of the Secret Santa."
wi-wrong-state: "&cThat is not possible right now (wrong phase)."
wi-too-few: "&cAt least 3 participants are needed."
wi-drawn: "&aDrawn! Everyone has a partner. Check with &e/xmas wichteln who &ayour gift receiver."
wi-who: "&aYou give a gift to: &e%name%"
wi-gift-empty: "&cHold the gift in your hand."
wi-gift-saved: "&aYour gift for &e%name% &ais saved. (Hold another item and repeat to replace it.)"
wi-delivered: "&6The Secret Santa gifts were handed out! (&e%count% &6gifts)"
wi-received: "&aYou received a Secret Santa gift!"
wi-received-from: "&aYou received a Secret Santa gift from &e%from%&a!"
wi-reset: "&7Secret Santa reset."
wi-status: "&aSecret Santa: &e%state% &7- participants: &e%count% &7- gifts: &e%gifts%"
# Advent wreath
wreath-created: "&aAdvent wreath built."
wreath-removed: "&7Advent wreath removed."
wreath-none: "&cNo advent wreath nearby."
wreath-usage: "&cUsage: /xmas wreath create | remove (look at the ground)"
wreath-advent: "&6Today is the &e%n%. Advent&6! &e%n% &6candles are burning on the advent wreath."
# Snowball game
sb-usage: "&cUsage: /xmas snowball arena [radius] | start [time] | stop | top"
sb-arena-set: "&aSnowball arena set (radius &e%radius%&a)."
sb-no-arena: "&cNo arena set yet (/xmas snowball arena)."
sb-already: "&cThe snowball game is already running."
sb-not-active: "&cThe snowball game is not running."
sb-started: "&b&lSnowball fight! &7For &e%time% &7in the arena (&e%world% %x% %z%&7). Throw snowballs at other players!"
sb-ended: "&b&lSnowball fight over! &7The best:"
sb-top-line: "&e%rank%. &f%player% &7- &a%points% points"
sb-no-scores: "&7No points yet."
sb-actionbar: "&fPoints: &a%points% &7| Rank &e%rank% &7| &c%time%s &7left"
sb-reward: "&aCongratulations on place %rank%! Your reward was handed out."
# Decoration
deco-menu-title: "Christmas decoration"
deco-click-hint: "&7Click to place it where you are looking."
deco-present: "&aGift"
deco-candy_cane: "&cCandy cane"
deco-lantern_post: "&eLantern post"
deco-tree: "&2Christmas tree"
deco-wreath: "&4Wreath"
deco-snowman: "&fSnowman"
deco-lights: "&6String lights"
deco-snow_pile: "&bSnow pile"
deco-placed: "&aDecoration placed. &7/xmas deco undo reverts it."
deco-undone: "&7Last decoration removed."
deco-nothing-undo: "&cNothing to undo."
deco-look: "&cLook at a block."
# Login streak
streak-reward: "&6Login streak: day &e%day% &6in a row! Reward (step &e%cycle%&6) received."
# Greetings
greeting-christmas-eve: "&6Today is Christmas Eve! &fWe wish everyone a lovely evening."
greeting-new-years-eve: "&6Today is New Year's Eve! &fHave a great start into the new year."
present-name: "&aGift"
# Stockings
stocking-hung: "&aYour stocking is hanging by the fireplace! On &e%date% &asomething will be inside."
stocking-already: "&7Your stocking is already hanging. On &e%date% &7there will be something inside."
stocking-too-early: "&cYou can hang your stocking from &e%from% &con."
stocking-nothing: "&cYou didn't hang up a stocking this year."
stocking-claimed: "&7You already emptied your stocking."
stocking-received: "&aThere was something in your stocking! Merry Christmas!"
stocking-placed: "&aYour stocking is now hanging on the wall! On Christmas Santa will fly by and leave a gift below it."
stocking-place-usage: "&cLook at a free wall (the side of a block) to hang your stocking."
stocking-place-limit: "&cYou can place at most &e%max% &cstockings."
stocking-removed: "&7Stocking removed."
stocking-none-near: "&cThere is none of your stockings nearby."
stocking-present-placed: "&aSanta left a gift below your stocking! (x=%x%, y=%y%, z=%z%)"
stocking-present-notyours: "&cThis gift belongs to someone else."
stocking-present-opened: "&aYou opened your stocking gift. Merry Christmas!"
stocking-flight-started: "&aHo ho ho! Santa is flying from stocking to stocking, bringing gifts!"
stocking-flight-ok: "&aSanta's stocking flight has started."
stocking-flight-none: "&cNo flight possible: no placed stockings in loaded chunks (or a flight is already running)."
# Santa hat
hat-name: "&cSanta hat"
hat-lore:
- "&7Ho ho ho!"
hat-offer: "&aWould you like a Santa hat?"
hat-offer-click: "&e[Click here]"
hat-offer-hover: "&7Gives you a Santa hat"
hat-given: "&aHo ho ho! You got a Santa hat."
hat-have: "&7You already have a Santa hat."
hat-season: "&cThe Santa hat is only available during the holiday season."
# Quiz
quiz-question: "&6&lChristmas quiz! &f%question% &7(%time%s)"
quiz-win: "&a%player% &fknew it first: &e%answer%"
quiz-timeout: "&7Nobody knew it. The answer was: &e%answer%"
quiz-reward: "&aYour reward was handed out."
quiz-usage: "&cUsage: /xmas quiz start [no.] | skip | count"
quiz-already: "&cA question is already running."
quiz-none: "&cThere are no quiz questions in christmas.yml."
quiz-stopped: "&7Question ended."
quiz-not-active: "&cNo question is running."
# Wish list
wish-usage: "&cUsage: /xmas wish <text> | list | clear"
wish-saved: "&aYour wish is on the wish list! Maybe Santa will fulfil it."
wish-limit: "&cYou already have enough open wishes. (/xmas wish list, /xmas wish clear)"
wish-too-long: "&cThe wish is too long (max. %max% characters)."
wish-empty: "&7You have no open wishes."
wish-list-header: "&aYour open wishes:"
wish-list-line: "&7- &f%text%"
wish-cleared: "&7%count% wish(es) deleted."
wish-fulfilled: "&aYour wish was fulfilled: &e%wish%"
wish-fulfilled-gift: "&aYour wish was fulfilled, a gift is in your inventory: &e%wish%"
wish-admin-new: "&6New wish from &e%player%&6. View with /xmas wishes"
wish-admin-done: "&aWish of %player% fulfilled (without a gift)."
wish-admin-done-gift: "&aWish of %player% fulfilled. Gift: &e%item%"
wish-admin-done-offline: "&7%player% is offline. The wish is fulfilled, the gift will be handed over on their next join."
wish-admin-hold: "&cHold the gift in your hand and then left-click. (Shift+left-click fulfils the wish without a gift.)"
wish-admin-deleted: "&7Wish of %player% deleted."
wish-menu-title: "Wish list"
wish-menu-item-name: "&e%player%"
wish-menu-hint-left: "&aLeft-click: fulfil &7(the item in your hand is given away)"
wish-menu-hint-shift: "&eShift+left-click: fulfil without a gift"
wish-menu-hint-right: "&cRight-click: delete"
wish-page-prev: "&7Previous page"
wish-page-next: "&7Next page"
wish-menu-empty: "&7No open wishes"
# Market stalls
market-usage: "&cUsage: /xmas market spawn <id> | remove | list"
market-unknown: "&cUnknown stall: %id%"
market-spawned: "&aMarket stall placed."
market-removed: "&7Market stall removed."
market-none: "&cNo market stall within 6 blocks."
# Portals and Christmas market
market-unset: "&7Market point removed. Portals work again once an admin sets it anew."
market-set: "&aChristmas market point set. Portals now lead here."
portal-usage: "&cUsage: /xmas portal create | remove | info (look at the ground / at the portal)"
portal-created: "&aYour portal to the Christmas market is ready! Step in and wait a moment."
portal-removed: "&7Portal removed."
portal-none: "&cYou are not looking at a portal."
portal-not-owner: "&cThis is not your portal."
portal-limit: "&cYou already have %limit% portal(s)."
portal-need: "&cYou need snow blocks for the portal: &f%cost%"
portal-no-space: "&cNot enough space here (5 blocks wide, 5 high, solid ground)."
portal-no-market: "&cThe Christmas market has not been set by an admin yet. Portals only work afterwards."
portal-world: "&cYou can't build a portal in this world."
portal-disabled: "&cPortals are disabled."
portal-countdown: "&bPortal: &f%s% s"
portal-teleported: "&aWelcome to the Christmas market!"
portal-back-click: "&e[Back to the portal]"
portal-back-ok: "&7You are back at the portal."
portal-back-none: "&cNo way back is saved."
portal-info: "&aPortal cost: &f%cost% &7- Your portals: &e%count%/%limit%"