feat: curiosidades plugin for stat-based server announcements
Announces a random Portuguese fact about a random online player on a
timer, sourced from vanilla statistics, with clickable reactions.
Block/item/mob names are emitted as translatable components so each
client renders them in its own language instead of shipping a
translation table. Sentences are phrased to avoid number agreement with
the translated noun ("5.966 blocos de Pedra", not "5.966 Pedras").
Statistic constants are resolved by name with fallbacks because they get
renamed across Minecraft releases; a rename degrades one curiosity
rather than breaking the announcement.
Chat cannot be edited after sending, so live reaction tallies ride on a
boss bar while the in-chat buttons stay frozen at send time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
package dev.marcospaulo.curiosidades;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
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.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public final class Curiosidades extends JavaPlugin implements CommandExecutor, TabCompleter, Listener {
|
||||
|
||||
private final Random random = new Random();
|
||||
private NamespacedKey optOutKey;
|
||||
private Map<String, String> reactionLabels;
|
||||
private int reactionWindowSeconds;
|
||||
private int nextSessionId = 1;
|
||||
private ReactionSession active;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
optOutKey = new NamespacedKey(this, "opt_out");
|
||||
loadReactionSettings();
|
||||
|
||||
getCommand("curiosidade").setExecutor(this);
|
||||
getCommand("curiosidade").setTabCompleter(this);
|
||||
getServer().getPluginManager().registerEvents(this, this);
|
||||
|
||||
long periodTicks = Math.max(1, getConfig().getLong("intervalo-minutos", 20)) * 60L * 20L;
|
||||
getServer().getScheduler().runTaskTimer(this, this::announceRandom, periodTicks, periodTicks);
|
||||
|
||||
getLogger().info("Curiosidades ativo — anúncio a cada "
|
||||
+ getConfig().getLong("intervalo-minutos", 20) + " minutos.");
|
||||
}
|
||||
|
||||
private void loadReactionSettings() {
|
||||
reactionWindowSeconds = getConfig().getInt("janela-reacao-segundos", 90);
|
||||
reactionLabels = new LinkedHashMap<>();
|
||||
var section = getConfig().getConfigurationSection("reacoes");
|
||||
if (section != null) {
|
||||
for (String key : section.getKeys(false)) {
|
||||
reactionLabels.put(key, section.getString(key, key));
|
||||
}
|
||||
}
|
||||
if (reactionLabels.isEmpty()) {
|
||||
reactionLabels.put("joia", "[+1]");
|
||||
}
|
||||
}
|
||||
|
||||
// --- announcing ---------------------------------------------------------
|
||||
|
||||
/** Picks a random eligible online player and broadcasts one fact about them. */
|
||||
void announceRandom() {
|
||||
List<Player> candidates = new ArrayList<>();
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (!isOptedOut(player)) {
|
||||
candidates.add(player);
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Player subject = candidates.get(random.nextInt(candidates.size()));
|
||||
Component fact = CuriosityFactory.random(subject, random);
|
||||
if (fact == null) {
|
||||
return; // Nothing notable about this player yet.
|
||||
}
|
||||
broadcast(subject, fact);
|
||||
}
|
||||
|
||||
private void broadcast(Player subject, Component fact) {
|
||||
if (active != null) {
|
||||
active.hide();
|
||||
}
|
||||
ReactionSession session = new ReactionSession(nextSessionId++, reactionLabels);
|
||||
active = session;
|
||||
|
||||
Component message = Component.text("[Curiosidade] ", NamedTextColor.GOLD, TextDecoration.BOLD)
|
||||
.append(Component.text("Sabia que o ", NamedTextColor.WHITE)
|
||||
.decoration(TextDecoration.BOLD, false))
|
||||
.append(Component.text(subject.getName(), NamedTextColor.GREEN)
|
||||
.decoration(TextDecoration.BOLD, false))
|
||||
.append(Component.text(" ", NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false))
|
||||
.append(fact.decoration(TextDecoration.BOLD, false))
|
||||
.append(Component.text("?", NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false));
|
||||
|
||||
Bukkit.broadcast(message);
|
||||
Bukkit.broadcast(buttons(session));
|
||||
|
||||
session.show();
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
session.hide();
|
||||
if (active == session) {
|
||||
active = null;
|
||||
}
|
||||
}, reactionWindowSeconds * 20L);
|
||||
}
|
||||
|
||||
/** The clickable reaction row. Counts are frozen at send time; the boss bar is live. */
|
||||
private Component buttons(ReactionSession session) {
|
||||
Component row = Component.text(" ");
|
||||
for (Map.Entry<String, String> entry : reactionLabels.entrySet()) {
|
||||
row = row.append(Component.text(entry.getValue() + " ", NamedTextColor.YELLOW)
|
||||
.clickEvent(ClickEvent.runCommand(
|
||||
"/curiosidade reagir " + session.id() + " " + entry.getKey()))
|
||||
.hoverEvent(HoverEvent.showText(
|
||||
Component.text("Clique para reagir com " + entry.getValue(),
|
||||
NamedTextColor.GRAY))));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// --- commands -----------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
if (!sender.hasPermission("curiosidades.forcar")) {
|
||||
sender.sendMessage(Component.text("Sem permissão.", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
announceRandom();
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "reagir" -> {
|
||||
if (!(sender instanceof Player player)) {
|
||||
return true;
|
||||
}
|
||||
if (args.length < 3 || active == null) {
|
||||
return true;
|
||||
}
|
||||
int id;
|
||||
try {
|
||||
id = Integer.parseInt(args[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
return true;
|
||||
}
|
||||
if (id != active.id()) {
|
||||
player.sendActionBar(Component.text("Essa curiosidade já expirou.", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
if (active.react(player, args[2])) {
|
||||
player.sendActionBar(Component.text("Você reagiu!", NamedTextColor.GREEN));
|
||||
}
|
||||
}
|
||||
case "toggle" -> {
|
||||
if (!(sender instanceof Player player)) {
|
||||
return true;
|
||||
}
|
||||
boolean nowOptedOut = !isOptedOut(player);
|
||||
player.getPersistentDataContainer()
|
||||
.set(optOutKey, PersistentDataType.BYTE, (byte) (nowOptedOut ? 1 : 0));
|
||||
player.sendMessage(nowOptedOut
|
||||
? Component.text("Você não aparecerá mais nas curiosidades.", NamedTextColor.YELLOW)
|
||||
: Component.text("Você voltou a aparecer nas curiosidades.", NamedTextColor.GREEN));
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!sender.hasPermission("curiosidades.admin")) {
|
||||
sender.sendMessage(Component.text("Sem permissão.", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
reloadConfig();
|
||||
loadReactionSettings();
|
||||
sender.sendMessage(Component.text("Configuração recarregada.", NamedTextColor.GREEN));
|
||||
}
|
||||
default -> sender.sendMessage(
|
||||
Component.text("Uso: /curiosidade [toggle|reload]", NamedTextColor.GRAY));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 1) {
|
||||
return List.of("toggle", "reload");
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// --- events -------------------------------------------------------------
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
if (active != null) {
|
||||
active.showTo(event.getPlayer());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOptedOut(Player player) {
|
||||
Byte value = player.getPersistentDataContainer()
|
||||
.get(optOutKey, PersistentDataType.BYTE);
|
||||
return value != null && value == 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user