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 Poll poll;
|
||||
private Titles titles;
|
||||
private DeathGift deathGift;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -113,6 +114,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
milestones = new Milestones(this);
|
||||
achievements = new Achievements(this);
|
||||
titles = new Titles(this);
|
||||
deathGift = new DeathGift(this);
|
||||
// Load the achievement catalogue from config, then silently bank any
|
||||
// history the current definitions already imply (both here and for
|
||||
// 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()));
|
||||
milestones.reload();
|
||||
achievements.syncCatalogue();
|
||||
deathGift.reload();
|
||||
}
|
||||
|
||||
/** The weekly ranking baseline. Never null. */
|
||||
@@ -895,6 +898,11 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
.append(Component.text(tail, NamedTextColor.AQUA));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
* 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) {
|
||||
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("] ", NamedTextColor.DARK_GRAY))
|
||||
.decoration(TextDecoration.BOLD, false);
|
||||
|
||||
@@ -329,3 +329,18 @@ ia:
|
||||
- "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."
|
||||
- "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() {
|
||||
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