i18n: per-player EN/PT via Adventure GlobalTranslator #1
@@ -98,6 +98,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
private GuessRound guessRound;
|
private GuessRound guessRound;
|
||||||
private Poll poll;
|
private Poll poll;
|
||||||
private Titles titles;
|
private Titles titles;
|
||||||
|
private DeathGift deathGift;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -113,6 +114,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
milestones = new Milestones(this);
|
milestones = new Milestones(this);
|
||||||
achievements = new Achievements(this);
|
achievements = new Achievements(this);
|
||||||
titles = new Titles(this);
|
titles = new Titles(this);
|
||||||
|
deathGift = new DeathGift(this);
|
||||||
// Load the achievement catalogue from config, then silently bank any
|
// Load the achievement catalogue from config, then silently bank any
|
||||||
// history the current definitions already imply (both here and for
|
// history the current definitions already imply (both here and for
|
||||||
// milestones), so an expanded catalogue never spams returning players.
|
// milestones), so an expanded catalogue never spams returning players.
|
||||||
@@ -247,6 +249,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
Achievement.load(Achievement.loadFrom(conquistasCatalogo(), getLogger()));
|
Achievement.load(Achievement.loadFrom(conquistasCatalogo(), getLogger()));
|
||||||
milestones.reload();
|
milestones.reload();
|
||||||
achievements.syncCatalogue();
|
achievements.syncCatalogue();
|
||||||
|
deathGift.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The weekly ranking baseline. Never null. */
|
/** The weekly ranking baseline. Never null. */
|
||||||
@@ -895,6 +898,11 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
|||||||
.append(Component.text(tail, NamedTextColor.AQUA));
|
.append(Component.text(tail, NamedTextColor.AQUA));
|
||||||
}
|
}
|
||||||
player.sendMessage(msg);
|
player.sendMessage(msg);
|
||||||
|
// A comic consolation item, given once they can actually hold it.
|
||||||
|
// Gameplay-neutral by design (a poppy, a wilted bush) — just a laugh.
|
||||||
|
if (deathGift.active()) {
|
||||||
|
deathGift.give(player);
|
||||||
|
}
|
||||||
}, 1L);
|
}, 1L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package dev.marcospaulo.canalhandia;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small consolation prize handed to a player when they respawn — a funeral
|
||||||
|
* poppy, a wilted bush, whatever. Comic, never useful: the point is a laugh at
|
||||||
|
* the death, not a leg up, so gifts are cosmetic-tier items given one at a time.
|
||||||
|
*
|
||||||
|
* <p>Config-driven like the achievement catalogue. If {@code mortes.presente} is
|
||||||
|
* absent the built-in list is used, so it works the moment the plugin loads;
|
||||||
|
* operators expand or mute it under that key and {@code /canalhandia reload}
|
||||||
|
* picks it up. Each line is {@code "MATERIAL | Nome | mensagem"}.
|
||||||
|
*/
|
||||||
|
final class DeathGift {
|
||||||
|
|
||||||
|
/** Baked-in default so a fresh server has something without editing config. */
|
||||||
|
private static final List<String> DEFAULTS = List.of(
|
||||||
|
"POPPY | Flor do Velório | Uma florzinha pro seu velório. Sentimos muito.",
|
||||||
|
"DEAD_BUSH | Buquê Murcho | Um buquê à altura do seu último desempenho.",
|
||||||
|
"WET_SPONGE | Esponja das Lágrimas | Toma, pra enxugar as lágrimas.",
|
||||||
|
"BONE | Osso da Sorte | Um ossinho pra você, campeão.",
|
||||||
|
"COOKIE | Cookie de Consolação | Cookie de consolação. Vai que melhora.",
|
||||||
|
"ROTTEN_FLESH | Carne Podre | É o que tinha sobrado na despensa.");
|
||||||
|
|
||||||
|
/** One gift: an item, the name it wears, and the line shown when it is given. */
|
||||||
|
record Gift(Material material, String name, String message) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Canalhandia plugin;
|
||||||
|
private final Random random = new Random();
|
||||||
|
private volatile boolean active;
|
||||||
|
private volatile List<Gift> gifts = List.of();
|
||||||
|
|
||||||
|
DeathGift(Canalhandia plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-reads the gift list from config (or the defaults). Driven by reload. */
|
||||||
|
void reload() {
|
||||||
|
ConfigurationSection section = plugin.getConfig().getConfigurationSection("mortes.presente");
|
||||||
|
boolean on = section == null || section.getBoolean("ativo", true);
|
||||||
|
List<String> raw = section == null ? DEFAULTS : section.getStringList("itens");
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
raw = DEFAULTS;
|
||||||
|
}
|
||||||
|
gifts = parse(raw, plugin.getLogger());
|
||||||
|
active = on && !gifts.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when a gift should be handed out on respawn. */
|
||||||
|
boolean active() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hands the player a random gift and a private comic line. Overflow is dropped
|
||||||
|
* at their feet rather than lost, so a full inventory never eats the joke. */
|
||||||
|
void give(Player player) {
|
||||||
|
Gift gift = pick(gifts, random);
|
||||||
|
if (gift == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ItemStack item = new ItemStack(gift.material());
|
||||||
|
item.editMeta(meta -> meta.displayName(Component.text(gift.name(), NamedTextColor.LIGHT_PURPLE)
|
||||||
|
.decoration(TextDecoration.ITALIC, false)));
|
||||||
|
Map<Integer, ItemStack> overflow = player.getInventory().addItem(item);
|
||||||
|
for (ItemStack leftover : overflow.values()) {
|
||||||
|
player.getWorld().dropItemNaturally(player.getLocation(), leftover);
|
||||||
|
}
|
||||||
|
player.sendMessage(Msg.tag("Consolação", NamedTextColor.LIGHT_PURPLE)
|
||||||
|
.append(Component.text(gift.message(), NamedTextColor.GRAY)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Picks one gift at random, or null if the list is empty. Pure, for tests. */
|
||||||
|
static Gift pick(List<Gift> gifts, Random random) {
|
||||||
|
return gifts.isEmpty() ? null : gifts.get(random.nextInt(gifts.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses {@code "MATERIAL | Nome | mensagem"} lines, skipping bad ones. */
|
||||||
|
static List<Gift> parse(List<String> raw, Logger log) {
|
||||||
|
List<Gift> out = new ArrayList<>();
|
||||||
|
for (String line : raw) {
|
||||||
|
String[] parts = line.split("\\|", 3);
|
||||||
|
if (parts.length != 3) {
|
||||||
|
log.warning("Presente de morte ignorado (formato 'ITEM | Nome | mensagem'): " + line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Material material = Material.matchMaterial(parts[0].trim().toUpperCase(Locale.ROOT));
|
||||||
|
if (material == null || !material.isItem()) {
|
||||||
|
log.warning("Presente de morte ignorado (item inválido): " + parts[0].trim());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.add(new Gift(material, parts[1].trim(), parts[2].trim()));
|
||||||
|
}
|
||||||
|
return List.copyOf(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,9 +45,15 @@ final class TitleChatListener implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The bracketed title chip that sits before the name, drawn in the title's
|
/** The bracketed title chip that sits before the name, drawn in the title's
|
||||||
* own tier colour so a legendary reads gold and a rare aqua. Pure, so testable. */
|
* own tier colour so a legendary reads gold and a rare aqua. Pure, so testable.
|
||||||
|
*
|
||||||
|
* <p>Rooted on an empty, colourless component on purpose: the chat message is
|
||||||
|
* appended to this in {@link #onChat}, and a coloured root would bleed its
|
||||||
|
* colour into any unstyled message text — which turned title-holders' chat
|
||||||
|
* grey. Empty root → the message falls back to the client default (white). */
|
||||||
static Component tag(Achievement achievement) {
|
static Component tag(Achievement achievement) {
|
||||||
return Component.text("[", NamedTextColor.DARK_GRAY)
|
return Component.empty()
|
||||||
|
.append(Component.text("[", NamedTextColor.DARK_GRAY))
|
||||||
.append(Component.text(achievement.title(), achievement.color()))
|
.append(Component.text(achievement.title(), achievement.color()))
|
||||||
.append(Component.text("] ", NamedTextColor.DARK_GRAY))
|
.append(Component.text("] ", NamedTextColor.DARK_GRAY))
|
||||||
.decoration(TextDecoration.BOLD, false);
|
.decoration(TextDecoration.BOLD, false);
|
||||||
|
|||||||
@@ -329,3 +329,18 @@ ia:
|
|||||||
- "O servidor se chama Canalhandia e roda Minecraft 26.2 (Paper)."
|
- "O servidor se chama Canalhandia e roda Minecraft 26.2 (Paper)."
|
||||||
- "Jogadores de Bedrock entram pelo Geyser e o nome deles começa com ponto."
|
- "Jogadores de Bedrock entram pelo Geyser e o nome deles começa com ponto."
|
||||||
- "O servidor tem BlueMap, voice chat e Distant Horizons."
|
- "O servidor tem BlueMap, voice chat e Distant Horizons."
|
||||||
|
|
||||||
|
# Presente de consolação: quando um jogador renasce, ganha um item cômico e
|
||||||
|
# inofensivo (uma flor de velório, um arbusto murcho). Parte do módulo "mortes".
|
||||||
|
# Sem esta seção, uma lista padrão embutida é usada. Rode /canalhandia reload
|
||||||
|
# depois de editar. Cada item é "MATERIAL | Nome | mensagem".
|
||||||
|
mortes:
|
||||||
|
presente:
|
||||||
|
ativo: true
|
||||||
|
itens:
|
||||||
|
- "POPPY | Flor do Velório | Uma florzinha pro seu velório. Sentimos muito."
|
||||||
|
- "DEAD_BUSH | Buquê Murcho | Um buquê à altura do seu último desempenho."
|
||||||
|
- "WET_SPONGE | Esponja das Lágrimas | Toma, pra enxugar as lágrimas."
|
||||||
|
- "BONE | Osso da Sorte | Um ossinho pra você, campeão."
|
||||||
|
- "COOKIE | Cookie de Consolação | Cookie de consolação. Vai que melhora."
|
||||||
|
- "ROTTEN_FLESH | Carne Podre | É o que tinha sobrado na despensa."
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package dev.marcospaulo.canalhandia;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The consolation-gift parsing and pick. Deliberately avoids the happy path of
|
||||||
|
* {@link DeathGift#parse}, which calls {@code Material.isItem()} — that throws in
|
||||||
|
* a unit JVM without a bootstrapped registry (same limit RecipeBookTest notes),
|
||||||
|
* so the item-resolution branch is verified live instead.
|
||||||
|
*/
|
||||||
|
class DeathGiftTest {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getAnonymousLogger();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void skipsMalformedAndUnknownLines() {
|
||||||
|
assertTrue(DeathGift.parse(List.of("sem as barras certas"), LOG).isEmpty());
|
||||||
|
assertTrue(DeathGift.parse(List.of("SÓ | DUAS_PARTES"), LOG).isEmpty());
|
||||||
|
// Unknown material name is rejected at matchMaterial, before isItem().
|
||||||
|
assertTrue(DeathGift.parse(List.of("ITEM_QUE_NAO_EXISTE_XYZ | Nome | msg"), LOG).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pickIsNullOnEmptyAndAMemberOtherwise() {
|
||||||
|
assertNull(DeathGift.pick(List.of(), new Random()));
|
||||||
|
DeathGift.Gift only = new DeathGift.Gift(Material.POPPY, "Flor do Velório", "oi");
|
||||||
|
assertSame(only, DeathGift.pick(List.of(only), new Random()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,4 +54,11 @@ class TitlesTest {
|
|||||||
void tagCarriesTheTitle() {
|
void tagCarriesTheTitle() {
|
||||||
assertNotNull(TitleChatListener.tag(Achievement.byKey("pedreiro")));
|
assertNotNull(TitleChatListener.tag(Achievement.byKey("pedreiro")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tagRootIsColourlessSoMessageStaysWhite() {
|
||||||
|
// The chat message is appended to this tag; a coloured root would bleed
|
||||||
|
// into unstyled message text and grey it out. Root must carry no colour.
|
||||||
|
assertNull(TitleChatListener.tag(Achievement.byKey("pedreiro")).color());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user