diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/Messages.java b/common/src/main/java/net/onelitefeather/cygnus/common/Messages.java index 6f9caad3..50721410 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/Messages.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/Messages.java @@ -36,6 +36,7 @@ public final class Messages { private static final Component SURVIVOR_JOIN_PART_UPPER; private static final Component SURVIVOR_JOIN_LOWER_PART; public static final Component SLENDER_JOIN_PART; + private static final Component STATS_HEADER; private static final int MAP_ANNOUNCEMENT_MIN_WIDTH = 20; static { @@ -74,6 +75,8 @@ public final class Messages { .append(withPrefix(Component.text("Eliminate all survivors to win the game!", NamedTextColor.YELLOW))) .append(Component.newline()) .append(withMiniPrefix("Right-click your SlenderEye to toggle invisibility!")); + + STATS_HEADER = withMini("---[Statistics]---"); } private Messages() { @@ -128,6 +131,51 @@ public static Component getDeathComponent(Player player) { return PREFIX.append(Component.space()).append(withMini(" was TAKEN!", playerTag)); } + /** + * Builds the round-end stats box for a survivor. + * + * @param player the survivor the box is about + * @param pageFounds how many pages the survivor found this round + * @param died whether the survivor died this round + * @return the formatted stats box + */ + @Contract(value = "_, _, _ -> new", pure = true) + public static Component getSurvivorRoundSummaryComponent(Player player, int pageFounds, boolean died) { + Component deathIcon = died + ? Component.text("✓", NamedTextColor.GREEN) + : Component.text("✗", NamedTextColor.RED); + var resolver = TagResolver.builder() + .tag("pages", (_, _) -> Tag.preProcessParsed(String.valueOf(pageFounds))) + .tag("died", Tag.inserting(deathIcon)) + .build(); + return Component.text(player.getUsername(), NamedTextColor.GOLD) + .append(Component.newline()) + .append(STATS_HEADER) + .append(Component.newline()) + .append(withMini("Pages: ", resolver)) + .append(Component.newline()) + .append(withMini("Death: ", resolver)); + } + + /** + * Builds the round-end stats box for the slender. + * + * @param player the slender the box is about + * @param kills how many survivors the slender caught this round + * @return the formatted stats box + */ + @Contract(value = "_, _ -> new", pure = true) + public static Component getSlenderRoundSummaryComponent(Player player, int kills) { + var resolver = TagResolver.builder() + .tag("kills", (_, _) -> Tag.preProcessParsed(String.valueOf(kills))) + .build(); + return Component.text(player.getUsername(), NamedTextColor.GOLD) + .append(Component.newline()) + .append(STATS_HEADER) + .append(Component.newline()) + .append(withMini("Kills: ", resolver)); + } + @Contract(value = "_, _ -> new", pure = true) public static Component getViewComponent(String time, Component pageStatus) { diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerDeathListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerDeathListener.java index 323a929f..631a7f68 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerDeathListener.java +++ b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerDeathListener.java @@ -14,6 +14,7 @@ import net.onelitefeather.cygnus.event.GameFinishEvent; import net.onelitefeather.cygnus.jumpscare.JumpScareManager; import net.onelitefeather.cygnus.phase.GamePhase; +import net.onelitefeather.cygnus.player.CygnusPlayer; import net.onelitefeather.cygnus.player.event.SpectatorAddEvent; import java.util.function.Consumer; @@ -39,7 +40,12 @@ public PlayerDeathListener(Supplier phaseSupplier, TeamService teamServic public void accept(PlayerDeathEvent event) { Player player = event.getPlayer(); - if (survivorTeam.getPlayers().contains(player) && player.getInstance() != null) { + if (survivorTeam.getPlayers().contains(player)) { + ((CygnusPlayer) player).setDeath(true); + this.slenderTeam.getPlayers().stream().findFirst() + .ifPresent(slender -> ((CygnusPlayer) slender).incrementKills()); + + if (player.getInstance() == null) return; Pos deathPos = player.getPosition(); DeadPlayerMannequin mannequin = DeadPlayerMannequin.sleeping(player); mannequin.setInstance(player.getInstance(), deathPos.add(0, 0.15, 0)); diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameFinishListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameFinishListener.java index afc26148..9b439d76 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameFinishListener.java +++ b/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameFinishListener.java @@ -2,8 +2,12 @@ import net.theevilreaper.aves.util.Broadcaster; import net.kyori.adventure.text.Component; +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Player; import net.onelitefeather.cygnus.common.Messages; import net.onelitefeather.cygnus.event.GameFinishEvent; +import net.onelitefeather.cygnus.player.CygnusPlayer; +import net.onelitefeather.cygnus.team.TeamHelper; import java.util.function.Consumer; @@ -19,5 +23,23 @@ public void accept(GameFinishEvent event) { case TIME_OVER, ALL_PAGES_FOUND, SLENDER_LEFT -> Messages.SURVIVOR_WIN_MESSAGE; }; Broadcaster.broadcast(endComponent); + Broadcaster.broadcast(buildRoundSummary()); + } + + /** + * Builds a per-player summary of this round's stats, one line per {@link CygnusPlayer} online. + * + * @return the summary component + */ + private Component buildRoundSummary() { + Component summary = Component.empty(); + for (Player onlinePlayer : MinecraftServer.getConnectionManager().getOnlinePlayers()) { + if (!(onlinePlayer instanceof CygnusPlayer cygnusPlayer)) continue; + Component box = TeamHelper.isSlenderTeam(onlinePlayer) + ? Messages.getSlenderRoundSummaryComponent(onlinePlayer, cygnusPlayer.getKills()) + : Messages.getSurvivorRoundSummaryComponent(onlinePlayer, cygnusPlayer.getPageFounds(), cygnusPlayer.hasDied()); + summary = summary.append(Component.newline()).append(box); + } + return summary; } } diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/page/PlayerPageInteractListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/page/PlayerPageInteractListener.java index 84e67a14..b907ffeb 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/listener/page/PlayerPageInteractListener.java +++ b/game/src/main/java/net/onelitefeather/cygnus/listener/page/PlayerPageInteractListener.java @@ -5,6 +5,7 @@ import net.minestom.server.event.player.PlayerEntityInteractEvent; import net.onelitefeather.cygnus.common.Tags; import net.onelitefeather.cygnus.common.page.PageProvider; +import net.onelitefeather.cygnus.player.CygnusPlayer; import net.onelitefeather.cygnus.team.TeamHelper; import java.util.UUID; @@ -24,6 +25,7 @@ public void accept(PlayerEntityInteractEvent event) { Entity target = event.getTarget(); if (!TeamHelper.isSurvivorTeam(player) || !target.hasTag(Tags.PAGE_TAG)) return; UUID uuid = target.getTag(Tags.PAGE_TAG); + ((CygnusPlayer) player).incrementPageFound(); pageProvider.triggerPageFound(player, uuid); } } diff --git a/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java b/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java index cf60254f..9b8445fd 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java +++ b/game/src/main/java/net/onelitefeather/cygnus/player/CygnusPlayer.java @@ -41,11 +41,18 @@ public final class CygnusPlayer extends InstanceSwitchChunkPlayer { private int heartbeatTicks; private boolean heartbeatActive; + private int pageFounds; + private int kills; + private boolean death; + public CygnusPlayer(PlayerConnection playerConnection, GameProfile gameProfile) { super(playerConnection, gameProfile); this.blockedSprinting = false; this.heartbeatTicks = 0; this.heartbeatActive = false; + this.pageFounds = 0; + this.kills = 0; + this.death = false; } /** @@ -100,6 +107,56 @@ public boolean hasBlockedSprinting() { return blockedSprinting; } + /** + * Increments the number of pages this player has found in the current round. + */ + public void incrementPageFound() { + this.pageFounds++; + } + + /** + * Returns how many pages this player has found in the current round. + * + * @return the page count + */ + public int getPageFounds() { + return pageFounds; + } + + /** + * Increments the number of survivors this player has killed in the current round. + */ + public void incrementKills() { + this.kills++; + } + + /** + * Returns how many survivors this player has killed in the current round. + * + * @return the kill count + */ + public int getKills() { + return kills; + } + + /** + * Marks whether this player died during the current round. + * + * @param death {@code true} if the player died this round + */ + public void setDeath(boolean death) { + this.death = death; + } + + /** + * Checks if the player died during the current round. + * + * @return {@code true} if the player died this round, otherwise {@code false}. + */ + public boolean hasDied() { + return death; + } + /** * Updates the heartbeat sound and red border vignette effect on player tick. */ diff --git a/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerDeathListenerTest.java b/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerDeathListenerTest.java index eaca4a30..f8b8037e 100644 --- a/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerDeathListenerTest.java +++ b/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerDeathListenerTest.java @@ -8,14 +8,15 @@ import net.onelitefeather.cygnus.common.Tags; import net.onelitefeather.cygnus.common.config.GameConfig; import net.onelitefeather.cygnus.jumpscare.JumpScareManager; +import net.onelitefeather.cygnus.player.CygnusPlayer; import net.onelitefeather.cygnus.player.event.SpectatorAddEvent; -import net.onelitefeather.cygnus.team.TeamHelper; -import net.theevilreaper.xerus.api.team.Team; + import net.theevilreaper.xerus.api.team.Team; import net.theevilreaper.xerus.api.team.TeamService; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class PlayerDeathListenerTest extends CygnusPlayerTestBase { @@ -42,4 +43,53 @@ void testDyingSurvivorFiresSpectatorAddEventForThemselves(@NotNull Env env) { env.destroyInstance(instance, true); } + + @Test + void testDyingSurvivorIsMarkedAsDied(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + CygnusPlayer player = (CygnusPlayer) env.createPlayer(instance); + + TeamService teamService = TeamService.of(); + Team slenderTeam = Team.of(GameConfig.SLENDER_KEY, 1); + Team survivorTeam = Team.of(GameConfig.SURVIVOR_KEY, 5); + teamService.add(slenderTeam); + teamService.add(survivorTeam); + + survivorTeam.addPlayer(player); + player.setTag(Tags.TEAM_KEY, GameConfig.SURVIVOR_KEY); + + PlayerDeathListener listener = new PlayerDeathListener(() -> null, teamService, new JumpScareManager()); + + listener.accept(new PlayerDeathEvent(player, null, null)); + + assertTrue(player.hasDied()); + + env.destroyInstance(instance, true); + } + + @Test + void testDyingSurvivorIncrementsSlenderKills(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + CygnusPlayer survivor = (CygnusPlayer) env.createPlayer(instance); + CygnusPlayer slender = (CygnusPlayer) env.createPlayer(instance); + + TeamService teamService = TeamService.of(); + Team slenderTeam = Team.of(GameConfig.SLENDER_KEY, 1); + Team survivorTeam = Team.of(GameConfig.SURVIVOR_KEY, 5); + teamService.add(slenderTeam); + teamService.add(survivorTeam); + + survivorTeam.addPlayer(survivor); + survivor.setTag(Tags.TEAM_KEY, GameConfig.SURVIVOR_KEY); + slenderTeam.addPlayer(slender); + slender.setTag(Tags.TEAM_KEY, GameConfig.SLENDER_KEY); + + PlayerDeathListener listener = new PlayerDeathListener(() -> null, teamService, new JumpScareManager()); + + listener.accept(new PlayerDeathEvent(survivor, null, null)); + + assertEquals(1, slender.getKills()); + + env.destroyInstance(instance, true); + } }