Upload via GUI (52 Dateien)

This commit is contained in:
2026-11-12 16:59:18 +00:00
parent 208ec12e0c
commit 6dd60de671
37 changed files with 7789 additions and 559 deletions
+879 -557
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

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