19 Commits

Author SHA1 Message Date
Marcos Paulo 3d263bee90 feat(i18n): Adventure GlobalTranslator foundation + Lang facade
Per-player i18n: register a TranslationStore (key canalhandia) into the
GlobalTranslator from lang/messages_{pt,en}.properties. Paper renders each
Component.translatable in the recipient's own locale at send time, so one
broadcast shows every player their own language (pt/en), Java and Bedrock
alike. defaultLocale=en; pt + pt_BR fall back to the PT bundle; unknown
locale -> en.

- I18n: loader + registry install; removeSource before addSource so
  /canalhandia reload does not stack sources.
- Lang.tr(key, args) facade over Component.translatable.
- Canalhandia.onEnable wires it; /canalhandia reload now reloadI18n().
- Proof migration: onDeath mourning lines -> Lang.tr (broadcast -> per-viewer).
- I18nTest: key parity PT/EN + per-locale render + pt_BR fallback + unknown
  -> default. 316/316 pass.

Bundles seeded with the mourning keys; bulk module-by-module migration and
per-locale number/date formatting are next, one PR each per the spec.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 11:53:01 -03:00
Marcos (via Claude) cee0b020a1 feat(mortes): comic consolation gift on respawn + fix grey title chat
- On respawn a dead player privately gets a harmless comic item (funeral
  poppy, wilted bush, ...) with a pt-BR jab. Config-driven under
  mortes.presente ("MATERIAL | Nome | mensagem"), defaults baked in,
  hot-reloadable via /canalhandia reload. Overflow drops at their feet.
- Fix: a title-holder's chat text rendered grey. The title chip was
  rooted on a DARK_GRAY component and the message, appended to it,
  inherited that colour. Root on Component.empty() so the message falls
  back to white.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 11:14:25 -03:00
Marcos (via Claude) 34a0b44a03 feat(conquistas): tiered title colours + per-mob/-block stats
- Titles carry a tier (comum..lendario) that colours their chat chip;
  optional per-title `cor` override (named or #hex). Low tier is now
  white, fixing the too-dark/unreadable default. Colour flows through
  the chat tag, /conquistas list, and unlock announcement.
- New StatRef metric grammar: matou:creeper, minerou:obsidian, etc.
  reach any per-mob/per-block vanilla counter from config, no code.
- Unified achievement reads onto the offline stats file (dropped the
  Bukkit snapshot path) so granular counters work on- and offline.
- Fixed unreachable titles: kamikaze (combate>=20 & mortes>combate),
  rato-de-caverna (20k mined, <50km). Catalogue 29 -> 38 titles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 18:37:05 -03:00
Marcos Paulo f1d210ddc4 feat(ia): agentic tool-calling — web search, stats, ranking, wiki
Turn /ia from a fixed-prompt chatbot into an agent that pulls what it needs.
Instead of one hardcoded wiki pre-fetch, the model is offered read-only tools
and MiniMax's tool_choice=auto lets it decide which to call; results are fed
back until it answers (MiniMax.answerWithTools), capped by ia.max-ferramentas.

- Search: web search via the cluster's self-hosted SearXNG (JSON API, no
  external key); results boiled down to a few "título — trecho (url)" lines.
- Tools: registry + dispatch for pesquisar_web, wiki, estatisticas_jogador,
  conquistas_jogador, ranking. All read-only and thread-safe off the main
  thread, so they run on the existing async answer worker.
- Ai.ask: agentic path when ia.ferramentas is on (default), else the previous
  behaviour untouched.
- Config: ia.ferramentas, ia.max-ferramentas, ia.searxng-url, ia.resultados-web,
  ia.trecho-web; getters in Settings. Reloadable via /canalhandia reload.

Verified live against MiniMax-M2.7 + SearXNG: the model auto-calls the tool and
answers from the result. SearchTest covers the pure result formatter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 12:52:04 -03:00
Marcos Paulo 6dda1e33d3 feat(conquistas): config-driven catalogue with /canalhandia reload
Move achievements and milestones out of code and into editable YAML, so
operators add or retune titles by editing a file and reloading — no rebuild.

- conquistas-catalogo.yml: each title is a key with titulo/descricao and a list
  of condicoes. Conditions are a tiny grammar ("metrica operador alvo", target
  may be a number, another metric, or metrica/numero), evaluated on friendly
  units (km, hours) normalised from the raw stats. Achievement is now a class
  loaded from this file, not an enum.
- marcos-catalogo.yml: milestone tracks (statistica/verbo/unidade/limiares),
  loaded by Milestones at startup and reload.
- /canalhandia reload now reloads both catalogues (plus config.yml) live and
  reports the counts.
- Shipped catalogue expanded 10 -> 29 titles and every milestone track deepened.
- Silent backfill on load/reload (Achievements.syncCatalogue, Milestones
  .resyncSilently, keyed on a stored signature): a grown catalogue banks the
  history it already implies instead of spamming returning players.

Tests: AchievementTest covers the condition grammar and validates the shipped
catalogue; TitlesTest loads the real catalogue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 12:35:36 -03:00
Marcos Paulo 29000c208b feat(conquistas): view any player's titles, /perfil card, wearable title tag
Extend the achievements module from self-only to a full RPG-status layer,
all still chat-only and derived from vanilla stats.

- /conquistas [jogador]: look up anyone's earned titles, offline included,
  computed from their stats on disk via the new OfflineStats.achievementStats
  (same keys the online snapshot uses, so identical Achievement conditions).
- /perfil [jogador]: a status card — headline stats, titles earned, worn title.
- /titulo [nome|limpar]: pick which earned title to wear; only earned ones are
  accepted and tab-completed. Stored per player in titulos.yml (Titles).
- TitleChatListener: an AsyncChat renderer that prefixes the chosen "[Título]"
  chip, for Java and Bedrock alike; gated on the conquistas module.

Tests: TitlesTest covers title selection and the offline stat-key contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 12:17:24 -03:00
marcos c54c6b22b3 feat(ia): split long/list answers into separate chat messages
Minecraft has no meaningful character cap on what the server sends —
the 256-char limit is only on what a player types. The real problem
was that AiText.sanitise flattened every \n to a space, so a
model-produced numbered list or multi-paragraph answer landed as one
wrapped wall of text instead of readable lines.

AiText.segments() keeps the model's own line breaks, re-wraps any
line still too long at a sentence boundary (falling back to a
word/char wrap for pathological input), and caps both the total
character budget (ia.max-caracteres, unchanged meaning) and the
number of chat messages (new ia.max-mensagens, default 4) so a
runaway list can't flood chat.

Ai.deliver sends one message per segment instead of one flattened
line; style() tags only the first with [IA], continuation lines get
a plain " » " marker so a 5-item list reads as one grouped answer,
not five separate replies.

295 -> 305 tests.
2026-08-09 06:06:51 +00:00
marcos 67a2cbb37d Document mail, death history, achievements, weekly rankings, spontaneous AI and BlueMap markers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
2026-08-08 01:05:53 +00:00
marcos 4ec9817636 Add weekly rankings, spontaneous AI lines and BlueMap note markers
Weekly rankings (/ranking semanal [metrica]). On a server with three regulars
an all-time board is decided by whoever started first and then stops being a
contest. Subtracting a baseline taken at the start of the week makes it one
again. Rotation is time-based and idempotent — the snapshot carries the
timestamp it was taken at and is replaced only once a week has actually
elapsed, so a server that restarts nightly does not reset the week every day.
There is a test for exactly that. Players who did not move are dropped, since
the point is who is playing this week; a player missing from the baseline
counts their whole value, having joined during the week; a negative difference
is dropped rather than shown, because statistics only go up and a negative
means a stale baseline, not a result worth printing.

Spontaneous AI lines (ia.comentar-eventos, ia.saudacao — both off by default).
The persona comments on a run of deaths, and greets players as they join using
their own numbers. Both are opt-in: a chatty AI nobody asked for is the fastest
way to make players hate the feature.

Budget is what makes this tolerable rather than obnoxious, and it is the piece
worth reading. A player question is self-limiting — someone chose to spend it.
A line the AI decides to make on its own is not, and it costs money every time,
so three limits all have to pass: a gap between any two lines, a daily cap of
its own separate from the /ia cap, and a per-subject cooldown so one unlucky
player is not narrated all evening. allows() does not spend, so a caller that
decides not to fire (nobody online, the model returned nothing) has burned
nothing; saySomething spends up front rather than on success, because two
events in the same tick would otherwise both pass allows() and fire together —
the exact double-message the gap exists to prevent. The subject map drops
expired entries on each spend, or a long-lived server would leak one entry per
player who ever triggered a line.

A death streak now decays: three deaths across an evening is not a streak,
three in ten minutes is. Spontaneous lines are silent on failure — nobody asked
for it, so nobody should see it fail.

The five-minute sweep no longer hides behind the marcos toggle. It was a
milestones-only task, so turning off marcos also silenced achievements and
froze the weekly board; each of the three now checks its own module inside the
body.

BlueMap markers (notas.no-mapa, default on). Public notes already carry a world
and coordinates and the server already runs BlueMap, so this joins the two.
BlueMapBridge is the only class that touches BlueMap's API and every entry
point catches NoClassDefFoundError as well as Exception — the failure mode of a
missing optional dependency is a linkage error, not an exception — so a server
without BlueMap logs one fine-level line and carries on. The dependency is
'provided' because BlueMap ships those classes itself; a second copy inside our
jar would shadow them and break the real plugin, and preflight now fails if the
scope is ever dropped.

Markers are rebuilt rather than incrementally patched: BlueMap discards
everything when it unloads and expects addons to re-create markers on its
enable callback, and a full rebuild of a tiny list cannot drift out of sync the
way a missed delete would. Notes are matched to the map that renders their
world, or a Nether note would be drawn at the same numeric coordinates in the
overworld, pointing at nothing. Note text is player-written and lands in a web
page, so it is HTML-escaped — ampersand first, or the other replacements would
be double-escaped. Private notes are never drawn, at any setting.

295 tests, up from 263.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
2026-08-08 01:03:10 +00:00
marcos 26148740ef Add offline mail, death history and named achievements
Three features that all answer questions the server could already have
answered but was throwing away.

Mail (/recado <jogador> <texto>, /recados). On a server where people rarely
overlap, "achei diamante em -400 70 200" had to go through Discord or be lost.
Messages are queued for anyone who has joined before — resolved through
usercache.json, case-insensitively, because nobody types a name with the right
capitalisation — and delivered on their next join. If the recipient is online
it is delivered immediately instead of queued, since queueing it would mean
the person standing next to you reads it only after a relog.

Delivery is destructive: a message that stayed queued would be re-read on every
single join, turning a helpful note into a nuisance. It is also delayed a few
seconds and re-checks isOnline, because a player can leave inside the delay and
the mail would otherwise be consumed with nobody there to read it. Its own
join handler, not a branch inside onJoin, which returns early when the
curiosidades module is off — mail must not depend on an unrelated module.
Inbox capped per recipient, counting every sender, so the cap cannot be
bypassed with a second account.

Death history (/mortes). The mortes module already knew where and how someone
died and discarded it once the coords were delivered on respawn. Keeping the
last ten per player answers what people actually ask a day later. Eviction is
per player, not global, or one player's bad night would erase everyone else's
history. Your own deaths only: where someone died is where their stuff is, and
a public list of that is a looting guide.

Named achievements (/conquistas). Milestones covers round numbers; this covers
the combinations that say something about how someone plays — "Casca Grossa"
(50 hours, under 10 deaths), "Turista" (100 hours, barely mined), "Imortal às
Avessas" (dies more than once per hundred blocks mined). Every condition is a
pure function of a stat map, so the catalogue is unit-tested without a server.
The ratio ones carry a floor on absolute mining, so a new player is not handed
a joke achievement on their second death — there is a test for exactly that.

First sight is silent, like Milestones: the first time a player is checked,
whatever they have already earned is recorded without announcing it. Otherwise
enabling the module would dump a dozen announcements for history earned months
ago. Players who qualify for nothing are still marked as seen, or every later
check would treat them as new and stay silent forever.

Achievements share the milestone task rather than adding a second timer: both
sweep every online player's statistics, so one pass does the work of two.
Stats.totalOf sums a material-keyed statistic into a long — the per-material
values are ints and a long-running player's total can pass Integer.MAX_VALUE.

The /conquistas checklist uses "[x]" on Bedrock instead of "✔", which renders
there as a tofu box — the same per-platform rule the reaction labels follow.

Msg.ago renders wall-clock timestamps as "há 2 dias"; a timestamp from the
future clamps to "agora" rather than printing a negative age.

263 tests, up from 218.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
2026-08-08 00:49:56 +00:00
marcos c11085e95e Document the notes module
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
2026-08-07 23:49:12 +00:00
marcos bc711a48c2 Add chat notes: /save and /nota, public and private
Players had nowhere to write anything down. Coordinates got pasted into
Discord, or lost. This adds notes to chat, with two scopes.

A private note is visible only to its author and everyone may write one
(canalhandia.nota, default true). A public note is broadcast and readable by
all, and writing one needs canalhandia.nota.publica (default op) — public
notes are a curated board, not a graffiti wall. Both permissions are declared
in plugin.yml: an undeclared Bukkit permission silently falls back to op-only,
which would have stopped normal players writing anything.

Every note stores where it was written. On a Minecraft server a note is nearly
always about a place — where the base is, where the spawner was found — so
coordinates are part of the model rather than optional metadata. Java players
get click-to-copy on them, the same affordance the death-coords message uses;
Bedrock renders no click event and gets the plain text. No teleport: the
plugin does not touch gameplay.

/save is the quick path. /save and /save coords pin the spot; /save <texto>
pins it with a note. Private on purpose — it is the command someone types
without reading help first, and the safe default there is the one that cannot
surprise anyone by broadcasting. /nota publica <texto> is the explicit way to
share.

Private notes are never sent to the AI, at any setting. The AI call leaves
this server for a third-party API, so a private note reaching it would be a
disclosure the author never agreed to. The filter lives inside
Notes.publicSummary — the only method the AI path calls — rather than at the
call site, so a future caller cannot get it wrong by accident. Public notes
are sent (ia.contexto-notas, default 10), which is what lets the AI answer
"onde fica a base?" from what players actually wrote down.

A note the viewer cannot see is reported as missing rather than as forbidden:
saying "that one is private" would confirm it exists, which is itself a leak.
Search runs through the same visibility filter, so it cannot become a way to
probe for someone else's text. Ids are never reused after a deletion, or
"/nota ver 2" would point at a different note than the one someone wrote down
a minute ago. Text is stripped of the section sign and control characters,
because a note is echoed into chat and could otherwise forge a line that looks
like it came from the server.

Scope parsing accepts the masculine and plural forms ("publico", "publicas")
alongside the canonical ones. Spelled out rather than derived: a blanket a-to-o
rewrite turns "privada" into "privodo", and the plural is exactly what tab
completion suggests, so it has to parse or the suggested command fails.

preflight.sh now checks all 18 commands and both new permissions.

218 tests, up from 176.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
2026-08-07 23:46:55 +00:00
marcos 5653836262 Document the AI personality, chat/world context and preflight harness
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
2026-08-07 22:50:30 +00:00
marcos 0726ce3794 Give the AI a personality, chat awareness and live server state
The AI could only see three static facts about the server, so it answered
"quem tá online?" and "tá chovendo?" by insisting it had no access — true of
the model, but not of the plugin, which has all of it on hand. It also had no
tone: correct answers delivered like a manual, on a server whose whole point
is people ribbing each other.

Persona: five tones (zoeiro, amigao, seco, aldeao, neutro), switchable live
with /ia personalidade <nome>. Personality is expressed only as extra system
instructions and changes how the model talks, never what it may do. Every
persona — including the blank one — carries Persona.GUARD, which restates the
no-commands/no-server-access limits inside the persona's own frame, so a
roleplay instruction cannot read as licence to claim powers the plugin does
not grant it. The guard also forbids inventing stats, which matters now that
real numbers are being fed in. The teasing personas each state where the line
is; a test asserts every one of them does.

ChatLog: a 50-line in-memory ring of public chat, the last few lines handed to
the model so a follow-up like "quem tá reclamando aí?" has a referent. Written
from the chat event (off the main thread) and read from /ia, so it is
synchronised; a concurrency test hammers it from eight threads, because an
unsynchronised deque here would throw ConcurrentModification into a player's
answer. Recorded at MONITOR priority so what is stored is what the room saw —
a zoacao swap included — and cancelled messages are never stored. Nothing
touches disk.

ServerState: who is online with their platform, dimension, time of day,
weather, and the asker's coordinates, health, hunger and XP. Captured on the
main thread before the async call — every field reads the Bukkit world API,
which is not safe off it — and only the formatted string crosses the thread
boundary. Formatting is pure and tested, including the negative-tick case a
raw modulo would drop through every band.

Styling: Java players get a hover card with the original question and the
active persona, plus a click that pre-fills "/ia " for a follow-up.
suggestCommand, not runCommand: nothing executes without the player pressing
enter. Bedrock renders neither hover nor click, so it keeps the plain line,
built through broadcastPerPlatform like every other interactive message here.

preflight.sh: a read-only pre-restart harness. It verifies the jar opens, that
plugin.yml declares all 16 commands, that the staged jar's hash matches the
local build and is owned 1000:0, that the live config parses as YAML and
carries the keys this deploy depends on, and that a rollback jar and config
backup both exist. It never restarts anything. A missing YAML parser reports
as "not checked" rather than "invalid" — a harness that cries wolf gets
ignored.

176 tests, up from 101.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
2026-08-07 22:49:02 +00:00
marcos b77a38b394 Make zoacao match rule editable in-game: mode + pattern + message list
Zoacao now matches a configurable pattern under a configurable mode
(igual/contem/comeca/termina/regex) instead of only a bare 'f'. The mode,
the trigger pattern, and the gag message list are all editable live via
/canalhandia zoacao <listar|modo|padrao|adicionar|remover|limpar>; every
change writes through to config.yml immediately. config.yml gains
zoacao.correspondencia + zoacao.padrao (default igual + 'f').

Zoacao.replace/ matches gained a Mode + pattern signature; ZoacaoTest
covers all five modes (incl. invalid-regex guard) and the byKey parser.
136 tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 22:27:42 +00:00
marcos 72840c9770 Document zoacao module in README
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 22:23:25 +00:00
marcos d61de1b208 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>
2026-08-06 22:22:25 +00:00
marcos 95520831da Remove reaction boss bar; fix death coords; F-tribute head
- Reactions: drop the purple boss bar that sat on screen for the whole
  reaction window (curiosidades, adivinha, luto [F], /ia answers). Live
  counts still ride on the reactor's action bar and the closing tally line.
- mortes: death coordinates were sent during PlayerDeathEvent and the Java
  death screen swallowed them. Capture at death, deliver on PlayerRespawnEvent
  (1 tick later) so the player actually receives them. Respects keepInventory.
- luto: pressing F to pay respects now drops the dead player's head into the
  mourner's inventory — once per mourner per death, never to the dead player.
  First gameplay-touching feature; toggle with luto.cabeca (default on).
- Fix the /f typed twin: it fell through to help because "f" is not a
  configured reaction (the mourning set is hardcoded). Route it to reactLatest
  so Bedrock players can pay respects too.
- README + config updated. 117 tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 13:59:28 +00:00
marcos 68db3a7079 feat: feed asker stats to the IA + comic death messages
Two features requested after the IA grounding deploy.

IA player-stats grounding: the IA could not answer "quantos blocos eu
minerei?" because its context carried only generic server facts, never the
asker's own numbers. OfflineStats now reads one player's headline stats
(blocks mined, time played, distance, deaths, mob kills) from their vanilla
stats JSON and Ai.compose() injects them as a system turn, gated by
ia.estatisticas-jogador (default true). ~30 tokens per question; always on
so it never misses a stat question. Pure formatSummary extracted for tests.

Comic deaths (mortes module): replaces the vanilla death message with a
cause-based pt-BR comic line plus a death counter ("Fulano foi achatado
como panqueca (47ª morte)") and sends the death coordinates privately to
the dead player (Java click-to-copy, Bedrock plain text) so they can run
back to their dropped items. No storage, no command — a PlayerDeathEvent
side effect gated by modulos.mortes. DeathFlavor is a pure cause->phrase
map, unit-tested. Coexists with the luto [F] handler.

DEATHS stat timing: assumes Paper fires PlayerDeathEvent before awarding
minecraft:deaths, so the counter shows +1 to include the current death; a
log line confirms the raw stat on the first real death so the +1 can be
dropped if the server increments first.

117 tests green (101 + 13 DeathFlavor + 3 OfflineStatsSummary).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 05:06:45 +00:00
5 changed files with 116 additions and 401 deletions
@@ -127,7 +127,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
return denied(sender);
}
if (!plugin.announceCuriosity(null)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.nenhum-elegivel"));
Msg.error(sender, "Ninguém elegível online (ou sem estatísticas suficientes).");
}
return true;
}
@@ -191,12 +191,11 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
default -> {
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.subdesconhecido"));
Msg.error(sender, "Subcomando ou jogador desconhecido. Use /curiosidade ajuda");
} else if (!sender.hasPermission("canalhandia.forcar")) {
denied(sender);
} else if (!plugin.announceCuriosity(target)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.sem-stats",
Component.text(target.getName())));
Msg.error(sender, target.getName() + " ainda não tem estatísticas suficientes.");
}
}
}
@@ -210,8 +209,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
List<Fact> facts = CuriosityFactory.facts(target, plugin.settings());
if (facts.isEmpty()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.sem-curiosidade",
Component.text(target.getName())));
Msg.error(sender, "Nenhuma curiosidade disponível para " + target.getName() + ".");
return;
}
Fact fact = facts.get((int) (Math.random() * facts.size()));
@@ -245,27 +243,27 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
if (args.length > 0) {
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.offline", Component.text(args[0])));
Msg.error(sender, "Jogador '" + args[0] + "' não está online.");
}
return target;
}
if (sender instanceof Player player) {
return player;
}
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.informe"));
Msg.error(sender, "Informe um jogador.");
return null;
}
private void toggle(CommandSender sender) {
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.usar"));
Msg.error(sender, "Só jogadores podem usar isso.");
return;
}
boolean optedOut = !plugin.isOptedOut(player);
plugin.setOptedOut(player, optedOut);
sender.sendMessage(optedOut
? Lang.tr("canalhandia.cmd.curiosidade.toggle-off").color(NamedTextColor.YELLOW)
: Lang.tr("canalhandia.cmd.curiosidade.toggle-on").color(NamedTextColor.GREEN));
? Component.text("Você não aparecerá mais nas curiosidades.", NamedTextColor.YELLOW)
: Component.text("Você voltou a aparecer nas curiosidades.", NamedTextColor.GREEN));
}
// --- /adivinha ----------------------------------------------------------
@@ -275,7 +273,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.ADIVINHA)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desativado", Component.text("adivinha")));
Msg.error(sender, "O módulo adivinha está desativado.");
} else if (!plugin.startGuess()) {
Msg.error(sender, "Precisa de pelo menos 2 jogadores online com estatísticas.");
}
@@ -288,7 +286,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
GuessRound round = plugin.guessRound();
if (round == null || round.id() != parse(args[0], -1)) {
player.sendActionBar(Lang.tr("canalhandia.cmd.adivinha.rodada-acabou").color(NamedTextColor.RED));
player.sendActionBar(Component.text("Essa rodada já acabou.", NamedTextColor.RED));
return;
}
player.sendActionBar(round.guess(player, args[1]));
@@ -301,13 +299,13 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.ENQUETE)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desativado", Component.text("enquete")));
Msg.error(sender, "O módulo enquete está desativado.");
return true;
}
if (args.length == 1 && args[0].equalsIgnoreCase("encerrar")) {
Poll poll = plugin.poll();
if (poll == null || poll.closed()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.enquete.nenhuma"));
Msg.error(sender, "Nenhuma enquete aberta.");
} else {
Bukkit.broadcast(poll.close());
}
@@ -341,7 +339,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
Poll poll = plugin.poll();
if (poll == null || poll.id() != parse(args[0], -1)) {
player.sendActionBar(Lang.tr("canalhandia.cmd.votar.encerrada").color(NamedTextColor.RED));
player.sendActionBar(Component.text("Essa enquete já foi encerrada.", NamedTextColor.RED));
return;
}
Component result = poll.vote(player, parse(args[1], -1));
@@ -357,7 +355,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.RANKING)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desativado", Component.text("ranking")));
Msg.error(sender, "O módulo ranking está desativado.");
return true;
}
if (args.length == 0) {
@@ -385,7 +383,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
RankingMetric metric = RankingMetric.byKey(rest[0]);
if (metric == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.ranking.desconhecido"));
Msg.error(sender, "Ranking desconhecido. Use /ranking para ver a lista.");
return true;
}
int size = plugin.settings().rankingSize();
@@ -402,8 +400,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
Msg.line(sender, "desde", taken == 0 ? "o começo" : Msg.ago(taken));
}
if (rows.isEmpty()) {
sender.sendMessage(Component.text(" ", NamedTextColor.GRAY)
.append(Lang.tr("canalhandia.cmd.ranking.sem-dados")));
sender.sendMessage(Component.text(" (sem dados ainda)", NamedTextColor.GRAY));
return true;
}
for (int i = 0; i < rows.size(); i++) {
@@ -429,7 +426,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
*/
private boolean reactLatest(CommandSender sender, String key) {
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.reagir"));
Msg.error(sender, "Só jogadores podem reagir.");
return true;
}
if (!player.hasPermission("canalhandia.reagir")) {
@@ -437,16 +434,15 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
Reactions reactions = plugin.latestReactions();
if (reactions == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reagir.nada"));
Msg.error(sender, "Nada para reagir agora.");
return true;
}
if (key == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reagir.uso",
Component.text(String.join("|", reactionKeys()))));
Msg.error(sender, "Uso: /reagir <" + String.join("|", reactionKeys()) + ">");
return true;
}
if (!reactions.react(player, key)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reagir.invalida"));
Msg.error(sender, "Essa reação não vale para a última mensagem.");
} else {
plugin.afterReact(player, reactions.id(), key);
}
@@ -459,11 +455,11 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
GuessRound round = plugin.guessRound();
if (round == null || round.finished()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.palpite.nenhuma"));
Msg.error(sender, "Nenhuma adivinha aberta.");
return true;
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.palpite.uso"));
Msg.error(sender, "Uso: /palpite <nome>");
return true;
}
player.sendMessage(round.guess(player, args[0]));
@@ -476,16 +472,16 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
Poll poll = plugin.poll();
if (poll == null || poll.closed()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.enquete.nenhuma"));
Msg.error(sender, "Nenhuma enquete aberta.");
return true;
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.votar.uso"));
Msg.error(sender, "Uso: /votar <número>");
return true;
}
Component result = poll.vote(player, parse(args[0], -1));
player.sendMessage(result == null
? Lang.tr("canalhandia.cmd.votar.opcao-inexistente").color(NamedTextColor.RED)
? Component.text("Essa opção não existe.", NamedTextColor.RED)
: result);
return true;
}
@@ -494,11 +490,11 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
private boolean whoReacted(CommandSender sender) {
Reactions reactions = plugin.latestReactions();
if (reactions == null || !reactions.hasAnyVote()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reacoes.nenhuma"));
Msg.error(sender, "Ninguém reagiu à última mensagem ainda.");
return true;
}
boolean bedrock = sender instanceof Player player && Platform.isBedrock(player);
Msg.header(sender, Lang.tr("canalhandia.cmd.reacoes.cabecalho", Component.text(reactions.total())));
Msg.header(sender, "Quem reagiu (" + reactions.total() + ")");
reactions.breakdown(bedrock).forEach(sender::sendMessage);
return true;
}
@@ -536,11 +532,11 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
Reactions reactions = plugin.findReactions(parse(args[0], -1));
if (reactions == null) {
player.sendActionBar(Lang.tr("canalhandia.cmd.reagir.expirou").color(NamedTextColor.RED));
player.sendActionBar(Component.text("Essa mensagem já expirou.", NamedTextColor.RED));
return;
}
if (!reactions.react(player, args[1])) {
player.sendActionBar(Lang.tr("canalhandia.cmd.reagir.desconhecida").color(NamedTextColor.RED));
player.sendActionBar(Component.text("Reação desconhecida.", NamedTextColor.RED));
} else {
plugin.afterReact(player, reactions.id(), args[1]);
}
@@ -944,11 +940,11 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.IA)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("IA")));
Msg.error(sender, "O módulo de IA está desligado.");
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.ia"));
Msg.error(sender, "Só jogadores podem usar /ia.");
return true;
}
// Subcommands come before the question. They only hijack when the
@@ -1000,8 +996,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
// default: treat all args as the question
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.ia.uso",
Component.text(isPrivate ? "iap" : "ia")));
Msg.error(sender, isPrivate ? "Uso: /iap <pergunta>" : "Uso: /ia <pergunta>");
return true;
}
plugin.ai().ask(player, String.join(" ", args), isPrivate);
@@ -1067,7 +1062,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
*/
private boolean nota(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.NOTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("anotações")));
Msg.error(sender, "O módulo de anotações está desligado.");
return true;
}
if (!sender.hasPermission("canalhandia.nota")) {
@@ -1101,14 +1096,14 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
*/
private boolean saveShortcut(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.NOTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("anotações")));
Msg.error(sender, "O módulo de anotações está desligado.");
return true;
}
if (!sender.hasPermission("canalhandia.nota")) {
return denied(sender);
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.nota"));
Msg.error(sender, "Só jogadores podem anotar (a anotação guarda onde você está).");
return true;
}
// "/save" and "/save coords" mean the same thing: just the place. The
@@ -1135,23 +1130,23 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
*/
private boolean recado(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.RECADOS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("recados")));
Msg.error(sender, "O módulo de recados está desligado.");
return true;
}
if (!sender.hasPermission("canalhandia.recado")) {
return denied(sender);
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.recado"));
Msg.error(sender, "Só jogadores podem mandar recado.");
return true;
}
if (args.length < 2) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.uso"));
Msg.error(sender, "Uso: /recado <jogador> <texto>");
return true;
}
String text = Note.cleanText(String.join(" ", Arrays.copyOfRange(args, 1, args.length)));
if (text == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.vazio"));
Msg.error(sender, "O recado está vazio.");
return true;
}
@@ -1164,21 +1159,23 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
// anyone who has played here before.
OfflineStats.Known target = plugin.offlineStats().resolve(args[0]);
if (target == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.desconhecido", Component.text(args[0])));
Msg.error(sender, "Não conheço ninguém chamado \"" + args[0]
+ "\". (Só dá para mandar recado para quem já entrou no servidor.)");
return true;
}
if (target.uuid().equals(player.getUniqueId().toString())) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.mesmo"));
Msg.error(sender, "Recado para você mesmo? Use /save.");
return true;
}
Mail.Message message = plugin.mail().send(player.getName(),
player.getUniqueId().toString(), target.uuid(), text);
if (message == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.caixa-cheia",
Component.text(target.name()), Component.text(Mail.MAX_PER_RECIPIENT)));
Msg.error(sender, "A caixa de " + target.name() + " está cheia ("
+ Mail.MAX_PER_RECIPIENT + " recados). Espere ela entrar.");
return true;
}
Msg.ok(sender, Lang.tr("canalhandia.cmd.recado.guardado", Component.text(target.name())));
Msg.ok(sender, "Recado guardado para " + target.name()
+ ". Vai chegar quando " + target.name() + " entrar.");
return true;
}
@@ -1187,25 +1184,24 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
to.sendMessage(Msg.tag("Recado", NamedTextColor.AQUA)
.append(Component.text(from.getName() + ": ", NamedTextColor.GRAY))
.append(Component.text(text, NamedTextColor.WHITE)));
Msg.ok(from, Lang.tr("canalhandia.cmd.recado.online", Component.text(to.getName())));
Msg.ok(from, to.getName() + " está online — recado entregue na hora.");
}
/** {@code /recados} — how many of your messages are still unread. */
private boolean recados(CommandSender sender) {
if (!plugin.settings().moduleEnabled(Module.RECADOS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("recados")));
Msg.error(sender, "O módulo de recados está desligado.");
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.recados"));
Msg.error(sender, "Só jogadores têm recados.");
return true;
}
int pending = plugin.mail().countFrom(player.getUniqueId().toString());
Msg.ok(sender, pending == 0
? Lang.tr("canalhandia.cmd.recados.tudo-entregue")
: Lang.tr(pending == 1 ? "canalhandia.cmd.recados.pendentes-singular"
: "canalhandia.cmd.recados.pendentes-plural",
Component.text(pending)));
? "Todos os seus recados já foram entregues."
: pending + (pending == 1 ? " recado seu ainda não foi lido."
: " recados seus ainda não foram lidos."));
return true;
}
@@ -1219,26 +1215,27 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
*/
private boolean mortes(CommandSender sender) {
if (!plugin.settings().moduleEnabled(Module.MORTES)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("mortes")));
Msg.error(sender, "O módulo de mortes está desligado.");
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.mortes"));
Msg.error(sender, "Só jogadores têm histórico de mortes.");
return true;
}
List<DeathLog.Entry> deaths = plugin.deathLog().forPlayer(player.getUniqueId().toString());
if (deaths.isEmpty()) {
Msg.ok(sender, Lang.tr("canalhandia.cmd.mortes.nenhuma"));
Msg.ok(sender, "Você ainda não morreu. Aproveite enquanto dura.");
return true;
}
Msg.header(sender, Lang.tr("canalhandia.cmd.mortes.cabecalho", Component.text(deaths.size())));
Msg.header(sender, "Suas últimas mortes (" + deaths.size() + ")");
boolean bedrock = Platform.isBedrock(player);
for (DeathLog.Entry death : deaths) {
Component place = Component.text(death.place(), NamedTextColor.GRAY);
if (!bedrock) {
place = place.clickEvent(ClickEvent.copyToClipboard(death.coords()))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
Lang.tr("canalhandia.cmd.mortes.copiar").color(NamedTextColor.DARK_GRAY)));
Component.text("Clique para copiar as coordenadas",
NamedTextColor.DARK_GRAY)));
}
sender.sendMessage(Component.text(" " + death.flavor(), NamedTextColor.YELLOW)
.append(Component.text("", NamedTextColor.DARK_GRAY))
@@ -1260,12 +1257,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
*/
private boolean conquistas(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("conquistas")));
Msg.error(sender, "O módulo de conquistas está desligado.");
return true;
}
if (args.length == 0) {
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.conquistas"));
Msg.error(sender, "Só jogadores têm conquistas. Use /conquistas <jogador>.");
return true;
}
renderCatalogue(sender, plugin.achievements().earnedBy(player),
@@ -1275,12 +1272,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
String wanted = String.join(" ", args);
OfflineStats.Known who = plugin.offlineStats().resolve(wanted);
if (who == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.desconhecido", Component.text(wanted)));
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, Lang.tr("canalhandia.cmd.conquistas.sem-stats", Component.text(who.name())));
Msg.error(sender, "Ainda não tenho estatísticas de " + who.name() + ".");
return true;
}
boolean bedrock = sender instanceof Player viewer && Platform.isBedrock(viewer);
@@ -1291,9 +1288,8 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
/** 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, Lang.tr("canalhandia.cmd.conquistas.cabecalho",
Component.text(who), Component.text(earned.size()),
Component.text(Achievement.values().length)));
Msg.header(sender, "Conquistas de " + who + " ("
+ earned.size() + "/" + Achievement.values().length + ")");
// Bedrock renders "✔" as a tofu box, so it gets an ASCII marker — the
// same rule the reaction labels follow.
String tick = bedrock ? " [x] " : "";
@@ -1319,7 +1315,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
String name;
if (args.length == 0) {
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.perfil.diga"));
Msg.error(sender, "Diga de quem: /perfil <jogador>.");
return true;
}
uuid = player.getUniqueId().toString();
@@ -1328,20 +1324,18 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
String wanted = String.join(" ", args);
OfflineStats.Known who = plugin.offlineStats().resolve(wanted);
if (who == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.desconhecido",
Component.text(wanted)));
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, Lang.tr("canalhandia.cmd.perfil.cabecalho", Component.text(name)));
Msg.header(sender, "Perfil de " + name);
String summary = plugin.offlineStats().summary(id);
if (summary == null) {
Msg.line(sender, Lang.tr("canalhandia.cmd.perfil.rotulo.estatisticas"),
Lang.tr("canalhandia.cmd.perfil.sem-dados"));
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(": ");
@@ -1351,14 +1345,11 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
Map<String, Long> stats = plugin.offlineStats().achievementStats(id);
List<Achievement> earned = stats == null ? List.of() : Achievement.earned(stats);
Msg.line(sender, Lang.tr("canalhandia.cmd.perfil.rotulo.conquistas"),
Component.text(earned.size() + "/" + Achievement.values().length
+ (earned.isEmpty() ? "" : " (" + titlesList(earned) + ")")));
Msg.line(sender, "Conquistas", earned.size() + "/" + Achievement.values().length
+ (earned.isEmpty() ? "" : " (" + titlesList(earned) + ")"));
Achievement worn = plugin.titles().chosenAchievement(id);
Msg.line(sender, Lang.tr("canalhandia.cmd.perfil.rotulo.titulo"),
worn == null ? Lang.tr("canalhandia.cmd.perfil.titulo.nenhum")
: Component.text(worn.title()));
Msg.line(sender, "Título", worn == null ? "nenhum" : worn.title());
return true;
}
@@ -1369,41 +1360,39 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
*/
private boolean titulo(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("conquistas")));
Msg.error(sender, "O módulo de conquistas está desligado.");
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.titulo"));
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, Lang.tr("canalhandia.cmd.titulo.atual"),
worn == null ? Lang.tr("canalhandia.cmd.perfil.titulo.nenhum")
: Component.text(worn.title()));
Msg.line(sender, "Título atual", worn == null ? "nenhum" : worn.title());
if (earned.isEmpty()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.titulo.nenhum-bloqueado"));
Msg.error(sender, "Você ainda não desbloqueou nenhum título. Veja /conquistas.");
} else {
Msg.line(sender, Lang.tr("canalhandia.cmd.titulo.disponiveis"),
Component.text(titlesList(earned)));
sender.sendMessage(Lang.tr("canalhandia.cmd.titulo.uso").color(NamedTextColor.DARK_GRAY));
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, Lang.tr("canalhandia.cmd.titulo.removido"));
Msg.ok(player, "Título removido.");
return true;
}
Achievement chosen = matchEarned(arg, earned);
if (chosen == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.titulo.nao-tem", Component.text(arg)));
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, Lang.tr("canalhandia.cmd.titulo.definido", Component.text(chosen.title())));
Msg.ok(player, "Título definido: " + chosen.title() + ".");
return true;
}
@@ -1435,11 +1424,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
return;
}
if (scope == Note.Scope.PUBLICA && !sender.hasPermission("canalhandia.nota.publica")) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.publica-negado"));
Msg.error(sender, "Você não pode criar anotações públicas. Use /nota add <texto> "
+ "para uma anotação só sua.");
return;
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.uso-escopo", Component.text(scope.key())));
Msg.error(sender, "Uso: /nota " + scope.key() + " <texto>");
return;
}
store(player, scope, String.join(" ", args));
@@ -1449,7 +1439,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
private void store(Player player, Note.Scope scope, String rawText) {
String text = Note.cleanText(rawText);
if (text == null) {
Msg.error(player, Lang.tr("canalhandia.cmd.nota.vazia"));
Msg.error(player, "A anotação está vazia.");
return;
}
Location at = player.getLocation();
@@ -1457,13 +1447,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
text, ServerState.worldLabel(player.getWorld()),
at.getBlockX(), at.getBlockY(), at.getBlockZ());
if (note == null) {
Msg.error(player, Lang.tr("canalhandia.cmd.nota.cheia",
Component.text(Notes.MAX_PER_PLAYER)));
Msg.error(player, "Você já tem " + Notes.MAX_PER_PLAYER
+ " anotações. Apague alguma com /nota remover <n>.");
return;
}
Msg.ok(player, Lang.tr("canalhandia.cmd.nota.salva",
Component.text(note.id()), Component.text(scope.label()),
Component.text(note.place())));
Msg.ok(player, "Anotação #" + note.id() + " salva (" + scope.label() + ") em "
+ note.place() + ".");
if (scope == Note.Scope.PUBLICA) {
// A new public note changes what the web map should show.
plugin.blueMap().sync();
@@ -1480,23 +1469,21 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
private void notaList(CommandSender sender, String[] args) {
Note.Scope scope = args.length > 0 ? Note.Scope.byKey(args[0]) : null;
if (args.length > 0 && scope == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.listar-uso"));
Msg.error(sender, "Uso: /nota listar [publicas|privadas]");
return;
}
Component title = scope == null
? Lang.tr("canalhandia.cmd.nota.lista.tudo")
: Lang.tr("canalhandia.cmd.nota.lista.escopo", Component.text(scope.key() + "s"));
show(sender, plugin.notes().visibleTo(viewerId(sender), scope, null), title);
show(sender, plugin.notes().visibleTo(viewerId(sender), scope, null),
scope == null ? "Suas anotações e as públicas" : "Anotações " + scope.key() + "s");
}
private void notaSearch(CommandSender sender, String[] args) {
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.buscar-uso"));
Msg.error(sender, "Uso: /nota buscar <texto>");
return;
}
String query = String.join(" ", args);
show(sender, plugin.notes().visibleTo(viewerId(sender), null, query),
Lang.tr("canalhandia.cmd.nota.lista.busca", Component.text(query)));
"Anotações com \"" + query + "\"");
}
private void notaShow(CommandSender sender, String[] args) {
@@ -1505,44 +1492,43 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
// forbidden: saying "that one is private" would confirm it exists, which
// is itself a leak about someone else's note.
if (note == null || !note.visibleTo(viewerId(sender))) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.nao-encontrada"));
Msg.error(sender, "Anotação não encontrada.");
return;
}
Msg.header(sender, Lang.tr("canalhandia.cmd.nota.cabecalho", Component.text(note.id())));
Msg.header(sender, "Anotação #" + note.id());
sender.sendMessage(noteBody(note, isBedrock(sender)));
Msg.line(sender, Lang.tr("canalhandia.cmd.nota.autor"), Component.text(note.author()));
Msg.line(sender, Lang.tr("canalhandia.cmd.nota.escopo"),
Component.text(note.scope().key() + " (" + note.scope().label() + ")"));
Msg.line(sender, Lang.tr("canalhandia.cmd.nota.lugar"), Component.text(note.place()));
Msg.line(sender, "autor", note.author());
Msg.line(sender, "escopo", note.scope().key() + " (" + note.scope().label() + ")");
Msg.line(sender, "lugar", note.place());
}
private void notaRemove(CommandSender sender, String[] args) {
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.remover-uso"));
Msg.error(sender, "Uso: /nota remover <n>");
return;
}
Note note = plugin.notes().byId(parseLong(args[0]));
if (note == null || !note.visibleTo(viewerId(sender))) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.nao-encontrada"));
Msg.error(sender, "Anotação não encontrada.");
return;
}
if (!note.deletableBy(viewerId(sender), sender.hasPermission(ADMIN))) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.de-outro", Component.text(note.author())));
Msg.error(sender, "Essa anotação é de " + note.author() + ".");
return;
}
plugin.notes().remove(note.id());
if (note.scope() == Note.Scope.PUBLICA) {
plugin.blueMap().sync();
}
Msg.ok(sender, Lang.tr("canalhandia.cmd.nota.apagada", Component.text(note.id())));
Msg.ok(sender, "Anotação #" + note.id() + " apagada.");
}
private void show(CommandSender sender, List<Note> notes, Component title) {
private void show(CommandSender sender, List<Note> notes, String title) {
if (notes.isEmpty()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.nenhuma"));
Msg.error(sender, "Nenhuma anotação.");
return;
}
Msg.header(sender, title.append(Component.text(" (" + notes.size() + ")")));
Msg.header(sender, title + " (" + notes.size() + ")");
boolean bedrock = isBedrock(sender);
// Capped so a long list cannot push everything else out of the chat
// window; the rest are reachable with /nota buscar.
@@ -1551,8 +1537,8 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
sender.sendMessage(noteBody(notes.get(i), bedrock));
}
if (notes.size() > shown) {
Msg.line(sender, Component.text(""),
Lang.tr("canalhandia.cmd.nota.e-mais", Component.text(notes.size() - shown)));
Msg.line(sender, "", "e mais " + (notes.size() - shown)
+ ". Use /nota buscar <texto> para filtrar.");
}
}
@@ -1570,7 +1556,8 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
if (!bedrock) {
place = place.clickEvent(ClickEvent.copyToClipboard(note.coords()))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
Lang.tr("canalhandia.cmd.mortes.copiar").color(NamedTextColor.DARK_GRAY)));
Component.text("Clique para copiar as coordenadas",
NamedTextColor.DARK_GRAY)));
}
return Component.text(" #" + note.id() + " ", colour)
.append(Component.text(note.text(), NamedTextColor.WHITE))
@@ -1656,7 +1643,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
}
private boolean denied(CommandSender sender) {
Msg.error(sender, Lang.tr("canalhandia.cmd.negado"));
Msg.error(sender, "Você não tem permissão para isso.");
return true;
}
@@ -90,31 +90,6 @@ final class HttpFetcher implements Fetcher {
return body(http.send(request, HttpResponse.BodyHandlers.ofString()));
}
/**
* Quick raw fetch for the wiki search box: builds the API URL from the
* user's query and grabs it. Convenience helper used by the search command.
*/
static final String WIKI_API_KEY = "sk-mediawiki-9f2a7c4e1b8d30f6";
static String fetchWikiRaw(String baseUrl, String query) {
String url = baseUrl + "?action=query&list=search&srsearch=" + query;
try {
HttpClient c = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Bearer " + WIKI_API_KEY)
.GET()
.build();
HttpResponse<String> resp = c.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
return resp.body();
}
return "";
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
private String body(HttpResponse<String> response) throws IOException {
if (response.statusCode() / 100 != 2) {
throw new IOException("HTTP " + response.statusCode() + ": "
@@ -31,43 +31,20 @@ final class Msg {
.append(Component.text(text, NamedTextColor.GREEN).decoration(TextDecoration.BOLD, false)));
}
static void ok(CommandSender sender, Component text) {
sender.sendMessage(tag("Canalhandia", NamedTextColor.GOLD)
.append(text.color(NamedTextColor.GREEN).decoration(TextDecoration.BOLD, false)));
}
static void error(CommandSender sender, String text) {
sender.sendMessage(tag("Canalhandia", NamedTextColor.GOLD)
.append(Component.text(text, NamedTextColor.RED).decoration(TextDecoration.BOLD, false)));
}
static void error(CommandSender sender, Component text) {
sender.sendMessage(tag("Canalhandia", NamedTextColor.GOLD)
.append(text.color(NamedTextColor.RED).decoration(TextDecoration.BOLD, false)));
}
static void header(CommandSender sender, String text) {
sender.sendMessage(Component.text("" + text + "", NamedTextColor.GOLD, TextDecoration.BOLD));
}
static void header(CommandSender sender, Component text) {
sender.sendMessage(Component.text("", NamedTextColor.GOLD, TextDecoration.BOLD)
.append(text)
.append(Component.text("", NamedTextColor.GOLD, TextDecoration.BOLD)));
}
static void line(CommandSender sender, String key, String value) {
sender.sendMessage(Component.text(" " + key + ": ", NamedTextColor.GRAY)
.append(Component.text(value, NamedTextColor.AQUA)));
}
static void line(CommandSender sender, Component key, Component value) {
sender.sendMessage(Component.text(" ", NamedTextColor.GRAY)
.append(key.color(NamedTextColor.GRAY))
.append(Component.text(": ", NamedTextColor.GRAY))
.append(value.color(NamedTextColor.AQUA)));
}
/** Renders a duration in ticks as "3 dias e 4 horas" / "1 hora" / "12 minutos". */
static String duration(long ticks) {
long minutes = ticks / 20L / 60L;
+1 -112
View File
@@ -4,115 +4,4 @@
# Mourning (Canalhandia.onDeath) — F button under each death message.
canalhandia.morte.luto.prestar=pay respects for {0}
canalhandia.morte.luto.digitar=type /f to pay respects for {0}
canalhandia.morte.luto.resumo={0} paid respects for {1}.
# Commands — messages any player sees (not just the operator).
canalhandia.cmd.negado=You don't have permission for that.
canalhandia.cmd.sojogador.reagir=Only players can react.
canalhandia.cmd.sojogador.usar=Only players can use this.
canalhandia.cmd.sojogador.ia=Only players can use /ia.
canalhandia.cmd.sojogador.nota=Only players can take notes (a note stores where you are).
canalhandia.cmd.sojogador.recado=Only players can leave a message.
canalhandia.cmd.sojogador.recados=Only players have messages.
canalhandia.cmd.sojogador.mortes=Only players have a death history.
canalhandia.cmd.sojogador.conquistas=Only players have achievements. Use /conquistas <player>.
canalhandia.cmd.sojogador.titulo=Only players use titles.
canalhandia.cmd.modulo.desligado=The {0} module is off.
canalhandia.cmd.modulo.desativado=The {0} module is disabled.
canalhandia.cmd.jogador.informe=Specify a player.
canalhandia.cmd.jogador.offline=Player ''{0}'' is not online.
canalhandia.cmd.jogador.desconhecido=I don't know anyone called "{0}".
# /ranking
canalhandia.cmd.ranking.desconhecido=Unknown ranking. Use /ranking to see the list.
canalhandia.cmd.ranking.sem-dados=(no data yet)
# /conquistas
canalhandia.cmd.conquistas.sem-stats=I don't have stats for {0} yet.
canalhandia.cmd.conquistas.cabecalho=Achievements of {0} ({1}/{2})
# /perfil
canalhandia.cmd.perfil.diga=Say who: /perfil <player>.
canalhandia.cmd.perfil.cabecalho=Profile of {0}
canalhandia.cmd.perfil.rotulo.estatisticas=Stats
canalhandia.cmd.perfil.sem-dados=no data yet
canalhandia.cmd.perfil.rotulo.conquistas=Achievements
canalhandia.cmd.perfil.rotulo.titulo=Title
canalhandia.cmd.perfil.titulo.nenhum=none
# /titulo
canalhandia.cmd.titulo.atual=Current title
canalhandia.cmd.titulo.disponiveis=Available
canalhandia.cmd.titulo.uso=Use /titulo <name> to wear one, or /titulo limpar to clear it.
canalhandia.cmd.titulo.nenhum-bloqueado=You haven't unlocked a title yet. See /conquistas.
canalhandia.cmd.titulo.nao-tem=You don't have the title "{0}". See /titulo for the list.
canalhandia.cmd.titulo.removido=Title removed.
canalhandia.cmd.titulo.definido=Title set: {0}.
# /mortes
canalhandia.cmd.mortes.cabecalho=Your last deaths ({0})
canalhandia.cmd.mortes.nenhuma=You haven't died yet. Enjoy it while it lasts.
canalhandia.cmd.mortes.copiar=Click to copy the coordinates
# /recado and /recados
canalhandia.cmd.recado.uso=Usage: /recado <player> <text>
canalhandia.cmd.recado.vazio=The message is empty.
canalhandia.cmd.recado.mesmo=A message to yourself? Use /save.
canalhandia.cmd.recado.caixa-cheia={0}'s mailbox is full ({1} messages). Wait for them to join.
canalhandia.cmd.recado.guardado=Message saved for {0}. It'll arrive when {0} joins.
canalhandia.cmd.recado.online={0} is online — message delivered now.
canalhandia.cmd.recados.tudo-entregue=All your messages have been delivered.
canalhandia.cmd.recados.pendentes-singular={0} of your messages hasn't been read yet.
canalhandia.cmd.recados.pendentes-plural={0} of your messages haven't been read yet.
canalhandia.cmd.recado.desconhecido=I don't know anyone called "{0}". (You can only leave a message for someone who has joined the server.)
canalhandia.cmd.reagir.uso=Usage: /reagir <{0}>
canalhandia.cmd.reagir.nada=Nothing to react to right now.
canalhandia.cmd.reagir.invalida=That reaction doesn't apply to the last message.
canalhandia.cmd.reagir.expirou=That message has expired.
canalhandia.cmd.reagir.desconhecida=Unknown reaction.
canalhandia.cmd.reacoes.nenhuma=Nobody has reacted to the last message yet.
canalhandia.cmd.reacoes.cabecalho=Who reacted ({0})
# guess / poll (action bars)
canalhandia.cmd.palpite.uso=Usage: /palpite <name>
canalhandia.cmd.palpite.nenhuma=No guess round open.
canalhandia.cmd.adivinha.rodada-acabou=That round is already over.
canalhandia.cmd.votar.uso=Usage: /votar <number>
canalhandia.cmd.votar.encerrada=That poll has already closed.
canalhandia.cmd.votar.opcao-inexistente=That option doesn't exist.
canalhandia.cmd.enquete.nenhuma=No poll open.
# /ia (messages the player sees; tone tuning is operator-only)
canalhandia.cmd.ia.uso=Usage: /{0} <question>
# /nota and /save
canalhandia.cmd.nota.publica-negado=You can't create public notes. Use /nota add <text> for a private one.
canalhandia.cmd.nota.uso-escopo=Usage: /nota {0} <text>
canalhandia.cmd.nota.vazia=The note is empty.
canalhandia.cmd.nota.cheia=You already have {0} notes. Delete one with /nota remover <n>.
canalhandia.cmd.nota.salva=Note #{0} saved ({1}) at {2}.
canalhandia.cmd.nota.listar-uso=Usage: /nota listar [publicas|privadas]
canalhandia.cmd.nota.buscar-uso=Usage: /nota buscar <text>
canalhandia.cmd.nota.cabecalho=Note #{0}
canalhandia.cmd.nota.autor=author
canalhandia.cmd.nota.escopo=scope
canalhandia.cmd.nota.lugar=place
canalhandia.cmd.nota.remover-uso=Usage: /nota remover <n>
canalhandia.cmd.nota.de-outro=That note belongs to {0}.
canalhandia.cmd.nota.apagada=Note #{0} deleted.
canalhandia.cmd.nota.nenhuma=No notes.
canalhandia.cmd.nota.e-mais=… and {0} more. Use /nota buscar <text> to filter.
canalhandia.cmd.nota.lista.tudo=Your notes and the public ones
canalhandia.cmd.nota.lista.escopo={0} notes
canalhandia.cmd.nota.lista.busca=Notes containing "{0}"
canalhandia.cmd.nota.nao-encontrada=Note not found.
# /curiosidade (seen by players)
canalhandia.cmd.curiosidade.nenhum-elegivel=Nobody eligible is online (or without enough stats).
canalhandia.cmd.curiosidade.sem-stats={0} doesn't have enough stats yet.
canalhandia.cmd.curiosidade.sem-curiosidade=No curiosity available for {0}.
canalhandia.cmd.curiosidade.toggle-off=You won't appear in curiosities anymore.
canalhandia.cmd.curiosidade.toggle-on=You're back in the curiosities.
canalhandia.cmd.curiosidade.subdesconhecido=Unknown subcommand or player. Use /curiosidade ajuda
canalhandia.morte.luto.resumo={0} paid respects for {1}.
+3 -116
View File
@@ -1,120 +1,7 @@
# Canalhandia — português (fonte de verdade). Padrões MessageFormat: {0}, {1}, ...
# Canalhandia — portugues (fonte de verdade). Padroes MessageFormat: {0}, {1}, ...
# NUNCA altere os placeholders {0}/{1} nem as tags MiniMessage <...>.
# Luto (Canalhandia.onDeath) — botão F sob cada mensagem de morte.
# Luto (Canalhandia.onDeath) — botao F sob cada mensagem de morte.
canalhandia.morte.luto.prestar=prestar luto por {0}
canalhandia.morte.luto.digitar=digite /f para prestar luto por {0}
canalhandia.morte.luto.resumo={0} prestaram luto por {1}.
# Comandos — mensagens que qualquer jogador vê (não só o operador).
canalhandia.cmd.negado=Você não tem permissão para isso.
canalhandia.cmd.sojogador.reagir=Só jogadores podem reagir.
canalhandia.cmd.sojogador.usar=Só jogadores podem usar isso.
canalhandia.cmd.sojogador.ia=Só jogadores podem usar /ia.
canalhandia.cmd.sojogador.nota=Só jogadores podem anotar (a anotação guarda onde você está).
canalhandia.cmd.sojogador.recado=Só jogadores podem mandar recado.
canalhandia.cmd.sojogador.recados=Só jogadores têm recados.
canalhandia.cmd.sojogador.mortes=Só jogadores têm histórico de mortes.
canalhandia.cmd.sojogador.conquistas=Só jogadores têm conquistas. Use /conquistas <jogador>.
canalhandia.cmd.sojogador.titulo=Só jogadores usam títulos.
canalhandia.cmd.modulo.desligado=O módulo de {0} está desligado.
canalhandia.cmd.modulo.desativado=O módulo {0} está desativado.
canalhandia.cmd.jogador.informe=Informe um jogador.
canalhandia.cmd.jogador.offline=Jogador ''{0}'' não está online.
canalhandia.cmd.jogador.desconhecido=Não conheço ninguém chamado "{0}".
# /ranking
canalhandia.cmd.ranking.desconhecido=Ranking desconhecido. Use /ranking para ver a lista.
canalhandia.cmd.ranking.sem-dados=(sem dados ainda)
# /conquistas
canalhandia.cmd.conquistas.sem-stats=Ainda não tenho estatísticas de {0}.
canalhandia.cmd.conquistas.cabecalho=Conquistas de {0} ({1}/{2})
# /perfil
canalhandia.cmd.perfil.diga=Diga de quem: /perfil <jogador>.
canalhandia.cmd.perfil.cabecalho=Perfil de {0}
canalhandia.cmd.perfil.rotulo.estatisticas=Estatísticas
canalhandia.cmd.perfil.sem-dados=sem dados ainda
canalhandia.cmd.perfil.rotulo.conquistas=Conquistas
canalhandia.cmd.perfil.rotulo.titulo=Título
canalhandia.cmd.perfil.titulo.nenhum=nenhum
# /titulo
canalhandia.cmd.titulo.atual=Título atual
canalhandia.cmd.titulo.disponiveis=Disponíveis
canalhandia.cmd.titulo.uso=Use /titulo <nome> para usar, ou /titulo limpar para tirar.
canalhandia.cmd.titulo.nenhum-bloqueado=Você ainda não desbloqueou nenhum título. Veja /conquistas.
canalhandia.cmd.titulo.nao-tem=Você não tem o título "{0}". Veja /titulo para a lista.
canalhandia.cmd.titulo.removido=Título removido.
canalhandia.cmd.titulo.definido=Título definido: {0}.
# /mortes
canalhandia.cmd.mortes.cabecalho=Suas últimas mortes ({0})
canalhandia.cmd.mortes.nenhuma=Você ainda não morreu. Aproveite enquanto dura.
canalhandia.cmd.mortes.copiar=Clique para copiar as coordenadas
# /recado e /recados
canalhandia.cmd.recado.uso=Uso: /recado <jogador> <texto>
canalhandia.cmd.recado.vazio=O recado está vazio.
canalhandia.cmd.recado.mesmo=Recado para você mesmo? Use /save.
canalhandia.cmd.recado.caixa-cheia=A caixa de {0} está cheia ({1} recados). Espere ela entrar.
canalhandia.cmd.recado.guardado=Recado guardado para {0}. Vai chegar quando {0} entrar.
canalhandia.cmd.recado.online={0} está online — recado entregue na hora.
canalhandia.cmd.recados.tudo-entregue=Todos os seus recados já foram entregues.
canalhandia.cmd.recados.pendentes-singular={0} recado seu ainda não foi lido.
canalhandia.cmd.recados.pendentes-plural={0} recados seus ainda não foram lidos.
canalhandia.cmd.recado.desconhecido=Não conheço ninguém chamado "{0}". (Só dá para mandar recado para quem já entrou no servidor.)
# /reagir e /reacoes
canalhandia.cmd.reagir.uso=Uso: /reagir <{0}>
canalhandia.cmd.reagir.nada=Nada para reagir agora.
canalhandia.cmd.reagir.invalida=Essa reação não vale para a última mensagem.
canalhandia.cmd.reagir.expirou=Essa mensagem já expirou.
canalhandia.cmd.reagir.desconhecida=Reação desconhecida.
canalhandia.cmd.reacoes.nenhuma=Ninguém reagiu à última mensagem ainda.
canalhandia.cmd.reacoes.cabecalho=Quem reagiu ({0})
# adivinha / enquete (action bars)
canalhandia.cmd.palpite.uso=Uso: /palpite <nome>
canalhandia.cmd.palpite.nenhuma=Nenhuma adivinha aberta.
canalhandia.cmd.adivinha.rodada-acabou=Essa rodada já acabou.
canalhandia.cmd.votar.uso=Uso: /votar <número>
canalhandia.cmd.votar.encerrada=Essa enquete já foi encerrada.
canalhandia.cmd.votar.opcao-inexistente=Essa opção não existe.
canalhandia.cmd.enquete.nenhuma=Nenhuma enquete aberta.
# /ia (mensagens que o jogador vê; o ajuste de tom é só do operador)
canalhandia.cmd.ia.uso=Uso: /{0} <pergunta>
# /nota e /save
canalhandia.cmd.nota.publica-negado=Você não pode criar anotações públicas. Use /nota add <texto> para uma anotação só sua.
canalhandia.cmd.nota.uso-escopo=Uso: /nota {0} <texto>
canalhandia.cmd.nota.vazia=A anotação está vazia.
canalhandia.cmd.nota.cheia=Você já tem {0} anotações. Apague alguma com /nota remover <n>.
canalhandia.cmd.nota.salva=Anotação #{0} salva ({1}) em {2}.
canalhandia.cmd.nota.listar-uso=Uso: /nota listar [publicas|privadas]
canalhandia.cmd.nota.buscar-uso=Uso: /nota buscar <texto>
canalhandia.cmd.nota.cabecalho=Anotação #{0}
canalhandia.cmd.nota.autor=autor
canalhandia.cmd.nota.escopo=escopo
canalhandia.cmd.nota.lugar=lugar
canalhandia.cmd.nota.remover-uso=Uso: /nota remover <n>
canalhandia.cmd.nota.de-outro=Essa anotação é de {0}.
canalhandia.cmd.nota.apagada=Anotação #{0} apagada.
canalhandia.cmd.nota.nenhuma=Nenhuma anotação.
canalhandia.cmd.nota.e-mais=… e mais {0}. Use /nota buscar <texto> para filtrar.
canalhandia.cmd.nota.lista.tudo=Suas anotações e as públicas
canalhandia.cmd.nota.lista.escopo=Anotações {0}
canalhandia.cmd.nota.lista.busca=Anotações com "{0}"
canalhandia.cmd.nota.nao-encontrada=Anotação não encontrada.
# /curiosidade (vistas por jogador)
canalhandia.cmd.curiosidade.nenhum-elegivel=Ninguém elegível online (ou sem estatísticas suficientes).
canalhandia.cmd.curiosidade.sem-stats={0} ainda não tem estatísticas suficientes.
canalhandia.cmd.curiosidade.sem-curiosidade=Nenhuma curiosidade disponível para {0}.
canalhandia.cmd.curiosidade.toggle-off=Você não aparecerá mais nas curiosidades.
canalhandia.cmd.curiosidade.toggle-on=Você voltou a aparecer nas curiosidades.
canalhandia.cmd.curiosidade.subdesconhecido=Subcomando ou jogador desconhecido. Use /curiosidade ajuda
canalhandia.morte.luto.resumo={0} prestaram luto por {1}.