Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/net/theevilreaper/bounce/Bounce.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private void handleGameLeave(Player player) {

if (profile == null) return;

profile.getJumpRunnable().cancel();
profile.stopJumpTask();
this.scoreboard.removeViewer(player);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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;
import net.theevilreaper.bounce.attribute.AttributeHelper;
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;

Expand All @@ -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());
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
78 changes: 73 additions & 5 deletions src/main/java/net/theevilreaper/bounce/profile/ProfileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Provides concurrent lookup, creation, removal, lifecycle startup triggers,
* winner determination based on profile score, and cleanup methods.</p>

* @author theEvilReaper
* @version 1.1.0
* @since 0.1.0
*/
public class ProfileService {

private final Map<UUID, BounceProfile> 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<BounceProfile> 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<UUID, BounceProfile> getProfileMap() {
return Collections.unmodifiableMap(profileMap);
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/net/theevilreaper/bounce/util/GameMessages.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading