Files
canalhandia/src/main/java/dev/marcospaulo/canalhandia/Achievements.java
T

195 lines
7.8 KiB
Java

package dev.marcospaulo.canalhandia;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Awards {@link Achievement}s once and remembers that it did.
*
* <p>Runs on the same timer as {@link Milestones} and follows the same
* first-sight rule: the first time a player is seen, whatever they have already
* earned is recorded <em>silently</em>. Without that, enabling the module would
* dump a dozen announcements for history earned months ago, and every existing
* player would be spammed at once.
*/
final class Achievements {
private final Canalhandia plugin;
private final File file;
private final YamlConfiguration data;
Achievements(Canalhandia plugin) {
this.plugin = plugin;
this.file = new File(plugin.getDataFolder(), "conquistas.yml");
this.data = YamlConfiguration.loadConfiguration(file);
}
/** The reserved node in conquistas.yml that records which keys the catalogue
* has already introduced. Not a UUID, so it never collides with a player. */
private static final String CATALOGUE = "_catalogo";
/**
* Silently banks history when the catalogue grows.
*
* <p>The per-player first-sight rule keeps a brand-new player quiet; this is
* its counterpart for a brand-new <em>achievement</em>. When the enum gains
* entries, every already-known player who already qualifies for them would
* otherwise be announced in a burst the next time they log in — months-old
* history dumped into chat, exactly what the module was careful to avoid.
*
* <p>So on enable: any achievement not previously in the stored catalogue is
* marked (silently) for every player already on record who currently meets
* it, computed from their stats on disk. Only crossings that happen
* <em>after</em> introduction announce. Idempotent — re-running with no new
* keys does nothing.
*/
void syncCatalogue() {
Set<String> known = new HashSet<>(data.getStringList(CATALOGUE));
List<String> current = new ArrayList<>();
for (Achievement achievement : Achievement.values()) {
current.add(achievement.key());
}
List<Achievement> added = new ArrayList<>();
for (Achievement achievement : Achievement.values()) {
if (!known.contains(achievement.key())) {
added.add(achievement);
}
}
if (added.isEmpty() && known.equals(new HashSet<>(current))) {
return;
}
for (String base : data.getKeys(false)) {
if (base.equals(CATALOGUE)) {
continue;
}
UUID uuid;
try {
uuid = UUID.fromString(base);
} catch (IllegalArgumentException notAPlayer) {
continue;
}
Map<String, Long> stats = plugin.offlineStats().achievementStats(uuid);
if (stats == null) {
continue;
}
for (Achievement achievement : added) {
if (achievement.met(stats) && !data.getBoolean(base + "." + achievement.key(), false)) {
data.set(base + "." + achievement.key(), true);
}
}
}
data.set(CATALOGUE, current);
save();
}
/** Checks every online player and announces anything newly earned. */
void check() {
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
return;
}
boolean changed = false;
for (Player player : Bukkit.getOnlinePlayers()) {
changed |= check(player);
}
if (changed) {
save();
}
}
/** @return true if anything was recorded, so the caller can save once */
private boolean check(Player player) {
// Read straight off the stats file, the same source /perfil and /conquistas
// use, so a title can hinge on any per-mob or per-block counter (matou:creeper)
// that Bukkit's typed API would make us enumerate by hand. The file lags a
// live session by seconds — invisible for cumulative threshold titles.
Map<String, Long> stats = plugin.offlineStats().achievementStats(player.getUniqueId());
if (stats == null) {
return false; // no stats file written yet — nothing to bank, retry next tick
}
String base = player.getUniqueId().toString();
// A player with no record yet is being seen for the first time: bank
// what they have without announcing it.
boolean firstSight = !data.contains(base);
boolean changed = false;
for (Achievement achievement : Achievement.values()) {
if (!achievement.met(stats)) {
continue;
}
String path = base + "." + achievement.key();
if (data.getBoolean(path, false)) {
continue;
}
data.set(path, true);
changed = true;
if (!firstSight) {
announce(player, achievement);
}
}
if (firstSight && !changed) {
// Mark the player as seen even when they qualified for nothing, or
// every future check would treat them as new and stay silent.
data.set(base + ".visto", true);
changed = true;
}
return changed;
}
private void announce(Player player, Achievement achievement) {
Bukkit.broadcast(Msg.tag("Conquista", NamedTextColor.GOLD)
.append(Component.text(player.getName(), NamedTextColor.GREEN)
.decoration(TextDecoration.BOLD, false))
.append(Component.text(" desbloqueou ", NamedTextColor.WHITE)
.decoration(TextDecoration.BOLD, false))
.append(Component.text(achievement.title(), achievement.color())
.decoration(TextDecoration.BOLD, false))
.append(Component.text("" + achievement.description(), NamedTextColor.GRAY)
.decoration(TextDecoration.BOLD, false)));
plugin.getLogger().info("[conquistas] " + player.getName() + "" + achievement.key());
if (plugin.settings().aiEvents() && plugin.settings().moduleEnabled(Module.IA)) {
Persona persona = plugin.playerMemory() != null
? plugin.playerMemory().persona(player.getUniqueId(), plugin.settings().aiPersona())
: plugin.settings().aiPersona();
plugin.ai().saySomething(player.getName(),
"O jogador " + player.getName() + " desbloqueou a conquista \""
+ achievement.title() + "\" (" + achievement.description()
+ "). Faça um breve comentário na sua personalidade.",
plugin.aiBudget(), persona);
}
}
/** Which achievements this player has already unlocked. */
List<Achievement> earnedBy(Player player) {
List<Achievement> out = new ArrayList<>();
String base = player.getUniqueId().toString();
for (Achievement achievement : Achievement.values()) {
if (data.getBoolean(base + "." + achievement.key(), false)) {
out.add(achievement);
}
}
return out;
}
private void save() {
try {
data.save(file);
} catch (IOException e) {
plugin.getLogger().warning("Não consegui salvar conquistas.yml: " + e.getMessage());
}
}
}