i18n: per-player EN/PT via Adventure GlobalTranslator #1
@@ -97,6 +97,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
private Reactions liveReactions;
|
private Reactions liveReactions;
|
||||||
private GuessRound guessRound;
|
private GuessRound guessRound;
|
||||||
private Poll poll;
|
private Poll poll;
|
||||||
|
private Titles titles;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -108,6 +109,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
offlineStats = new OfflineStats(this);
|
offlineStats = new OfflineStats(this);
|
||||||
milestones = new Milestones(this);
|
milestones = new Milestones(this);
|
||||||
achievements = new Achievements(this);
|
achievements = new Achievements(this);
|
||||||
|
titles = new Titles(this);
|
||||||
weeklyStats = new WeeklyStats(new java.io.File(getDataFolder(), "semana.yml"));
|
weeklyStats = new WeeklyStats(new java.io.File(getDataFolder(), "semana.yml"));
|
||||||
aiBudget = new Budget(settings.aiSpontaneousPerDay(),
|
aiBudget = new Budget(settings.aiSpontaneousPerDay(),
|
||||||
settings.aiSpontaneousGapMinutes() * 60_000L,
|
settings.aiSpontaneousGapMinutes() * 60_000L,
|
||||||
@@ -127,11 +129,13 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
CanalhandiaCommand root = new CanalhandiaCommand(this);
|
CanalhandiaCommand root = new CanalhandiaCommand(this);
|
||||||
for (String name : List.of("canalhandia", "curiosidade", "adivinha", "enquete", "ranking",
|
for (String name : List.of("canalhandia", "curiosidade", "adivinha", "enquete", "ranking",
|
||||||
"reagir", "reacoes", "palpite", "votar", "legal", "wow", "top", "f", "ia", "iap",
|
"reagir", "reacoes", "palpite", "votar", "legal", "wow", "top", "f", "ia", "iap",
|
||||||
"errado", "nota", "save", "recado", "recados", "mortes", "conquistas")) {
|
"errado", "nota", "save", "recado", "recados", "mortes", "conquistas",
|
||||||
|
"perfil", "titulo")) {
|
||||||
register(name, root);
|
register(name, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
getServer().getPluginManager().registerEvents(this, this);
|
getServer().getPluginManager().registerEvents(this, this);
|
||||||
|
getServer().getPluginManager().registerEvents(new TitleChatListener(this), this);
|
||||||
rescheduleTimer();
|
rescheduleTimer();
|
||||||
rescheduleMilestones();
|
rescheduleMilestones();
|
||||||
|
|
||||||
@@ -202,6 +206,11 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
return achievements;
|
return achievements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The title each player has chosen to wear in chat. Never null. */
|
||||||
|
Titles titles() {
|
||||||
|
return titles;
|
||||||
|
}
|
||||||
|
|
||||||
/** The weekly ranking baseline. Never null. */
|
/** The weekly ranking baseline. Never null. */
|
||||||
WeeklyStats weeklyStats() {
|
WeeklyStats weeklyStats() {
|
||||||
return weeklyStats;
|
return weeklyStats;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every command the plugin owns.
|
* Every command the plugin owns.
|
||||||
@@ -58,7 +59,9 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
|||||||
case "recado" -> recado(sender, args);
|
case "recado" -> recado(sender, args);
|
||||||
case "recados" -> recados(sender);
|
case "recados" -> recados(sender);
|
||||||
case "mortes" -> mortes(sender);
|
case "mortes" -> mortes(sender);
|
||||||
case "conquistas" -> conquistas(sender);
|
case "conquistas" -> conquistas(sender, args);
|
||||||
|
case "perfil" -> perfil(sender, args);
|
||||||
|
case "titulo" -> titulo(sender, args);
|
||||||
// Typed twin of the [F] mourning button — "f" is not a configured
|
// Typed twin of the [F] mourning button — "f" is not a configured
|
||||||
// reaction (the mourning set is hardcoded), so it can't be found via
|
// reaction (the mourning set is hardcoded), so it can't be found via
|
||||||
// reactionForCommand; route it directly. Acts on the latest message,
|
// reactionForCommand; route it directly. Acts on the latest message,
|
||||||
@@ -1239,24 +1242,52 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code /conquistas} — the full catalogue, with the ones you have earned
|
* {@code /conquistas [jogador]} — the full catalogue, with the ones earned
|
||||||
* marked. Showing the locked ones too is the point: an achievement nobody
|
* marked. Showing the locked ones too is the point: an achievement nobody
|
||||||
* can see is one nobody chases.
|
* can see is one nobody chases.
|
||||||
|
*
|
||||||
|
* <p>With no name it is your own list, from the recorded flags (what was
|
||||||
|
* actually announced). With a name it is that player's, computed from their
|
||||||
|
* current stats on disk, so it works for anyone the server has seen — online
|
||||||
|
* or not.
|
||||||
*/
|
*/
|
||||||
private boolean conquistas(CommandSender sender) {
|
private boolean conquistas(CommandSender sender, String[] args) {
|
||||||
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
|
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
|
||||||
Msg.error(sender, "O módulo de conquistas está desligado.");
|
Msg.error(sender, "O módulo de conquistas está desligado.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!(sender instanceof Player player)) {
|
if (args.length == 0) {
|
||||||
Msg.error(sender, "Só jogadores têm conquistas.");
|
if (!(sender instanceof Player player)) {
|
||||||
|
Msg.error(sender, "Só jogadores têm conquistas. Use /conquistas <jogador>.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
renderCatalogue(sender, plugin.achievements().earnedBy(player),
|
||||||
|
Platform.isBedrock(player), player.getName());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
List<Achievement> earned = plugin.achievements().earnedBy(player);
|
String wanted = String.join(" ", args);
|
||||||
Msg.header(sender, "Conquistas (" + earned.size() + "/" + Achievement.values().length + ")");
|
OfflineStats.Known who = plugin.offlineStats().resolve(wanted);
|
||||||
|
if (who == null) {
|
||||||
|
Msg.error(sender, "Não conheço ninguém chamado \"" + wanted + "\".");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Map<String, Long> stats = plugin.offlineStats().achievementStats(UUID.fromString(who.uuid()));
|
||||||
|
if (stats == null) {
|
||||||
|
Msg.error(sender, "Ainda não tenho estatísticas de " + who.name() + ".");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
boolean bedrock = sender instanceof Player viewer && Platform.isBedrock(viewer);
|
||||||
|
renderCatalogue(sender, Achievement.earned(stats), bedrock, who.name());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The catalogue with earned entries ticked — shared by self and lookup. */
|
||||||
|
private void renderCatalogue(CommandSender sender, java.util.Collection<Achievement> earned,
|
||||||
|
boolean bedrock, String who) {
|
||||||
|
Msg.header(sender, "Conquistas de " + who + " ("
|
||||||
|
+ earned.size() + "/" + Achievement.values().length + ")");
|
||||||
// Bedrock renders "✔" as a tofu box, so it gets an ASCII marker — the
|
// Bedrock renders "✔" as a tofu box, so it gets an ASCII marker — the
|
||||||
// same rule the reaction labels follow.
|
// same rule the reaction labels follow.
|
||||||
boolean bedrock = Platform.isBedrock(player);
|
|
||||||
String tick = bedrock ? " [x] " : " ✔ ";
|
String tick = bedrock ? " [x] " : " ✔ ";
|
||||||
String blank = bedrock ? " [ ] " : " · ";
|
String blank = bedrock ? " [ ] " : " · ";
|
||||||
for (Achievement achievement : Achievement.values()) {
|
for (Achievement achievement : Achievement.values()) {
|
||||||
@@ -1268,9 +1299,121 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
|||||||
.append(Component.text(" — " + achievement.description(),
|
.append(Component.text(" — " + achievement.description(),
|
||||||
NamedTextColor.DARK_GRAY)));
|
NamedTextColor.DARK_GRAY)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /perfil [jogador]} — a status card: headline stats, how many titles
|
||||||
|
* earned and which are worn. Works for anyone the server has seen, online or
|
||||||
|
* not, reading the same numbers the rankings do.
|
||||||
|
*/
|
||||||
|
private boolean perfil(CommandSender sender, String[] args) {
|
||||||
|
String uuid;
|
||||||
|
String name;
|
||||||
|
if (args.length == 0) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
Msg.error(sender, "Diga de quem: /perfil <jogador>.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
uuid = player.getUniqueId().toString();
|
||||||
|
name = player.getName();
|
||||||
|
} else {
|
||||||
|
String wanted = String.join(" ", args);
|
||||||
|
OfflineStats.Known who = plugin.offlineStats().resolve(wanted);
|
||||||
|
if (who == null) {
|
||||||
|
Msg.error(sender, "Não conheço ninguém chamado \"" + wanted + "\".");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
uuid = who.uuid();
|
||||||
|
name = who.name();
|
||||||
|
}
|
||||||
|
UUID id = UUID.fromString(uuid);
|
||||||
|
Msg.header(sender, "Perfil de " + name);
|
||||||
|
|
||||||
|
String summary = plugin.offlineStats().summary(id);
|
||||||
|
if (summary == null) {
|
||||||
|
Msg.line(sender, "Estatísticas", "sem dados ainda");
|
||||||
|
} else {
|
||||||
|
// summary() prefixes the name; the header already has it, so drop it.
|
||||||
|
int colon = summary.indexOf(": ");
|
||||||
|
sender.sendMessage(Component.text(" "
|
||||||
|
+ (colon >= 0 ? summary.substring(colon + 2) : summary), NamedTextColor.GRAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Long> stats = plugin.offlineStats().achievementStats(id);
|
||||||
|
List<Achievement> earned = stats == null ? List.of() : Achievement.earned(stats);
|
||||||
|
Msg.line(sender, "Conquistas", earned.size() + "/" + Achievement.values().length
|
||||||
|
+ (earned.isEmpty() ? "" : " (" + titlesList(earned) + ")"));
|
||||||
|
|
||||||
|
Achievement worn = plugin.titles().chosenAchievement(id);
|
||||||
|
Msg.line(sender, "Título", worn == null ? "nenhum" : worn.title());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /titulo [nome|limpar]} — choose which earned title to wear in chat,
|
||||||
|
* or clear it. Only titles you have actually earned can be worn; the list
|
||||||
|
* with no argument shows exactly those, so nobody has to guess the spelling.
|
||||||
|
*/
|
||||||
|
private boolean titulo(CommandSender sender, String[] args) {
|
||||||
|
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
|
||||||
|
Msg.error(sender, "O módulo de conquistas está desligado.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
Msg.error(sender, "Só jogadores usam títulos.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
List<Achievement> earned = plugin.achievements().earnedBy(player);
|
||||||
|
if (args.length == 0) {
|
||||||
|
Achievement worn = plugin.titles().chosenAchievement(player.getUniqueId());
|
||||||
|
Msg.line(sender, "Título atual", worn == null ? "nenhum" : worn.title());
|
||||||
|
if (earned.isEmpty()) {
|
||||||
|
Msg.error(sender, "Você ainda não desbloqueou nenhum título. Veja /conquistas.");
|
||||||
|
} else {
|
||||||
|
Msg.line(sender, "Disponíveis", titlesList(earned));
|
||||||
|
sender.sendMessage(Component.text(" Use /titulo <nome> para usar, "
|
||||||
|
+ "ou /titulo limpar para tirar.", NamedTextColor.DARK_GRAY));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String arg = String.join(" ", args);
|
||||||
|
if (arg.equalsIgnoreCase("limpar") || arg.equalsIgnoreCase("nenhum")) {
|
||||||
|
plugin.titles().clear(player.getUniqueId());
|
||||||
|
Msg.ok(player, "Título removido.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Achievement chosen = matchEarned(arg, earned);
|
||||||
|
if (chosen == null) {
|
||||||
|
Msg.error(sender, "Você não tem o título \"" + arg + "\". Veja /titulo para a lista.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
plugin.titles().set(player.getUniqueId(), chosen);
|
||||||
|
Msg.ok(player, "Título definido: " + chosen.title() + ".");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Matches typed text to an earned achievement by key or (case-insensitive) title. */
|
||||||
|
static Achievement matchEarned(String text, List<Achievement> earned) {
|
||||||
|
Achievement byKey = Achievement.byKey(text);
|
||||||
|
if (byKey != null && earned.contains(byKey)) {
|
||||||
|
return byKey;
|
||||||
|
}
|
||||||
|
for (Achievement achievement : earned) {
|
||||||
|
if (achievement.title().equalsIgnoreCase(text.trim())) {
|
||||||
|
return achievement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String titlesList(List<Achievement> earned) {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
for (Achievement achievement : earned) {
|
||||||
|
names.add(achievement.title());
|
||||||
|
}
|
||||||
|
return String.join(", ", names);
|
||||||
|
}
|
||||||
|
|
||||||
private void notaAdd(CommandSender sender, String[] args, Note.Scope scope) {
|
private void notaAdd(CommandSender sender, String[] args, Note.Scope scope) {
|
||||||
if (!(sender instanceof Player player)) {
|
if (!(sender instanceof Player player)) {
|
||||||
Msg.error(sender, "Só jogadores podem anotar (a anotação guarda onde você está).");
|
Msg.error(sender, "Só jogadores podem anotar (a anotação guarda onde você está).");
|
||||||
@@ -1547,6 +1690,22 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
|||||||
if (name.equals("save")) {
|
if (name.equals("save")) {
|
||||||
return args.length == 1 ? filter(List.of("coords"), args[0]) : List.of();
|
return args.length == 1 ? filter(List.of("coords"), args[0]) : List.of();
|
||||||
}
|
}
|
||||||
|
if ((name.equals("perfil") || name.equals("conquistas")) && args.length == 1) {
|
||||||
|
// Any player the server has seen — these commands read off disk.
|
||||||
|
return filter(plugin.offlineStats().knownNames(), args[0]);
|
||||||
|
}
|
||||||
|
if (name.equals("titulo") && args.length == 1) {
|
||||||
|
// Only the titles this player has actually earned, plus the clear
|
||||||
|
// word — completion should never suggest a title you cannot wear.
|
||||||
|
List<String> options = new ArrayList<>();
|
||||||
|
options.add("limpar");
|
||||||
|
if (sender instanceof Player player) {
|
||||||
|
for (Achievement achievement : plugin.achievements().earnedBy(player)) {
|
||||||
|
options.add(achievement.title());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filter(options, args[0]);
|
||||||
|
}
|
||||||
if (name.equals("ia") || name.equals("iap")) {
|
if (name.equals("ia") || name.equals("iap")) {
|
||||||
// Only the tuning subcommands are suggested — the rest of /ia is
|
// Only the tuning subcommands are suggested — the rest of /ia is
|
||||||
// free text, and completing a question would be noise.
|
// free text, and completing a question would be noise.
|
||||||
|
|||||||
@@ -123,6 +123,32 @@ final class OfflineStats {
|
|||||||
+ RankingMetric.COMBATE.format(mobKills) + " derrotados.";
|
+ RankingMetric.COMBATE.format(mobKills) + " derrotados.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full stat map an {@link Achievement} reads, for a player who may be
|
||||||
|
* offline. Keyed by {@link RankingMetric#commandKey()} — the same names the
|
||||||
|
* online {@link Achievements#snapshot} produces — so the identical pure
|
||||||
|
* conditions in {@link Achievement} evaluate the same whether the player is
|
||||||
|
* on- or offline.
|
||||||
|
*
|
||||||
|
* @return null when there is no stats file for this player (never played, or
|
||||||
|
* the directory is missing), which the caller shows as "sem dados".
|
||||||
|
*/
|
||||||
|
Map<String, Long> achievementStats(UUID uuid) {
|
||||||
|
File dir = statsDirectory();
|
||||||
|
if (dir == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
File file = new File(dir, uuid + ".json");
|
||||||
|
if (!file.isFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, Long> stats = new HashMap<>();
|
||||||
|
for (RankingMetric metric : RankingMetric.values()) {
|
||||||
|
stats.put(metric.commandKey(), read(file, metric));
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
private long read(File file, RankingMetric metric) {
|
private long read(File file, RankingMetric metric) {
|
||||||
try (Reader reader = new FileReader(file)) {
|
try (Reader reader = new FileReader(file)) {
|
||||||
JsonElement root = JsonParser.parseReader(reader);
|
JsonElement root = JsonParser.parseReader(reader);
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package dev.marcospaulo.canalhandia;
|
||||||
|
|
||||||
|
import io.papermc.paper.chat.ChatRenderer;
|
||||||
|
import io.papermc.paper.event.player.AsyncChatEvent;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefixes a player's chosen title to their chat line, when they wear one.
|
||||||
|
*
|
||||||
|
* <p>Chat-only, like the rest of the plugin: this changes how a message renders,
|
||||||
|
* never the player. It wraps the existing {@link ChatRenderer} instead of
|
||||||
|
* rewriting the message, so it composes with anything else that touches chat,
|
||||||
|
* and every viewer — Java and Bedrock alike, since Geyser/Floodgate deliver
|
||||||
|
* Bedrock chat through this same event — sees one "[Título] Nome: mensagem".
|
||||||
|
*
|
||||||
|
* <p>Gated on the {@code conquistas} module: switching achievements off also
|
||||||
|
* stops the titles they feed, in one place.
|
||||||
|
*/
|
||||||
|
final class TitleChatListener implements Listener {
|
||||||
|
|
||||||
|
private final Canalhandia plugin;
|
||||||
|
|
||||||
|
TitleChatListener(Canalhandia plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.NORMAL)
|
||||||
|
void onChat(AsyncChatEvent event) {
|
||||||
|
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Achievement worn = plugin.titles().chosenAchievement(event.getPlayer().getUniqueId());
|
||||||
|
if (worn == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Component tag = tag(worn);
|
||||||
|
ChatRenderer previous = event.renderer();
|
||||||
|
event.renderer((source, sourceDisplayName, message, viewer) ->
|
||||||
|
tag.append(previous.render(source, sourceDisplayName, message, viewer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The bracketed title chip that sits before the name. Pure, so it is testable. */
|
||||||
|
static Component tag(Achievement achievement) {
|
||||||
|
return Component.text("[", NamedTextColor.DARK_GRAY)
|
||||||
|
.append(Component.text(achievement.title(), NamedTextColor.AQUA))
|
||||||
|
.append(Component.text("] ", NamedTextColor.DARK_GRAY))
|
||||||
|
.decoration(TextDecoration.BOLD, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package dev.marcospaulo.canalhandia;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one title a player has chosen to wear in chat.
|
||||||
|
*
|
||||||
|
* <p>{@link Achievements} decides what a player has <em>earned</em>; this only
|
||||||
|
* remembers which of those they picked to show as a chat tag — one per player,
|
||||||
|
* stored by UUID in {@code titulos.yml}. Earning is not wearing: a player can
|
||||||
|
* hold ten titles and display none, or swap between them at will.
|
||||||
|
*
|
||||||
|
* <p>The stored value is the achievement <em>key</em>, not its display text, so
|
||||||
|
* a title's wording can change in code without rewriting everyone's file. A key
|
||||||
|
* that no longer resolves (an achievement removed from the enum) simply reads
|
||||||
|
* back as no title via {@link Achievement#byKey}, which is the safe direction to
|
||||||
|
* fail.
|
||||||
|
*/
|
||||||
|
final class Titles {
|
||||||
|
|
||||||
|
private final Canalhandia plugin;
|
||||||
|
private final File file;
|
||||||
|
private final YamlConfiguration data;
|
||||||
|
|
||||||
|
Titles(Canalhandia plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.file = new File(plugin.getDataFolder(), "titulos.yml");
|
||||||
|
this.data = YamlConfiguration.loadConfiguration(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The achievement this player wears, or null if none / unknown key. */
|
||||||
|
Achievement chosenAchievement(UUID player) {
|
||||||
|
return Achievement.byKey(data.getString(player.toString(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the worn title to an achievement's key and persists. */
|
||||||
|
void set(UUID player, Achievement achievement) {
|
||||||
|
data.set(player.toString(), achievement.key());
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the worn title and persists. */
|
||||||
|
void clear(UUID player) {
|
||||||
|
data.set(player.toString(), null);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save() {
|
||||||
|
try {
|
||||||
|
data.save(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning("Não consegui salvar titulos.yml: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,9 +87,17 @@ commands:
|
|||||||
usage: /mortes
|
usage: /mortes
|
||||||
aliases: [minhasmortes]
|
aliases: [minhasmortes]
|
||||||
conquistas:
|
conquistas:
|
||||||
description: Lista as conquistas e marca as que você já desbloqueou.
|
description: Lista as conquistas e marca as que você (ou outro jogador) já desbloqueou.
|
||||||
usage: /conquistas
|
usage: /conquistas [jogador]
|
||||||
aliases: [conquista]
|
aliases: [conquista]
|
||||||
|
perfil:
|
||||||
|
description: Mostra o perfil de um jogador — estatísticas, conquistas e título.
|
||||||
|
usage: /perfil [jogador]
|
||||||
|
aliases: [status]
|
||||||
|
titulo:
|
||||||
|
description: Escolhe qual conquista você exibe como título no chat.
|
||||||
|
usage: /titulo [nome|limpar]
|
||||||
|
aliases: [titulos, title]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
# Declared explicitly: an undeclared Bukkit permission falls back to op-only,
|
# Declared explicitly: an undeclared Bukkit permission falls back to op-only,
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package dev.marcospaulo.canalhandia;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/** The pure parts of the titles feature: selection and the offline stat contract. */
|
||||||
|
class TitlesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchesEarnedTitleByKeyAndByName() {
|
||||||
|
List<Achievement> earned = List.of(Achievement.PEDREIRO, Achievement.VETERANO);
|
||||||
|
// by key
|
||||||
|
assertSame(Achievement.PEDREIRO, CanalhandiaCommand.matchEarned("pedreiro", earned));
|
||||||
|
// by display name, case-insensitively
|
||||||
|
assertSame(Achievement.VETERANO, CanalhandiaCommand.matchEarned("veterano", earned));
|
||||||
|
assertSame(Achievement.PEDREIRO, CanalhandiaCommand.matchEarned("Pedreiro", earned));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refusesTitlesNotYetEarned() {
|
||||||
|
List<Achievement> earned = List.of(Achievement.PEDREIRO);
|
||||||
|
// A real achievement, but not one this player has: cannot be worn.
|
||||||
|
assertNull(CanalhandiaCommand.matchEarned("veterano", earned));
|
||||||
|
assertNull(CanalhandiaCommand.matchEarned("Casca Grossa", earned));
|
||||||
|
// Not an achievement at all.
|
||||||
|
assertNull(CanalhandiaCommand.matchEarned("rei do mundo", earned));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void offlineStatKeysFeedAchievementConditions() {
|
||||||
|
// OfflineStats.achievementStats keys its map by RankingMetric.commandKey();
|
||||||
|
// Achievement conditions read the same names. If those two drift apart, a
|
||||||
|
// lookup by name silently awards nothing — so pin the contract here.
|
||||||
|
Map<String, Long> stats = new HashMap<>();
|
||||||
|
stats.put(RankingMetric.MINERACAO.commandKey(), 10_000L);
|
||||||
|
assertTrue(Achievement.earned(stats).contains(Achievement.PEDREIRO));
|
||||||
|
|
||||||
|
stats.put(RankingMetric.MINERACAO.commandKey(), 9_999L);
|
||||||
|
assertFalse(Achievement.earned(stats).contains(Achievement.PEDREIRO));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user