diff --git a/src/main/java/com/alttd/ctf/Main.java b/src/main/java/com/alttd/ctf/Main.java index 7f20c14..f0ad6d8 100644 --- a/src/main/java/com/alttd/ctf/Main.java +++ b/src/main/java/com/alttd/ctf/Main.java @@ -147,10 +147,11 @@ public class Main extends JavaPlugin { gameManager.setPlayerStats(playerStats); final JsonConfigManager 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); diff --git a/src/main/java/com/alttd/ctf/commands/CommandManager.java b/src/main/java/com/alttd/ctf/commands/CommandManager.java index dda631f..31d6d8d 100644 --- a/src/main/java/com/alttd/ctf/commands/CommandManager.java +++ b/src/main/java/com/alttd/ctf/commands/CommandManager.java @@ -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); } -} \ No newline at end of file +} diff --git a/src/main/java/com/alttd/ctf/commands/subcommands/HighestStat.java b/src/main/java/com/alttd/ctf/commands/subcommands/HighestStat.java new file mode 100644 index 0000000..fb08fc4 --- /dev/null +++ b/src/main/java/com/alttd/ctf/commands/subcommands/HighestStat.java @@ -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 highestStatValues = new ArrayList<>(); + Collection playerStats = gameManager.getPlayerStats(); + for (Stat stat : Stat.values()) { + Optional 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 getTabComplete(CommandSender commandSender, String[] args) { + return List.of(); + } + + @Override + public String getHelpMessage() { + return Messages.HELP.HIGHEST_STAT; + } +} diff --git a/src/main/java/com/alttd/ctf/config/Messages.java b/src/main/java/com/alttd/ctf/config/Messages.java index 3184137..3e61240 100644 --- a/src/main/java/com/alttd/ctf/config/Messages.java +++ b/src/main/java/com/alttd/ctf/config/Messages.java @@ -28,6 +28,7 @@ public class Messages extends AbstractConfig { public static String START = "Start a new game: /ctf start "; public static String SELECT_CLASS = "Open class selection: /ctf selectclass"; public static String SKIP_PHASE = "Skip the current phase: /ctf skipphase"; + public static String HIGHEST_STAT = "Display the highest stat and the player: /ctf higheststat"; @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); } } diff --git a/src/main/java/com/alttd/ctf/game/GameManager.java b/src/main/java/com/alttd/ctf/game/GameManager.java index 21e895f..48ebf76 100644 --- a/src/main/java/com/alttd/ctf/game/GameManager.java +++ b/src/main/java/com/alttd/ctf/game/GameManager.java @@ -121,4 +121,11 @@ public class GameManager { public Collection getPlayerStats() { return this.playerStats.values(); } + + public Optional getRemainingTime() { + if (runningGame == null || runningGame.getCurrentPhase().equals(GamePhase.ENDED)) { + return Optional.empty(); + } + return Optional.of(runningGame.getRemainingTime()); + } } diff --git a/src/main/java/com/alttd/ctf/game/RunningGame.java b/src/main/java/com/alttd/ctf/game/RunningGame.java index e19d26d..2501a42 100644 --- a/src/main/java/com/alttd/ctf/game/RunningGame.java +++ b/src/main/java/com/alttd/ctf/game/RunningGame.java @@ -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())); + } } diff --git a/src/main/java/com/alttd/ctf/stats/PlayerStat.java b/src/main/java/com/alttd/ctf/stats/PlayerStat.java index 487d72c..9b4d6d6 100644 --- a/src/main/java/com/alttd/ctf/stats/PlayerStat.java +++ b/src/main/java/com/alttd/ctf/stats/PlayerStat.java @@ -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(" will receive a consolation prize for being the first to die in powdered snow.", + 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; + }; + } } diff --git a/src/main/java/com/alttd/ctf/team/Team.java b/src/main/java/com/alttd/ctf/team/Team.java index f02fb8e..3529325 100644 --- a/src/main/java/com/alttd/ctf/team/Team.java +++ b/src/main/java/com/alttd/ctf/team/Team.java @@ -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 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("CTF score")); - objective.setDisplaySlot(DisplaySlot.SIDEBAR); - } - return objective; + scoreboard.setScore(newScore); } @Override diff --git a/src/main/java/com/alttd/ctf/team/TeamScoreboard.java b/src/main/java/com/alttd/ctf/team/TeamScoreboard.java new file mode 100644 index 0000000..e01f460 --- /dev/null +++ b/src/main/java/com/alttd/ctf/team/TeamScoreboard.java @@ -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", "CTF score"); + 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", "CTF score") + .getScore(ChatColor.GREEN + PlainTextComponentSerializer.plainText().serialize(gamePhase.getDisplayName()))); + } else if (phaseScore.gamePhase() != gamePhase) { + phaseScore.score.resetScore(); + phaseScore = new PhaseScore(gamePhase, getOrCreateObjective("teamScores", "CTF score") + .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? + } + +} diff --git a/version.properties b/version.properties index d2a38f0..0dbdc80 100644 --- a/version.properties +++ b/version.properties @@ -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