feat: join-triggered by default, plus runtime admin controls
Default trigger is now player join rather than a timer, with a short delay so the curiosity lands after the join message instead of racing it. Modes entrada/intervalo/ambos/manual select the triggers. Adds a full /curiosidade tree so behaviour is adjustable in-game without editing config.yml: mode, interval, join delay, per-player cooldown, no-repeat history, reaction window, reaction labels, and per-category toggles. Every setter writes through to disk immediately so changes survive a restart. Facts are now tagged with a category so they can be filtered, and a per-player cooldown plus recent-fact history stop repeats when someone relogs. Config changes are gated behind curiosidades.admin; reacting and opting out stay default-true. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
This commit is contained in:
@@ -5,94 +5,147 @@ 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 net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
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 org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class Curiosidades extends JavaPlugin implements CommandExecutor, TabCompleter, Listener {
|
||||
public final class Curiosidades extends JavaPlugin implements Listener {
|
||||
|
||||
private final Random random = new Random();
|
||||
private final Map<UUID, Long> lastFeatured = new HashMap<>();
|
||||
private final Deque<String> recentFacts = new ArrayDeque<>();
|
||||
|
||||
private Settings settings;
|
||||
private NamespacedKey optOutKey;
|
||||
private Map<String, String> reactionLabels;
|
||||
private int reactionWindowSeconds;
|
||||
private BukkitTask timerTask;
|
||||
private int nextSessionId = 1;
|
||||
private ReactionSession active;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
settings = new Settings(this);
|
||||
optOutKey = new NamespacedKey(this, "opt_out");
|
||||
loadReactionSettings();
|
||||
|
||||
getCommand("curiosidade").setExecutor(this);
|
||||
getCommand("curiosidade").setTabCompleter(this);
|
||||
CuriosidadeCommand command = new CuriosidadeCommand(this);
|
||||
if (getCommand("curiosidade") != null) {
|
||||
getCommand("curiosidade").setExecutor(command);
|
||||
getCommand("curiosidade").setTabCompleter(command);
|
||||
}
|
||||
getServer().getPluginManager().registerEvents(this, this);
|
||||
rescheduleTimer();
|
||||
|
||||
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.");
|
||||
getLogger().info("Curiosidades ativo — modo " + settings.mode()
|
||||
+ (settings.mode().firesOnTimer()
|
||||
? " (intervalo de " + settings.intervalMinutes() + " min)" : ""));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (active != null) {
|
||||
active.hide();
|
||||
}
|
||||
if (reactionLabels.isEmpty()) {
|
||||
reactionLabels.put("joia", "[+1]");
|
||||
}
|
||||
|
||||
Settings settings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
/** Starts, stops or restarts the repeating announcement task to match the mode. */
|
||||
void rescheduleTimer() {
|
||||
if (timerTask != null) {
|
||||
timerTask.cancel();
|
||||
timerTask = null;
|
||||
}
|
||||
if (!settings.mode().firesOnTimer()) {
|
||||
return;
|
||||
}
|
||||
long ticks = settings.intervalMinutes() * 60L * 20L;
|
||||
timerTask = getServer().getScheduler().runTaskTimer(this, () -> announceRandom(null), ticks, ticks);
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
/**
|
||||
* Announces one curiosity.
|
||||
*
|
||||
* @param subject who to talk about, or null to pick a random eligible player
|
||||
* @return false if there was nobody eligible or nothing notable to say
|
||||
*/
|
||||
boolean announceRandom(Player subject) {
|
||||
if (subject == null) {
|
||||
List<Player> candidates = new ArrayList<>();
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (isEligible(player)) {
|
||||
candidates.add(player);
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
subject = candidates.get(random.nextInt(candidates.size()));
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return;
|
||||
|
||||
List<Fact> facts = CuriosityFactory.facts(subject, settings);
|
||||
if (facts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
|
||||
// Prefer a fact that has not been announced recently; fall back to any.
|
||||
List<Fact> fresh = new ArrayList<>(facts);
|
||||
fresh.removeIf(fact -> recentFacts.contains(plain(fact.text())));
|
||||
Fact chosen = (fresh.isEmpty() ? facts : fresh).get(random.nextInt(
|
||||
(fresh.isEmpty() ? facts : fresh).size()));
|
||||
|
||||
remember(chosen);
|
||||
lastFeatured.put(subject.getUniqueId(), System.currentTimeMillis());
|
||||
broadcast(subject, chosen.text());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void broadcast(Player subject, Component fact) {
|
||||
if (active != null) {
|
||||
active.hide();
|
||||
private void remember(Fact fact) {
|
||||
recentFacts.addLast(plain(fact.text()));
|
||||
while (recentFacts.size() > settings.noRepeat()) {
|
||||
recentFacts.removeFirst();
|
||||
}
|
||||
ReactionSession session = new ReactionSession(nextSessionId++, reactionLabels);
|
||||
active = session;
|
||||
}
|
||||
|
||||
Component message = Component.text("[Curiosidade] ", NamedTextColor.GOLD, TextDecoration.BOLD)
|
||||
private static String plain(Component component) {
|
||||
return PlainTextComponentSerializer.plainText().serialize(component);
|
||||
}
|
||||
|
||||
/** True if the player can currently be the subject of an announcement. */
|
||||
boolean isEligible(Player player) {
|
||||
if (isOptedOut(player) || player.hasPermission("curiosidades.isento")) {
|
||||
return false;
|
||||
}
|
||||
Long last = lastFeatured.get(player.getUniqueId());
|
||||
if (last == null) {
|
||||
return true;
|
||||
}
|
||||
long cooldownMillis = settings.cooldownMinutes() * 60_000L;
|
||||
return System.currentTimeMillis() - last >= cooldownMillis;
|
||||
}
|
||||
|
||||
/** Builds the message for {@code subject} without broadcasting it. */
|
||||
Component render(Player subject, Component fact) {
|
||||
return 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)
|
||||
@@ -100,23 +153,34 @@ public final class Curiosidades extends JavaPlugin implements CommandExecutor, T
|
||||
.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);
|
||||
private void broadcast(Player subject, Component fact) {
|
||||
if (active != null) {
|
||||
active.hide();
|
||||
active = null;
|
||||
}
|
||||
Bukkit.broadcast(render(subject, fact));
|
||||
|
||||
if (!settings.reactionsEnabled()) {
|
||||
return;
|
||||
}
|
||||
ReactionSession session = new ReactionSession(nextSessionId++, settings.reactions());
|
||||
active = session;
|
||||
Bukkit.broadcast(buttons(session));
|
||||
|
||||
session.show();
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
session.hide();
|
||||
if (active == session) {
|
||||
active = null;
|
||||
}
|
||||
}, reactionWindowSeconds * 20L);
|
||||
}, settings.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()) {
|
||||
for (Map.Entry<String, String> entry : settings.reactions().entrySet()) {
|
||||
row = row.append(Component.text(entry.getValue() + " ", NamedTextColor.YELLOW)
|
||||
.clickEvent(ClickEvent.runCommand(
|
||||
"/curiosidade reagir " + session.id() + " " + entry.getKey()))
|
||||
@@ -127,87 +191,46 @@ public final class Curiosidades extends JavaPlugin implements CommandExecutor, T
|
||||
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();
|
||||
ReactionSession activeSession() {
|
||||
return active;
|
||||
}
|
||||
|
||||
// --- events -------------------------------------------------------------
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (active != null) {
|
||||
active.showTo(event.getPlayer());
|
||||
active.showTo(player);
|
||||
}
|
||||
if (!settings.mode().firesOnJoin() || !isEligible(player)) {
|
||||
return;
|
||||
}
|
||||
// Delayed so the curiosity lands after the join message rather than racing it.
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
if (player.isOnline()) {
|
||||
announceRandom(player);
|
||||
}
|
||||
}, settings.joinDelaySeconds() * 20L);
|
||||
}
|
||||
|
||||
private boolean isOptedOut(Player player) {
|
||||
Byte value = player.getPersistentDataContainer()
|
||||
.get(optOutKey, PersistentDataType.BYTE);
|
||||
// --- per-player opt out -------------------------------------------------
|
||||
|
||||
boolean isOptedOut(Player player) {
|
||||
Byte value = player.getPersistentDataContainer().get(optOutKey, PersistentDataType.BYTE);
|
||||
return value != null && value == 1;
|
||||
}
|
||||
|
||||
void setOptedOut(Player player, boolean optedOut) {
|
||||
player.getPersistentDataContainer()
|
||||
.set(optOutKey, PersistentDataType.BYTE, (byte) (optedOut ? 1 : 0));
|
||||
}
|
||||
|
||||
void clearCooldowns() {
|
||||
lastFeatured.clear();
|
||||
}
|
||||
|
||||
void clearHistory() {
|
||||
recentFacts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user