Add zoacao chat gag: bare 'f' replaced with a random line

New module keyed 'zoacao'. An AsyncPlayerChatEvent listener swaps any
bare 'f'/'F' (trimmed, nothing else) for a random line from the
configurable zoacao.mensagens list (default Sou gay + 5 others). Pure
chat swap — player name still prefixes it; luto tribute untouched (still
needs the [F] button or /f command). Gated by modulos.zoacao (default on).

Zoacao.replace is pure for unit tests; ZoacaoTest covers bare f, case,
whitespace, f-with-extra-text, empty/null lists. 124 tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
marcos
2026-08-06 22:22:25 +00:00
parent 95520831da
commit d61de1b208
6 changed files with 150 additions and 0 deletions
@@ -18,6 +18,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@@ -478,6 +479,23 @@ public final class Canalhandia extends JavaPlugin implements Listener {
}, settings.joinDelaySeconds() * 20L); }, settings.joinDelaySeconds() * 20L);
} }
/**
* Chat gag: a bare "f" (or "F", trimmed, nothing else) gets replaced with a
* random line from {@code zoacao.mensagens}. Pure chat swap — the player's
* name still prefixes it as normal. Independent of {@code luto}: paying
* respects still needs the {@code [F]} button (Java) or {@code /f} command.
*/
@EventHandler
public void onChatF(AsyncPlayerChatEvent event) {
if (!settings.moduleEnabled(Module.ZOACAO)) {
return;
}
String gag = Zoacao.replace(event.getMessage(), settings.zoacaoMessages(), random);
if (gag != null) {
event.setMessage(gag);
}
}
/** "Press F" — a mourning button under each death message. */ /** "Press F" — a mourning button under each death message. */
@EventHandler @EventHandler
public void onDeath(PlayerDeathEvent event) { public void onDeath(PlayerDeathEvent event) {
@@ -10,6 +10,7 @@ enum Module {
RANKING("ranking", "Rankings"), RANKING("ranking", "Rankings"),
MARCOS("marcos", "Marcos e conquistas"), MARCOS("marcos", "Marcos e conquistas"),
MORTES("mortes", "Mortes com humor e coordenadas"), MORTES("mortes", "Mortes com humor e coordenadas"),
ZOACAO("zoacao", "Zoa de quem manda só 'f' no chat"),
IA("ia", "Perguntas para a IA"); IA("ia", "Perguntas para a IA");
private final String key; private final String key;
@@ -344,6 +344,27 @@ final class Settings {
set("ia.estatisticas-jogador", value); set("ia.estatisticas-jogador", value);
} }
// --- zoacao (f-gag) -----------------------------------------------------
/**
* Lines a bare "f" in chat gets replaced with, picked at random. Defaults
* to a small built-in list if unset/empty so the feature works out of the
* box; operators edit {@code zoacao.mensagens} in config.yml to customise.
*/
List<String> zoacaoMessages() {
List<String> messages = plugin.getConfig().getStringList("zoacao.mensagens");
if (messages == null || messages.isEmpty()) {
return List.of(
"Sou gay",
"Gosto de anime",
"Jogo no celular",
"Tenho 12 anos",
"Sou noob",
"Uso Windows");
}
return messages;
}
// --- content ------------------------------------------------------------ // --- content ------------------------------------------------------------
boolean categoryEnabled(Category category) { boolean categoryEnabled(Category category) {
@@ -0,0 +1,37 @@
package dev.marcospaulo.canalhandia;
import java.util.List;
import java.util.Random;
/**
* Pure logic for the {@code zoacao} chat gag: when a player sends a bare
* {@code "f"} (or {@code "F"}, trimmed, nothing else) their message is swapped
* for a random line from a configurable list. Extracted so the trigger rule is
* unit-testable without a server.
*
* <p>This is a standalone chat gag. It does not interact with the {@code luto}
* tribute — paying respects still happens via the {@code [F]} button (Java) or
* the {@code /f} command (Bedrock), neither of which is a chat message.
*/
final class Zoacao {
private Zoacao() {
}
/**
* @param message the chat message as sent by the player
* @param gags the configured replacement lines; if null/empty, no gag
* @param random shared random used to pick a line
* @return the replacement line if the message is a bare "f", otherwise null
* (meaning "leave the message alone")
*/
static String replace(String message, List<String> gags, Random random) {
if (message == null || gags == null || gags.isEmpty()) {
return null;
}
if (!message.trim().equalsIgnoreCase("f")) {
return null;
}
return gags.get(random.nextInt(gags.size()));
}
}
+13
View File
@@ -17,6 +17,8 @@ modulos:
# Mensagem de morte engraçada + coordenadas da morte mandadas em privado # Mensagem de morte engraçada + coordenadas da morte mandadas em privado
# só para quem morreu (para correr buscar os itens). # só para quem morreu (para correr buscar os itens).
mortes: true mortes: true
# Quem manda só "f" no chat (sem mais nada) leva uma zoada no lugar da mensagem.
zoacao: true
ia: true # /ia <pergunta> — só para quem tem canalhandia.ia ia: true # /ia <pergunta> — só para quem tem canalhandia.ia
# --- Curiosidades ------------------------------------------------------------ # --- Curiosidades ------------------------------------------------------------
@@ -69,6 +71,17 @@ janela-reacao-segundos: 90
luto: luto:
cabeca: true cabeca: true
# Frases que substituem um "f" sozinho no chat (módulo zoacao). Uma é sorteada
# por mensagem. Edite à vontade — a graça é ser inesperado.
zoacao:
mensagens:
- "Sou gay"
- "Gosto de anime"
- "Jogo no celular"
- "Tenho 12 anos"
- "Sou noob"
- "Uso Windows"
# Quantos nomes cabem no resumo de reações antes do resto virar "+N". # Quantos nomes cabem no resumo de reações antes do resto virar "+N".
# O resumo é sempre UMA linha, por mais gente que reaja; para ver a lista # O resumo é sempre UMA linha, por mais gente que reaja; para ver a lista
# completa use /reacoes (privado, não polui o chat). # completa use /reacoes (privado, não polui o chat).
@@ -0,0 +1,60 @@
package dev.marcospaulo.canalhandia;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Random;
import org.junit.jupiter.api.Test;
class ZoacaoTest {
private static final List<String> GAGS = List.of(
"Sou gay", "Gosto de anime", "Jogo no celular");
@Test
void bareLowercaseFIsReplacedWithAGagFromTheList() {
String gag = Zoacao.replace("f", GAGS, new Random(0L));
assertTrue(GAGS.contains(gag), "expected a gag from the list, got " + gag);
}
@Test
void bareUppercaseFIsReplaced() {
// A single-element list makes the random pick deterministic.
String gag = Zoacao.replace("F", List.of("alvo"), new Random(0L));
assertEquals("alvo", gag);
}
@Test
void surroundingWhitespaceStillCountsAsBareF() {
assertEquals("alvo", Zoacao.replace(" f ", List.of("alvo"), new Random(0L)));
assertEquals("alvo", Zoacao.replace("\tF\n", List.of("alvo"), new Random(0L)));
}
@Test
void fWithAnythingElseIsLeftAlone() {
assertNull(Zoacao.replace("f lol", GAGS, new Random(0L)));
assertNull(Zoacao.replace("ff", GAGS, new Random(0L)));
assertNull(Zoacao.replace("f.", GAGS, new Random(0L)));
assertNull(Zoacao.replace("pra você f", GAGS, new Random(0L)));
}
@Test
void nonFMessagesAreLeftAlone() {
assertNull(Zoacao.replace("oi", GAGS, new Random(0L)));
assertNull(Zoacao.replace("", GAGS, new Random(0L)));
assertNull(Zoacao.replace("F para o morto", GAGS, new Random(0L)));
}
@Test
void emptyOrNullGagsLeaveMessageAlone() {
assertNull(Zoacao.replace("f", List.of(), new Random(0L)));
assertNull(Zoacao.replace("f", null, new Random(0L)));
}
@Test
void nullMessageLeftAlone() {
assertNull(Zoacao.replace(null, GAGS, new Random(0L)));
}
}