From 950c1fbf470ef59895a3778f1f157c531caa6ddd Mon Sep 17 00:00:00 2001 From: M_Viper Date: Fri, 13 Nov 2026 17:46:26 +0000 Subject: [PATCH] Upload via GUI (53 Dateien) --- .../christmas/ChristmasCommand.java | 45 +- .../christmas/ChristmasListener.java | 11 + .../christmas/ChristmasManager.java | 47 +- .../christmas/CookieManager.java | 156 +++--- .../christmas/MagicTreeManager.java | 98 +++- .../adventskalender/christmas/MarketLife.java | 481 ++++++++++++++++++ .../adventskalender/christmas/SantaEvent.java | 90 +++- .../christmas/SantaMelody.java | 41 +- .../christmas/SnowmanManager.java | 4 +- src/main/resources/christmas.yml | 106 +++- src/main/resources/christmas_messages_de.yml | 15 +- src/main/resources/christmas_messages_en.yml | 15 +- 12 files changed, 986 insertions(+), 123 deletions(-) create mode 100644 src/main/java/de/mviper/adventskalender/christmas/MarketLife.java diff --git a/src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java b/src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java index 6ea661d..8bae7a2 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java +++ b/src/main/java/de/mviper/adventskalender/christmas/ChristmasCommand.java @@ -32,7 +32,7 @@ public final class ChristmasCommand implements CommandExecutor, TabCompleter { } switch (a[0].toLowerCase()) { case "help" -> mgr.rawList("help").forEach(s::sendMessage); - case "snow" -> snow(s); + case "snow" -> snow(s, a); case "santa" -> santa(s, a); case "snowman" -> snowman(s, a); case "countdown" -> { @@ -295,6 +295,15 @@ public final class ChristmasCommand implements CommandExecutor, TabCompleter { if (dest == null) { s.sendMessage(mgr.msg("portal-no-market")); return; } p.teleport(dest); } + case "santa" -> { + String op = a.length > 2 ? a[2].toLowerCase() : ""; + switch (op) { + case "set" -> { mgr.marketLife().setSanta(p.getLocation()); s.sendMessage(mgr.msg("market-santa-set")); } + case "remove" -> { mgr.marketLife().removeSantaSpot(); s.sendMessage(mgr.msg("market-santa-removed")); } + case "reset" -> { mgr.marketLife().resetClaims(); s.sendMessage(mgr.msg("market-santa-reset")); } + default -> s.sendMessage(mgr.msg("market-usage")); + } + } 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; } @@ -375,13 +384,36 @@ public final class ChristmasCommand implements CommandExecutor, TabCompleter { // ---------- Snow ---------- - private void snow(CommandSender s) { + private void snow(CommandSender s, String[] a) { + if (a.length > 1 && a[1].equalsIgnoreCase("world")) { snowWorld(s, a); return; } 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")); } + /** /xmas snow world add|remove|list [Welt]: Schnee-Blacklist der Welten. */ + private void snowWorld(CommandSender s, String[] a) { + if (!admin(s)) return; + String op = a.length > 2 ? a[2].toLowerCase() : "list"; + if (op.equals("list")) { + List l = mgr.snowBlockedWorlds(); + s.sendMessage(mgr.msg("snow-world-list", "%worlds%", l.isEmpty() ? "-" : String.join(", ", l))); + return; + } + if (a.length < 4 || !(op.equals("add") || op.equals("remove"))) { s.sendMessage(mgr.msg("snow-world-usage")); return; } + String name = a[3]; + org.bukkit.World w = Bukkit.getWorld(name); + if (op.equals("add")) { + if (w == null) { s.sendMessage(mgr.msg("snow-world-unknown", "%world%", name)); return; } + mgr.setSnowWorldBlocked(w.getName(), true); + s.sendMessage(mgr.msg("snow-world-added", "%world%", w.getName())); + } else { + mgr.setSnowWorldBlocked(w != null ? w.getName() : name, false); + s.sendMessage(mgr.msg("snow-world-removed", "%world%", w != null ? w.getName() : name)); + } + } + // ---------- Santa ---------- private void santa(CommandSender s, String[] a) { @@ -487,6 +519,15 @@ public final class ChristmasCommand implements CommandExecutor, TabCompleter { 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 == 3 && a[0].equalsIgnoreCase("market") && a[1].equalsIgnoreCase("santa")) { + out.addAll(Arrays.asList("set", "remove", "reset")); + } else if (a.length == 2 && a[0].equalsIgnoreCase("snow")) { + out.add("world"); + } else if (a.length == 3 && a[0].equalsIgnoreCase("snow") && a[1].equalsIgnoreCase("world")) { + out.addAll(Arrays.asList("add", "remove", "list")); + } else if (a.length == 4 && a[0].equalsIgnoreCase("snow") && a[1].equalsIgnoreCase("world")) { + if (a[2].equalsIgnoreCase("remove")) out.addAll(mgr.snowBlockedWorlds()); + else Bukkit.getWorlds().forEach(w -> out.add(w.getName())); } 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")) { diff --git a/src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java b/src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java index 249633c..8330580 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java +++ b/src/main/java/de/mviper/adventskalender/christmas/ChristmasListener.java @@ -235,6 +235,17 @@ public final class ChristmasListener implements Listener { if (mgr.quiz().onChat(e.getPlayer(), e.getMessage())) e.setCancelled(true); } + @EventHandler(priority = EventPriority.HIGH) + public void onMarketFigureClick(org.bukkit.event.player.PlayerInteractEntityEvent e) { + if (e.getHand() != EquipmentSlot.HAND) return; + if (mgr.marketLife().onInteract(e.getPlayer(), e.getRightClicked())) e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onMarketFigureManipulate(org.bukkit.event.player.PlayerArmorStandManipulateEvent e) { + if (mgr.marketLife().isFigure(e.getRightClicked())) e.setCancelled(true); + } + @EventHandler(ignoreCancelled = true) public void onSnowmanTrail(org.bukkit.event.block.EntityBlockFormEvent e) { mgr.snowmen().onSnowForm(e); diff --git a/src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java b/src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java index bbae9e3..14afe8f 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java +++ b/src/main/java/de/mviper/adventskalender/christmas/ChristmasManager.java @@ -44,6 +44,7 @@ public final class ChristmasManager { private QuizManager quiz; private WishManager wishes; private MarketManager market; + private MarketLife marketLife; private PortalManager portals; private YamlConfiguration msgs; @@ -73,6 +74,7 @@ public final class ChristmasManager { instance.quiz = new QuizManager(instance); instance.wishes = new WishManager(instance); instance.market = new MarketManager(instance); + instance.marketLife = new MarketLife(instance); instance.portals = new PortalManager(instance); instance.crackers.reload(); instance.trees.load(); @@ -87,6 +89,7 @@ public final class ChristmasManager { instance.quiz.start(); instance.wishes.load(); instance.market.load(); + instance.marketLife.start(); instance.portals.load(); instance.startSnow(); if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { @@ -111,6 +114,7 @@ public final class ChristmasManager { instance.trees.save(); instance.trees.shutdown(); instance.snowmen.stop(); + instance.marketLife.stop(); instance.countdown.stop(); instance.wichteln.save(); instance.wreaths.stop(); @@ -132,6 +136,7 @@ public final class ChristmasManager { instance.countdown.start(); instance.quiz.start(); instance.market.refreshAll(); + instance.marketLife.start(); instance.startSnow(); } @@ -168,6 +173,7 @@ public final class ChristmasManager { public QuizManager quiz() { return quiz; } public WishManager wishes() { return wishes; } public MarketManager market() { return market; } + public MarketLife marketLife() { return marketLife; } public PortalManager portals() { return portals; } /** Baum-, Geschenk- und Kranzbloecke, die vor Zerstoerung geschuetzt sind. */ @@ -316,20 +322,57 @@ public final class ChristmasManager { return now; } + /** + * Entfernt eine Schneeschicht und setzt den Block darunter zurueck: Gras/Myzel/Podsol behalten sonst + * den Zustand "verschneit" und sehen grau-weiss aus, weil ohne Physik-Update niemand sie aktualisiert. + */ + public static void removeSnowLayer(org.bukkit.block.Block b) { + if (b.getType() != org.bukkit.Material.SNOW) return; + b.setType(org.bukkit.Material.AIR, false); + org.bukkit.block.Block below = b.getRelative(0, -1, 0); + if (below.getBlockData() instanceof org.bukkit.block.data.Snowable s && s.isSnowy()) { + s.setSnowy(false); + below.setBlockData(s, false); + } + } + + /** Steht die Welt auf der Schnee-Blacklist (snow.disable-in-worlds, Gross-/Kleinschreibung egal)? */ + public boolean isSnowWorldBlocked(String world) { + for (String b : cfg.getStringList("snow.disable-in-worlds")) if (b.equalsIgnoreCase(world)) return true; + return false; + } + + public List snowBlockedWorlds() { + return new java.util.ArrayList<>(cfg.getStringList("snow.disable-in-worlds")); + } + + /** Setzt eine Welt auf die Schnee-Blacklist bzw. nimmt sie herunter und speichert christmas.yml. */ + public void setSnowWorldBlocked(String world, boolean blocked) { + List list = snowBlockedWorlds(); + list.removeIf(x -> x.equalsIgnoreCase(world)); + if (blocked) list.add(world); + cfg.set("snow.disable-in-worlds", list); + try { + cfg.save(new File(plugin.getDataFolder(), "christmas.yml")); + } catch (java.io.IOException e) { + plugin.getLogger().warning("christmas.yml konnte nicht gespeichert werden: " + e.getMessage()); + } + } + private void startSnow() { if (snowTask != null) snowTask.cancel(); snowTask = null; if (!cfg.getBoolean("snow.enable", true)) return; int interval = Math.max(1, cfg.getInt("snow.display-task-interval", 2)); int level = Math.max(1, Math.min(5, cfg.getInt("snow.level", 3))); - List blocked = cfg.getStringList("snow.disable-in-worlds"); boolean regionsOnly = cfg.getBoolean("snow.enable-only-in-regions", false) && Bukkit.getPluginManager().getPlugin("WorldGuard") != null; List regions = cfg.getStringList("snow.regions"); snowTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { for (Player p : Bukkit.getOnlinePlayers()) { World w = p.getWorld(); - if (blocked.contains(w.getName()) || !isSnowEnabledFor(p)) continue; + if (isSnowWorldBlocked(w.getName()) || !isSnowEnabledFor(p)) continue; + if (!marketLife.allowsSnowAt(p.getLocation())) continue; var loc = p.getLocation(); if (regionsOnly && !WorldGuardHook.inAnyRegion(loc, regions)) continue; for (int i = 0; i < level * 4; i++) { diff --git a/src/main/java/de/mviper/adventskalender/christmas/CookieManager.java b/src/main/java/de/mviper/adventskalender/christmas/CookieManager.java index d391f14..4284e87 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/CookieManager.java +++ b/src/main/java/de/mviper/adventskalender/christmas/CookieManager.java @@ -1,75 +1,81 @@ -package de.mviper.adventskalender.christmas; - -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.persistence.PersistentDataType; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -/** Plaetzchen und Lebkuchen: Essbare Items mit Traenke-Effekten. */ -public final class CookieManager { - private final ChristmasManager mgr; - private final NamespacedKey key; - - CookieManager(ChristmasManager mgr) { - this.mgr = mgr; - this.key = new NamespacedKey(mgr.plugin(), "cookie_id"); - } - - public Set ids() { - ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies"); - return s == null ? new LinkedHashSet<>() : s.getKeys(false); - } - - public ItemStack create(String id, int amount) { - ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies." + id); - if (s == null) return null; - Material m = Material.matchMaterial(s.getString("material", "COOKIE")); - if (m == null || !m.isItem() || !m.isEdible()) m = Material.COOKIE; - ItemStack it = new ItemStack(m, Math.max(1, amount)); - ItemMeta meta = it.getItemMeta(); - if (meta == null) return it; - meta.setDisplayName(ChristmasManager.color(s.getString("name", "&6" + id))); - List lore = new ArrayList<>(); - for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l)); - meta.setLore(lore); - meta.setEnchantmentGlintOverride(true); - meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, id); - it.setItemMeta(meta); - return it; - } - - public String idOf(ItemStack it) { - if (it == null || !it.hasItemMeta()) return null; - return it.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING); - } - - /** Wendet die Effekte eines gegessenen Plaetzchens an. */ - public void eat(Player p, String id) { - for (String spec : mgr.cfg().getStringList("cookies." + id + ".effects")) { - String[] parts = spec.split(":"); - PotionEffectType type = effect(parts[0].trim()); - if (type == null) continue; - int seconds = parts.length > 1 ? ChristmasManager.toInt(parts[1], 30) : 30; - int amp = parts.length > 2 ? ChristmasManager.toInt(parts[2], 0) : 0; - p.addPotionEffect(new PotionEffect(type, seconds * 20, Math.max(0, amp))); - } - } - - @SuppressWarnings("deprecation") - private static PotionEffectType effect(String name) { - String n = name.toLowerCase(); - PotionEffectType t = Registry.EFFECT.get(NamespacedKey.minecraft(n)); - return t != null ? t : PotionEffectType.getByName(name.toUpperCase()); - } -} +package de.mviper.adventskalender.christmas; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** Plaetzchen und Lebkuchen: Essbare Items mit Traenke-Effekten. */ +public final class CookieManager { + private final ChristmasManager mgr; + private final NamespacedKey key; + + CookieManager(ChristmasManager mgr) { + this.mgr = mgr; + this.key = new NamespacedKey(mgr.plugin(), "cookie_id"); + } + + public Set ids() { + ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies"); + return s == null ? new LinkedHashSet<>() : s.getKeys(false); + } + + public ItemStack create(String id, int amount) { + ConfigurationSection s = mgr.cfg().getConfigurationSection("cookies." + id); + if (s == null) return null; + Material m = Material.matchMaterial(s.getString("material", "COOKIE")); + if (m == null || !m.isItem() || !m.isEdible()) m = Material.COOKIE; + ItemStack it = new ItemStack(m, Math.max(1, amount)); + ItemMeta meta = it.getItemMeta(); + if (meta == null) return it; + meta.setDisplayName(ChristmasManager.color(s.getString("name", "&6" + id))); + List lore = new ArrayList<>(); + for (String l : s.getStringList("lore")) lore.add(ChristmasManager.color(l)); + meta.setLore(lore); + meta.setEnchantmentGlintOverride(true); + meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, id); + it.setItemMeta(meta); + return it; + } + + public String idOf(ItemStack it) { + if (it == null || !it.hasItemMeta()) return null; + return it.getItemMeta().getPersistentDataContainer().get(key, PersistentDataType.STRING); + } + + /** Wendet die Effekte eines gegessenen Plaetzchens an. */ + public void eat(Player p, String id) { + if (mgr.cfg().getBoolean("cookies." + id + ".steam", false)) { + var at = p.getEyeLocation().add(p.getLocation().getDirection().multiply(0.4)); + p.getWorld().spawnParticle(org.bukkit.Particle.CLOUD, at, 14, 0.25, 0.2, 0.25, 0.02); + p.getWorld().spawnParticle(org.bukkit.Particle.HEART, at.clone().add(0, 0.4, 0), 3, 0.3, 0.2, 0.3, 0); + p.getWorld().playSound(p.getLocation(), org.bukkit.Sound.BLOCK_BREWING_STAND_BREW, 0.6f, 1.4f); + } + for (String spec : mgr.cfg().getStringList("cookies." + id + ".effects")) { + String[] parts = spec.split(":"); + PotionEffectType type = effect(parts[0].trim()); + if (type == null) continue; + int seconds = parts.length > 1 ? ChristmasManager.toInt(parts[1], 30) : 30; + int amp = parts.length > 2 ? ChristmasManager.toInt(parts[2], 0) : 0; + p.addPotionEffect(new PotionEffect(type, seconds * 20, Math.max(0, amp))); + } + } + + @SuppressWarnings("deprecation") + private static PotionEffectType effect(String name) { + String n = name.toLowerCase(); + PotionEffectType t = Registry.EFFECT.get(NamespacedKey.minecraft(n)); + return t != null ? t : PotionEffectType.getByName(name.toUpperCase()); + } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java b/src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java index da0f9bc..75c2827 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java +++ b/src/main/java/de/mviper/adventskalender/christmas/MagicTreeManager.java @@ -11,6 +11,8 @@ import org.bukkit.Sound; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.Skull; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.Rotatable; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Leaves; import org.bukkit.configuration.ConfigurationSection; @@ -318,6 +320,9 @@ public final class MagicTreeManager { Leaves leaves = (Leaves) Material.SPRUCE_LEAVES.createBlockData(); leaves.setPersistent(true); Material[] swag = {Material.RED_CONCRETE, Material.GOLD_BLOCK, Material.SEA_LANTERN, Material.BLUE_CONCRETE}; + boolean heads = ornamentHeads(); + int perRing = Math.max(1, Math.min(12, mgr.cfg().getInt("magic-tree.ornaments.per-ring", 3))); + int layerStep = Math.max(1, Math.min(6, mgr.cfg().getInt("magic-tree.ornaments.every-nth-layer", 2))); 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++) { @@ -333,8 +338,9 @@ public final class MagicTreeManager { 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()); + if (lv != Level.SMALL_TREE && edge && isOrnamentSpot(dx, dz, y, r, perRing, layerStep)) { + m.put(new Vector(dx, y, dz), heads ? Material.PLAYER_HEAD.createBlockData() + : swag[Math.floorMod(dx + dz + y, swag.length)].createBlockData()); } else { m.put(new Vector(dx, y, dz), leaves); } @@ -344,6 +350,19 @@ public final class MagicTreeManager { return m; } + /** + * Schmuck gleichmaessig verteilt: pro Ring per-ring Stueck, nur jede n-te Ebene, jede Ebene leicht + * versetzt (ergibt eine Spirale) statt zufaellig an einer Seite. + */ + private static boolean isOrnamentSpot(int dx, int dz, int y, int r, int perRing, int layerStep) { + if (Math.floorMod(y, layerStep) != 0) return false; + double a = (Math.atan2(dz, dx) + Math.PI) / (2 * Math.PI); // 0..1 rund um den Stamm + double phase = a * perRing + y * 0.45; + double frac = phase - Math.floor(phase); + int edgePerSector = Math.max(1, (int) Math.round(4.0 * r / perRing)); + return frac < 1.0 / edgePerSector; + } + private boolean canGrow(Tree t, Level lv) { Location base = t.loc(); if (base == null) return false; @@ -373,13 +392,65 @@ public final class MagicTreeManager { 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); + BlockData bd = e.getValue(); + if (bd.getMaterial() == Material.PLAYER_HEAD && bd.clone() instanceof Rotatable rot) { + BlockFace[] faces = BlockFace.values(); + rot.setRotation(rotationFaces()[ChristmasManager.RANDOM.nextInt(rotationFaces().length)]); + b.setBlockData(rot, false); + List urls = headUrls(); + if (!urls.isEmpty()) applyHead(b, urls.get(ChristmasManager.RANDOM.nextInt(urls.size()))); + } else { + b.setBlockData(bd, false); + } t.blocks.add(key(b)); blockMap.put(key(b), t); } t.indexed = true; } + private static BlockFace[] rotationFaces() { + return new BlockFace[]{BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST, + BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST, + BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST, + BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST}; + } + + private static final java.util.Set ORNAMENTS = java.util.EnumSet.of(Material.RED_CONCRETE, + Material.GOLD_BLOCK, Material.SEA_LANTERN, Material.BLUE_CONCRETE, Material.PLAYER_HEAD); + + /** Schmuck oder Nadeln: aeltere Baeume haben die Verteilung anders, beides gehoert zum Baum. */ + private static boolean isTreeDecor(Material m) { + return ORNAMENTS.contains(m) || m == Material.SPRUCE_LEAVES; + } + + /** Deko-Koepfe statt Betonbloecken (magic-tree.ornaments.mode = heads, wenn Texturen eingetragen sind). */ + private boolean ornamentHeads() { + return "heads".equalsIgnoreCase(mgr.cfg().getString("magic-tree.ornaments.mode", "heads")) && !headUrls().isEmpty(); + } + + /** Texturen der Deko-Koepfe als URL (auch Base64-"Value" von minecraft-heads.com ist erlaubt). */ + private List headUrls() { + List out = new ArrayList<>(); + for (String s : mgr.cfg().getStringList("magic-tree.ornaments.heads")) { + String u = textureUrl(s); + if (u != null) out.add(u); + } + return out; + } + + private static String textureUrl(String s) { + if (s == null) return null; + s = s.trim(); + if (s.startsWith("http")) return s; + try { + String json = new String(java.util.Base64.getDecoder().decode(s), java.nio.charset.StandardCharsets.UTF_8); + java.util.regex.Matcher m = java.util.regex.Pattern.compile("\"url\"\\s*:\\s*\"([^\"]+)\"").matcher(json); + return m.find() ? m.group(1) : null; + } catch (IllegalArgumentException e) { + return null; + } + } + /** 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); @@ -388,7 +459,8 @@ public final class MagicTreeManager { 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()) { + Material expected = e.getValue().getMaterial(); + if (b.getType() == expected || (isTreeDecor(expected) && isTreeDecor(b.getType()))) { t.blocks.add(key(b)); blockMap.put(key(b), t); } @@ -556,13 +628,22 @@ public final class MagicTreeManager { // ================= Geschenke ================= + /** Ab welcher Stufe Geschenke entstehen (magic-tree.presents-from-level, Standard SMALL_TREE). */ + private Level presentsFromLevel() { + String s = mgr.cfg().getString("magic-tree.presents-from-level", "SMALL_TREE"); + try { return Level.valueOf(s.trim().toUpperCase()); } catch (Exception e) { return Level.SMALL_TREE; } + } + private long cooldown(Level lv) { - return mgr.cfg().getLong("magic-tree.levels." + lv.key + ".gift-cooldown", -1); + if (lv.ordinal() < presentsFromLevel().ordinal()) return -1; + long cd = mgr.cfg().getLong("magic-tree.levels." + lv.key + ".gift-cooldown", -1); + // Ab der eingestellten Stufe gibt es immer Geschenke, auch wenn dort -1 steht + return cd > 0 ? cd : Math.max(30, mgr.cfg().getLong("magic-tree.default-gift-cooldown", 300)); } public int spawnPresentsEverywhere() { int n = 0; - for (Tree t : new ArrayList<>(trees)) if (t.level != Level.SAPLING && spawnPresent(t, true)) n++; + for (Tree t : new ArrayList<>(trees)) if (t.level.ordinal() >= presentsFromLevel().ordinal() && spawnPresent(t, true)) n++; return n; } @@ -596,6 +677,11 @@ public final class MagicTreeManager { void setSkin(Block b, String url) { b.setType(Material.PLAYER_HEAD, false); + applyHead(b, url); + } + + /** Setzt die Textur auf einen vorhandenen Kopf-Block (Drehung bleibt erhalten). */ + private void applyHead(Block b, String url) { if (!(b.getState() instanceof Skull sk)) return; try { PlayerProfile prof = Bukkit.createPlayerProfile(UUID.nameUUIDFromBytes(url.getBytes()), "present"); diff --git a/src/main/java/de/mviper/adventskalender/christmas/MarketLife.java b/src/main/java/de/mviper/adventskalender/christmas/MarketLife.java new file mode 100644 index 0000000..1bfac3d --- /dev/null +++ b/src/main/java/de/mviper/adventskalender/christmas/MarketLife.java @@ -0,0 +1,481 @@ +package de.mviper.adventskalender.christmas; + +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.SoundCategory; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.EulerAngle; + +import java.io.File; +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Leben auf dem Weihnachtsmarkt: Begruessung, Musik, Santa mit Tagesgeschenk und herumlaufende Wichtel. + * Alles bezieht sich auf den Marktpunkt (/xmas market set). + */ +public final class MarketLife { + private static final class Elf { + LivingEntity entity; + Location loc; + Location target; + int pause = 20; + float yaw; + double phase; + } + + private final ChristmasManager mgr; + private final File file; + private final NamespacedKey santaKey; + private final NamespacedKey elfKey; + private final SantaMelody music; + + private Location santaLoc; + private final Map claimed = new HashMap<>(); + private LivingEntity santa; + private final List elves = new ArrayList<>(); + + private final Set inside = new HashSet<>(); + private final Map welcomeAt = new HashMap<>(); + private final Map hintAt = new HashMap<>(); + private List audience = new ArrayList<>(); + private long musicNext; + private int tune; + private int tick; + + private BukkitTask lifeTask; + private BukkitTask moveTask; + + MarketLife(ChristmasManager mgr) { + this.mgr = mgr; + this.file = new File(mgr.plugin().getDataFolder(), "market-life.yml"); + this.santaKey = new NamespacedKey(mgr.plugin(), "market_santa"); + this.elfKey = new NamespacedKey(mgr.plugin(), "market_elf"); + this.music = new SantaMelody(mgr); + } + + // ---------- Start / Stop / Speichern ---------- + + void start() { + stop(); + load(); + lifeTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::life, 40L, 20L); + moveTask = Bukkit.getScheduler().runTaskTimer(mgr.plugin(), this::move, 41L, 1L); + } + + void stop() { + if (lifeTask != null) lifeTask.cancel(); + if (moveTask != null) moveTask.cancel(); + lifeTask = null; + moveTask = null; + music.stop(); + removeSanta(); + removeElves(); + inside.clear(); + } + + private void load() { + santaLoc = null; + claimed.clear(); + if (!file.exists()) return; + YamlConfiguration y = YamlConfiguration.loadConfiguration(file); + World w = Bukkit.getWorld(y.getString("santa.world", "")); + if (w != null) { + santaLoc = new Location(w, y.getDouble("santa.x"), y.getDouble("santa.y"), y.getDouble("santa.z"), + (float) y.getDouble("santa.yaw"), 0f); + } + ConfigurationSection c = y.getConfigurationSection("claimed"); + if (c != null) { + for (String k : c.getKeys(false)) { + try { claimed.put(UUID.fromString(k), c.getString(k, "")); } catch (IllegalArgumentException ignored) { } + } + } + } + + private void save() { + YamlConfiguration y = new YamlConfiguration(); + if (santaLoc != null && santaLoc.getWorld() != null) { + y.set("santa.world", santaLoc.getWorld().getName()); + y.set("santa.x", santaLoc.getX()); + y.set("santa.y", santaLoc.getY()); + y.set("santa.z", santaLoc.getZ()); + y.set("santa.yaw", santaLoc.getYaw()); + } + // Nur heutige Eintraege behalten, die Datei bleibt klein + String today = today(); + for (var e : claimed.entrySet()) if (today.equals(e.getValue())) y.set("claimed." + e.getKey(), e.getValue()); + try { + y.save(file); + } catch (IOException e) { + mgr.plugin().getLogger().warning("market-life.yml konnte nicht gespeichert werden: " + e.getMessage()); + } + } + + private String today() { return LocalDate.now(mgr.plugin().getZone()).toString(); } + + // ---------- Bereich ---------- + + private Location center() { return mgr.market().spawn(); } + + boolean inside(Location l) { + Location c = center(); + if (c == null || l == null || l.getWorld() == null || !l.getWorld().equals(c.getWorld())) return false; + double r = Math.max(5, mgr.cfg().getDouble("market.radius", 45)); + double dx = l.getX() - c.getX(), dz = l.getZ() - c.getZ(); + return dx * dx + dz * dz <= r * r && Math.abs(l.getY() - c.getY()) <= 40; + } + + /** Schnee faellt nur im Markt, wenn market.snow-only-in-market gilt und ein Marktpunkt gesetzt ist. */ + boolean allowsSnowAt(Location l) { + if (!mgr.cfg().getBoolean("market.snow-only-in-market", true)) return true; + if (center() == null) return true; + return inside(l); + } + + boolean isFigure(Entity e) { + var pdc = e.getPersistentDataContainer(); + return pdc.has(santaKey, PersistentDataType.BYTE) || pdc.has(elfKey, PersistentDataType.BYTE); + } + + // ---------- Sekundentakt ---------- + + private void life() { + Location c = center(); + List in = new ArrayList<>(); + if (c != null) for (Player p : Bukkit.getOnlinePlayers()) if (inside(p.getLocation())) in.add(p); + audience = in; + + // Begruessung beim Betreten + Set ids = new HashSet<>(); + for (Player p : in) { + ids.add(p.getUniqueId()); + if (!inside.contains(p.getUniqueId())) enter(p); + } + inside.retainAll(ids); + inside.addAll(ids); + + // Musik + if (in.isEmpty()) { + music.stop(); + } else if (mgr.cfg().getBoolean("market.music.enable", true) && !music.playing() + && System.currentTimeMillis() >= musicNext) { + final float vol = (float) mgr.cfg().getDouble("market.music.volume", 0.35); + final int tempo = Math.max(1, mgr.cfg().getInt("market.music.tempo", 3)); + final long pause = Math.max(5, mgr.cfg().getLong("market.music.pause-seconds", 40)) * 1000L; + music.play(() -> audience, tune++, vol, tempo, () -> musicNext = System.currentTimeMillis() + pause); + } + + ensureSanta(); + ensureElves(!in.isEmpty()); + santaHints(); + } + + private void enter(Player p) { + if (!mgr.cfg().getBoolean("market.welcome.enable", true)) return; + long now = System.currentTimeMillis(); + Long last = welcomeAt.get(p.getUniqueId()); + if (last != null && now - last < 60_000L) return; + welcomeAt.put(p.getUniqueId(), now); + p.sendTitle(mgr.raw("market-welcome-title"), mgr.raw("market-welcome-subtitle"), 10, 70, 20); + float[] pitches = {1.0f, 1.26f, 1.5f, 2.0f}; + for (int i = 0; i < pitches.length; i++) { + final float pitch = pitches[i]; + Bukkit.getScheduler().runTaskLater(mgr.plugin(), () -> { + if (p.isOnline()) p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BELL, SoundCategory.RECORDS, 1.2f, pitch); + }, i * 4L); + } + } + + // ---------- Santa auf dem Markt ---------- + + public void setSanta(Location l) { + santaLoc = l.clone(); + santaLoc.setPitch(0f); + save(); + removeSanta(); + ensureSanta(); + } + + public void removeSantaSpot() { + santaLoc = null; + save(); + removeSanta(); + } + + public void resetClaims() { + claimed.clear(); + save(); + } + + private void ensureSanta() { + if (santaLoc == null || santaLoc.getWorld() == null || !mgr.cfg().getBoolean("market.santa.enable", true)) { + removeSanta(); + return; + } + World w = santaLoc.getWorld(); + boolean near = false; + if (w.isChunkLoaded(santaLoc.getBlockX() >> 4, santaLoc.getBlockZ() >> 4)) { + for (Player p : w.getPlayers()) { + if (p.getLocation().distanceSquared(santaLoc) <= 56 * 56) { near = true; break; } + } + } + if (!near) { removeSanta(); return; } + if (santa != null && santa.isValid()) return; + try { + LivingEntity e = mgr.santa().createModel(santaLoc.clone()); + e.setCustomName(ChristmasManager.color(mgr.cfg().getString("market.santa.name", "&c&lSanta Claus"))); + e.setGravity(false); + e.setInvulnerable(true); + e.setSilent(true); + e.setPersistent(false); + e.getPersistentDataContainer().set(santaKey, PersistentDataType.BYTE, (byte) 1); + santa = e; + } catch (Throwable t) { + mgr.plugin().getLogger().warning("Markt-Santa konnte nicht erzeugt werden: " + t); + } + } + + private void removeSanta() { + if (santa != null) { + try { santa.remove(); } catch (Throwable ignored) { } + santa = null; + } + } + + /** Hinweis in der Actionbar, wenn man bei Santa steht und das Tagesgeschenk noch offen ist. */ + private void santaHints() { + if (santa == null || !santa.isValid()) return; + long now = System.currentTimeMillis(); + for (Player p : santa.getWorld().getPlayers()) { + if (p.getLocation().distanceSquared(santa.getLocation()) > 25) continue; + if (today().equals(claimed.get(p.getUniqueId()))) continue; + Long last = hintAt.get(p.getUniqueId()); + if (last != null && now - last < 8000L) continue; + hintAt.put(p.getUniqueId(), now); + p.spigot().sendMessage(net.md_5.bungee.api.ChatMessageType.ACTION_BAR, + new net.md_5.bungee.api.chat.TextComponent(mgr.raw("market-santa-hint"))); + } + } + + /** Rechtsklick auf eine Markt-Figur. Gibt true zurueck, wenn die Figur uns gehoert (Ereignis abbrechen). */ + boolean onInteract(Player p, Entity e) { + if (!isFigure(e)) return false; + if (!e.getPersistentDataContainer().has(santaKey, PersistentDataType.BYTE)) return true; // Wichtel: nichts + if (!p.hasPermission("xmas.use")) return true; + String today = today(); + if (today.equals(claimed.get(p.getUniqueId()))) { + p.sendMessage(mgr.msg("market-santa-claimed")); + return true; + } + claimed.put(p.getUniqueId(), today); + save(); + for (Map reward : mgr.cfg().getMapList("market.santa.rewards")) ChristmasExtras.applyMap(p, reward); + p.sendMessage(mgr.msg("market-santa-gift")); + World w = e.getWorld(); + Location at = e.getLocation().add(0, 1.6, 0); + w.spawnParticle(Particle.END_ROD, at, 25, 0.4, 0.4, 0.4, 0.05); + w.spawnParticle(Particle.SNOWFLAKE, at, 20, 0.5, 0.5, 0.5, 0.03); + w.playSound(at, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.3f); + w.playSound(at, Sound.BLOCK_NOTE_BLOCK_BELL, SoundCategory.RECORDS, 1.5f, 1.5f); + try { santa.swingMainHand(); } catch (Throwable ignored) { } + return true; + } + + // ---------- Wichtel ---------- + + private void ensureElves(boolean playersInside) { + Location c = center(); + elves.removeIf(el -> { + if (el.entity != null && el.entity.isValid()) return false; + return true; + }); + if (c == null || !playersInside || !mgr.cfg().getBoolean("market.elves.enable", true)) { + removeElves(); + return; + } + int count = Math.max(0, Math.min(20, mgr.cfg().getInt("market.elves.count", 4))); + while (elves.size() > count) { + Elf el = elves.remove(elves.size() - 1); + el.entity.remove(); + } + if (elves.size() < count) spawnElf(c); // pro Sekunde einer, das verteilt die Last + } + + private void spawnElf(Location c) { + double radius = Math.max(3, mgr.cfg().getDouble("market.elves.radius", 20)); + World w = c.getWorld(); + for (int attempt = 0; attempt < 15; attempt++) { + double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2; + double dist = ChristmasManager.RANDOM.nextDouble() * radius; + double x = c.getX() + Math.cos(ang) * dist, z = c.getZ() + Math.sin(ang) * dist; + double gy = groundAt(w, x, z, c.getY()); + if (Double.isNaN(gy)) continue; + List names = mgr.cfg().getStringList("market.elves.names"); + String name = names.isEmpty() ? "&aWichtel" : names.get(ChristmasManager.RANDOM.nextInt(names.size())); + Location at = new Location(w, x, gy, z, ChristmasManager.RANDOM.nextFloat() * 360f, 0f); + try { + LivingEntity e = mgr.santa().createFigure(at, ChristmasManager.color(name), + mgr.cfg().getString("market.elves.skin-url", ""), mgr.cfg().getString("market.elves.skin-player", ""), + Color.fromRGB(0x2E8B2E), true); + e.setGravity(false); + e.setInvulnerable(true); + e.setSilent(true); + e.setPersistent(false); + e.getPersistentDataContainer().set(elfKey, PersistentDataType.BYTE, (byte) 1); + Elf el = new Elf(); + el.entity = e; + el.loc = at; + el.yaw = at.getYaw(); + el.pause = 20 + ChristmasManager.RANDOM.nextInt(60); + elves.add(el); + } catch (Throwable t) { + mgr.plugin().getLogger().warning("Wichtel konnte nicht erzeugt werden: " + t); + } + return; + } + } + + private void removeElves() { + for (Elf el : elves) { + try { el.entity.remove(); } catch (Throwable ignored) { } + } + elves.clear(); + } + + // ---------- Bewegung (jeden Tick) ---------- + + private void move() { + tick++; + for (Elf el : new ArrayList<>(elves)) walk(el); + if (tick % 10 == 0) santaLook(); + if (tick % 160 == 0 && santa != null && santa.isValid() && !audience.isEmpty()) { + try { santa.swingMainHand(); } catch (Throwable ignored) { } + } + } + + /** Santa dreht sich langsam zum naechsten Spieler. */ + private void santaLook() { + if (santa == null || !santa.isValid()) return; + Player best = null; + double bd = 64; + for (Player p : santa.getWorld().getPlayers()) { + double d = p.getLocation().distanceSquared(santa.getLocation()); + if (d < bd) { bd = d; best = p; } + } + if (best == null) return; + Location l = santa.getLocation(); + Location face = l.clone(); + face.setDirection(best.getEyeLocation().toVector().subtract(l.toVector().add(new org.bukkit.util.Vector(0, 1.6, 0)))); + float diff = ((face.getYaw() - l.getYaw() + 540f) % 360f) - 180f; + if (Math.abs(diff) < 8f) return; + l.setYaw(l.getYaw() + Math.max(-40f, Math.min(40f, diff))); + l.setPitch(Math.max(-30f, Math.min(30f, face.getPitch()))); + santa.teleport(l); + } + + 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 walk(Elf el) { + LivingEntity e = el.entity; + if (e == null || !e.isValid()) return; + Location c = center(); + if (c == null) return; + if (el.pause > 0) { + el.pause--; + if (el.pause == 0) idle(el, false); + else if (el.pause % 40 == 20) idle(el, true); + return; + } + World w = c.getWorld(); + double radius = Math.max(3, mgr.cfg().getDouble("market.elves.radius", 20)); + if (el.target == null) { + double ang = ChristmasManager.RANDOM.nextDouble() * Math.PI * 2; + double dist = 2 + ChristmasManager.RANDOM.nextDouble() * 8; + double tx = el.loc.getX() + Math.cos(ang) * dist, tz = el.loc.getZ() + Math.sin(ang) * dist; + double cx = tx - c.getX(), cz = tz - c.getZ(); + if (cx * cx + cz * cz > radius * radius) { el.pause = 10; return; } + double gy = groundAt(w, tx, tz, el.loc.getY()); + if (Double.isNaN(gy)) { el.pause = 10; return; } + el.target = new Location(w, tx, gy, tz); + } + double dx = el.target.getX() - el.loc.getX(), dz = el.target.getZ() - el.loc.getZ(); + if (Math.sqrt(dx * dx + dz * dz) < 0.25) { + el.target = null; + el.pause = 40 + ChristmasManager.RANDOM.nextInt(120); + return; + } + float want = (float) Math.toDegrees(Math.atan2(-dx, dz)); + float diff = ((want - el.yaw + 540f) % 360f) - 180f; + el.yaw += Math.max(-12f, Math.min(12f, diff)); + if (Math.abs(diff) > 45f) return; + + double speed = Math.max(0.03, mgr.cfg().getDouble("market.elves.speed", 0.1)); + double rad = Math.toRadians(el.yaw); + double nx = el.loc.getX() - Math.sin(rad) * speed; + double nz = el.loc.getZ() + Math.cos(rad) * speed; + double gy = groundAt(w, nx, nz, el.loc.getY()); + if (Double.isNaN(gy) || Math.abs(gy - el.loc.getY()) > 1.01) { + el.target = null; + el.pause = 8 + ChristmasManager.RANDOM.nextInt(20); + return; + } + double ny = el.loc.getY() + Math.max(-0.25, Math.min(0.25, gy - el.loc.getY())); + Location next = new Location(w, nx, ny, nz, el.yaw, 0f); + e.teleport(next); + el.loc = next; + if (e instanceof ArmorStand as) { + el.phase += 0.4 + speed * 3; + double a = Math.sin(el.phase) * 0.7; + as.setRightLegPose(new EulerAngle(a, 0, 0)); + as.setLeftLegPose(new EulerAngle(-a, 0, 0)); + as.setRightArmPose(new EulerAngle(-a * 0.8, 0, 0)); + as.setLeftArmPose(new EulerAngle(a * 0.8, 0, 0)); + } + } + + private void idle(Elf el, boolean wave) { + if (el.entity instanceof ArmorStand as) { + as.setRightLegPose(EulerAngle.ZERO); + as.setLeftLegPose(EulerAngle.ZERO); + as.setRightArmPose(EulerAngle.ZERO); + as.setLeftArmPose(EulerAngle.ZERO); + } + if (wave) { + try { el.entity.swingMainHand(); } catch (Throwable ignored) { } + } + } + + Collection audience() { return audience; } +} diff --git a/src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java b/src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java index 2baf81e..3285d56 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java +++ b/src/main/java/de/mviper/adventskalender/christmas/SantaEvent.java @@ -114,55 +114,69 @@ public final class SantaEvent { /** 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", ""); + return createFigure(loc, name, skin, skinPlayer, Color.RED, false); + } + + /** Erzeugt eine Figur (Santa, Wichtel ...): Spielermodell wenn moeglich, sonst Ruestungsstaender. */ + LivingEntity createFigure(Location loc, String name, String skin, String skinPlayer, Color color, boolean small) { + LivingEntity figure = null; + String model = mgr.cfg().getString("santa.model", "AUTO").toUpperCase(); boolean mannequin = !model.equals("ARMOR_STAND") && MannequinSupport.supported(); if (mannequin) { try { - stand = MannequinSupport.spawn(loc, name, skin, skinPlayer); + figure = 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)); + // Ohne eigenen Skin: gefaerbte Lederruestung + var eq = figure.getEquipment(); + eq.setHelmet(dyed(Material.LEATHER_HELMET, color)); + eq.setChestplate(dyed(Material.LEATHER_CHESTPLATE, color)); + eq.setLeggings(dyed(Material.LEATHER_LEGGINGS, color)); + eq.setBoots(dyed(Material.LEATHER_BOOTS, color)); + } + if (small) { + try { + var attr = org.bukkit.Registry.ATTRIBUTE.get(org.bukkit.NamespacedKey.minecraft("scale")); + if (attr != null && figure.getAttribute(attr) != null) figure.getAttribute(attr).setBaseValue(0.6); + } catch (Throwable ignored) { } } } 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 (figure != null) figure.remove(); + figure = null; } } - if (stand == null) { - stand = loc.getWorld().spawn(loc, ArmorStand.class, s -> { + if (figure == null) { + figure = loc.getWorld().spawn(loc, ArmorStand.class, s -> { s.setPersistent(false); s.setInvulnerable(true); s.setGravity(false); s.setArms(true); + s.setSmall(small); s.setCustomName(name); s.setCustomNameVisible(true); - ItemStack head = skin.isBlank() ? dyed(Material.LEATHER_HELMET) : skull(skin); + ItemStack head = skin.isBlank() ? dyed(Material.LEATHER_HELMET, color) : 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)); + s.getEquipment().setChestplate(dyed(Material.LEATHER_CHESTPLATE, color)); + s.getEquipment().setLeggings(dyed(Material.LEATHER_LEGGINGS, color)); + s.getEquipment().setBoots(dyed(Material.LEATHER_BOOTS, color)); }); } - return stand; + return figure; } private String barTitle() { return mgr.raw("santa-bossbar").replace("%time%", String.valueOf(left)); } - private static ItemStack dyed(Material m) { + private static ItemStack dyed(Material m) { return dyed(m, Color.RED); } + + private static ItemStack dyed(Material m, Color c) { ItemStack it = new ItemStack(m); LeatherArmorMeta meta = (LeatherArmorMeta) it.getItemMeta(); - if (meta != null) { meta.setColor(Color.RED); it.setItemMeta(meta); } + if (meta != null) { meta.setColor(c); it.setItemMeta(meta); } return it; } @@ -189,6 +203,33 @@ public final class SantaEvent { private int walkPause; private float walkYaw; private Block lastStep; + private double stepPhase; + + /** Beine und Arme schwingen beim Laufen (Ruestungsstaender per Pose, Spielermodell macht es selbst). */ + private void animateLegs(double speed) { + if (!mgr.cfg().getBoolean("santa.walk-animation", true)) return; + stepPhase += 0.35 + speed * 3; + if (stand instanceof ArmorStand as) { + double a = Math.sin(stepPhase) * 0.7; + as.setRightLegPose(new org.bukkit.util.EulerAngle(a, 0, 0)); + as.setLeftLegPose(new org.bukkit.util.EulerAngle(-a, 0, 0)); + as.setRightArmPose(new org.bukkit.util.EulerAngle(-a * 0.8, 0, 0)); + as.setLeftArmPose(new org.bukkit.util.EulerAngle(a * 0.8, 0, 0)); + } + } + + /** Beim Stehenbleiben: Beine gerade, hin und wieder winkt Santa. */ + private void idle() { + if (stand instanceof ArmorStand as) { + as.setRightLegPose(org.bukkit.util.EulerAngle.ZERO); + as.setLeftLegPose(org.bukkit.util.EulerAngle.ZERO); + as.setRightArmPose(org.bukkit.util.EulerAngle.ZERO); + as.setLeftArmPose(org.bukkit.util.EulerAngle.ZERO); + } + if (mgr.cfg().getBoolean("santa.wave", true)) { + try { stand.swingMainHand(); } catch (Throwable ignored) { } + } + } private void startWalking() { stopWalking(); @@ -221,7 +262,11 @@ public final class SantaEvent { 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; } + if (walkPause > 0) { + walkPause--; + if (walkPause == 0 || walkPause % 30 == 15) idle(); // stehen: Beine ruhig, ab und zu winken + return; + } World w = loc.getWorld(); if (walkTarget == null) { @@ -263,6 +308,7 @@ public final class SantaEvent { Location next = new Location(w, nx, ny, nz, walkYaw, 0f); stand.teleport(next); loc = next; + animateLegs(speed); // Schritte: Geraeusch und kurze Schneespur if (stand.getTicksLived() % 9 == 0) w.playSound(next, Sound.BLOCK_SNOW_STEP, 0.6f, 1f); @@ -283,7 +329,7 @@ public final class SantaEvent { while (it.hasNext()) { var e = it.next(); if (all || e.getValue() <= now) { - if (e.getKey().getType() == Material.SNOW) e.getKey().setType(Material.AIR, false); + ChristmasManager.removeSnowLayer(e.getKey()); it.remove(); } } diff --git a/src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java b/src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java index 583db10..227a330 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java +++ b/src/main/java/de/mviper/adventskalender/christmas/SantaMelody.java @@ -7,39 +7,62 @@ 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). */ +import java.util.Collection; +import java.util.function.Supplier; + +/** Spielt Weihnachtsmelodien mit Glocken (Notenblock-Klaenge, keine Dateien noetig). */ final class SantaMelody { - // Melodie: {Halbtoene ueber C4 (-1 = Pause), Dauer in Viertelschlag-Achteln (1 = Achtel)} + // Melodie: {Halbtoene ueber C4, Dauer in 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}, }; + // "We Wish You a Merry Christmas" + private static final int[][] WISH = { + {2, 2}, {7, 2}, {7, 1}, {9, 1}, {7, 1}, {6, 1}, {4, 2}, {4, 2}, {4, 2}, {9, 2}, {9, 1}, {11, 1}, {9, 1}, {7, 1}, + {6, 2}, {2, 2}, {2, 2}, {11, 2}, {11, 1}, {12, 1}, {11, 1}, {9, 1}, {7, 2}, {4, 2}, {2, 1}, {2, 1}, {4, 2}, + {9, 2}, {6, 2}, {7, 6}, + }; + static final int[][][] TUNES = {JINGLE, WISH}; private final ChristmasManager mgr; private BukkitTask task; SantaMelody(ChristmasManager mgr) { this.mgr = mgr; } + boolean playing() { return task != null; } + + /** Santa-Event: Melodie fuer die Spieler der Welt (oder alle, santa.melody.everyone). */ 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 float volume = (float) mgr.cfg().getDouble("santa.melody.volume", 1.0); + final int tempo = Math.max(1, mgr.cfg().getInt("santa.melody.tempo", 3)); + play(() -> everyone ? Bukkit.getOnlinePlayers() : world.getPlayers(), 0, volume, tempo, null); + } + + /** Spielt eine Melodie fuer ein sich aenderndes Publikum; done wird nach dem letzten Ton aufgerufen. */ + void play(Supplier> audience, int tune, float volume, int tempo, Runnable done) { + stop(); + final int[][] notes = TUNES[Math.floorMod(tune, TUNES.length)]; 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]++]; + if (pos[0] >= notes.length) { + stop(); + if (done != null) done.run(); + return; + } + int[] n = notes[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()) { + for (Player p : audience.get()) { 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; + wait[0] = n[1] * tempo - 1; }, 10L, 1L); } diff --git a/src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java b/src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java index 5a56af2..8b331e3 100644 --- a/src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java +++ b/src/main/java/de/mviper/adventskalender/christmas/SnowmanManager.java @@ -60,7 +60,7 @@ public final class SnowmanManager { void stop() { if (task != null) task.cancel(); task = null; - for (Block b : trail) if (b.getType() == Material.SNOW) b.setType(Material.AIR, false); + for (Block b : trail) ChristmasManager.removeSnowLayer(b); trail.clear(); } @@ -75,7 +75,7 @@ public final class SnowmanManager { 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); + if (trail.remove(b)) ChristmasManager.removeSnowLayer(b); }, ticks); } diff --git a/src/main/resources/christmas.yml b/src/main/resources/christmas.yml index 6d67c2e..42d4e76 100644 --- a/src/main/resources/christmas.yml +++ b/src/main/resources/christmas.yml @@ -27,6 +27,7 @@ snow: # Ticks zwischen zwei Partikel-Wellen (höher = weniger Last) display-task-interval: 2 default-on: true + # Blacklist: in diesen Welten fällt kein Schnee (auch per /xmas snow world add ) disable-in-worlds: [] # Nur in diesen WorldGuard-Regionen (nur wenn WorldGuard installiert ist). Leer = überall. enable-only-in-regions: false @@ -86,8 +87,11 @@ santa: 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 + # Lauftempo in Blöcken pro Tick (0.12 = zügiger Spaziergang, 0.2 = Spielertempo) + walk-speed: 0.12 + # Beine und Arme schwingen beim Laufen, Santa winkt beim Stehenbleiben + walk-animation: true + wave: true # Anflug mit Schlitten und zwei Rentieren, bevor Santa landet sleigh: enable: true @@ -150,6 +154,29 @@ magic-tree: # 0.2 (dezent) bis 5.0 (sehr viel). Standard 2.0 intensity: 2.0 max-presents-per-tree: 5 + # Ab welcher Stufe Geschenke entstehen: SAPLING, SMALL_TREE, TREE oder MAGIC_TREE + # (darunter gibt es keine; ab dort gilt gift-cooldown der Stufe, bei -1 der Wert unten) + presents-from-level: SMALL_TREE + default-gift-cooldown: 300 + # Schmuck am Baum: heads = Deko-Köpfe, blocks = die alten Blöcke (Beton, Gold, Seelaterne) + ornaments: + mode: heads + # Wie viel Schmuck am Baum hängt: Stück pro Ring (1-12) und nur jede n-te Ebene. + # Weniger = dezenter. Gilt für neu gebaute Bäume bzw. nach dem nächsten Wachstum. + per-ring: 3 + every-nth-layer: 2 + # Texturen der Deko-Köpfe: URL von textures.minecraft.net oder der "Value" (Base64) + # von minecraft-heads.com. Eigene Köpfe (Christbaumkugeln, Sterne ...) einfach eintragen. + heads: + - "http://textures.minecraft.net/texture/33ba9a948ef125aafdb6bc91be6c804d71aee164ba2197b0b225a847fff048c0" + - "http://textures.minecraft.net/texture/8c8a824b9b94939af2e8ce188482618dad933ae5dd671a7b9ac999d3a3b61a5" + - "http://textures.minecraft.net/texture/83a2768de2af52b27c7bc1416dd535197b65158422673da9d3b0e61d49f33662" + - "http://textures.minecraft.net/texture/70cd83be2b45225497325d958543465fd56bd65cd94a97e8fdfb1c1ec93b08f" + - "http://textures.minecraft.net/texture/908b772ecaee706235d1adbf0b9a29b58ba9c0ed60de6f10efb1a7f86d9e" + - "http://textures.minecraft.net/texture/e467e5795a6d4dcfd9f298c480117a761efbfa43a913ffec825bd07660faeff4" + - "http://textures.minecraft.net/texture/845f39318890a8abfee5b320b35b959fc66e125e8f8cf3b7066d24f65de4b9" + - "http://textures.minecraft.net/texture/ea2d228a34b93c51fb374fbb9543dabb40b9969e101b267f8a6a36ba7096c5d9" + - "http://textures.minecraft.net/texture/f8cbd74cea8120322e72f55c39d0b5a8f3af31d5b883c30afdcacd1f592f801e" # Geschenk-Köpfe (Texturen-URLs von textures.minecraft.net) presents: - "http://textures.minecraft.net/texture/21bc9d42b0041e8f95cb9b26628fdaf50cd0e36f7bb9d6b3a4d2af3949da97d6" @@ -171,7 +198,8 @@ magic-tree: - SPONGE - DIAMOND_HOE - NAME_TAG - # gift-cooldown in Sekunden (-1 = keine Geschenke), lvlup = Items, die zum nächsten Level nötig sind + # gift-cooldown in Sekunden, lvlup = Items, die zum nächsten Level nötig sind + # (Stufen unter presents-from-level geben keine Geschenke, egal was hier steht) levels: sapling: gift-cooldown: -1 @@ -241,6 +269,31 @@ cookies: - "&7Warm und süß." - "&7Gibt Absorption." effects: ["absorption:60:1"] + # Getränke vom Glühwein-Stand: Dampf-Partikel beim Trinken (steam: true) + gluehwein: + material: HONEY_BOTTLE + name: "&5Glühwein" + lore: + - "&7Heiß und würzig." + - "&7Wärmt und regeneriert." + effects: ["regeneration:8:1", "fire_resistance:60:0"] + steam: true + kinderpunsch: + material: HONEY_BOTTLE + name: "&dKinderpunsch" + lore: + - "&7Fruchtig und warm." + - "&7Gibt kurz Tempo." + effects: ["speed:40:0", "jump_boost:40:0"] + steam: true + kakao: + material: HONEY_BOTTLE + name: "&6Heiße Schokolade" + lore: + - "&7Süß und cremig." + - "&7Gibt Absorption." + effects: ["absorption:60:1"] + steam: true # --- Wichteln ---------------------------------------------------------------- wichteln: @@ -381,7 +434,54 @@ wishes: # Aufstellen mit /xmas market spawn . give: wie bei allen Items, auch cookie:/cracker:/crystal:/present:. # price: bis zu 2 Items. Nach /xmas reload werden die Angebote aktualisiert. market: + # Bereich des Marktes rund um den Marktpunkt (/xmas market set), in Blöcken + radius: 45 + # Schnee fällt nur im Markt (gilt nur, wenn ein Marktpunkt gesetzt ist) + snow-only-in-market: true + # Titel mit Glockenklang beim Betreten + welcome: + enable: true + # Leise Weihnachtsmusik im Markt (Glocken), dazwischen Pausen + music: + enable: true + volume: 0.35 + # Ticks pro Achtelnote (kleiner = schneller) + tempo: 3 + pause-seconds: 40 + # Santa auf dem Markt: aufstellen mit /xmas market santa set (an der Stelle, an der du stehst). + # Rechtsklick gibt jedem Spieler einmal pro Tag ein Geschenk. + santa: + enable: true + name: "&c&lSanta Claus" + rewards: + - { item: "COOKIE:4" } + - { item: "cookie:lebkuchen:1" } + - { item: "GOLD_NUGGET:3" } + # Wichtel laufen zwischen den Ständen herum (nur, solange Spieler im Markt sind) + elves: + enable: true + count: 4 + # Umkreis um den Marktpunkt, in dem sie laufen + radius: 20 + # Tempo in Blöcken pro Tick + speed: 0.1 + # Eigener Skin (URL von textures.minecraft.net), leer = grüne Lederrüstung + skin-url: "" + skin-player: "" + names: + - "&aWichtel Fridolin" + - "&aWichtel Pip" + - "&aWichtel Tinka" + - "&aWichtel Knut" + - "&aWichtel Lotta" stalls: + gluehwein: + name: "&5Glühwein-Stand" + profession: FARMER + trades: + - { give: "cookie:gluehwein:1", price: ["EMERALD:2"] } + - { give: "cookie:kinderpunsch:1", price: ["EMERALD:1"] } + - { give: "cookie:kakao:1", price: ["COCOA_BEANS:4", "SUGAR:1"] } plaetzchen: name: "&6Plätzchen-Stand" profession: BUTCHER diff --git a/src/main/resources/christmas_messages_de.yml b/src/main/resources/christmas_messages_de.yml index 4e27096..09f4a80 100644 --- a/src/main/resources/christmas_messages_de.yml +++ b/src/main/resources/christmas_messages_de.yml @@ -24,7 +24,7 @@ messages: - "&2/xmas wish | list | clear &7- Wunschzettel" - "&2/xmas wishes &7- Wunschzettel ansehen (Admin)" - "&2/xmas quiz start [Nr] | skip &7- Weihnachtsquiz (Admin)" - - "&2/xmas market spawn | remove | list &7- Marktstände (Admin)" + - "&2/xmas market spawn | remove | list | santa &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" @@ -50,6 +50,11 @@ messages: # Schnee snow-on: "&aSchnee ist für dich &leingeschaltet&a." snow-off: "&7Schnee ist für dich &causgeschaltet&7." + snow-world-list: "&7Welten ohne Schnee: &e%worlds%" + snow-world-usage: "&cNutzung: /christmas snow world [Welt]" + snow-world-unknown: "&cDie Welt &e%world% &cgibt es nicht." + snow-world-added: "&aIn der Welt &e%world% &afällt jetzt kein Schnee mehr." + snow-world-removed: "&aIn der Welt &e%world% &afällt wieder Schnee." snow-disabled-global: "&cSchnee ist auf dem Server deaktiviert." # Schneemänner @@ -248,6 +253,14 @@ messages: # Marktstände market-usage: "&cNutzung: /xmas market spawn | remove | list" + market-welcome-title: "&c&lWeihnachtsmarkt" + market-welcome-subtitle: "&7Willkommen! Besuche Santa für dein Tagesgeschenk" + market-santa-set: "&aSanta steht jetzt hier auf dem Markt. Spieler bekommen per Rechtsklick einmal pro Tag ein Geschenk." + market-santa-removed: "&7Markt-Santa entfernt." + market-santa-reset: "&7Die Tagesgeschenke von Santa sind für alle zurückgesetzt." + market-santa-hint: "&6Rechtsklick auf Santa für dein Tagesgeschenk" + market-santa-gift: "&aHo ho ho! Santa hat ein Geschenk für dich. Komm morgen wieder!" + market-santa-claimed: "&7Ho ho ho! Das Geschenk für heute hast du schon. Komm morgen wieder!" market-unknown: "&cUnbekannter Stand: %id%" market-spawned: "&aMarktstand aufgestellt." market-removed: "&7Marktstand entfernt." diff --git a/src/main/resources/christmas_messages_en.yml b/src/main/resources/christmas_messages_en.yml index 1671ccc..cc9c99a 100644 --- a/src/main/resources/christmas_messages_en.yml +++ b/src/main/resources/christmas_messages_en.yml @@ -25,7 +25,7 @@ messages: - "&2/xmas wish | list | clear &7- wish list" - "&2/xmas wishes &7- view wishes (admin)" - "&2/xmas quiz start [no.] | skip &7- Christmas quiz (admin)" - - "&2/xmas market spawn | remove | list &7- market stalls (admin)" + - "&2/xmas market spawn | remove | list | santa &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" @@ -53,6 +53,11 @@ messages: # Snow snow-on: "&aSnow is now &eenabled &afor you." snow-off: "&7Snow is now &cdisabled &7for you." + snow-world-list: "&7Worlds without snow: &e%worlds%" + snow-world-usage: "&cUsage: /christmas snow world [world]" + snow-world-unknown: "&cThe world &e%world% &cdoes not exist." + snow-world-added: "&aSnow no longer falls in the world &e%world%&a." + snow-world-removed: "&aSnow falls again in the world &e%world%&a." snow-disabled-global: "&cSnow is disabled on this server." # Snowmen @@ -249,6 +254,14 @@ messages: # Market stalls market-usage: "&cUsage: /xmas market spawn | remove | list" + market-welcome-title: "&c&lChristmas Market" + market-welcome-subtitle: "&7Welcome! Visit Santa for your daily gift" + market-santa-set: "&aSanta now stands here at the market. Players get one gift per day with a right-click." + market-santa-removed: "&7Market Santa removed." + market-santa-reset: "&7Santa's daily gifts have been reset for everyone." + market-santa-hint: "&6Right-click Santa for your daily gift" + market-santa-gift: "&aHo ho ho! Santa has a gift for you. Come back tomorrow!" + market-santa-claimed: "&7Ho ho ho! You already got today's gift. Come back tomorrow!" market-unknown: "&cUnknown stall: %id%" market-spawned: "&aMarket stall placed." market-removed: "&7Market stall removed."