Commit Graph

11 Commits

Author SHA1 Message Date
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 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 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 a3b2e7dd27 fix(ia): wire canalhandia.ia.privado gate, tighten subcommand hijack
Review of Task 11 (commit 5f616d3) flagged a blocking spec regression:
canalhandia.ia.privado was declared (default: true) but never checked --
ia(sender, args, isPrivate) only checked canalhandia.ia, so /iap silently
required canalhandia.ia and the privado perm did nothing.

Per operator decision, /ia and /iap are now separate gates, both default
op, so LuckPerms can grant them independently (operator + permitted only,
not everyone). /ia needs canalhandia.ia; /iap needs
canalhandia.ia.privado.

Also fixes three should-fix findings:
- Subcommand hijack is no longer greedy: perfil and feedback only hijack
  when the second token is one they act on (a known profile key, or "ruim"),
  so "/ia perfil do servidor" and "/ia feedback do mapa?" fall through and
  are asked. corrigir stays greedy (a correction always reads the rest).
- AiProfile.isValid tells a real key from the PRECISO fallback, so /ia perfil
  blah no longer silently switches to PRECISO.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 03:54:01 +00:00
marcos 5f616d3d99 feat: /iap, reactions on answers, feedback and /ia corrigir 2026-08-06 03:43:19 +00:00
marcos 0d84a606d4 feat: /ia — chat question-and-answer backed by MiniMax
Adds an `ia` module: `/ia <pergunta>` sends the question to an
OpenAI-compatible chat-completions endpoint (MiniMax by default) and
posts the reply to chat.

Access is gated by `canalhandia.ia`, declared `default: op` so the
operator has it out of the box and LuckPerms can grant it to anyone
else.

The model can only ever produce chat text:

- the reply goes to sendMessage and nowhere else — it is never passed
  to the command dispatcher;
- no `tools`/`tool_choice` are sent, so there is nothing for the model
  to call;
- the system prompt states it has no server, shell or command access;
- replies are sanitised — colour codes stripped so they cannot forge
  server messages, newlines folded so one answer is one chat entry,
  and leading slashes removed so nothing reads as a command to run.

The API key is deliberately not a config value, since config.yml is
committed. It is read from MINIMAX_API_KEY or from
plugins/Canalhandia/minimax.key, which is now gitignored.

Cost is bounded by a per-player cooldown and a server-wide daily cap,
both visible in /canalhandia status. The HTTP call runs off the main
thread; only the delivery hops back onto it.
2026-08-05 13:41:28 +00:00
marcos be6d34f456 feat: show who reacted, and stop curiosities flooding chat
Reactions only ever showed counts, so with several reactors nobody could
tell who had reacted. Naming everyone on its own line does not scale, so
names now appear in three progressively larger places: a hover tooltip per
button (Java only), a one-line closing summary naming the first
resumo-nomes (3) and collapsing the rest to "+N", and /reacoes for the full
breakdown sent privately.

Reactors' names are captured at react time so the summary survives them
logging off.

Two separate causes of chat flooding:
- every join fired its own curiosity, so a wave of joins produced one each.
  Automatic triggers are now rate-limited by intervalo-minimo-segundos
  (120); a typed /curiosidade still bypasses it.
- a curiosity cost two messages. The button row is now appended to the
  headline instead of being broadcast separately.

Also fixes "1 horas" — CuriosityFactory had its own duration formatting
that missed the earlier plural fix; it now uses Msg.duration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 21:38:28 +00:00
marcos 987ee13d74 feat: Bedrock support — typed commands and ASCII labels
Geyser cannot deliver a chat clickEvent to Bedrock, and most emoji render
as tofu boxes there, so Bedrock players could see the reaction row but
neither read nor use it.

Messages carrying buttons are now built twice and sent per player rather
than via Bukkit.broadcast. Each reaction gains a per-platform label plus a
typed shortcut:

    uau: { java: "[😮]", texto: "[UAU]", comando: "wow" }

Typed fallbacks for every clickable interaction: /reagir, /legal, /wow,
/top, /f, /palpite and /votar, all acting on the most recent message so no
message id is needed.

Bedrock detection uses Floodgate's UUID scheme (high 64 bits zero) rather
than the Floodgate API, keeping it an optional runtime dependency.
/canalhandia plataformas lists who is online and on which platform.

Reaction config moves from a flat key->label map to a section per
reaction; the old flat form is still accepted and gets an ASCII fallback
derived from the key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 21:31:24 +00:00
marcos 801b43610f feat!: rename to Canalhandia and add four chat games
Renames the plugin from Curiosidades to Canalhandia (the server name) now
that it covers more than curiosities, and adds:

- adivinha: the curiosity with the name hidden and clickable player names,
  revealing who guessed right. Curiosity sentences never contained the
  subject's name, so the same generator feeds both with no redaction step.
- luto: a clickable [F] under death messages with a closing count.
- enquete: polls with clickable options and a live boss-bar tally.
- ranking + marcos: leaderboards covering offline players, and one-time
  milestone announcements persisted to marcos.yml.

Fixes the reaction counter being invisible. Chat cannot be edited after
sending, so the button counts are necessarily frozen; previously the only
live surface was the boss bar and the buttons showed no number at all.
Counts now appear in the buttons at send time, on the boss bar, on the
reactor's action bar, and as a final tally line when the window closes.

Late clicks used to be dropped silently once the 90s window passed. The
last 8 reaction sets now stay live for reacao-validade-minutos (15).

Rankings read the stats JSON directly because Bukkit only exposes
statistics for online players.

Adds README.md documenting the design constraints that are easy to
mistake for bugs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 20:46:42 +00:00
marcos 7fd3fc157c feat: join-triggered by default, plus runtime admin controls
Default trigger is now player join rather than a timer, with a short
delay so the curiosity lands after the join message instead of racing
it. Modes entrada/intervalo/ambos/manual select the triggers.

Adds a full /curiosidade tree so behaviour is adjustable in-game
without editing config.yml: mode, interval, join delay, per-player
cooldown, no-repeat history, reaction window, reaction labels, and
per-category toggles. Every setter writes through to disk immediately
so changes survive a restart.

Facts are now tagged with a category so they can be filtered, and a
per-player cooldown plus recent-fact history stop repeats when someone
relogs.

Config changes are gated behind curiosidades.admin; reacting and
opting out stay default-true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 20:24:54 +00:00
marcos c528ed55c8 feat: curiosidades plugin for stat-based server announcements
Announces a random Portuguese fact about a random online player on a
timer, sourced from vanilla statistics, with clickable reactions.

Block/item/mob names are emitted as translatable components so each
client renders them in its own language instead of shipping a
translation table. Sentences are phrased to avoid number agreement with
the translated noun ("5.966 blocos de Pedra", not "5.966 Pedras").

Statistic constants are resolved by name with fallbacks because they get
renamed across Minecraft releases; a rename degrades one curiosity
rather than breaking the announcement.

Chat cannot be edited after sending, so live reaction tallies ride on a
boss bar while the in-chat buttons stay frozen at send time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 20:18:05 +00:00