From 6dd60de6711833d7ab0f9d1ed92825da95dc5278 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 12 Nov 2026 16:59:18 +0000 Subject: [PATCH] Upload via GUI (52 Dateien) --- README.md | 1436 ++++++++++------- santa_skin.png | Bin 0 -> 503 bytes santa_skin_user.png | Bin 0 -> 391 bytes .../mviper/adventskalender/AdventCommand.java | 20 +- .../adventskalender/AdventListener.java | 18 +- .../adventskalender/Adventskalender.java | 3 + .../christmas/CalendarPerks.java | 27 + .../christmas/ChristmasCommand.java | 529 ++++++ .../christmas/ChristmasExtras.java | 123 ++ .../christmas/ChristmasListener.java | 262 +++ .../christmas/ChristmasManager.java | 344 ++++ .../christmas/CookieManager.java | 75 + .../christmas/CountdownManager.java | 123 ++ .../christmas/CrackerManager.java | 91 ++ .../christmas/DecoManager.java | 217 +++ .../christmas/GreetingManager.java | 97 ++ .../adventskalender/christmas/HatManager.java | 100 ++ .../christmas/LoginStreakManager.java | 109 ++ .../christmas/MagicTreeManager.java | 812 ++++++++++ .../christmas/MannequinSupport.java | 68 + .../christmas/MarketManager.java | 167 ++ .../christmas/PortalManager.java | 340 ++++ .../christmas/QuizManager.java | 127 ++ .../adventskalender/christmas/SantaEvent.java | 429 +++++ .../christmas/SantaMelody.java | 50 + .../christmas/SnowballGame.java | 192 +++ .../christmas/SnowmanManager.java | 262 +++ .../christmas/StockingFlight.java | 274 ++++ .../christmas/StockingManager.java | 344 ++++ .../christmas/WichtelnManager.java | 198 +++ .../christmas/WishManager.java | 270 ++++ .../christmas/WorldGuardHook.java | 22 + .../christmas/WreathManager.java | 199 +++ .../christmas/XmasExpansion.java | 50 + src/main/resources/christmas.yml | 419 +++++ src/main/resources/christmas_messages_de.yml | 275 ++++ src/main/resources/christmas_messages_en.yml | 276 ++++ 37 files changed, 7789 insertions(+), 559 deletions(-) create mode 100644 santa_skin.png create mode 100644 santa_skin_user.png create mode 100644 src/main/java/de/mviper/adventskalender/christmas/CalendarPerks.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/ChristmasExtras.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/CookieManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/CountdownManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/CrackerManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/DecoManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/GreetingManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/HatManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/LoginStreakManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/MannequinSupport.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/MarketManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/PortalManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/QuizManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/SnowballGame.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/StockingFlight.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/StockingManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/WichtelnManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/WishManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/WorldGuardHook.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/WreathManager.java create mode 100644 src/main/java/de/mviper/adventskalender/christmas/XmasExpansion.java create mode 100644 src/main/resources/christmas.yml create mode 100644 src/main/resources/christmas_messages_de.yml create mode 100644 src/main/resources/christmas_messages_en.yml diff --git a/README.md b/README.md index 47cf791..b2713f8 100644 --- a/README.md +++ b/README.md @@ -1,557 +1,879 @@ -# 🎄 Adventskalender – Event-Kalender & Geburtstage für Minecraft - -Ein Plugin für deinen Minecraft-Server, das Spielern jeden Tag ein Geschenk gibt. -Es hat **drei Kalender** und ein **Geburtstags-Geschenk**: - -| Was | Wann | Türchen | -|---|---|---| -| 🎄 **Adventskalender** | ab 1. Dezember | 24 | -| 🎃 **Halloween-Kalender** | ab 1. Oktober | 31 | -| 🐰 **Oster-Kalender** | 13 Tage vor Ostersonntag bis Ostersonntag | 14 | -| 🎂 **Geburtstags-Geschenk** | am Geburtstag des Spielers + 2 Tage danach | 1 pro Jahr | - -Du musst **nichts programmieren**. Plugin hineinkopieren, Server starten, fertig. -Alles Weitere (Belohnungen, Texte, Zeiten) kannst du in normalen Textdateien ändern. - ---- - -## Inhaltsverzeichnis - -1. [Was brauche ich?](#1-was-brauche-ich) -2. [Installation in 5 Schritten](#2-installation-in-5-schritten) -3. [So benutzen Spieler das Plugin](#3-so-benutzen-spieler-das-plugin) -4. [Befehle für Admins](#4-befehle-für-admins) -5. [Rechte (Permissions)](#5-rechte-permissions) -6. [Die Dateien des Plugins](#6-die-dateien-des-plugins) -7. [Belohnungen ändern](#7-belohnungen-ändern) -8. [Kalender ein- und ausschalten oder anpassen](#8-kalender-ein--und-ausschalten-oder-anpassen) -9. [Geburtstage einstellen](#9-geburtstage-einstellen) -10. [Texte und Sprache ändern](#10-texte-und-sprache-ändern) -11. [Alles testen, ohne auf den Dezember zu warten](#11-alles-testen-ohne-auf-den-dezember-zu-warten) -12. [Jeder darf nur einmal – oder der ganze Server nur einmal?](#12-jeder-darf-nur-einmal--oder-der-ganze-server-nur-einmal) -13. [Mehrere Server mit einer Datenbank (MySQL)](#13-mehrere-server-mit-einer-datenbank-mysql) -14. [Platzhalter für Scoreboards, Hologramme & Co.](#14-platzhalter-für-scoreboards-hologramme--co) -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) - ---- - -## 1. Was brauche ich? - -- 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: 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. - ---- - -## 2. Installation in 5 Schritten - -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`. -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. - ---- - -## 3. So benutzen Spieler das Plugin - -### Kalender öffnen - -| Befehl | Was passiert | -|---|---| -| `/ak` | Öffnet den Kalender, der gerade läuft. Läuft keiner, wird der Adventskalender gezeigt (mit gesperrten Türchen). | -| `/ak halloween` | Öffnet den Halloween-Kalender | -| `/ak easter` | Öffnet den Oster-Kalender | -| `/ak advent` | Öffnet den Adventskalender | - -Statt `/ak` funktionieren auch `/adventskalender`, `/advent` und `/kalender`. - -### Türchen öffnen - -- Die **Köpfe (Advent) bzw. Items (Halloween, Ostern)** in der Mitte des Menüs sind die Türchen. Die rot-grünen Glasscheiben am Rand sind nur Dekoration. -- Ein Türchen kannst du nur öffnen, wenn der **Tag erreicht** ist. Der 5. Dezember schaltet z. B. Türchen 1 bis 5 frei, Türchen 6 bleibt zu. -- **Klick auf ein Türchen** = du bekommst das Geschenk. Passt es nicht ins Inventar, fällt es vor deine Füße. -- Ein geöffnetes Türchen **glitzert** und kann nicht nochmal geöffnet werden. -- Hast du einen Tag **verpasst**, ist das kein Problem: Du kannst alte Türchen jederzeit nachholen, solange der Kalender läuft. - -### Geburtstags-Geschenk - -| Befehl | Was passiert | -|---|---| -| `/ak birthday set 15.03` | Trägt deinen Geburtstag ein (Tag.Monat, **ohne Jahr**) | -| `/ak birthday` | Zeigt deinen eingetragenen Geburtstag | -| `/ak birthday claim` | Holt dein Geschenk ab | - -- Du kannst das Geschenk **am Geburtstag und bis zu 2 Tage danach** abholen (Familie, Freunde, Feier – du hast Zeit). -- Es gibt **ein Geschenk pro Jahr**. -- Wenn du dich an deinem Geburtstag (oder danach) einloggst, bekommst du eine **anklickbare Nachricht**. Ein Klick darauf holt das Geschenk ab. -- Dein Geburtstag kann **nur einmal** eingetragen werden. Hast du dich vertippt, muss ein Admin das ändern (siehe unten). - ---- - -## 4. Befehle für Admins - -Alle Admin-Befehle beginnen mit `/ak admin`. Du brauchst dafür das Recht `adventskalender.admin` (Operatoren haben es automatisch). - -Die Teile in `[eckigen Klammern]` sind **optional**. Lässt du `[event]` weg, gilt der Befehl für **alle** Kalender. -Gültige Namen für `[event]`: `advent`, `halloween`, `easter`. - -| Befehl | Was er macht | -|---|---| -| `/ak admin reload` | Lädt Config und Sprachdateien neu, ohne Neustart | -| `/ak admin test [event] ` | Tut so, als wäre heute der gewählte Tag (siehe [Testen](#11-alles-testen-ohne-auf-den-dezember-zu-warten)) | -| `/ak admin test [event] reset` | Beendet den Test, das echte Datum gilt wieder | -| `/ak admin reset [event] ` | Löscht **alle** geöffneten Türchen eines Spielers | -| `/ak admin reset [event] global` | Löscht **alle** Daten aller Spieler (**Vorsicht!**) | -| `/ak admin resetday [event] ` | Macht **ein** Türchen für einen Spieler wieder zu | -| `/ak admin resetday [event] global ` | Macht ein Türchen im Server-weiten Modus wieder frei | -| `/ak admin birthday set ` | Trägt den Geburtstag eines Spielers ein oder ändert ihn | -| `/ak admin birthday remove ` | Löscht den Geburtstag eines Spielers | -| `/ak admin birthday reset ` | Erlaubt dem Spieler, das Geburtstagsgeschenk **nochmal** abzuholen (zum Testen oder bei Problemen) | - -**Beispiele:** - -``` -/ak admin test halloween 15 (Halloween-Kalender: so tun, als wäre der 15. Tag) -/ak admin test reset (alle Tests beenden) -/ak admin resetday advent Steve 3 (Steve darf Türchen 3 im Advent nochmal öffnen) -/ak admin reset halloween Steve (Steves Halloween-Daten löschen) -/ak admin birthday set Steve 24.12 (Steves Geburtstag auf den 24.12. setzen) -``` - -> Mit **Tab** vervollständigt Minecraft die Namen und Zahlen für dich. - ---- - -## 5. Rechte (Permissions) - -Rechte vergibst du mit einem Rechte-Plugin (z. B. LuckPerms). - -| Recht | Wofür | Standard | -|---|---|---| -| `adventskalender.use` | Kalender öffnen und benutzen | **alle Spieler** | -| `adventskalender.birthday` | Geburtstag eintragen und Geschenk abholen | **alle Spieler** | -| `adventskalender.admin` | Alle Admin-Befehle und Update-Hinweis beim Beitritt | nur Operatoren | - ---- - -## 6. Die Dateien des Plugins - -Alles liegt in `plugins/Adventskalender/`: - -| Datei | Wofür | -|---|---| -| `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** | -| `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. | - -### So bearbeitest du die Dateien richtig - -1. Öffne die Datei mit einem **Texteditor** (Notepad++, VS Code oder der Editor deines Hosters). **Nicht** mit Word. -2. Ändere nur die Werte, **nicht** die Einrückung am Zeilenanfang. Die Leerzeichen am Anfang sind wichtig! -3. Benutze **Leerzeichen**, keine Tabulatortaste. -4. Speichern und im Spiel `/ak admin reload` eingeben. -5. Wenn der Server nach einem Fehler die Datei nicht mehr lesen kann: Zeile prüfen, die du zuletzt geändert hast (fehlendes Leerzeichen, fehlende Anführungszeichen). - ---- - -## 7. Belohnungen ändern - -Jedes Türchen hat in der `config.yml` einen Eintrag. Beispiel Advent, Tag 3: - -```yaml -rewards: - 3: - material: DIAMOND_SWORD - amount: 1 - enchantments: - - type: SHARPNESS - level: 4 - - type: UNBREAKING - level: 2 -``` - -**Das bedeutet:** - -- `3:` – die Nummer des Türchens -- `material:` – welches Item es gibt (siehe unten) -- `amount:` – wie viele Stück -- `enchantments:` – Verzauberungen. Möchtest du keine, schreibe `enchantments: []` - -### Wo stehen die Belohnungen der anderen Kalender? - -- **Advent:** ganz oben unter `rewards:` -- **Halloween:** weiter unten unter `events:` → `halloween:` → `rewards:` -- **Ostern:** weiter unten unter `events:` → `easter:` → `rewards:` -- **Geburtstag:** ganz unten unter `birthday:` → `rewards:` - -### Item-Namen (`material`) - -Es sind die englischen Minecraft-Namen in **GROSSBUCHSTABEN** mit Unterstrich statt Leerzeichen. -Beispiele: `DIAMOND`, `GOLDEN_APPLE`, `NETHERITE_INGOT`, `ELYTRA`, `FIREWORK_ROCKET`. -Eine Liste findest du, wenn du im Internet nach „Spigot Material list“ suchst. - -### Verzauberungen (`type`) - -Auch hier die englischen Namen: `SHARPNESS`, `UNBREAKING`, `PROTECTION`, `EFFICIENCY`, `FORTUNE`, `LOOTING`, `MENDING` usw. -`level` ist die Stufe (z. B. `5` für Schärfe V). - -### Wenn du etwas falsch schreibst - -Das Plugin gibt dann eine **Warnung in der Konsole** aus (z. B. „Ungültiges Material …“). Der Spieler bekommt die Meldung, dass das Geschenk falsch eingestellt ist, und das **Türchen bleibt zu**. Er verliert also nichts. Korrigiere den Fehler und lade mit `/ak admin reload` neu. - -### Eigener Name und eigene Beschreibung der Geschenke - -Der Name und der Beschreibungstext, die im Menü über einem Türchen stehen, kommen **nicht** aus der `config.yml`, sondern aus der Sprachdatei. Siehe [Texte und Sprache ändern](#10-texte-und-sprache-ändern). - ---- - -## 8. Kalender ein- und ausschalten oder anpassen - -In der `config.yml` ganz unten gibt es den Bereich `events:`. Dort steht jeder Kalender mit seinen Einstellungen: - -```yaml -events: - halloween: - enabled: true - days: 31 - start: "10-01" - grace_days: 0 - closed_material: PUMPKIN - opened_material: JACK_O_LANTERN -``` - -| Einstellung | Bedeutung | -|---|---| -| `enabled` | `true` = Kalender an, `false` = Kalender aus (die Daten bleiben erhalten) | -| `days` | Wie viele Türchen der Kalender hat (1 bis 36) | -| `start` | Wann der Kalender beginnt. Format `Monat-Tag`, z. B. `"10-01"` für den 1. Oktober | -| `grace_days` | Wie viele Tage der Kalender nach dem letzten Türchen noch offen bleibt, damit man verpasste Türchen nachholen kann | -| `closed_material` | Item für ein **geschlossenes** Türchen | -| `opened_material` | Item für ein **geöffnetes** Türchen | - -### Zeitpunkt für Ostern - -Ostern liegt jedes Jahr woanders. Deshalb gibt es dort eine Sonderschreibweise: - -```yaml -start: "easter-13" -``` - -Das heißt: **13 Tage vor Ostersonntag**. Das Plugin rechnet das Datum **jedes Jahr automatisch** aus, du musst nie wieder etwas ändern. -Weitere Beispiele: `easter+1` = Ostermontag, `easter-2` = Karfreitag. - -### Eigene Köpfe statt Items (für Fortgeschrittene) - -Möchtest du für Halloween oder Ostern hübsche Kopf-Texturen, kannst du bei einem Kalender zusätzlich diese Zeilen einfügen: - -```yaml - closed_texture: "http://textures.minecraft.net/texture/DEINE_TEXTUR" - opened_texture: "http://textures.minecraft.net/texture/DEINE_TEXTUR" -``` - -Passende Texturen findest du auf Seiten wie *minecraft-heads.com* (dort den Wert „Minecraft-URL“ kopieren). - -### Zeitzone - -Das Plugin richtet sich nach der Uhrzeit des Servers. Wenn dein Server in einem anderen Land steht und die Türchen zu früh oder zu spät aufgehen, trage deine Zeitzone ein: - -```yaml -general: - timezone: "Europe/Berlin" -``` - ---- - -## 9. Geburtstage einstellen - -Alles steht unten in der `config.yml` im Bereich `birthday:`: - -```yaml -birthday: - enabled: true - grace_days: 2 - allow_change: false - broadcast: true - commands: [] - rewards: - 1: - material: CAKE - amount: 1 - enchantments: [] -``` - -| Einstellung | Bedeutung | -|---|---| -| `enabled` | `false` schaltet die Geburtstags-Funktion komplett ab | -| `grace_days` | Wie viele Tage **nach** dem Geburtstag das Geschenk noch abholbar ist (Standard 2) | -| `allow_change` | `false` = Spieler können ihren Geburtstag nur **einmal** eintragen, Änderungen macht nur ein Admin. `true` = Spieler dürfen selbst ändern (siehe Hinweis) | -| `broadcast` | `true` = der ganze Server sieht „Steve feiert Geburtstag – alles Gute!“, wenn das Geschenk abgeholt wird | -| `commands` | Zusätzliche Befehle, die der Server ausführt (siehe unten) | -| `rewards` | Die Geschenke. Funktioniert genau wie bei den Kalendern, du kannst beliebig viele Einträge anlegen (1, 2, 3, …) | - -> **Warum ist `allow_change` standardmäßig aus?** -> Wäre es an, könnte ein Spieler sein Datum jeden Tag umstellen und sich ständig Geschenke holen. Auch mit `true` kann er pro Jahr nur **einmal** abholen. - -### Geld oder andere Belohnungen per Befehl - -Wenn du z. B. ein Wirtschafts-Plugin hast, kannst du Geld verschenken: - -```yaml - commands: - - "eco give %player% 500" - - "give %player% minecraft:cake 1" -``` - -`%player%` wird automatisch durch den Namen des Spielers ersetzt. Ohne Schrägstrich am Anfang schreiben. - -### Wichtige Hinweise - -- Das **Jahr** wird nie gespeichert, nur Tag und Monat. Das Plugin weiß also nie, wie alt jemand ist. -- Wer am **29. Februar** Geburtstag hat, feiert in Jahren ohne Schalttag am **28. Februar**. -- Wer am **31. Dezember** Geburtstag hat, kann bis in den Januar abholen. Das funktioniert korrekt. - ---- - -## 10. Texte und Sprache ändern - -### Sprache umstellen - -In der `config.yml` ganz oben: - -```yaml -general: - language: "de" -``` - -`"de"` = Deutsch, `"en"` = Englisch. Danach `/ak admin reload`. - -### Eigene Texte - -Öffne `messages_de.yml` (oder `messages_en.yml`). Dort stehen alle Texte. Farben schreibst du mit `&`: - -| Code | Farbe | Code | Farbe | -|---|---|---|---| -| `&0` | Schwarz | `&8` | Dunkelgrau | -| `&1` | Dunkelblau | `&9` | Blau | -| `&2` | Dunkelgrün | `&a` | Grün | -| `&3` | Türkis | `&b` | Hellblau | -| `&4` | Dunkelrot | `&c` | Rot | -| `&5` | Lila | `&d` | Pink | -| `&6` | Gold | `&e` | Gelb | -| `&7` | Grau | `&f` | Weiß | - -Formatierung: `&l` = **fett**, `&o` = *kursiv*, `&n` = unterstrichen, `&r` = zurücksetzen. - -### Platzhalter in Texten - -Diese Platzhalter dürfen in den Texten stehen und werden automatisch ersetzt: - -- `%day%` – die Nummer des Tages -- `%date%` – das Datum eines gesperrten Türchens -- `%player%` – der Spielername -- `%event%` – der Name des Kalenders -- `%days%` – die Anzahl Tage (bei Geburtstagen) - -### Namen und Beschreibung der Geschenke im Menü - -Beispiel für den Adventskalender in `messages_de.yml`: - -```yaml -rewards: - day_3: - name: "&cTag 3: Schwert des Helden" - lore: ["&7Ein mächtiges Schwert, um Monster zu jagen."] -``` - -- `name` ist die Überschrift des Türchens. -- `lore` sind die Zeilen darunter. Mehrere Zeilen schreibst du so: `["Zeile 1", "Zeile 2"]`. - -Für Halloween und Ostern stehen die Texte weiter unten in der Datei unter `events:` → `halloween:` bzw. `easter:`. -Wenn du ein Geschenk in der `config.yml` änderst, solltest du hier den Namen anpassen, sonst steht „Schwert des Helden“, obwohl es jetzt Diamanten gibt. - -> Wenn du neue Texte vermisst („Key error: …“ im Spiel), siehe [Plugin aktualisieren](#15-plugin-aktualisieren). - ---- - -## 11. Alles testen, ohne auf den Dezember zu warten - -Du musst nicht bis zum 1. Dezember warten, um alles auszuprobieren: - -1. `/ak admin test advent 10` – der Adventskalender denkt jetzt, es sei der 10. Dezember. -2. `/ak advent` – öffne ihn und probiere die Türchen 1 bis 10 aus. -3. Zum Zurücksetzen deiner Test-Türchen: `/ak admin reset advent DEINNAME` -4. Test beenden: `/ak admin test reset` - -Genauso für `halloween` und `easter`. Den Test-Tag **vergisst** das Plugin beim nächsten Neustart. Du kannst also nichts „vergessen auszuschalten“. - -**Geburtstag testen:** - -1. `/ak admin birthday set DEINNAME TT.MM` – statt TT.MM das **heutige** Datum eintragen (z. B. `24.12`) -2. `/ak birthday claim` – du bekommst das Geschenk -3. Nochmal testen: `/ak admin birthday reset DEINNAME` - ---- - -## 12. Jeder darf nur einmal – oder der ganze Server nur einmal? - -Es gibt zwei Betriebsarten: - -- **Normal (Standard):** Jeder Spieler hat seinen **eigenen** Kalender. Jeder darf jedes Türchen einmal öffnen. -- **Global:** Jedes Türchen darf **nur ein einziger Spieler** auf dem ganzen Server öffnen. Wer zuerst klickt, gewinnt. - -Umstellen in der `config.yml`: - -```yaml -calendar: - use_global_calendar: false # true = Global-Modus -``` - -Du kannst das auch nur für **einen** Kalender ändern, indem du bei diesem im Bereich `events:` die Zeile `global: true` oder `global: false` einfügst. - ---- - -## 13. Mehrere Server mit einer Datenbank (MySQL) - -Nur nötig, wenn du z. B. ein BungeeCord-Netzwerk hast und alle Server dieselben Daten benutzen sollen. Für **einen** Server einfach ignorieren. - -In der `config.yml`: - -```yaml -mysql: - enable: true - host: "localhost" - port: 3306 - database: "adventskalender" - user: "root" - password: "DEIN_PASSWORT" -``` - -- Die Datenbank (`adventskalender`) musst du vorher selbst anlegen. Die Tabellen darin erstellt das Plugin allein. -- Danach den Server **neu starten** (oder `/ak admin reload`). -- In der Konsole muss stehen: **„MySQL-Verbindung erfolgreich hergestellt!“** -- Klappt es nicht, benutzt das Plugin automatisch die `data.yml` als Notlösung und schreibt eine Warnung in die Konsole. -- Sollte die Datenbank später kurz ausfallen, bekommen Spieler eine Meldung und es wird **nichts** vergeben. Sobald die Datenbank wieder da ist, geht alles weiter. -- Alle Kalender und Geburtstage werden in der Datenbank gespeichert. Daten aus einer älteren Version werden dabei automatisch übernommen. - -> **Wichtig:** Daten aus der `data.yml` werden **nicht automatisch** in die Datenbank kopiert. Schaltest du MySQL ein, fangen die Spieler dort bei null an. - ---- - -## 14. Platzhalter für Scoreboards, Hologramme & Co. - -Wenn das Plugin **PlaceholderAPI** installiert ist, kannst du diese Werte überall anzeigen, wo PlaceholderAPI funktioniert: - -### Für das gerade laufende Kalender-Event - -| Platzhalter | Zeigt | -|---|---| -| `%ak_event%` | Name des laufenden Kalenders (`advent`, `halloween`, `easter`) | -| `%ak_day%` | Welcher Tag des Kalenders heute ist (0 = Kalender läuft nicht) | -| `%ak_claimed%` | Wie viele Türchen der Spieler schon geöffnet hat | -| `%ak_remaining%` | Wie viele Türchen noch offen sind | -| `%ak_has_claimed_today%` | „Ja“ oder „Nein“: Hat der Spieler das heutige Türchen schon geöffnet? | -| `%ak_is_global%` | „Global“ oder „Spieler-basiert“ | - -### Für einen bestimmten Kalender - -Schreibe den Namen des Kalenders vor den Platzhalter: - -- `%ak_halloween_claimed%` -- `%ak_easter_day%` -- `%ak_advent_remaining%` - -### Geburtstag - -| Platzhalter | Zeigt | -|---|---| -| `%ak_birthday%` | Eingetragenes Datum (z. B. `15.03.`) oder `-` | -| `%ak_birthday_active%` | „Ja“, wenn der Spieler gerade sein Geschenk abholen kann, sonst „Nein“ | - ---- - -## 15. Plugin aktualisieren - -1. Server **ausschalten**. -2. Die **alte `.jar`-Datei** aus dem `plugins`-Ordner löschen. -3. Die **neue `.jar`-Datei** hineinkopieren. -4. Server starten. - -Deine **Daten** (`data.yml`) und deine **Einstellungen** bleiben erhalten. - -### Neue Einstellungen sichtbar machen - -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.) -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**. - -### 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. - ---- - -## 16. Probleme und ihre Lösung - -| Problem | Lösung | -|---|---| -| **Plugin startet nicht** | Prüfe die Java-Version (**Java 21** nötig) und ob es Spigot/Paper ab 1.20 ist. Die genaue Fehlermeldung steht in der Server-Konsole. | -| **`/ak` tut nichts / „Unbekannter Befehl“** | Plugin wurde nicht geladen. Schau in die Konsole oder tippe `/plugins` und prüfe, ob „Adventskalender“ grün ist. | -| **„Dafür hast du keine Berechtigung“** | Der Spieler hat `adventskalender.use` (bzw. `adventskalender.admin`) nicht. Rechte prüfen. | -| **Alle Türchen sind gesperrt** | Es ist gerade nicht die Zeit des Kalenders. Prüfe das Datum oder teste mit `/ak admin test`. | -| **Im Menü steht „Key error: …“** | In deiner Sprachdatei fehlt ein Text. Siehe [Plugin aktualisieren](#15-plugin-aktualisieren), neue Sprachdatei erzeugen lassen. | -| **Ein Türchen lässt sich nicht öffnen, Meldung „falsch konfiguriert“** | In der `config.yml` ist bei diesem Tag ein Item- oder Verzauberungsname falsch geschrieben. Die Konsole sagt dir genau, wo. | -| **Eine Verzauberung fehlt am Geschenk** | Der Name in `type:` ist falsch (englisch und GROSS schreiben). Die Konsole zeigt eine Warnung. | -| **Kopf zeigt Steve statt Weihnachtsmotiv** | Der Server konnte die Textur nicht laden (Internet-/Mojang-Problem). Neustart versuchen. Bei Halloween/Ostern gibt es Items statt Köpfen, das ist normal. | -| **Meldung „Die Datenbank ist gerade nicht erreichbar“** | Nur mit MySQL. Prüfe Zugangsdaten und ob die Datenbank läuft. | -| **Nach dem Reload stimmt etwas nicht** | Server einmal komplett neu starten. | -| **Ein Spieler hat sich beim Geburtstag vertippt** | `/ak admin birthday set ` | -| **Spieler hat sein Geburtstagsgeschenk verloren / Fehler beim Abholen** | `/ak admin birthday reset `, dann darf er nochmal abholen. | -| **Kalender geht zur falschen Uhrzeit auf** | Zeitzone in der `config.yml` einstellen (siehe [Zeitzone](#zeitzone)). | - -Hilft nichts davon: Kopiere die **Fehlermeldung aus der Konsole** (nicht nur „geht nicht“) und frage auf der Seite des Plugins bei SpigotMC nach. - ---- - -## 17. Häufige Fragen - -**Kann ich Geschenke für jeden Tag selbst festlegen?** -Ja, siehe [Belohnungen ändern](#7-belohnungen-ändern). - -**Kann ich einen Kalender ausschalten, den ich nicht will?** -Ja: In der `config.yml` bei `events:` den Eintrag `enabled: false` setzen. - -**Kann ich mehr als 24, 31 oder 14 Türchen haben?** -Ja, bis zu 36 pro Kalender (Einstellung `days`). Du musst dann aber auch für jedes neue Türchen eine Belohnung eintragen und im Sprachfile einen Namen vergeben. - -**Was passiert, wenn der Kalender vorbei ist?** -Er bleibt noch `grace_days` Tage offen (Advent: bis zum 31.12., Ostern: 7 Tage), danach sind alle Türchen gesperrt, bis er im nächsten Jahr wieder startet. - -**Gehen die Türchen jedes Jahr von vorne los?** -Nein. Das Plugin merkt sich dauerhaft, was jemand geöffnet hat. Soll im nächsten Jahr wieder alles neu sein, benutze `/ak admin reset global` (oder `/ak admin reset advent global` nur für den Advent). **Achtung:** Das löscht die Daten für **alle**. - -**Sehen alle Spieler, wann ich Geburtstag habe?** -Nein. Das Datum sieht nur der Spieler selbst (und Admins in der Datenbank/Datei). Im Chat steht beim Abholen nur „Spieler feiert Geburtstag“, ohne Datum. - -**Funktioniert das Plugin mit Bedrock-Spielern (Geyser)?** -Das wurde nicht getestet. - -**Ich habe `data.yml` gelöscht, was jetzt?** -Dann haben alle Spieler wieder alle Türchen und Geburtstage sind weg. Spiele vorher eine Sicherung ein, wenn du eine hast. Mach vor Änderungen am Server grundsätzlich eine Sicherheitskopie des Plugin-Ordners. - ---- - -*Autor: M_Viper • Frohe Weihnachten, schaurig-schönes Halloween, frohe Ostern und alles Gute zum Geburtstag! 🎄🎃🐰🎂* +# 🎄 Adventskalender – Event-Kalender & Geburtstage für Minecraft + +Ein Plugin für deinen Minecraft-Server, das Spielern jeden Tag ein Geschenk gibt. +Es hat **drei Kalender** und ein **Geburtstags-Geschenk**: + +| Was | Wann | Türchen | +|---|---|---| +| 🎄 **Adventskalender** | ab 1. Dezember | 24 | +| 🎃 **Halloween-Kalender** | ab 1. Oktober | 31 | +| 🐰 **Oster-Kalender** | 13 Tage vor Ostersonntag bis Ostersonntag | 14 | +| 🎂 **Geburtstags-Geschenk** | am Geburtstag des Spielers + 2 Tage danach | 1 pro Jahr | + +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 + +1. [Was brauche ich?](#1-was-brauche-ich) +2. [Installation in 5 Schritten](#2-installation-in-5-schritten) +3. [So benutzen Spieler das Plugin](#3-so-benutzen-spieler-das-plugin) +4. [Befehle für Admins](#4-befehle-für-admins) +5. [Rechte (Permissions)](#5-rechte-permissions) +6. [Die Dateien des Plugins](#6-die-dateien-des-plugins) +7. [Belohnungen ändern](#7-belohnungen-ändern) +8. [Kalender ein- und ausschalten oder anpassen](#8-kalender-ein--und-ausschalten-oder-anpassen) +9. [Geburtstage einstellen](#9-geburtstage-einstellen) +10. [Texte und Sprache ändern](#10-texte-und-sprache-ändern) +11. [Alles testen, ohne auf den Dezember zu warten](#11-alles-testen-ohne-auf-den-dezember-zu-warten) +12. [Jeder darf nur einmal – oder der ganze Server nur einmal?](#12-jeder-darf-nur-einmal--oder-der-ganze-server-nur-einmal) +13. [Mehrere Server mit einer Datenbank (MySQL)](#13-mehrere-server-mit-einer-datenbank-mysql) +14. [Platzhalter für Scoreboards, Hologramme & Co.](#14-platzhalter-für-scoreboards-hologramme--co) +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) + +--- + +## 1. Was brauche ich? + +- 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 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 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. + +--- + +## 2. Installation in 5 Schritten + +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`, `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. + +--- + +## 3. So benutzen Spieler das Plugin + +### Kalender öffnen + +| Befehl | Was passiert | +|---|---| +| `/ak` | Öffnet den Kalender, der gerade läuft. Läuft keiner, wird der Adventskalender gezeigt (mit gesperrten Türchen). | +| `/ak halloween` | Öffnet den Halloween-Kalender | +| `/ak easter` | Öffnet den Oster-Kalender | +| `/ak advent` | Öffnet den Adventskalender | + +Statt `/ak` funktionieren auch `/adventskalender`, `/advent` und `/kalender`. + +### Türchen öffnen + +- Die **Köpfe (Advent) bzw. Items (Halloween, Ostern)** in der Mitte des Menüs sind die Türchen. Die rot-grünen Glasscheiben am Rand sind nur Dekoration. +- Ein Türchen kannst du nur öffnen, wenn der **Tag erreicht** ist. Der 5. Dezember schaltet z. B. Türchen 1 bis 5 frei, Türchen 6 bleibt zu. +- **Klick auf ein Türchen** = du bekommst das Geschenk. Passt es nicht ins Inventar, fällt es vor deine Füße. +- Ein geöffnetes Türchen **glitzert** und kann nicht nochmal geöffnet werden. +- Hast du einen Tag **verpasst**, ist das kein Problem: Du kannst alte Türchen jederzeit nachholen, solange der Kalender läuft. + +### Geburtstags-Geschenk + +| Befehl | Was passiert | +|---|---| +| `/ak birthday set 15.03` | Trägt deinen Geburtstag ein (Tag.Monat, **ohne Jahr**) | +| `/ak birthday` | Zeigt deinen eingetragenen Geburtstag | +| `/ak birthday claim` | Holt dein Geschenk ab | + +- Du kannst das Geschenk **am Geburtstag und bis zu 2 Tage danach** abholen (Familie, Freunde, Feier – du hast Zeit). +- Es gibt **ein Geschenk pro Jahr**. +- Wenn du dich an deinem Geburtstag (oder danach) einloggst, bekommst du eine **anklickbare Nachricht**. Ein Klick darauf holt das Geschenk ab. +- Dein Geburtstag kann **nur einmal** eingetragen werden. Hast du dich vertippt, muss ein Admin das ändern (siehe unten). + +--- + +## 4. Befehle für Admins + +Alle Admin-Befehle beginnen mit `/ak admin`. Du brauchst dafür das Recht `adventskalender.admin` (Operatoren haben es automatisch). + +Die Teile in `[eckigen Klammern]` sind **optional**. Lässt du `[event]` weg, gilt der Befehl für **alle** Kalender. +Gültige Namen für `[event]`: `advent`, `halloween`, `easter`. + +| Befehl | Was er macht | +|---|---| +| `/ak admin reload` | Lädt Config und Sprachdateien neu, ohne Neustart | +| `/ak admin test [event] ` | Tut so, als wäre heute der gewählte Tag (siehe [Testen](#11-alles-testen-ohne-auf-den-dezember-zu-warten)) | +| `/ak admin test [event] reset` | Beendet den Test, das echte Datum gilt wieder | +| `/ak admin reset [event] ` | Löscht **alle** geöffneten Türchen eines Spielers | +| `/ak admin reset [event] global` | Löscht **alle** Daten aller Spieler (**Vorsicht!**) | +| `/ak admin resetday [event] ` | Macht **ein** Türchen für einen Spieler wieder zu | +| `/ak admin resetday [event] global ` | Macht ein Türchen im Server-weiten Modus wieder frei | +| `/ak admin birthday set ` | Trägt den Geburtstag eines Spielers ein oder ändert ihn | +| `/ak admin birthday remove ` | Löscht den Geburtstag eines Spielers | +| `/ak admin birthday reset ` | Erlaubt dem Spieler, das Geburtstagsgeschenk **nochmal** abzuholen (zum Testen oder bei Problemen) | + +**Beispiele:** + +``` +/ak admin test halloween 15 (Halloween-Kalender: so tun, als wäre der 15. Tag) +/ak admin test reset (alle Tests beenden) +/ak admin resetday advent Steve 3 (Steve darf Türchen 3 im Advent nochmal öffnen) +/ak admin reset halloween Steve (Steves Halloween-Daten löschen) +/ak admin birthday set Steve 24.12 (Steves Geburtstag auf den 24.12. setzen) +``` + +> Mit **Tab** vervollständigt Minecraft die Namen und Zahlen für dich. + +--- + +## 5. Rechte (Permissions) + +Rechte vergibst du mit einem Rechte-Plugin (z. B. LuckPerms). + +| Recht | Wofür | Standard | +|---|---|---| +| `adventskalender.use` | Kalender öffnen und benutzen | **alle Spieler** | +| `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 + +Alles liegt in `plugins/Adventskalender/`: + +| Datei | Wofür | +|---|---| +| `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 + +1. Öffne die Datei mit einem **Texteditor** (Notepad++, VS Code oder der Editor deines Hosters). **Nicht** mit Word. +2. Ändere nur die Werte, **nicht** die Einrückung am Zeilenanfang. Die Leerzeichen am Anfang sind wichtig! +3. Benutze **Leerzeichen**, keine Tabulatortaste. +4. Speichern und im Spiel `/ak admin reload` eingeben. +5. Wenn der Server nach einem Fehler die Datei nicht mehr lesen kann: Zeile prüfen, die du zuletzt geändert hast (fehlendes Leerzeichen, fehlende Anführungszeichen). + +--- + +## 7. Belohnungen ändern + +Jedes Türchen hat in der `config.yml` einen Eintrag. Beispiel Advent, Tag 3: + +```yaml +rewards: + 3: + material: DIAMOND_SWORD + amount: 1 + enchantments: + - type: SHARPNESS + level: 4 + - type: UNBREAKING + level: 2 +``` + +**Das bedeutet:** + +- `3:` – die Nummer des Türchens +- `material:` – welches Item es gibt (siehe unten) +- `amount:` – wie viele Stück +- `enchantments:` – Verzauberungen. Möchtest du keine, schreibe `enchantments: []` + +### Wo stehen die Belohnungen der anderen Kalender? + +- **Advent:** ganz oben unter `rewards:` +- **Halloween:** weiter unten unter `events:` → `halloween:` → `rewards:` +- **Ostern:** weiter unten unter `events:` → `easter:` → `rewards:` +- **Geburtstag:** ganz unten unter `birthday:` → `rewards:` + +### Item-Namen (`material`) + +Es sind die englischen Minecraft-Namen in **GROSSBUCHSTABEN** mit Unterstrich statt Leerzeichen. +Beispiele: `DIAMOND`, `GOLDEN_APPLE`, `NETHERITE_INGOT`, `ELYTRA`, `FIREWORK_ROCKET`. +Eine Liste findest du, wenn du im Internet nach „Spigot Material list“ suchst. + +### Verzauberungen (`type`) + +Auch hier die englischen Namen: `SHARPNESS`, `UNBREAKING`, `PROTECTION`, `EFFICIENCY`, `FORTUNE`, `LOOTING`, `MENDING` usw. +`level` ist die Stufe (z. B. `5` für Schärfe V). + +### Wenn du etwas falsch schreibst + +Das Plugin gibt dann eine **Warnung in der Konsole** aus (z. B. „Ungültiges Material …“). Der Spieler bekommt die Meldung, dass das Geschenk falsch eingestellt ist, und das **Türchen bleibt zu**. Er verliert also nichts. Korrigiere den Fehler und lade mit `/ak admin reload` neu. + +### Eigener Name und eigene Beschreibung der Geschenke + +Der Name und der Beschreibungstext, die im Menü über einem Türchen stehen, kommen **nicht** aus der `config.yml`, sondern aus der Sprachdatei. Siehe [Texte und Sprache ändern](#10-texte-und-sprache-ändern). + +--- + +## 8. Kalender ein- und ausschalten oder anpassen + +In der `config.yml` ganz unten gibt es den Bereich `events:`. Dort steht jeder Kalender mit seinen Einstellungen: + +```yaml +events: + halloween: + enabled: true + days: 31 + start: "10-01" + grace_days: 0 + closed_material: PUMPKIN + opened_material: JACK_O_LANTERN +``` + +| Einstellung | Bedeutung | +|---|---| +| `enabled` | `true` = Kalender an, `false` = Kalender aus (die Daten bleiben erhalten) | +| `days` | Wie viele Türchen der Kalender hat (1 bis 36) | +| `start` | Wann der Kalender beginnt. Format `Monat-Tag`, z. B. `"10-01"` für den 1. Oktober | +| `grace_days` | Wie viele Tage der Kalender nach dem letzten Türchen noch offen bleibt, damit man verpasste Türchen nachholen kann | +| `closed_material` | Item für ein **geschlossenes** Türchen | +| `opened_material` | Item für ein **geöffnetes** Türchen | + +### Zeitpunkt für Ostern + +Ostern liegt jedes Jahr woanders. Deshalb gibt es dort eine Sonderschreibweise: + +```yaml +start: "easter-13" +``` + +Das heißt: **13 Tage vor Ostersonntag**. Das Plugin rechnet das Datum **jedes Jahr automatisch** aus, du musst nie wieder etwas ändern. +Weitere Beispiele: `easter+1` = Ostermontag, `easter-2` = Karfreitag. + +### Eigene Köpfe statt Items (für Fortgeschrittene) + +Möchtest du für Halloween oder Ostern hübsche Kopf-Texturen, kannst du bei einem Kalender zusätzlich diese Zeilen einfügen: + +```yaml + closed_texture: "http://textures.minecraft.net/texture/DEINE_TEXTUR" + opened_texture: "http://textures.minecraft.net/texture/DEINE_TEXTUR" +``` + +Passende Texturen findest du auf Seiten wie *minecraft-heads.com* (dort den Wert „Minecraft-URL“ kopieren). + +### Zeitzone + +Das Plugin richtet sich nach der Uhrzeit des Servers. Wenn dein Server in einem anderen Land steht und die Türchen zu früh oder zu spät aufgehen, trage deine Zeitzone ein: + +```yaml +general: + timezone: "Europe/Berlin" +``` + +--- + +## 9. Geburtstage einstellen + +Alles steht unten in der `config.yml` im Bereich `birthday:`: + +```yaml +birthday: + enabled: true + grace_days: 2 + allow_change: false + broadcast: true + commands: [] + rewards: + 1: + material: CAKE + amount: 1 + enchantments: [] +``` + +| Einstellung | Bedeutung | +|---|---| +| `enabled` | `false` schaltet die Geburtstags-Funktion komplett ab | +| `grace_days` | Wie viele Tage **nach** dem Geburtstag das Geschenk noch abholbar ist (Standard 2) | +| `allow_change` | `false` = Spieler können ihren Geburtstag nur **einmal** eintragen, Änderungen macht nur ein Admin. `true` = Spieler dürfen selbst ändern (siehe Hinweis) | +| `broadcast` | `true` = der ganze Server sieht „Steve feiert Geburtstag – alles Gute!“, wenn das Geschenk abgeholt wird | +| `commands` | Zusätzliche Befehle, die der Server ausführt (siehe unten) | +| `rewards` | Die Geschenke. Funktioniert genau wie bei den Kalendern, du kannst beliebig viele Einträge anlegen (1, 2, 3, …) | + +> **Warum ist `allow_change` standardmäßig aus?** +> Wäre es an, könnte ein Spieler sein Datum jeden Tag umstellen und sich ständig Geschenke holen. Auch mit `true` kann er pro Jahr nur **einmal** abholen. + +### Geld oder andere Belohnungen per Befehl + +Wenn du z. B. ein Wirtschafts-Plugin hast, kannst du Geld verschenken: + +```yaml + commands: + - "eco give %player% 500" + - "give %player% minecraft:cake 1" +``` + +`%player%` wird automatisch durch den Namen des Spielers ersetzt. Ohne Schrägstrich am Anfang schreiben. + +### Wichtige Hinweise + +- Das **Jahr** wird nie gespeichert, nur Tag und Monat. Das Plugin weiß also nie, wie alt jemand ist. +- Wer am **29. Februar** Geburtstag hat, feiert in Jahren ohne Schalttag am **28. Februar**. +- Wer am **31. Dezember** Geburtstag hat, kann bis in den Januar abholen. Das funktioniert korrekt. + +--- + +## 10. Texte und Sprache ändern + +### Sprache umstellen + +In der `config.yml` ganz oben: + +```yaml +general: + language: "de" +``` + +`"de"` = Deutsch, `"en"` = Englisch. Danach `/ak admin reload`. + +### Eigene Texte + +Öffne `messages_de.yml` (oder `messages_en.yml`). Dort stehen alle Texte. Farben schreibst du mit `&`: + +| Code | Farbe | Code | Farbe | +|---|---|---|---| +| `&0` | Schwarz | `&8` | Dunkelgrau | +| `&1` | Dunkelblau | `&9` | Blau | +| `&2` | Dunkelgrün | `&a` | Grün | +| `&3` | Türkis | `&b` | Hellblau | +| `&4` | Dunkelrot | `&c` | Rot | +| `&5` | Lila | `&d` | Pink | +| `&6` | Gold | `&e` | Gelb | +| `&7` | Grau | `&f` | Weiß | + +Formatierung: `&l` = **fett**, `&o` = *kursiv*, `&n` = unterstrichen, `&r` = zurücksetzen. + +### Platzhalter in Texten + +Diese Platzhalter dürfen in den Texten stehen und werden automatisch ersetzt: + +- `%day%` – die Nummer des Tages +- `%date%` – das Datum eines gesperrten Türchens +- `%player%` – der Spielername +- `%event%` – der Name des Kalenders +- `%days%` – die Anzahl Tage (bei Geburtstagen) + +### Namen und Beschreibung der Geschenke im Menü + +Beispiel für den Adventskalender in `messages_de.yml`: + +```yaml +rewards: + day_3: + name: "&cTag 3: Schwert des Helden" + lore: ["&7Ein mächtiges Schwert, um Monster zu jagen."] +``` + +- `name` ist die Überschrift des Türchens. +- `lore` sind die Zeilen darunter. Mehrere Zeilen schreibst du so: `["Zeile 1", "Zeile 2"]`. + +Für Halloween und Ostern stehen die Texte weiter unten in der Datei unter `events:` → `halloween:` bzw. `easter:`. +Wenn du ein Geschenk in der `config.yml` änderst, solltest du hier den Namen anpassen, sonst steht „Schwert des Helden“, obwohl es jetzt Diamanten gibt. + +> Wenn du neue Texte vermisst („Key error: …“ im Spiel), siehe [Plugin aktualisieren](#15-plugin-aktualisieren). + +--- + +## 11. Alles testen, ohne auf den Dezember zu warten + +Du musst nicht bis zum 1. Dezember warten, um alles auszuprobieren: + +1. `/ak admin test advent 10` – der Adventskalender denkt jetzt, es sei der 10. Dezember. +2. `/ak advent` – öffne ihn und probiere die Türchen 1 bis 10 aus. +3. Zum Zurücksetzen deiner Test-Türchen: `/ak admin reset advent DEINNAME` +4. Test beenden: `/ak admin test reset` + +Genauso für `halloween` und `easter`. Den Test-Tag **vergisst** das Plugin beim nächsten Neustart. Du kannst also nichts „vergessen auszuschalten“. + +**Geburtstag testen:** + +1. `/ak admin birthday set DEINNAME TT.MM` – statt TT.MM das **heutige** Datum eintragen (z. B. `24.12`) +2. `/ak birthday claim` – du bekommst das Geschenk +3. Nochmal testen: `/ak admin birthday reset DEINNAME` + +--- + +## 12. Jeder darf nur einmal – oder der ganze Server nur einmal? + +Es gibt zwei Betriebsarten: + +- **Normal (Standard):** Jeder Spieler hat seinen **eigenen** Kalender. Jeder darf jedes Türchen einmal öffnen. +- **Global:** Jedes Türchen darf **nur ein einziger Spieler** auf dem ganzen Server öffnen. Wer zuerst klickt, gewinnt. + +Umstellen in der `config.yml`: + +```yaml +calendar: + use_global_calendar: false # true = Global-Modus +``` + +Du kannst das auch nur für **einen** Kalender ändern, indem du bei diesem im Bereich `events:` die Zeile `global: true` oder `global: false` einfügst. + +--- + +## 13. Mehrere Server mit einer Datenbank (MySQL) + +Nur nötig, wenn du z. B. ein BungeeCord-Netzwerk hast und alle Server dieselben Daten benutzen sollen. Für **einen** Server einfach ignorieren. + +In der `config.yml`: + +```yaml +mysql: + enable: true + host: "localhost" + port: 3306 + database: "adventskalender" + user: "root" + password: "DEIN_PASSWORT" +``` + +- Die Datenbank (`adventskalender`) musst du vorher selbst anlegen. Die Tabellen darin erstellt das Plugin allein. +- Danach den Server **neu starten** (oder `/ak admin reload`). +- In der Konsole muss stehen: **„MySQL-Verbindung erfolgreich hergestellt!“** +- Klappt es nicht, benutzt das Plugin automatisch die `data.yml` als Notlösung und schreibt eine Warnung in die Konsole. +- Sollte die Datenbank später kurz ausfallen, bekommen Spieler eine Meldung und es wird **nichts** vergeben. Sobald die Datenbank wieder da ist, geht alles weiter. +- Alle Kalender und Geburtstage werden in der Datenbank gespeichert. Daten aus einer älteren Version werden dabei automatisch übernommen. + +> **Wichtig:** Daten aus der `data.yml` werden **nicht automatisch** in die Datenbank kopiert. Schaltest du MySQL ein, fangen die Spieler dort bei null an. + +--- + +## 14. Platzhalter für Scoreboards, Hologramme & Co. + +Wenn das Plugin **PlaceholderAPI** installiert ist, kannst du diese Werte überall anzeigen, wo PlaceholderAPI funktioniert: + +### Für das gerade laufende Kalender-Event + +| Platzhalter | Zeigt | +|---|---| +| `%ak_event%` | Name des laufenden Kalenders (`advent`, `halloween`, `easter`) | +| `%ak_day%` | Welcher Tag des Kalenders heute ist (0 = Kalender läuft nicht) | +| `%ak_claimed%` | Wie viele Türchen der Spieler schon geöffnet hat | +| `%ak_remaining%` | Wie viele Türchen noch offen sind | +| `%ak_has_claimed_today%` | „Ja“ oder „Nein“: Hat der Spieler das heutige Türchen schon geöffnet? | +| `%ak_is_global%` | „Global“ oder „Spieler-basiert“ | + +### Für einen bestimmten Kalender + +Schreibe den Namen des Kalenders vor den Platzhalter: + +- `%ak_halloween_claimed%` +- `%ak_easter_day%` +- `%ak_advent_remaining%` + +### Geburtstag + +| Platzhalter | Zeigt | +|---|---| +| `%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 + +1. Server **ausschalten**. +2. Die **alte `.jar`-Datei** aus dem `plugins`-Ordner löschen. +3. Die **neue `.jar`-Datei** hineinkopieren. +4. Server starten. + +Deine **Daten** (`data.yml`) und deine **Einstellungen** bleiben erhalten. + +### Neue Einstellungen sichtbar machen + +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 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. + +--- + +## 16. Probleme und ihre Lösung + +| Problem | Lösung | +|---|---| +| **Plugin startet nicht** | Prüfe die Java-Version (**Java 21** nötig) und ob es Spigot/Paper ab 1.20 ist. Die genaue Fehlermeldung steht in der Server-Konsole. | +| **`/ak` tut nichts / „Unbekannter Befehl“** | Plugin wurde nicht geladen. Schau in die Konsole oder tippe `/plugins` und prüfe, ob „Adventskalender“ grün ist. | +| **„Dafür hast du keine Berechtigung“** | Der Spieler hat `adventskalender.use` (bzw. `adventskalender.admin`) nicht. Rechte prüfen. | +| **Alle Türchen sind gesperrt** | Es ist gerade nicht die Zeit des Kalenders. Prüfe das Datum oder teste mit `/ak admin test`. | +| **Im Menü steht „Key error: …“** | In deiner Sprachdatei fehlt ein Text. Siehe [Plugin aktualisieren](#15-plugin-aktualisieren), neue Sprachdatei erzeugen lassen. | +| **Ein Türchen lässt sich nicht öffnen, Meldung „falsch konfiguriert“** | In der `config.yml` ist bei diesem Tag ein Item- oder Verzauberungsname falsch geschrieben. Die Konsole sagt dir genau, wo. | +| **Eine Verzauberung fehlt am Geschenk** | Der Name in `type:` ist falsch (englisch und GROSS schreiben). Die Konsole zeigt eine Warnung. | +| **Kopf zeigt Steve statt Weihnachtsmotiv** | Der Server konnte die Textur nicht laden (Internet-/Mojang-Problem). Neustart versuchen. Bei Halloween/Ostern gibt es Items statt Köpfen, das ist normal. | +| **Meldung „Die Datenbank ist gerade nicht erreichbar“** | Nur mit MySQL. Prüfe Zugangsdaten und ob die Datenbank läuft. | +| **Nach dem Reload stimmt etwas nicht** | Server einmal komplett neu starten. | +| **Ein Spieler hat sich beim Geburtstag vertippt** | `/ak admin birthday set ` | +| **Spieler hat sein Geburtstagsgeschenk verloren / Fehler beim Abholen** | `/ak admin birthday reset `, dann darf er nochmal abholen. | +| **Kalender geht zur falschen Uhrzeit auf** | Zeitzone in der `config.yml` einstellen (siehe [Zeitzone](#zeitzone)). | + +Hilft nichts davon: Kopiere die **Fehlermeldung aus der Konsole** (nicht nur „geht nicht“) und frage auf der Seite des Plugins bei SpigotMC nach. + +--- + +## 17. Häufige Fragen + +**Kann ich Geschenke für jeden Tag selbst festlegen?** +Ja, siehe [Belohnungen ändern](#7-belohnungen-ändern). + +**Kann ich einen Kalender ausschalten, den ich nicht will?** +Ja: In der `config.yml` bei `events:` den Eintrag `enabled: false` setzen. + +**Kann ich mehr als 24, 31 oder 14 Türchen haben?** +Ja, bis zu 36 pro Kalender (Einstellung `days`). Du musst dann aber auch für jedes neue Türchen eine Belohnung eintragen und im Sprachfile einen Namen vergeben. + +**Was passiert, wenn der Kalender vorbei ist?** +Er bleibt noch `grace_days` Tage offen (Advent: bis zum 31.12., Ostern: 7 Tage), danach sind alle Türchen gesperrt, bis er im nächsten Jahr wieder startet. + +**Gehen die Türchen jedes Jahr von vorne los?** +Nein. Das Plugin merkt sich dauerhaft, was jemand geöffnet hat. Soll im nächsten Jahr wieder alles neu sein, benutze `/ak admin reset global` (oder `/ak admin reset advent global` nur für den Advent). **Achtung:** Das löscht die Daten für **alle**. + +**Sehen alle Spieler, wann ich Geburtstag habe?** +Nein. Das Datum sieht nur der Spieler selbst (und Admins in der Datenbank/Datei). Im Chat steht beim Abholen nur „Spieler feiert Geburtstag“, ohne Datum. + +**Funktioniert das Plugin mit Bedrock-Spielern (Geyser)?** +Das wurde nicht getestet. + +**Ich habe `data.yml` gelöscht, was jetzt?** +Dann haben alle Spieler wieder alle Türchen und Geburtstage sind weg. Spiele vorher eine Sicherung ein, wenn du eine hast. Mach vor Änderungen am Server grundsätzlich eine Sicherheitskopie des Plugin-Ordners. + +--- + +## 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::`, `cracker::`, `crystal:` und `present:` (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 [id] [Anzahl]` | `christmasplus.cracker.give` | + +Rechtsklick öffnet einen Cracker. Die Belohnungen stehen unter `crackers..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 [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 [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 ` 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 ` 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 ` | 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..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.` 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 ` 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! 🎄🎃🐰🎂* diff --git a/santa_skin.png b/santa_skin.png new file mode 100644 index 0000000000000000000000000000000000000000..fb0e64cffedbe1d28ac81d51e1b1809088e76ad6 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVBF{F;uumf=k4vaL5Cd#*gyP# z8T-TA*FV4J2g^f_LxL^J9PR?= z7A?Bv-+y0w-tjJHfpZ1xKgT>iS32{k^B;~4(#1OYRqR*%^u$m1x9`4SHT`+znw2?; zvIRATyCe8^eZ5e1^`^?qd&k#Hl+510|8HCUQggoPFC%@HU*4JB@}u&P;Pd_i54P?2 zdsFs8#AT1X$CEkCF694Ml^ZG8VtlbBF4_FbYV)&mFMK42vvFO zIaNW%(W0q9t?JD3(vbUJrkkZ@w*;tMw>pfRk9?KVI*`RFx`b$wWttb3FapvQVJD(P*uTHpm z{_mrJo1y_;noFkyfe5WxC#@6i-4`#o|9`gtByb9tpZu+LjQTZSQLU)}DbR|TL;n6| oiq3k;y61nnB8Piw_=24IN^k literal 0 HcmV?d00001 diff --git a/santa_skin_user.png b/santa_skin_user.png new file mode 100644 index 0000000000000000000000000000000000000000..8c8a8fd76d878bf34fe9fa1a3e763cc78eff6e33 GIT binary patch literal 391 zcmV;20eJq2P)`k3?Rah^KDd+ zbbyX<+fy5<;{GiA*-mt_ly~VlzaUAe&{KdJH*~rc~BU + 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; } diff --git a/src/main/java/de/mviper/adventskalender/AdventListener.java b/src/main/java/de/mviper/adventskalender/AdventListener.java index bb77439..fdbea38 100644 --- a/src/main/java/de/mviper/adventskalender/AdventListener.java +++ b/src/main/java/de/mviper/adventskalender/AdventListener.java @@ -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))); } } diff --git a/src/main/java/de/mviper/adventskalender/Adventskalender.java b/src/main/java/de/mviper/adventskalender/Adventskalender.java index bce8c3e..2d1a57a 100644 --- a/src/main/java/de/mviper/adventskalender/Adventskalender.java +++ b/src/main/java/de/mviper/adventskalender/Adventskalender.java @@ -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(); } diff --git a/src/main/java/de/mviper/adventskalender/christmas/CalendarPerks.java b/src/main/java/de/mviper/adventskalender/christmas/CalendarPerks.java new file mode 100644 index 0000000..706614f --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/CalendarPerks.java @@ -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 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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java b/src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java new file mode 100644 index 0000000..6ea661d --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java @@ -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 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 onTabComplete(CommandSender s, Command cmd, String label, String[] a) { + List 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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/ChristmasExtras.java b/src/main/java/de/mviper/adventskalender/christmas/ChristmasExtras.java new file mode 100644 index 0000000..c2aa80b --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/ChristmasExtras.java @@ -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.<Tag> der config.yml moeglich: + *
+ * 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
+ * 
+ */ +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> 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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java b/src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java new file mode 100644 index 0000000..249633c --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java @@ -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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java b/src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java new file mode 100644 index 0000000..bbae9e3 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java @@ -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 rawList(String key) { + List l = msgs == null ? List.of() : msgs.getStringList("messages." + key); + if (l.isEmpty()) l = cfg.getStringList("messages." + key); + List 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 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 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 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 blocked = cfg.getStringList("snow.disable-in-worlds"); + boolean regionsOnly = cfg.getBoolean("snow.enable-only-in-regions", false) + && Bukkit.getPluginManager().getPlugin("WorldGuard") != null; + List 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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/CookieManager.java b/src/main/java/de/mviper/adventskalender/christmas/CookieManager.java new file mode 100644 index 0000000..d391f14 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/CookieManager.java @@ -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 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 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()); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/CountdownManager.java b/src/main/java/de/mviper/adventskalender/christmas/CountdownManager.java new file mode 100644 index 0000000..6526618 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/CountdownManager.java @@ -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); + } + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/CrackerManager.java b/src/main/java/de/mviper/adventskalender/christmas/CrackerManager.java new file mode 100644 index 0000000..e4d4340 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/CrackerManager.java @@ -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 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 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> 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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/DecoManager.java b/src/main/java/de/mviper/adventskalender/christmas/DecoManager.java new file mode 100644 index 0000000..639d74b --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/DecoManager.java @@ -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> 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 before = new ArrayList<>(); + final List 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 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 stack = undo.get(p.getUniqueId()); + while (stack.size() > 30) stack.removeLast(); + p.sendMessage(mgr.msg("deco-placed")); + } + + public void undoLast(Player p) { + Deque 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 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); + } + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/GreetingManager.java b/src/main/java/de/mviper/adventskalender/christmas/GreetingManager.java new file mode 100644 index 0000000..1ca6ddc --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/GreetingManager.java @@ -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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/HatManager.java b/src/main/java/de/mviper/adventskalender/christmas/HatManager.java new file mode 100644 index 0000000..adc9768 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/HatManager.java @@ -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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/LoginStreakManager.java b/src/main/java/de/mviper/adventskalender/christmas/LoginStreakManager.java new file mode 100644 index 0000000..0b85be1 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/LoginStreakManager.java @@ -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 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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java b/src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java new file mode 100644 index 0000000..da0f9bc --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java @@ -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 req = new EnumMap<>(Material.class); + final Map spent = new EnumMap<>(Material.class); + final Set blocks = new HashSet<>(); + final Set 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 trees = new ArrayList<>(); + private final Map blockMap = new HashMap<>(); + private final Map presentMap = new HashMap<>(); + private final Map destroyers = new HashMap<>(); + private final File file; + private final File giftFile; + private final NamespacedKey crystalKey; + private final List 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 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 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 structure(Level lv) { + Map 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 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 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 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; + } + } + } + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/MannequinSupport.java b/src/main/java/de/mviper/adventskalender/christmas/MannequinSupport.java new file mode 100644 index 0000000..bdf2085 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/MannequinSupport.java @@ -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); })); + } + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/MarketManager.java b/src/main/java/de/mviper/adventskalender/christmas/MarketManager.java new file mode 100644 index 0000000..139f572 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/MarketManager.java @@ -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 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 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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/PortalManager.java b/src/main/java/de/mviper/adventskalender/christmas/PortalManager.java new file mode 100644 index 0000000..94fc8d8 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/PortalManager.java @@ -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 portals = new ArrayList<>(); + private final Set blocks = new HashSet<>(); + private final Map entering = new HashMap<>(); + private final Map cooldown = new HashMap<>(); + private final Map 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 parts() { + List 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 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 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 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 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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/QuizManager.java b/src/main/java/de/mviper/adventskalender/christmas/QuizManager.java new file mode 100644 index 0000000..bec281d --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/QuizManager.java @@ -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 answers = List.of(); + private volatile List> 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> 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 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> 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")); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java b/src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java new file mode 100644 index 0000000..2baf81e --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java @@ -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 trail = new HashMap<>(); + private final SantaMelody melody; + private long lastDropMsg; + + // Schlitten-Anflug + private boolean flying; + private BukkitTask flightTask; + private final List 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> 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> 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 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 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(); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java b/src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java new file mode 100644 index 0000000..583db10 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java @@ -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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/SnowballGame.java b/src/main/java/de/mviper/adventskalender/christmas/SnowballGame.java new file mode 100644 index 0000000..7e2f5d3 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/SnowballGame.java @@ -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 points = new HashMap<>(); + private final Map 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> 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> ranking() { + List> list = new ArrayList<>(points.entrySet()); + list.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); + return list; + } + + public int rank(UUID id) { + List> r = ranking(); + for (int i = 0; i < r.size(); i++) if (r.get(i).getKey().equals(id)) return i + 1; + return 0; + } + + public List topLines(int n) { + List out = new ArrayList<>(); + List> 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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java b/src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java new file mode 100644 index 0000000..5a56af2 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java @@ -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 nextGift = new HashMap<>(); + private BukkitTask task; + private final Set 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 playerSnowmen(UUID owner) { + List 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 gifts = mgr.cfg().getStringList("snowman.player.gifts"); + + Map> 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 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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/StockingFlight.java b/src/main/java/de/mviper/adventskalender/christmas/StockingFlight.java new file mode 100644 index 0000000..d4d8218 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/StockingFlight.java @@ -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 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> 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 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 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 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; } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/StockingManager.java b/src/main/java/de/mviper/adventskalender/christmas/StockingManager.java new file mode 100644 index 0000000..61659f8 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/StockingManager.java @@ -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 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 placed = new HashMap<>(); + private final Map 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 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> 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); } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/WichtelnManager.java b/src/main/java/de/mviper/adventskalender/christmas/WichtelnManager.java new file mode 100644 index 0000000..b468f5b --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/WichtelnManager.java @@ -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 participants = new LinkedHashMap<>(); + private final Map assign = new LinkedHashMap<>(); + private final Map gifts = new LinkedHashMap<>(); + private final Map> 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 givers = new ArrayList<>(participants.keySet()); + List 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 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 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> maps = new ArrayList<>(); + for (Pending p : list) { + Map 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()); + } + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/WishManager.java b/src/main/java/de/mviper/adventskalender/christmas/WishManager.java new file mode 100644 index 0000000..cf3bb95 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/WishManager.java @@ -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 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 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 openOf(UUID owner) { + List 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 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 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 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 allOpen() { + List list = new ArrayList<>(); + for (Wish w : wishes.values()) if (!w.done) list.add(w); + return list; + } + + public void openMenu(Player admin, int page) { + List 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 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 wrap(String text, int width) { + List 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); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/WorldGuardHook.java b/src/main/java/de/mviper/adventskalender/christmas/WorldGuardHook.java new file mode 100644 index 0000000..c3db1a5 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/WorldGuardHook.java @@ -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 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; + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/WreathManager.java b/src/main/java/de/mviper/adventskalender/christmas/WreathManager.java new file mode 100644 index 0000000..fb31cf2 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/WreathManager.java @@ -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 wreaths = new ArrayList<>(); + private final Set 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))); + } + } + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/XmasExpansion.java b/src/main/java/de/mviper/adventskalender/christmas/XmasExpansion.java new file mode 100644 index 0000000..b9aadd7 --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/XmasExpansion.java @@ -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. + *
+ * %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)
+ * 
+ */ +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; + } + } +} diff --git a/src/main/resources/christmas.yml b/src/main/resources/christmas.yml new file mode 100644 index 0000000..6d67c2e --- /dev/null +++ b/src/main/resources/christmas.yml @@ -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::" 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.) +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 oder als Item "cookie::" +# 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 . 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: [] diff --git a/src/main/resources/christmas_messages_de.yml b/src/main/resources/christmas_messages_de.yml new file mode 100644 index 0000000..4e27096 --- /dev/null +++ b/src/main/resources/christmas_messages_de.yml @@ -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: + - "&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 [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 [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 | list | clear &7- Wunschzettel" + - "&2/xmas wishes &7- Wunschzettel ansehen (Admin)" + - "&2/xmas quiz start [Nr] | skip &7- Weihnachtsquiz (Admin)" + - "&2/xmas market spawn | 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 [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 [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 [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 | 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 | 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%" diff --git a/src/main/resources/christmas_messages_en.yml b/src/main/resources/christmas_messages_en.yml new file mode 100644 index 0000000..1671ccc --- /dev/null +++ b/src/main/resources/christmas_messages_en.yml @@ -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: + - "&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 [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 [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 | 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 | 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 [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 [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 [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 | 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 | 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%"