195 lines
7.8 KiB
Java
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());
|
|
}
|
|
}
|
|
}
|