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:
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.marcospaulo.curiosidades;
|
||||||
|
|
||||||
|
/** Groups of curiosities that can be switched on and off independently. */
|
||||||
|
enum Category {
|
||||||
|
|
||||||
|
MINERACAO("mineracao", "Mineração"),
|
||||||
|
ITENS("itens", "Itens"),
|
||||||
|
MORTES("mortes", "Mortes"),
|
||||||
|
COMBATE("combate", "Combate"),
|
||||||
|
DISTANCIA("distancia", "Distância"),
|
||||||
|
TEMPO("tempo", "Tempo"),
|
||||||
|
DIVERSOS("diversos", "Diversos");
|
||||||
|
|
||||||
|
private final String key;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
Category(String key, String label) {
|
||||||
|
this.key = key;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lowercase ASCII key used in commands and config. */
|
||||||
|
String key() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Accented name shown to players. */
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Category byKey(String key) {
|
||||||
|
for (Category category : values()) {
|
||||||
|
if (category.key.equalsIgnoreCase(key)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
package dev.marcospaulo.curiosidades;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickEvent;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** The whole /curiosidade command tree. */
|
||||||
|
final class CuriosidadeCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
private static final String ADMIN = "curiosidades.admin";
|
||||||
|
|
||||||
|
private final Curiosidades plugin;
|
||||||
|
|
||||||
|
CuriosidadeCommand(Curiosidades plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
Settings settings = plugin.settings();
|
||||||
|
|
||||||
|
if (args.length == 0) {
|
||||||
|
return force(sender, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
String sub = args[0].toLowerCase();
|
||||||
|
String[] rest = java.util.Arrays.copyOfRange(args, 1, args.length);
|
||||||
|
|
||||||
|
switch (sub) {
|
||||||
|
case "ajuda", "help" -> help(sender);
|
||||||
|
case "reagir" -> react(sender, rest);
|
||||||
|
case "toggle" -> toggle(sender);
|
||||||
|
case "status" -> status(sender);
|
||||||
|
case "listar" -> list(sender, rest);
|
||||||
|
case "ver", "preview" -> preview(sender, rest);
|
||||||
|
case "modo" -> mode(sender, rest);
|
||||||
|
case "intervalo" -> intervalMinutes(sender, rest);
|
||||||
|
case "atraso" -> joinDelay(sender, rest);
|
||||||
|
case "cooldown" -> cooldown(sender, rest);
|
||||||
|
case "repetir" -> noRepeat(sender, rest);
|
||||||
|
case "janela" -> window(sender, rest);
|
||||||
|
case "reacoes" -> reactionsToggle(sender, rest);
|
||||||
|
case "reacao" -> reactionEdit(sender, rest);
|
||||||
|
case "categoria" -> categoryToggle(sender, rest);
|
||||||
|
case "categorias" -> categories(sender);
|
||||||
|
case "limpar" -> clear(sender, rest);
|
||||||
|
case "reload" -> reload(sender);
|
||||||
|
default -> {
|
||||||
|
// Bare player name: /curiosidade <jogador>
|
||||||
|
Player target = Bukkit.getPlayerExact(args[0]);
|
||||||
|
if (target != null) {
|
||||||
|
return force(sender, target);
|
||||||
|
}
|
||||||
|
error(sender, "Subcomando desconhecido. Use /curiosidade ajuda");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- announcing ---------------------------------------------------------
|
||||||
|
|
||||||
|
private boolean force(CommandSender sender, Player target) {
|
||||||
|
if (!sender.hasPermission("curiosidades.forcar")) {
|
||||||
|
return denied(sender);
|
||||||
|
}
|
||||||
|
if (!plugin.announceRandom(target)) {
|
||||||
|
error(sender, target == null
|
||||||
|
? "Ninguém elegível online (ou sem estatísticas suficientes)."
|
||||||
|
: target.getName() + " ainda não tem estatísticas suficientes.");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shows a curiosity only to the sender, without broadcasting or touching cooldowns. */
|
||||||
|
private void preview(CommandSender sender, String[] args) {
|
||||||
|
if (!sender.hasPermission("curiosidades.ver")) {
|
||||||
|
denied(sender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player target = resolveTarget(sender, args);
|
||||||
|
if (target == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Fact> facts = CuriosityFactory.facts(target, plugin.settings());
|
||||||
|
if (facts.isEmpty()) {
|
||||||
|
error(sender, "Nenhuma curiosidade disponível para " + target.getName() + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Fact fact = facts.get((int) (Math.random() * facts.size()));
|
||||||
|
sender.sendMessage(plugin.render(target, fact.text()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dumps every available fact for a player, privately. Handy for tuning thresholds. */
|
||||||
|
private void list(CommandSender sender, String[] args) {
|
||||||
|
if (!sender.hasPermission("curiosidades.ver")) {
|
||||||
|
denied(sender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player target = resolveTarget(sender, args);
|
||||||
|
if (target == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Fact> facts = CuriosityFactory.facts(target, plugin.settings());
|
||||||
|
header(sender, "Curiosidades de " + target.getName() + " (" + facts.size() + ")");
|
||||||
|
for (Fact fact : facts) {
|
||||||
|
sender.sendMessage(Component.text(" [" + fact.category().label() + "] ", NamedTextColor.DARK_AQUA)
|
||||||
|
.append(fact.text()));
|
||||||
|
}
|
||||||
|
if (facts.isEmpty()) {
|
||||||
|
sender.sendMessage(Component.text(" (nada ainda)", NamedTextColor.GRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Player resolveTarget(CommandSender sender, String[] args) {
|
||||||
|
if (args.length > 0) {
|
||||||
|
Player target = Bukkit.getPlayerExact(args[0]);
|
||||||
|
if (target == null) {
|
||||||
|
error(sender, "Jogador '" + args[0] + "' não está online.");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
if (sender instanceof Player player) {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
error(sender, "Informe um jogador: /curiosidade listar <jogador>");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void react(CommandSender sender, String[] args) {
|
||||||
|
if (!(sender instanceof Player player) || args.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!player.hasPermission("curiosidades.reagir")) {
|
||||||
|
denied(sender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ReactionSession active = plugin.activeSession();
|
||||||
|
int id;
|
||||||
|
try {
|
||||||
|
id = Integer.parseInt(args[0]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active == null || active.id() != id) {
|
||||||
|
player.sendActionBar(Component.text("Essa curiosidade já expirou.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active.react(player, args[1])) {
|
||||||
|
player.sendActionBar(Component.text("Você reagiu!", NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggle(CommandSender sender) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
error(sender, "Só jogadores podem usar isso.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean optedOut = !plugin.isOptedOut(player);
|
||||||
|
plugin.setOptedOut(player, optedOut);
|
||||||
|
sender.sendMessage(optedOut
|
||||||
|
? Component.text("Você não aparecerá mais nas curiosidades.", NamedTextColor.YELLOW)
|
||||||
|
: Component.text("Você voltou a aparecer nas curiosidades.", NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- admin settings -----------------------------------------------------
|
||||||
|
|
||||||
|
private void mode(CommandSender sender, String[] args) {
|
||||||
|
if (!admin(sender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length == 0) {
|
||||||
|
error(sender, "Uso: /curiosidade modo <entrada|intervalo|ambos|manual>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Mode mode = Mode.byKey(args[0]);
|
||||||
|
if (mode == null) {
|
||||||
|
error(sender, "Modo inválido. Use entrada, intervalo, ambos ou manual.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.settings().mode(mode);
|
||||||
|
plugin.rescheduleTimer();
|
||||||
|
ok(sender, "Modo alterado para " + mode + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void intervalMinutes(CommandSender sender, String[] args) {
|
||||||
|
Integer value = adminNumber(sender, args, "/curiosidade intervalo <minutos>");
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.settings().intervalMinutes(value);
|
||||||
|
plugin.rescheduleTimer();
|
||||||
|
ok(sender, "Intervalo alterado para " + plugin.settings().intervalMinutes() + " minutos.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinDelay(CommandSender sender, String[] args) {
|
||||||
|
Integer value = adminNumber(sender, args, "/curiosidade atraso <segundos>");
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.settings().joinDelaySeconds(value);
|
||||||
|
ok(sender, "Atraso após entrar: " + plugin.settings().joinDelaySeconds() + "s.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cooldown(CommandSender sender, String[] args) {
|
||||||
|
Integer value = adminNumber(sender, args, "/curiosidade cooldown <minutos>");
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.settings().cooldownMinutes(value);
|
||||||
|
ok(sender, "Cooldown por jogador: " + plugin.settings().cooldownMinutes() + " minutos.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void noRepeat(CommandSender sender, String[] args) {
|
||||||
|
Integer value = adminNumber(sender, args, "/curiosidade repetir <quantidade>");
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.settings().noRepeat(value);
|
||||||
|
ok(sender, "Evitando repetir as últimas " + plugin.settings().noRepeat() + " curiosidades.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void window(CommandSender sender, String[] args) {
|
||||||
|
Integer value = adminNumber(sender, args, "/curiosidade janela <segundos>");
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.settings().reactionWindowSeconds(value);
|
||||||
|
ok(sender, "Janela de reação: " + plugin.settings().reactionWindowSeconds() + "s.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reactionsToggle(CommandSender sender, String[] args) {
|
||||||
|
if (!admin(sender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length == 0) {
|
||||||
|
error(sender, "Uso: /curiosidade reacoes <on|off>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean on = args[0].equalsIgnoreCase("on");
|
||||||
|
plugin.settings().reactionsEnabled(on);
|
||||||
|
ok(sender, "Reações " + (on ? "ativadas" : "desativadas") + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reactionEdit(CommandSender sender, String[] args) {
|
||||||
|
if (!admin(sender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length >= 3 && args[0].equalsIgnoreCase("add")) {
|
||||||
|
String label = String.join(" ", java.util.Arrays.copyOfRange(args, 2, args.length));
|
||||||
|
plugin.settings().reaction(args[1].toLowerCase(), label);
|
||||||
|
ok(sender, "Reação '" + args[1] + "' definida como " + label + ".");
|
||||||
|
} else if (args.length >= 2 && args[0].equalsIgnoreCase("remover")) {
|
||||||
|
plugin.settings().removeReaction(args[1].toLowerCase());
|
||||||
|
ok(sender, "Reação '" + args[1] + "' removida.");
|
||||||
|
} else {
|
||||||
|
error(sender, "Uso: /curiosidade reacao add <chave> <rótulo> | remover <chave>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void categoryToggle(CommandSender sender, String[] args) {
|
||||||
|
if (!admin(sender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length < 2) {
|
||||||
|
error(sender, "Uso: /curiosidade categoria <nome> <on|off>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Category category = Category.byKey(args[0]);
|
||||||
|
if (category == null) {
|
||||||
|
error(sender, "Categoria desconhecida. Veja /curiosidade categorias");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean on = args[1].equalsIgnoreCase("on");
|
||||||
|
plugin.settings().categoryEnabled(category, on);
|
||||||
|
ok(sender, "Categoria " + category.label() + " " + (on ? "ativada" : "desativada") + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void categories(CommandSender sender) {
|
||||||
|
header(sender, "Categorias");
|
||||||
|
for (Category category : Category.values()) {
|
||||||
|
boolean on = plugin.settings().categoryEnabled(category);
|
||||||
|
sender.sendMessage(Component.text(" " + category.key() + " ", NamedTextColor.WHITE)
|
||||||
|
.append(Component.text(on ? "ativada" : "desativada",
|
||||||
|
on ? NamedTextColor.GREEN : NamedTextColor.RED))
|
||||||
|
.append(Component.text(" [alternar]", NamedTextColor.DARK_AQUA)
|
||||||
|
.clickEvent(ClickEvent.runCommand(
|
||||||
|
"/curiosidade categoria " + category.key() + (on ? " off" : " on")))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clear(CommandSender sender, String[] args) {
|
||||||
|
if (!admin(sender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String what = args.length > 0 ? args[0].toLowerCase() : "tudo";
|
||||||
|
if (what.equals("cooldown") || what.equals("tudo")) {
|
||||||
|
plugin.clearCooldowns();
|
||||||
|
}
|
||||||
|
if (what.equals("historico") || what.equals("tudo")) {
|
||||||
|
plugin.clearHistory();
|
||||||
|
}
|
||||||
|
ok(sender, "Limpo: " + what + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reload(CommandSender sender) {
|
||||||
|
if (!admin(sender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.reloadConfig();
|
||||||
|
plugin.rescheduleTimer();
|
||||||
|
ok(sender, "Configuração recarregada.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void status(CommandSender sender) {
|
||||||
|
Settings settings = plugin.settings();
|
||||||
|
header(sender, "Curiosidades — status");
|
||||||
|
line(sender, "modo", settings.mode().name().toLowerCase());
|
||||||
|
line(sender, "intervalo", settings.intervalMinutes() + " min"
|
||||||
|
+ (settings.mode().firesOnTimer() ? "" : " (inativo neste modo)"));
|
||||||
|
line(sender, "atraso após entrar", settings.joinDelaySeconds() + "s"
|
||||||
|
+ (settings.mode().firesOnJoin() ? "" : " (inativo neste modo)"));
|
||||||
|
line(sender, "cooldown por jogador", settings.cooldownMinutes() + " min");
|
||||||
|
line(sender, "evitar repetir", settings.noRepeat() + " últimas");
|
||||||
|
line(sender, "reações", settings.reactionsEnabled() ? "ativas" : "desativadas");
|
||||||
|
line(sender, "janela de reação", settings.reactionWindowSeconds() + "s");
|
||||||
|
line(sender, "reações definidas", String.join(" ", settings.reactions().values()));
|
||||||
|
List<String> enabled = new ArrayList<>();
|
||||||
|
for (Category category : Category.values()) {
|
||||||
|
if (settings.categoryEnabled(category)) {
|
||||||
|
enabled.add(category.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line(sender, "categorias ativas", enabled.isEmpty() ? "nenhuma" : String.join(", ", enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void help(CommandSender sender) {
|
||||||
|
header(sender, "Curiosidades — comandos");
|
||||||
|
Map<String, String> commands = new java.util.LinkedHashMap<>();
|
||||||
|
commands.put("/curiosidade", "anuncia uma curiosidade agora");
|
||||||
|
commands.put("/curiosidade <jogador>", "anuncia sobre um jogador específico");
|
||||||
|
commands.put("/curiosidade ver [jogador]", "mostra uma curiosidade só para você");
|
||||||
|
commands.put("/curiosidade listar [jogador]", "lista todas as curiosidades disponíveis");
|
||||||
|
commands.put("/curiosidade toggle", "entra/sai da lista de jogadores sorteados");
|
||||||
|
commands.put("/curiosidade status", "mostra a configuração atual");
|
||||||
|
commands.put("/curiosidade categorias", "lista as categorias e seu estado");
|
||||||
|
commands.put("/curiosidade modo <m>", "entrada | intervalo | ambos | manual");
|
||||||
|
commands.put("/curiosidade intervalo <min>", "intervalo do modo temporizado");
|
||||||
|
commands.put("/curiosidade atraso <seg>", "espera após o jogador entrar");
|
||||||
|
commands.put("/curiosidade cooldown <min>", "tempo mínimo entre citar o mesmo jogador");
|
||||||
|
commands.put("/curiosidade repetir <n>", "quantas curiosidades recentes evitar repetir");
|
||||||
|
commands.put("/curiosidade janela <seg>", "duração da barra de reações");
|
||||||
|
commands.put("/curiosidade reacoes <on|off>", "liga/desliga as reações");
|
||||||
|
commands.put("/curiosidade reacao add <chave> <rótulo>", "cria ou muda uma reação");
|
||||||
|
commands.put("/curiosidade reacao remover <chave>", "remove uma reação");
|
||||||
|
commands.put("/curiosidade categoria <nome> <on|off>", "liga/desliga uma categoria");
|
||||||
|
commands.put("/curiosidade limpar [cooldown|historico|tudo]", "zera cooldowns/histórico");
|
||||||
|
commands.put("/curiosidade reload", "recarrega o config.yml");
|
||||||
|
commands.forEach((cmd, description) -> sender.sendMessage(
|
||||||
|
Component.text(" " + cmd, NamedTextColor.AQUA)
|
||||||
|
.append(Component.text(" — " + description, NamedTextColor.GRAY))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ------------------------------------------------------------
|
||||||
|
|
||||||
|
private boolean admin(CommandSender sender) {
|
||||||
|
if (sender.hasPermission(ADMIN)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
denied(sender);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a positive integer argument for an admin-only setting. */
|
||||||
|
private Integer adminNumber(CommandSender sender, String[] args, String usage) {
|
||||||
|
if (!admin(sender)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (args.length == 0) {
|
||||||
|
error(sender, "Uso: " + usage);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(args[0]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
error(sender, "'" + args[0] + "' não é um número.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean denied(CommandSender sender) {
|
||||||
|
error(sender, "Você não tem permissão para isso.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ok(CommandSender sender, String text) {
|
||||||
|
sender.sendMessage(Component.text("[Curiosidades] ", NamedTextColor.GOLD)
|
||||||
|
.append(Component.text(text, NamedTextColor.GREEN)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void error(CommandSender sender, String text) {
|
||||||
|
sender.sendMessage(Component.text("[Curiosidades] ", NamedTextColor.GOLD)
|
||||||
|
.append(Component.text(text, NamedTextColor.RED)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void header(CommandSender sender, String text) {
|
||||||
|
sender.sendMessage(Component.text("— " + text + " —", NamedTextColor.GOLD, TextDecoration.BOLD));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void line(CommandSender sender, String key, String value) {
|
||||||
|
sender.sendMessage(Component.text(" " + key + ": ", NamedTextColor.GRAY)
|
||||||
|
.append(Component.text(value, NamedTextColor.AQUA)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tab completion -----------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (args.length == 1) {
|
||||||
|
List<String> options = new ArrayList<>(List.of("ver", "listar", "toggle", "status",
|
||||||
|
"categorias", "ajuda"));
|
||||||
|
if (sender.hasPermission(ADMIN)) {
|
||||||
|
options.addAll(List.of("modo", "intervalo", "atraso", "cooldown", "repetir",
|
||||||
|
"janela", "reacoes", "reacao", "categoria", "limpar", "reload"));
|
||||||
|
}
|
||||||
|
Bukkit.getOnlinePlayers().forEach(p -> options.add(p.getName()));
|
||||||
|
return filter(options, args[0]);
|
||||||
|
}
|
||||||
|
if (args.length == 2) {
|
||||||
|
return switch (args[0].toLowerCase()) {
|
||||||
|
case "modo" -> filter(List.of("entrada", "intervalo", "ambos", "manual"), args[1]);
|
||||||
|
case "reacoes" -> filter(List.of("on", "off"), args[1]);
|
||||||
|
case "reacao" -> filter(List.of("add", "remover"), args[1]);
|
||||||
|
case "limpar" -> filter(List.of("cooldown", "historico", "tudo"), args[1]);
|
||||||
|
case "categoria" -> filter(categoryKeys(), args[1]);
|
||||||
|
case "ver", "listar" -> filter(onlineNames(), args[1]);
|
||||||
|
default -> List.of();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (args.length == 3 && args[0].equalsIgnoreCase("categoria")) {
|
||||||
|
return filter(List.of("on", "off"), args[2]);
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> categoryKeys() {
|
||||||
|
List<String> keys = new ArrayList<>();
|
||||||
|
for (Category category : Category.values()) {
|
||||||
|
keys.add(category.key());
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> onlineNames() {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
Bukkit.getOnlinePlayers().forEach(p -> names.add(p.getName()));
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> filter(List<String> options, String prefix) {
|
||||||
|
List<String> matches = new ArrayList<>();
|
||||||
|
for (String option : options) {
|
||||||
|
if (option.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||||
|
matches.add(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,94 +5,147 @@ import net.kyori.adventure.text.event.ClickEvent;
|
|||||||
import net.kyori.adventure.text.event.HoverEvent;
|
import net.kyori.adventure.text.event.HoverEvent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import net.kyori.adventure.text.format.TextDecoration;
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.NamespacedKey;
|
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.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.Deque;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
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 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 NamespacedKey optOutKey;
|
||||||
private Map<String, String> reactionLabels;
|
private BukkitTask timerTask;
|
||||||
private int reactionWindowSeconds;
|
|
||||||
private int nextSessionId = 1;
|
private int nextSessionId = 1;
|
||||||
private ReactionSession active;
|
private ReactionSession active;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
saveDefaultConfig();
|
saveDefaultConfig();
|
||||||
|
settings = new Settings(this);
|
||||||
optOutKey = new NamespacedKey(this, "opt_out");
|
optOutKey = new NamespacedKey(this, "opt_out");
|
||||||
loadReactionSettings();
|
|
||||||
|
|
||||||
getCommand("curiosidade").setExecutor(this);
|
CuriosidadeCommand command = new CuriosidadeCommand(this);
|
||||||
getCommand("curiosidade").setTabCompleter(this);
|
if (getCommand("curiosidade") != null) {
|
||||||
|
getCommand("curiosidade").setExecutor(command);
|
||||||
|
getCommand("curiosidade").setTabCompleter(command);
|
||||||
|
}
|
||||||
getServer().getPluginManager().registerEvents(this, this);
|
getServer().getPluginManager().registerEvents(this, this);
|
||||||
|
rescheduleTimer();
|
||||||
|
|
||||||
long periodTicks = Math.max(1, getConfig().getLong("intervalo-minutos", 20)) * 60L * 20L;
|
getLogger().info("Curiosidades ativo — modo " + settings.mode()
|
||||||
getServer().getScheduler().runTaskTimer(this, this::announceRandom, periodTicks, periodTicks);
|
+ (settings.mode().firesOnTimer()
|
||||||
|
? " (intervalo de " + settings.intervalMinutes() + " min)" : ""));
|
||||||
getLogger().info("Curiosidades ativo — anúncio a cada "
|
|
||||||
+ getConfig().getLong("intervalo-minutos", 20) + " minutos.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadReactionSettings() {
|
@Override
|
||||||
reactionWindowSeconds = getConfig().getInt("janela-reacao-segundos", 90);
|
public void onDisable() {
|
||||||
reactionLabels = new LinkedHashMap<>();
|
if (active != null) {
|
||||||
var section = getConfig().getConfigurationSection("reacoes");
|
active.hide();
|
||||||
if (section != null) {
|
|
||||||
for (String key : section.getKeys(false)) {
|
|
||||||
reactionLabels.put(key, section.getString(key, key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 ---------------------------------------------------------
|
// --- announcing ---------------------------------------------------------
|
||||||
|
|
||||||
/** Picks a random eligible online player and broadcasts one fact about them. */
|
/**
|
||||||
void announceRandom() {
|
* Announces one curiosity.
|
||||||
List<Player> candidates = new ArrayList<>();
|
*
|
||||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
* @param subject who to talk about, or null to pick a random eligible player
|
||||||
if (!isOptedOut(player)) {
|
* @return false if there was nobody eligible or nothing notable to say
|
||||||
candidates.add(player);
|
*/
|
||||||
|
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);
|
// Prefer a fact that has not been announced recently; fall back to any.
|
||||||
if (fact == null) {
|
List<Fact> fresh = new ArrayList<>(facts);
|
||||||
return; // Nothing notable about this player yet.
|
fresh.removeIf(fact -> recentFacts.contains(plain(fact.text())));
|
||||||
}
|
Fact chosen = (fresh.isEmpty() ? facts : fresh).get(random.nextInt(
|
||||||
broadcast(subject, fact);
|
(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) {
|
private void remember(Fact fact) {
|
||||||
if (active != null) {
|
recentFacts.addLast(plain(fact.text()));
|
||||||
active.hide();
|
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)
|
.append(Component.text("Sabia que o ", NamedTextColor.WHITE)
|
||||||
.decoration(TextDecoration.BOLD, false))
|
.decoration(TextDecoration.BOLD, false))
|
||||||
.append(Component.text(subject.getName(), NamedTextColor.GREEN)
|
.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(Component.text(" ", NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false))
|
||||||
.append(fact.decoration(TextDecoration.BOLD, false))
|
.append(fact.decoration(TextDecoration.BOLD, false))
|
||||||
.append(Component.text("?", NamedTextColor.WHITE).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));
|
Bukkit.broadcast(buttons(session));
|
||||||
|
|
||||||
session.show();
|
session.show();
|
||||||
getServer().getScheduler().runTaskLater(this, () -> {
|
getServer().getScheduler().runTaskLater(this, () -> {
|
||||||
session.hide();
|
session.hide();
|
||||||
if (active == session) {
|
if (active == session) {
|
||||||
active = null;
|
active = null;
|
||||||
}
|
}
|
||||||
}, reactionWindowSeconds * 20L);
|
}, settings.reactionWindowSeconds() * 20L);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The clickable reaction row. Counts are frozen at send time; the boss bar is live. */
|
/** The clickable reaction row. Counts are frozen at send time; the boss bar is live. */
|
||||||
private Component buttons(ReactionSession session) {
|
private Component buttons(ReactionSession session) {
|
||||||
Component row = Component.text(" ");
|
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)
|
row = row.append(Component.text(entry.getValue() + " ", NamedTextColor.YELLOW)
|
||||||
.clickEvent(ClickEvent.runCommand(
|
.clickEvent(ClickEvent.runCommand(
|
||||||
"/curiosidade reagir " + session.id() + " " + entry.getKey()))
|
"/curiosidade reagir " + session.id() + " " + entry.getKey()))
|
||||||
@@ -127,87 +191,46 @@ public final class Curiosidades extends JavaPlugin implements CommandExecutor, T
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- commands -----------------------------------------------------------
|
ReactionSession activeSession() {
|
||||||
|
return active;
|
||||||
@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 -------------------------------------------------------------
|
// --- events -------------------------------------------------------------
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onJoin(PlayerJoinEvent event) {
|
public void onJoin(PlayerJoinEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
if (active != null) {
|
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) {
|
// --- per-player opt out -------------------------------------------------
|
||||||
Byte value = player.getPersistentDataContainer()
|
|
||||||
.get(optOutKey, PersistentDataType.BYTE);
|
boolean isOptedOut(Player player) {
|
||||||
|
Byte value = player.getPersistentDataContainer().get(optOutKey, PersistentDataType.BYTE);
|
||||||
return value != null && value == 1;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,108 +36,106 @@ final class CuriosityFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every sentence that currently applies to {@code player}. May be empty for a
|
* Every sentence that currently applies to {@code player}, restricted to the
|
||||||
* brand-new player who has not done anything worth mentioning yet.
|
* enabled categories. May be empty for a brand-new player.
|
||||||
*/
|
*/
|
||||||
static List<Component> facts(Player player) {
|
static List<Fact> facts(Player player, Settings settings) {
|
||||||
List<Component> facts = new ArrayList<>();
|
List<Fact> facts = new ArrayList<>();
|
||||||
|
|
||||||
materialFact(facts, player, Stats.resolve("MINE_BLOCK"), MIN_COUNT,
|
material(facts, player, Category.MINERACAO, Stats.resolve("MINE_BLOCK"), MIN_COUNT,
|
||||||
"já minerou ", " blocos de ");
|
"já minerou ", " blocos de ");
|
||||||
materialFact(facts, player, Stats.resolve("BREAK_ITEM"), 3,
|
material(facts, player, Category.ITENS, Stats.resolve("BREAK_ITEM"), 3,
|
||||||
"já quebrou ", " unidades de ");
|
"já quebrou ", " unidades de ");
|
||||||
materialFact(facts, player, Stats.resolve("CRAFT_ITEM"), MIN_COUNT,
|
material(facts, player, Category.ITENS, Stats.resolve("CRAFT_ITEM"), MIN_COUNT,
|
||||||
"já fabricou ", " unidades de ");
|
"já fabricou ", " unidades de ");
|
||||||
materialFact(facts, player, Stats.resolve("USE_ITEM"), 200,
|
material(facts, player, Category.ITENS, Stats.resolve("USE_ITEM"), 200,
|
||||||
"já usou ", " vezes o item ");
|
"já usou ", " vezes o item ");
|
||||||
materialFact(facts, player, Stats.resolve("PICKUP", "PICKED_UP"), 500,
|
material(facts, player, Category.ITENS, Stats.resolve("PICKUP", "PICKED_UP"), 500,
|
||||||
"já coletou ", " unidades de ");
|
"já coletou ", " unidades de ");
|
||||||
|
|
||||||
entityFacts(facts, player, Stats.resolve("ENTITY_KILLED_BY"), MIN_DEATHS,
|
entities(facts, player, Category.MORTES, Stats.resolve("ENTITY_KILLED_BY"), MIN_DEATHS,
|
||||||
"já morreu ", " vezes para ");
|
"já morreu ", " vezes para ");
|
||||||
entityFacts(facts, player, Stats.resolve("KILL_ENTITY"), MIN_COUNT,
|
entities(facts, player, Category.COMBATE, Stats.resolve("KILL_ENTITY"), MIN_COUNT,
|
||||||
"já derrotou ", " inimigos do tipo ");
|
"já derrotou ", " inimigos do tipo ");
|
||||||
|
|
||||||
distanceFact(facts, player, Stats.resolve("WALK_ONE_CM"), "já caminhou ", " a pé");
|
distance(facts, player, Stats.resolve("WALK_ONE_CM"), "já caminhou ", " a pé");
|
||||||
distanceFact(facts, player, Stats.resolve("SPRINT_ONE_CM"), "já correu ", "");
|
distance(facts, player, Stats.resolve("SPRINT_ONE_CM"), "já correu ", "");
|
||||||
distanceFact(facts, player, Stats.resolve("FLY_ONE_CM"), "já voou ", "");
|
distance(facts, player, Stats.resolve("FLY_ONE_CM"), "já voou ", "");
|
||||||
distanceFact(facts, player, Stats.resolve("BOAT_ONE_CM"), "já navegou ", " de barco");
|
distance(facts, player, Stats.resolve("BOAT_ONE_CM"), "já navegou ", " de barco");
|
||||||
distanceFact(facts, player, Stats.resolve("HORSE_ONE_CM"), "já cavalgou ", "");
|
distance(facts, player, Stats.resolve("HORSE_ONE_CM"), "já cavalgou ", "");
|
||||||
distanceFact(facts, player, Stats.resolve("SWIM_ONE_CM"), "já nadou ", "");
|
distance(facts, player, Stats.resolve("SWIM_ONE_CM"), "já nadou ", "");
|
||||||
distanceFact(facts, player, Stats.resolve("MINECART_ONE_CM"), "já andou ", " de carrinho");
|
distance(facts, player, Stats.resolve("MINECART_ONE_CM"), "já andou ", " de carrinho");
|
||||||
|
|
||||||
timeFact(facts, player, Stats.resolve("PLAY_TIME", "PLAY_ONE_MINUTE"),
|
time(facts, player, Stats.resolve("PLAY_TIME", "PLAY_ONE_MINUTE"),
|
||||||
"já passou ", " dentro do servidor");
|
"já passou ", " dentro do servidor");
|
||||||
timeFact(facts, player, Stats.resolve("TIME_SINCE_DEATH"),
|
time(facts, player, Stats.resolve("TIME_SINCE_DEATH"), "está há ", " sem morrer");
|
||||||
"está há ", " sem morrer");
|
time(facts, player, Stats.resolve("TIME_SINCE_REST"), "está há ", " sem dormir");
|
||||||
timeFact(facts, player, Stats.resolve("TIME_SINCE_REST"),
|
|
||||||
"está há ", " sem dormir");
|
|
||||||
|
|
||||||
countFact(facts, player, Stats.resolve("JUMP"), 500, "já pulou ", " vezes");
|
count(facts, player, Category.MORTES, Stats.resolve("DEATHS"), MIN_DEATHS,
|
||||||
countFact(facts, player, Stats.resolve("DEATHS"), MIN_DEATHS, "já morreu ", " vezes no total");
|
"já morreu ", " vezes no total");
|
||||||
countFact(facts, player, Stats.resolve("MOB_KILLS"), MIN_COUNT, "já derrotou ", " monstros");
|
count(facts, player, Category.COMBATE, Stats.resolve("MOB_KILLS"), MIN_COUNT,
|
||||||
countFact(facts, player, Stats.resolve("DAMAGE_DEALT"), 1000, "já causou ", " de dano");
|
"já derrotou ", " monstros");
|
||||||
countFact(facts, player, Stats.resolve("DAMAGE_TAKEN"), 1000, "já levou ", " de dano");
|
count(facts, player, Category.COMBATE, Stats.resolve("DAMAGE_DEALT"), 1000,
|
||||||
countFact(facts, player, Stats.resolve("FISH_CAUGHT"), 5, "já pescou ", " peixes");
|
"já causou ", " de dano");
|
||||||
countFact(facts, player, Stats.resolve("ANIMALS_BRED"), 5, "já acasalou ", " animais");
|
count(facts, player, Category.COMBATE, Stats.resolve("DAMAGE_TAKEN"), 1000,
|
||||||
countFact(facts, player, Stats.resolve("ITEM_ENCHANTED"), 3, "já encantou ", " itens");
|
"já levou ", " de dano");
|
||||||
countFact(facts, player, Stats.resolve("TRADED_WITH_VILLAGER"), 5,
|
count(facts, player, Category.DIVERSOS, Stats.resolve("JUMP"), 500, "já pulou ", " vezes");
|
||||||
|
count(facts, player, Category.DIVERSOS, Stats.resolve("FISH_CAUGHT"), 5,
|
||||||
|
"já pescou ", " peixes");
|
||||||
|
count(facts, player, Category.DIVERSOS, Stats.resolve("ANIMALS_BRED"), 5,
|
||||||
|
"já acasalou ", " animais");
|
||||||
|
count(facts, player, Category.DIVERSOS, Stats.resolve("ITEM_ENCHANTED"), 3,
|
||||||
|
"já encantou ", " itens");
|
||||||
|
count(facts, player, Category.DIVERSOS, Stats.resolve("TRADED_WITH_VILLAGER"), 5,
|
||||||
"já negociou ", " vezes com aldeões");
|
"já negociou ", " vezes com aldeões");
|
||||||
countFact(facts, player, Stats.resolve("RAID_WIN"), 1, "já venceu ", " invasões");
|
count(facts, player, Category.DIVERSOS, Stats.resolve("RAID_WIN"), 1,
|
||||||
|
"já venceu ", " invasões");
|
||||||
|
|
||||||
|
facts.removeIf(fact -> !settings.categoryEnabled(fact.category()));
|
||||||
return facts;
|
return facts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One random applicable sentence, or null if the player has nothing notable yet. */
|
|
||||||
static Component random(Player player, java.util.Random random) {
|
|
||||||
List<Component> facts = facts(player);
|
|
||||||
if (facts.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return facts.get(random.nextInt(facts.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- builders -----------------------------------------------------------
|
// --- builders -----------------------------------------------------------
|
||||||
|
|
||||||
private static void materialFact(List<Component> out, Player player, Statistic statistic,
|
private static void material(List<Fact> out, Player player, Category category,
|
||||||
int minimum, String prefix, String middle) {
|
Statistic statistic, int minimum, String prefix, String middle) {
|
||||||
Stats.Entry<Material> top = Stats.topMaterial(player, statistic, minimum);
|
Stats.Entry<Material> top = Stats.topMaterial(player, statistic, minimum);
|
||||||
if (top == null) {
|
if (top == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
out.add(Component.text(prefix, NamedTextColor.WHITE)
|
out.add(new Fact(category, Component.text(prefix, NamedTextColor.WHITE)
|
||||||
.append(number(top.value()))
|
.append(number(top.value()))
|
||||||
.append(Component.text(middle, NamedTextColor.WHITE))
|
.append(Component.text(middle, NamedTextColor.WHITE))
|
||||||
.append(name(top.subject())));
|
.append(name(top.subject()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void entityFacts(List<Component> out, Player player, Statistic statistic,
|
private static void entities(List<Fact> out, Player player, Category category,
|
||||||
int minimum, String prefix, String middle) {
|
Statistic statistic, int minimum, String prefix, String middle) {
|
||||||
List<Stats.Entry<EntityType>> entries = Stats.entities(player, statistic, minimum);
|
List<Stats.Entry<EntityType>> entries = Stats.entities(player, statistic, minimum);
|
||||||
// Deaths-by-mob are interesting per mob, not just for the worst offender.
|
// Deaths-by-mob are interesting per mob, not just for the worst offender.
|
||||||
Collections.shuffle(entries);
|
Collections.shuffle(entries);
|
||||||
for (Stats.Entry<EntityType> entry : entries.subList(0, Math.min(3, entries.size()))) {
|
for (Stats.Entry<EntityType> entry : entries.subList(0, Math.min(3, entries.size()))) {
|
||||||
out.add(Component.text(prefix, NamedTextColor.WHITE)
|
out.add(new Fact(category, Component.text(prefix, NamedTextColor.WHITE)
|
||||||
.append(number(entry.value()))
|
.append(number(entry.value()))
|
||||||
.append(Component.text(middle, NamedTextColor.WHITE))
|
.append(Component.text(middle, NamedTextColor.WHITE))
|
||||||
.append(name(entry.subject())));
|
.append(name(entry.subject()))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void distanceFact(List<Component> out, Player player, Statistic statistic,
|
private static void distance(List<Fact> out, Player player, Statistic statistic,
|
||||||
String prefix, String suffix) {
|
String prefix, String suffix) {
|
||||||
int centimetres = Stats.untyped(player, statistic);
|
int centimetres = Stats.untyped(player, statistic);
|
||||||
if (centimetres < MIN_CENTIMETRES) {
|
if (centimetres < MIN_CENTIMETRES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double km = centimetres / 100_000.0;
|
double km = centimetres / 100_000.0;
|
||||||
out.add(Component.text(prefix, NamedTextColor.WHITE)
|
out.add(new Fact(Category.DISTANCIA, Component.text(prefix, NamedTextColor.WHITE)
|
||||||
.append(Component.text(String.format(PT_BR, "%,.1f km", km), NamedTextColor.AQUA))
|
.append(Component.text(String.format(PT_BR, "%,.1f km", km), NamedTextColor.AQUA))
|
||||||
.append(Component.text(suffix, NamedTextColor.WHITE)));
|
.append(Component.text(suffix, NamedTextColor.WHITE))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void timeFact(List<Component> out, Player player, Statistic statistic,
|
private static void time(List<Fact> out, Player player, Statistic statistic,
|
||||||
String prefix, String suffix) {
|
String prefix, String suffix) {
|
||||||
int ticks = Stats.untyped(player, statistic);
|
int ticks = Stats.untyped(player, statistic);
|
||||||
long hours = ticks / 20L / 3600L;
|
long hours = ticks / 20L / 3600L;
|
||||||
if (hours < 1) {
|
if (hours < 1) {
|
||||||
@@ -146,13 +144,13 @@ final class CuriosityFactory {
|
|||||||
String text = hours >= 24
|
String text = hours >= 24
|
||||||
? String.format(PT_BR, "%d dias e %d horas", hours / 24, hours % 24)
|
? String.format(PT_BR, "%d dias e %d horas", hours / 24, hours % 24)
|
||||||
: hours + " horas";
|
: hours + " horas";
|
||||||
out.add(Component.text(prefix, NamedTextColor.WHITE)
|
out.add(new Fact(Category.TEMPO, Component.text(prefix, NamedTextColor.WHITE)
|
||||||
.append(Component.text(text, NamedTextColor.AQUA))
|
.append(Component.text(text, NamedTextColor.AQUA))
|
||||||
.append(Component.text(suffix, NamedTextColor.WHITE)));
|
.append(Component.text(suffix, NamedTextColor.WHITE))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void countFact(List<Component> out, Player player, Statistic statistic,
|
private static void count(List<Fact> out, Player player, Category category,
|
||||||
int minimum, String prefix, String suffix) {
|
Statistic statistic, int minimum, String prefix, String suffix) {
|
||||||
int value = Stats.untyped(player, statistic);
|
int value = Stats.untyped(player, statistic);
|
||||||
if (value < minimum) {
|
if (value < minimum) {
|
||||||
return;
|
return;
|
||||||
@@ -161,9 +159,9 @@ final class CuriosityFactory {
|
|||||||
if (statistic != null && statistic.name().startsWith("DAMAGE_")) {
|
if (statistic != null && statistic.name().startsWith("DAMAGE_")) {
|
||||||
value = value / 10;
|
value = value / 10;
|
||||||
}
|
}
|
||||||
out.add(Component.text(prefix, NamedTextColor.WHITE)
|
out.add(new Fact(category, Component.text(prefix, NamedTextColor.WHITE)
|
||||||
.append(number(value))
|
.append(number(value))
|
||||||
.append(Component.text(suffix, NamedTextColor.WHITE)));
|
.append(Component.text(suffix, NamedTextColor.WHITE))));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- pieces -------------------------------------------------------------
|
// --- pieces -------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.marcospaulo.curiosidades;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
|
/** One generated curiosity sentence, tagged with the category it came from. */
|
||||||
|
record Fact(Category category, Component text) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.marcospaulo.curiosidades;
|
||||||
|
|
||||||
|
/** When curiosities fire on their own. */
|
||||||
|
enum Mode {
|
||||||
|
|
||||||
|
/** Only when a player joins the world. This is the default. */
|
||||||
|
ENTRADA,
|
||||||
|
/** Only on a repeating timer. */
|
||||||
|
INTERVALO,
|
||||||
|
/** Both triggers at once. */
|
||||||
|
AMBOS,
|
||||||
|
/** Never automatically — only via /curiosidade. */
|
||||||
|
MANUAL;
|
||||||
|
|
||||||
|
static Mode byKey(String key) {
|
||||||
|
for (Mode mode : values()) {
|
||||||
|
if (mode.name().equalsIgnoreCase(key)) {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean firesOnJoin() {
|
||||||
|
return this == ENTRADA || this == AMBOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean firesOnTimer() {
|
||||||
|
return this == INTERVALO || this == AMBOS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package dev.marcospaulo.curiosidades;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed view over config.yml.
|
||||||
|
*
|
||||||
|
* <p>Every setter writes through to disk immediately, so a command that changes
|
||||||
|
* behaviour survives a restart without anyone having to remember to save.
|
||||||
|
*/
|
||||||
|
final class Settings {
|
||||||
|
|
||||||
|
private final Curiosidades plugin;
|
||||||
|
|
||||||
|
Settings(Curiosidades plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- triggering ---------------------------------------------------------
|
||||||
|
|
||||||
|
Mode mode() {
|
||||||
|
Mode mode = Mode.byKey(plugin.getConfig().getString("modo", "ENTRADA"));
|
||||||
|
return mode == null ? Mode.ENTRADA : mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mode(Mode mode) {
|
||||||
|
set("modo", mode.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
int intervalMinutes() {
|
||||||
|
return Math.max(1, plugin.getConfig().getInt("intervalo-minutos", 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
void intervalMinutes(int minutes) {
|
||||||
|
set("intervalo-minutos", Math.max(1, minutes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seconds to wait after a join before announcing, so it lands after the join message. */
|
||||||
|
int joinDelaySeconds() {
|
||||||
|
return Math.max(0, plugin.getConfig().getInt("atraso-entrada-segundos", 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
void joinDelaySeconds(int seconds) {
|
||||||
|
set("atraso-entrada-segundos", Math.max(0, seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minutes before the same player can be the subject again. */
|
||||||
|
int cooldownMinutes() {
|
||||||
|
return Math.max(0, plugin.getConfig().getInt("cooldown-minutos", 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
void cooldownMinutes(int minutes) {
|
||||||
|
set("cooldown-minutos", Math.max(0, minutes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many recent facts to remember and avoid repeating. */
|
||||||
|
int noRepeat() {
|
||||||
|
return Math.max(0, plugin.getConfig().getInt("evitar-repetir", 15));
|
||||||
|
}
|
||||||
|
|
||||||
|
void noRepeat(int count) {
|
||||||
|
set("evitar-repetir", Math.max(0, count));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- presentation -------------------------------------------------------
|
||||||
|
|
||||||
|
boolean reactionsEnabled() {
|
||||||
|
return plugin.getConfig().getBoolean("reacoes-ativas", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reactionsEnabled(boolean enabled) {
|
||||||
|
set("reacoes-ativas", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
int reactionWindowSeconds() {
|
||||||
|
return Math.max(5, plugin.getConfig().getInt("janela-reacao-segundos", 90));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reactionWindowSeconds(int seconds) {
|
||||||
|
set("janela-reacao-segundos", Math.max(5, seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> reactions() {
|
||||||
|
Map<String, String> reactions = new LinkedHashMap<>();
|
||||||
|
ConfigurationSection section = plugin.getConfig().getConfigurationSection("reacoes");
|
||||||
|
if (section != null) {
|
||||||
|
for (String key : section.getKeys(false)) {
|
||||||
|
reactions.put(key, section.getString(key, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reactions.isEmpty()) {
|
||||||
|
reactions.put("joia", "[+1]");
|
||||||
|
}
|
||||||
|
return reactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reaction(String key, String label) {
|
||||||
|
set("reacoes." + key, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeReaction(String key) {
|
||||||
|
set("reacoes." + key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- content ------------------------------------------------------------
|
||||||
|
|
||||||
|
boolean categoryEnabled(Category category) {
|
||||||
|
return plugin.getConfig().getBoolean("categorias." + category.key(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void categoryEnabled(Category category, boolean enabled) {
|
||||||
|
set("categorias." + category.key(), enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- plumbing -----------------------------------------------------------
|
||||||
|
|
||||||
|
private void set(String path, Object value) {
|
||||||
|
plugin.getConfig().set(path, value);
|
||||||
|
plugin.saveConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,49 @@
|
|||||||
# Curiosidades — anúncios automáticos sobre os jogadores online.
|
# Curiosidades — anúncios automáticos sobre os jogadores.
|
||||||
|
#
|
||||||
|
# Tudo aqui também pode ser mudado em jogo com /curiosidade, e os comandos
|
||||||
|
# gravam neste arquivo na hora. Veja /curiosidade ajuda.
|
||||||
|
|
||||||
# Intervalo entre anúncios, em minutos.
|
# Quando as curiosidades disparam sozinhas:
|
||||||
|
# entrada - só quando um jogador entra no mundo (padrão)
|
||||||
|
# intervalo - só num temporizador
|
||||||
|
# ambos - os dois
|
||||||
|
# manual - nunca sozinho, só com /curiosidade
|
||||||
|
modo: ENTRADA
|
||||||
|
|
||||||
|
# Segundos de espera depois que o jogador entra, para a curiosidade aparecer
|
||||||
|
# depois da mensagem de entrada em vez de disputar com ela.
|
||||||
|
atraso-entrada-segundos: 6
|
||||||
|
|
||||||
|
# Intervalo do modo temporizado, em minutos. Ignorado nos modos entrada/manual.
|
||||||
intervalo-minutos: 20
|
intervalo-minutos: 20
|
||||||
|
|
||||||
|
# Tempo mínimo antes do mesmo jogador ser citado de novo. Evita spam quando
|
||||||
|
# alguém fica entrando e saindo.
|
||||||
|
cooldown-minutos: 30
|
||||||
|
|
||||||
|
# Quantas curiosidades recentes lembrar para não repetir.
|
||||||
|
evitar-repetir: 15
|
||||||
|
|
||||||
|
# Reações clicáveis embaixo de cada curiosidade.
|
||||||
|
reacoes-ativas: true
|
||||||
|
|
||||||
# Por quanto tempo a barra de reações fica visível, em segundos.
|
# Por quanto tempo a barra de reações fica visível, em segundos.
|
||||||
janela-reacao-segundos: 90
|
janela-reacao-segundos: 90
|
||||||
|
|
||||||
# Reações disponíveis. A chave é usada no comando; o valor é o que aparece no chat.
|
# As reações disponíveis. A chave é usada no comando; o valor aparece no chat.
|
||||||
# Emoji funcionam no Java; no Bedrock (Geyser) alguns não renderizam, então
|
# Emoji funcionam no Java; no Bedrock (Geyser) alguns não renderizam, então
|
||||||
# rótulos em texto puro são a opção segura para servidores com muitos jogadores
|
# rótulos em texto puro são a opção segura se você tem muitos jogadores Bedrock.
|
||||||
# de Bedrock.
|
|
||||||
reacoes:
|
reacoes:
|
||||||
joia: "[👍]"
|
joia: "[👍]"
|
||||||
uau: "[😮]"
|
uau: "[😮]"
|
||||||
fogo: "[🔥]"
|
fogo: "[🔥]"
|
||||||
|
|
||||||
|
# Tipos de curiosidade que podem ser sorteados.
|
||||||
|
categorias:
|
||||||
|
mineracao: true
|
||||||
|
itens: true
|
||||||
|
mortes: true
|
||||||
|
combate: true
|
||||||
|
distancia: true
|
||||||
|
tempo: true
|
||||||
|
diversos: true
|
||||||
|
|||||||
@@ -8,19 +8,25 @@ folia-supported: false
|
|||||||
|
|
||||||
commands:
|
commands:
|
||||||
curiosidade:
|
curiosidade:
|
||||||
description: Anuncia uma curiosidade agora, ou gerencia suas preferências.
|
description: Anuncia curiosidades e ajusta como elas funcionam.
|
||||||
usage: /curiosidade [toggle|reload]
|
usage: /curiosidade ajuda
|
||||||
aliases: [curiosidades]
|
aliases: [curiosidades, cur]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
# Declared explicitly: an undeclared Bukkit permission falls back to op-only,
|
# Declared explicitly: an undeclared Bukkit permission falls back to op-only,
|
||||||
# which would silently stop normal players from being able to react.
|
# which would silently stop normal players reacting or opting out.
|
||||||
curiosidades.reagir:
|
curiosidades.reagir:
|
||||||
description: Permite reagir às curiosidades.
|
description: Permite reagir às curiosidades.
|
||||||
default: true
|
default: true
|
||||||
|
curiosidades.ver:
|
||||||
|
description: Permite usar /curiosidade ver e /curiosidade listar.
|
||||||
|
default: true
|
||||||
curiosidades.forcar:
|
curiosidades.forcar:
|
||||||
description: Permite forçar um anúncio com /curiosidade.
|
description: Permite disparar um anúncio manualmente.
|
||||||
default: op
|
default: op
|
||||||
curiosidades.admin:
|
curiosidades.admin:
|
||||||
description: Permite recarregar a configuração.
|
description: Permite mudar modo, intervalo, categorias e reações.
|
||||||
default: op
|
default: op
|
||||||
|
curiosidades.isento:
|
||||||
|
description: Quem tem isto nunca é sorteado como assunto.
|
||||||
|
default: false
|
||||||
|
|||||||
Reference in New Issue
Block a user