diff --git a/src/main/java/net/theevilreaper/bounce/Bounce.java b/src/main/java/net/theevilreaper/bounce/Bounce.java
index e31d22d..8483692 100644
--- a/src/main/java/net/theevilreaper/bounce/Bounce.java
+++ b/src/main/java/net/theevilreaper/bounce/Bounce.java
@@ -145,7 +145,7 @@ private void handleGameLeave(Player player) {
if (profile == null) return;
- profile.getJumpRunnable().cancel();
+ profile.stopJumpTask();
this.scoreboard.removeViewer(player);
}
diff --git a/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java b/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java
index c302ad9..f8e1660 100644
--- a/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java
+++ b/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java
@@ -1,7 +1,6 @@
package net.theevilreaper.bounce.listener.game;
import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.TitlePart;
import net.minestom.server.entity.Player;
@@ -9,6 +8,7 @@
import net.theevilreaper.bounce.event.BounceGameFinishEvent;
import net.theevilreaper.bounce.profile.BounceProfile;
import net.theevilreaper.bounce.profile.ProfileService;
+import net.theevilreaper.bounce.util.GameMessages;
import java.util.function.Consumer;
@@ -26,17 +26,18 @@ public void accept(BounceGameFinishEvent event) {
BounceProfile winnerProfile = profileService.getWinner();
if (winnerProfile == null) {
- profileService.clear(bounceProfile -> bounceProfile.getJumpRunnable().cancel());
+ profileService.clear(BounceProfile::stopJumpTask);
return;
}
profileService.clear(profile -> {
- profile.getJumpRunnable().cancel();
- Player player = winnerProfile.getPlayer();
+ profile.stopJumpTask();
+ Player player = profile.getPlayer();
AttributeHelper.resetJumpStrength(player);
boolean isWinner = profile.equals(winnerProfile);
profile.sendStats(isWinner);
- var title = Title.title(player.getDisplayName(), Component.text("wons the game", NamedTextColor.GRAY), Title.DEFAULT_TIMES);
+ Component displayName = winnerProfile.getPlayer().getDisplayName();
+ Title title = Title.title(displayName, GameMessages.WON_COMPONENT, Title.DEFAULT_TIMES);
player.sendTitlePart(TitlePart.TITLE, title.title());
player.sendTitlePart(TitlePart.SUBTITLE, title.subtitle());
player.sendTitlePart(TitlePart.TIMES, title.times());
diff --git a/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java b/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java
index e021590..9be0e62 100644
--- a/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java
+++ b/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java
@@ -169,6 +169,16 @@ public Player getPlayer() {
return player;
}
+ /**
+ * Stops and cancels the active jump task for this profile, if registered.
+ */
+ public void stopJumpTask() {
+ if (this.jumpRunnable != null) {
+ this.jumpRunnable.cancel();
+ this.jumpRunnable = null;
+ }
+ }
+
/**
* Returns the jump runnable associated with this profile's player.
*
diff --git a/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java b/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java
index 2355e50..e8b6605 100644
--- a/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java
+++ b/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java
@@ -5,61 +5,129 @@
import net.theevilreaper.bounce.common.map.GameMap;
import org.jetbrains.annotations.Nullable;
-import java.util.*;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
+/**
+ * Manages player profiles ({@link BounceProfile}) during the lifecycle of the game.
+ *
+ *
Provides concurrent lookup, creation, removal, lifecycle startup triggers,
+ * winner determination based on profile score, and cleanup methods.
+
+ * @author theEvilReaper
+ * @version 1.1.0
+ * @since 0.1.0
+ */
public class ProfileService {
private final Map profileMap;
+ /**
+ * Constructs a new {@link ProfileService} backed by a concurrent hash map.
+ */
public ProfileService() {
- profileMap = new ConcurrentHashMap<>();
+ this.profileMap = new ConcurrentHashMap<>();
}
+ /**
+ * Adds or retrieves the {@link BounceProfile} for the specified player.
+ *
+ * @param player whose profile should be retrieved or created
+ * @return existing or newly created profile
+ */
public BounceProfile add(Player player) {
return this.profileMap.computeIfAbsent(player.getUuid(), uuid -> new BounceProfile(player));
}
+ /**
+ * Removes the profile associated with the specified player.
+ *
+ * @param player whose profile should be removed
+ * @return removed profile, or {@code null} if no profile was stored
+ */
public @Nullable BounceProfile remove(Player player) {
return this.profileMap.remove(player.getUuid());
}
+ /**
+ * Retrieves the profile associated with the specified player.
+ *
+ * @param player whose profile to retrieve
+ * @return player's profile, or {@code null} if not found
+ */
public @Nullable BounceProfile get(Player player) {
return this.profileMap.get(player.getUuid());
}
- public @Nullable BounceProfile get(UUID uuid) {
+ /**
+ * Retrieves the profile associated with the specified UUID.
+ *
+ * @param uuid unique identifier of the player
+ * @return player's profile, or {@code null} if not found
+ */
+ public @Nullable BounceProfile get(@Nullable UUID uuid) {
+ if (uuid == null) return null;
return this.profileMap.get(uuid);
}
+ /**
+ * Starts the jump task for all stored profiles on the given map and invokes the consumer callback.
+ *
+ * @param gameMap active game map
+ * @param consumer action executed for each online player profile
+ */
public void start(GameMap gameMap, PlayerConsumer consumer) {
for (BounceProfile value : this.profileMap.values()) {
- value.getJumpRunnable().start(gameMap);
+ if (value.getJumpRunnable() != null) {
+ value.getJumpRunnable().start(gameMap);
+ }
consumer.accept(value.getPlayer());
}
}
+ /**
+ * Determines and returns the winning profile (the profile with the highest points).
+ *
+ * @return winning profile, or {@code null} if no profiles are registered
+ */
public @Nullable BounceProfile getWinner() {
if (this.profileMap.isEmpty()) return null;
return profileMap.values().stream()
- .min(Comparator.naturalOrder()) // because "highest points" sorts first
+ .max(Comparator.naturalOrder())
.orElse(null);
}
+ /**
+ * Clears all stored player profiles.
+ */
public void clear() {
if (this.profileMap.isEmpty()) return;
this.profileMap.clear();
}
+ /**
+ * Executes the given callback consumer for every profile and clears the profile map.
+ *
+ * @param callback action to execute for each profile before clearing
+ */
public void clear(Consumer callback) {
if (this.profileMap.isEmpty()) return;
for (BounceProfile value : this.profileMap.values()) {
callback.accept(value);
}
+ this.profileMap.clear();
}
+ /**
+ * Returns an unmodifiable view of the internal profile map.
+ *
+ * @return unmodifiable map mapping player UUIDs to their {@link BounceProfile}
+ */
public Map getProfileMap() {
return Collections.unmodifiableMap(profileMap);
}
diff --git a/src/main/java/net/theevilreaper/bounce/util/GameMessages.java b/src/main/java/net/theevilreaper/bounce/util/GameMessages.java
index 9fd5dd2..b85dc8c 100644
--- a/src/main/java/net/theevilreaper/bounce/util/GameMessages.java
+++ b/src/main/java/net/theevilreaper/bounce/util/GameMessages.java
@@ -19,6 +19,8 @@ public class GameMessages extends Messages {
public static final Component INVALID_PLAYER_NAME;
public static final Component PLAYER_NOT_FOUND;
+ public static final Component WON_COMPONENT;
+
public static final Component STATS_LINE;
private static final Component LEAVE_PART;
@@ -46,6 +48,8 @@ public class GameMessages extends Messages {
POINT_PART = Component.space().append(Component.text("Points", NamedTextColor.GRAY));
STATS_LINE = Component.text("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n", NamedTextColor.GRAY);
+
+ WON_COMPONENT = Component.text("won the game", NamedTextColor.GRAY);
}
@Contract
diff --git a/src/test/java/net/theevilreaper/bounce/profile/ProfileServiceIntegrationTest.java b/src/test/java/net/theevilreaper/bounce/profile/ProfileServiceIntegrationTest.java
new file mode 100644
index 0000000..03449c5
--- /dev/null
+++ b/src/test/java/net/theevilreaper/bounce/profile/ProfileServiceIntegrationTest.java
@@ -0,0 +1,79 @@
+package net.theevilreaper.bounce.profile;
+
+import net.minestom.server.entity.Player;
+import net.minestom.server.instance.Instance;
+import net.minestom.testing.Env;
+import net.minestom.testing.extension.MicrotusExtension;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(MicrotusExtension.class)
+class ProfileServiceIntegrationTest {
+
+ @Test
+ void testProfileServiceAddAndGet(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+ Player player = env.createPlayer(instance);
+
+ ProfileService service = new ProfileService();
+ assertTrue(service.getProfileMap().isEmpty());
+
+ BounceProfile profile = service.add(player);
+ assertNotNull(profile);
+ assertEquals(player.getUuid(), profile.getPlayer().getUuid());
+
+ assertEquals(profile, service.get(player));
+ assertEquals(profile, service.get(player.getUuid()));
+ assertEquals(1, service.getProfileMap().size());
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void testProfileServiceClearWithCallbackClearsMap(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+ Player player1 = env.createPlayer(instance);
+ Player player2 = env.createPlayer(instance);
+
+ ProfileService service = new ProfileService();
+ service.add(player1);
+ service.add(player2);
+
+ assertEquals(2, service.getProfileMap().size());
+
+ AtomicInteger processedCount = new AtomicInteger(0);
+ service.clear(profile -> processedCount.incrementAndGet());
+
+ assertEquals(2, processedCount.get(), "Callback should be executed for each profile");
+ assertTrue(service.getProfileMap().isEmpty(), "Profile map should be cleared after clear(callback)");
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void testProfileServiceGetWinnerReturnsHighestScoringProfile(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+ Player player1 = env.createPlayer(instance);
+ Player player2 = env.createPlayer(instance);
+
+ ProfileService service = new ProfileService();
+ assertNull(service.getWinner(), "Winner should be null when profile service is empty");
+
+ BounceProfile profile1 = service.add(player1);
+ BounceProfile profile2 = service.add(player2);
+
+ profile1.addPoints(20);
+ profile2.addPoints(5);
+
+ BounceProfile winner = service.getWinner();
+ assertNotNull(winner, "Winner profile should not be null");
+ assertEquals(profile1, winner, "Winner should be the profile with the highest score");
+
+ env.destroyInstance(instance, true);
+ }
+}