Introduce TeamScoreboard and highest stat tracking functionality.
This commit refactors scoreboard management into a dedicated TeamScoreboard class, improving modularity and readability. It also adds a new command to track and display the highest player stats across all game metrics, enhancing gameplay insights.
This commit is contained in:
parent
14158f73b8
commit
3550d75634
|
|
@ -147,10 +147,11 @@ public class Main extends JavaPlugin {
|
|||
gameManager.setPlayerStats(playerStats);
|
||||
final JsonConfigManager<PlayerStat> jsonConfigManager = new JsonConfigManager<>(JacksonConfig.configureMapper());
|
||||
Runnable runnable = () -> {
|
||||
log.info("Saving player stats");
|
||||
File playerStatsDirectory = new File(getDataFolder(), "player_stats");
|
||||
gameManager.getPlayerStats().stream().filter(PlayerStat::isTouched).forEach(playerStat -> {
|
||||
try {
|
||||
jsonConfigManager.saveConfig(playerStat, playerStatsDirectory, String.format("%s.%s", playerStat.getUuid(), "json"));
|
||||
jsonConfigManager.saveConfig(playerStat, playerStatsDirectory, playerStat.getUuid().toString());
|
||||
playerStat.setUnTouched();
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to save player stats for [{}].", playerStat.getUuid(), e);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ public class CommandManager implements CommandExecutor, TabExecutor {
|
|||
new SkipPhase(gameManager),
|
||||
new Start(gameManager, flag),
|
||||
new CreateTeam(main, gameManager),
|
||||
new HighestStat(gameManager),
|
||||
new SelectClass(gameManager, worldBorderApi),
|
||||
new Reload(main)
|
||||
);
|
||||
|
|
@ -120,4 +121,4 @@ public class CommandManager implements CommandExecutor, TabExecutor {
|
|||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package com.alttd.ctf.commands.subcommands;
|
||||
|
||||
import com.alttd.ctf.commands.SubCommand;
|
||||
import com.alttd.ctf.config.Messages;
|
||||
import com.alttd.ctf.game.GameManager;
|
||||
import com.alttd.ctf.stats.PlayerStat;
|
||||
import com.alttd.ctf.stats.Stat;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class HighestStat extends SubCommand {
|
||||
|
||||
private final GameManager gameManager;
|
||||
private final MiniMessage miniMessage = MiniMessage.miniMessage();
|
||||
|
||||
public HighestStat(GameManager gameManager) {
|
||||
this.gameManager = gameManager;
|
||||
}
|
||||
|
||||
private record HighestStatValue(Stat stat, UUID uuid, double value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onCommand(CommandSender commandSender, String[] args) {
|
||||
ArrayList<HighestStatValue> highestStatValues = new ArrayList<>();
|
||||
Collection<PlayerStat> playerStats = gameManager.getPlayerStats();
|
||||
for (Stat stat : Stat.values()) {
|
||||
Optional<PlayerStat> max = playerStats.stream().max(Comparator.comparingDouble(playerStat -> playerStat.getStat(stat)));
|
||||
if (max.isEmpty()) {
|
||||
log.warn("No max stat found for {}", stat.toString());
|
||||
continue;
|
||||
}
|
||||
PlayerStat playerStat = max.get();
|
||||
highestStatValues.add(new HighestStatValue(stat, playerStat.getUuid(), playerStat.getStat(stat)));
|
||||
}
|
||||
Component message = highestStatValues.stream().map(highestStatValue -> {
|
||||
TextComponent messageBuilder = Component.empty();
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(highestStatValue.uuid);
|
||||
Player player = offlinePlayer.getPlayer();
|
||||
if (!offlinePlayer.isOnline() || player == null) {
|
||||
messageBuilder = messageBuilder.append(miniMessage.deserialize(offlinePlayer.getName() == null ? highestStatValue.uuid.toString() : offlinePlayer.getName()));
|
||||
} else {
|
||||
messageBuilder = messageBuilder.append(player.name());
|
||||
}
|
||||
messageBuilder = messageBuilder.append(Component.text(": "));
|
||||
messageBuilder = messageBuilder.append(Component.text(highestStatValue.stat.toString()));
|
||||
messageBuilder = messageBuilder.append(Component.text(": "));
|
||||
messageBuilder = messageBuilder.append(Component.text(highestStatValue.value()));
|
||||
return messageBuilder;
|
||||
}).reduce(Component.empty(), (a, b) -> a.append(Component.newline()).append(b));
|
||||
commandSender.sendMessage(message);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "higheststat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpMessage() {
|
||||
return Messages.HELP.HIGHEST_STAT;
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ public class Messages extends AbstractConfig {
|
|||
public static String START = "<green>Start a new game: <gold>/ctf start <time_in_minutes></gold></green>";
|
||||
public static String SELECT_CLASS = "<green>Open class selection: <gold>/ctf selectclass</gold></green>";
|
||||
public static String SKIP_PHASE = "<green>Skip the current phase: <gold>/ctf skipphase</gold></green>";
|
||||
public static String HIGHEST_STAT = "<green>Display the highest stat and the player: <gold>/ctf higheststat</gold></green>";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void load() {
|
||||
|
|
@ -39,6 +40,7 @@ public class Messages extends AbstractConfig {
|
|||
START = config.getString(prefix, "start", START);
|
||||
SELECT_CLASS = config.getString(prefix, "select-class", SELECT_CLASS);
|
||||
SKIP_PHASE = config.getString(prefix, "skip-phase", SKIP_PHASE);
|
||||
HIGHEST_STAT = config.getString(prefix, "highest-stat", HIGHEST_STAT);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,4 +121,11 @@ public class GameManager {
|
|||
public Collection<PlayerStat> getPlayerStats() {
|
||||
return this.playerStats.values();
|
||||
}
|
||||
|
||||
public Optional<Duration> getRemainingTime() {
|
||||
if (runningGame == null || runningGame.getCurrentPhase().equals(GamePhase.ENDED)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(runningGame.getRemainingTime());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.alttd.ctf.game;
|
|||
|
||||
import com.alttd.ctf.config.GameConfig;
|
||||
import com.alttd.ctf.flag.Flag;
|
||||
import com.alttd.ctf.team.TeamScoreboard;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
|
|
@ -14,6 +15,7 @@ import java.time.Duration;
|
|||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
@Slf4j
|
||||
|
|
@ -51,6 +53,7 @@ public class RunningGame implements Runnable {
|
|||
} else {
|
||||
executorService.shutdown();
|
||||
}
|
||||
TeamScoreboard.refreshScoreboard(gameManager);
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error in running game", e);
|
||||
throw new RuntimeException(e);
|
||||
|
|
@ -115,4 +118,9 @@ public class RunningGame implements Runnable {
|
|||
//TODO say the phase ended early?
|
||||
nextPhaseActions(currentPhase, GamePhase.ENDED);
|
||||
}
|
||||
|
||||
public Duration getRemainingTime() {
|
||||
Duration duration = phaseDurations.get(currentPhase);
|
||||
return duration.minus(Duration.between(phaseStartTime, Instant.now()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
package com.alttd.ctf.stats;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@NoArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public final class PlayerStat {
|
||||
|
|
@ -16,11 +23,15 @@ public final class PlayerStat {
|
|||
private boolean touched;
|
||||
@JsonIgnore
|
||||
private static final CommandSender commandSender = Bukkit.getConsoleSender();
|
||||
@JsonIgnore
|
||||
private static boolean someoneDiedInPowderedSnow = false;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private final UUID uuid;
|
||||
private UUID uuid;
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private final String inGameName;
|
||||
private String inGameName;
|
||||
|
||||
private boolean completedGame = false;
|
||||
private int flagsCaptured = 0;
|
||||
|
|
@ -37,7 +48,7 @@ public final class PlayerStat {
|
|||
switch (stat) {
|
||||
case COMPLETED_GAME -> {
|
||||
if (!completedGame) {
|
||||
Bukkit.dispatchCommand(commandSender, String.format("lp user %s permission set ctf.game.completed", inGameName));
|
||||
Bukkit.dispatchCommand(commandSender, String.format("lp user %s permission settemp ctf.game.completed true 14d", inGameName));
|
||||
}
|
||||
completedGame = true;
|
||||
}
|
||||
|
|
@ -47,7 +58,24 @@ public final class PlayerStat {
|
|||
case BLOCKS_PLACED -> blocksPlaced++;
|
||||
case SNOWBALLS_THROWN -> snowballsThrown++;
|
||||
case TIME_SPEND_CAPTURING_FLAG -> timeSpendCapturingFlag++;
|
||||
case DEATHS_IN_POWDERED_SNOW -> deathsInPowderedSnow++; //TODO announce if they are the first person to do this and save they are the first
|
||||
case DEATHS_IN_POWDERED_SNOW -> {
|
||||
if (someoneDiedInPowderedSnow) {
|
||||
deathsInPowderedSnow++; //TODO announce if they are the first person to do this and save they are the first
|
||||
return;
|
||||
}
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
|
||||
if (!offlinePlayer.isOnline()) {
|
||||
return;
|
||||
}
|
||||
Player player = offlinePlayer.getPlayer();
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<red><player> will receive a consolation prize for being the first to die in powdered snow.</red>",
|
||||
Placeholder.component("player",player.displayName())));
|
||||
Bukkit.dispatchCommand(commandSender, String.format("lp user %s permission settemp ctf.game.first_powdered_snow_death true 14d", inGameName));
|
||||
someoneDiedInPowderedSnow = true;
|
||||
}
|
||||
case DAMAGE_DONE, DAMAGE_HEALED -> throw new IllegalArgumentException(String.format("%s requires a number", stat.name()));
|
||||
}
|
||||
touched = true;
|
||||
|
|
@ -66,4 +94,18 @@ public final class PlayerStat {
|
|||
touched = false;
|
||||
}
|
||||
|
||||
public double getStat(Stat stat) {
|
||||
return switch (stat) {
|
||||
case COMPLETED_GAME -> completedGame ? 1 : 0;
|
||||
case FLAGS_CAPTURED -> flagsCaptured;
|
||||
case KILLS -> kills;
|
||||
case DAMAGE_DONE -> damageDone;
|
||||
case DAMAGE_HEALED -> damageHealed;
|
||||
case SNOW_MINED -> snowMined;
|
||||
case BLOCKS_PLACED -> blocksPlaced;
|
||||
case SNOWBALLS_THROWN -> snowballsThrown;
|
||||
case TIME_SPEND_CAPTURING_FLAG -> timeSpendCapturingFlag;
|
||||
case DEATHS_IN_POWDERED_SNOW -> deathsInPowderedSnow;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,9 @@ import lombok.NoArgsConstructor;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
|
@ -28,7 +22,7 @@ import java.util.*;
|
|||
public class Team {
|
||||
|
||||
@JsonIgnore
|
||||
private final static Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
|
||||
private final TeamScoreboard scoreboard = new TeamScoreboard(this);
|
||||
@JsonIgnore
|
||||
private final HashMap<UUID, TeamPlayer> players = new HashMap<>();
|
||||
@JsonProperty("name")
|
||||
|
|
@ -75,7 +69,7 @@ public class Team {
|
|||
}
|
||||
|
||||
public TeamPlayer addPlayer(Player player, PlayerStat playerStat) {
|
||||
removeFromScoreBoard(player);
|
||||
removeFromScoreboard(player);
|
||||
UUID uuid = player.getUniqueId();
|
||||
TeamPlayer teamPlayer = new TeamPlayer(player, this, playerStat);
|
||||
players.put(uuid, teamPlayer);
|
||||
|
|
@ -101,7 +95,7 @@ public class Team {
|
|||
}
|
||||
|
||||
public void removePlayer(@NotNull Player player) {
|
||||
removeFromScoreBoard(player);
|
||||
removeFromScoreboard(player);
|
||||
TeamPlayer remove = players.remove(player.getUniqueId());
|
||||
if (remove == null) {
|
||||
return;
|
||||
|
|
@ -115,38 +109,15 @@ public class Team {
|
|||
}
|
||||
|
||||
public void addToScoreboard(Player player) {
|
||||
org.bukkit.scoreboard.Team team = scoreboard.getTeam("ctf_" + id);
|
||||
if (team == null) {
|
||||
team = scoreboard.registerNewTeam("ctf_" + id);
|
||||
team.displayName(name);
|
||||
NamedTextColor namedTextColor = NamedTextColor.nearestTo(TextColor.color(color.r(), color.g(), color.b()));
|
||||
team.color(namedTextColor);
|
||||
}
|
||||
team.addPlayer(player);
|
||||
player.setScoreboard(scoreboard);
|
||||
scoreboard.addToScoreboard(player);
|
||||
}
|
||||
|
||||
private void removeFromScoreBoard(Player player) {
|
||||
scoreboard.getTeams().stream()
|
||||
.filter(team -> team.getName().startsWith("ctf_"))
|
||||
.filter(team -> team.hasPlayer(player))
|
||||
.forEach(team -> team.removePlayer(player));
|
||||
private void removeFromScoreboard(Player player) {
|
||||
scoreboard.removeFromScoreboard(player);
|
||||
}
|
||||
|
||||
public void setScore(int newScore) {
|
||||
Objective objective = getOrCreateObjective();
|
||||
Score score = objective.getScore(legacyTeamColor + PlainTextComponentSerializer.plainText().serialize(name));
|
||||
score.setScore(newScore);
|
||||
}
|
||||
|
||||
private Objective getOrCreateObjective() {
|
||||
Objective objective = scoreboard.getObjective("teamScores");
|
||||
if (objective == null) {
|
||||
objective = scoreboard.registerNewObjective("teamScores", Criteria.DUMMY,
|
||||
MiniMessage.miniMessage().deserialize("<gold>CTF score</gold>"));
|
||||
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||
}
|
||||
return objective;
|
||||
scoreboard.setScore(newScore);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
86
src/main/java/com/alttd/ctf/team/TeamScoreboard.java
Normal file
86
src/main/java/com/alttd/ctf/team/TeamScoreboard.java
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package com.alttd.ctf.team;
|
||||
|
||||
import com.alttd.ctf.game.GameManager;
|
||||
import com.alttd.ctf.game.GamePhase;
|
||||
import io.papermc.paper.scoreboard.numbers.NumberFormat;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.*;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.*;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class TeamScoreboard {
|
||||
|
||||
private final static Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
|
||||
private final Team team;
|
||||
|
||||
public TeamScoreboard(Team team) {
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
public void addToScoreboard(Player player) {
|
||||
org.bukkit.scoreboard.Team scoreboardTeam = scoreboard.getTeam("ctf_" + team.getId());
|
||||
if (scoreboardTeam == null) {
|
||||
scoreboardTeam = scoreboard.registerNewTeam("ctf_" + team.getId());
|
||||
scoreboardTeam.displayName(team.getName());
|
||||
TeamColor color = team.getColor();
|
||||
NamedTextColor namedTextColor = NamedTextColor.nearestTo(TextColor.color(color.r(), color.g(), color.b()));
|
||||
scoreboardTeam.color(namedTextColor);
|
||||
}
|
||||
scoreboardTeam.addPlayer(player);
|
||||
player.setScoreboard(scoreboard);
|
||||
}
|
||||
|
||||
protected void removeFromScoreboard(Player player) {
|
||||
scoreboard.getTeams().stream()
|
||||
.filter(team -> team.getName().startsWith("ctf_"))
|
||||
.filter(team -> team.hasPlayer(player))
|
||||
.forEach(team -> team.removePlayer(player));
|
||||
}
|
||||
|
||||
protected void setScore(int newScore) {
|
||||
Objective objective = getOrCreateObjective("teamScores", "<gold>CTF score</gold>");
|
||||
Score score = objective.getScore(team.getLegacyTeamColor() +
|
||||
PlainTextComponentSerializer.plainText().serialize(team.getName()));
|
||||
score.setScore(newScore);
|
||||
}
|
||||
|
||||
private static Objective getOrCreateObjective(String internalName, String displayName) {
|
||||
Objective objective = scoreboard.getObjective(internalName);
|
||||
if (objective == null) {
|
||||
objective = scoreboard.registerNewObjective(internalName, Criteria.DUMMY,
|
||||
MiniMessage.miniMessage().deserialize(displayName));
|
||||
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||
}
|
||||
return objective;
|
||||
}
|
||||
|
||||
private record PhaseScore(GamePhase gamePhase, Score score) {}
|
||||
private static PhaseScore phaseScore = null;
|
||||
private static void updateTime(GamePhase gamePhase, Duration duration) {
|
||||
if (phaseScore == null) {
|
||||
phaseScore = new PhaseScore(gamePhase, getOrCreateObjective("teamScores", "<gold>CTF score</gold>")
|
||||
.getScore(ChatColor.GREEN + PlainTextComponentSerializer.plainText().serialize(gamePhase.getDisplayName())));
|
||||
} else if (phaseScore.gamePhase() != gamePhase) {
|
||||
phaseScore.score.resetScore();
|
||||
phaseScore = new PhaseScore(gamePhase, getOrCreateObjective("teamScores", "<gold>CTF score</gold>")
|
||||
.getScore(ChatColor.GREEN + PlainTextComponentSerializer.plainText().serialize(gamePhase.getDisplayName())));
|
||||
}
|
||||
phaseScore.score.setScore(duration.toMinutesPart() == 0 ? duration.toSecondsPart() : duration.toMinutesPart());
|
||||
}
|
||||
|
||||
public static void refreshScoreboard(GameManager gameManager) {
|
||||
gameManager.getGamePhase().ifPresent(gamePhase ->
|
||||
gameManager.getRemainingTime().ifPresent(duration ->
|
||||
updateTime(gamePhase, duration)));
|
||||
// TODO if game is active show time + game phase
|
||||
// if someone has flag show that
|
||||
// show how many of each team in circle?
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
#Sun Feb 23 01:14:21 CET 2025
|
||||
buildNumber=70
|
||||
#Fri Feb 28 22:11:49 CET 2025
|
||||
buildNumber=80
|
||||
version=0.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user