Introduce singleton pattern for services, add safe phase to round state, and implement PlayerTeleporterService with destination-based teleporting

This commit is contained in:
akastijn 2026-05-30 00:03:32 +02:00
parent 2746a43837
commit 0cf662dfef
8 changed files with 175 additions and 4 deletions

View File

@ -30,7 +30,7 @@ public final class Main extends JavaPlugin {
private void registerServices() { private void registerServices() {
Round round = Round.createSingletonInstance(); Round round = Round.createSingletonInstance();
roundService = new RoundService(round); roundService = RoundService.createSingletonInstance(round);
playerService = new PlayerService(roundService); playerService = new PlayerService(roundService);
} }

View File

@ -5,6 +5,9 @@ import com.alttd.hunger_games.Main;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -18,6 +21,7 @@ import java.lang.reflect.Modifier;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -107,6 +111,38 @@ abstract class AbstractConfig {
return yaml.getDouble(path, yaml.getDouble(path)); return yaml.getDouble(path, yaml.getDouble(path));
} }
boolean contains(String prefix, String path) {
path = prefix + path;
return !yaml.contains(path);
}
Optional<Location> getLocation(String prefix, String path) {
if (contains(prefix, path)) {
return Optional.empty();
}
String rootPath = prefix + path + ".";
if (contains(rootPath, "world") || contains(rootPath, path + "x") || contains(rootPath, path + "y") || contains(rootPath, path + "z")) {
log.error("Invalid location configuration for {}", path);
return Optional.empty();
}
String worldString = getString(rootPath, "world", null);
double x = getDouble(rootPath, "x", 0);
double y = getDouble(rootPath, "y", 0);
double z = getDouble(rootPath, "z", 0);
World world = Bukkit.getWorld(worldString);
if (world == null) {
log.error("Invalid world for location {}", path);
return Optional.empty();
}
return Optional.of(new Location(world, x, y, z));
}
<T> List<String> getList(String prefix, String path, T def) { <T> List<String> getList(String prefix, String path, T def) {
path = prefix + path; path = prefix + path;
yaml.addDefault(path, def); yaml.addDefault(path, def);

View File

@ -2,12 +2,15 @@ package com.alttd.hunger_games.config;
import com.alttd.hunger_games.data_objects.GameStage; import com.alttd.hunger_games.data_objects.GameStage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import java.io.File; import java.io.File;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
@Slf4j @Slf4j
@ -93,4 +96,27 @@ public class Config extends AbstractConfig {
return gameStageList; return gameStageList;
} }
} }
public static class DESTINATION {
private static final String prefix = "destination.";
public static Location START_CENTER = null;
public static int START_RADIUS = 15;
public static Location FINALE_CENTER = null;
public static int FINALE_RADIUS = 10;
public static Location SPECTATOR_AREA = null;
@SuppressWarnings("unused")
private static void load() {
Config.DESTINATION.START_RADIUS = config.getInt(prefix, "start-radius", START_RADIUS);
config.getLocation(prefix, "start-center")
.ifPresent(location -> DESTINATION.START_CENTER = location);
config.getLocation(prefix, "spectator-area")
.ifPresent(location -> DESTINATION.SPECTATOR_AREA = location);
DESTINATION.FINALE_RADIUS = config.getInt(prefix, "finale-radius", FINALE_RADIUS);
config.getLocation(prefix, "finale-center")
.ifPresent(location -> DESTINATION.FINALE_CENTER = location);
}
}
} }

View File

@ -0,0 +1,7 @@
package com.alttd.hunger_games.data_objects;
public enum DESTINATION {
START_AREA,
FINALE_AREA,
SPECTATOR_AREA;
}

View File

@ -5,6 +5,7 @@ import java.util.Optional;
public enum ROUND_STATE { public enum ROUND_STATE {
PLAYER_REGISTRATION, PLAYER_REGISTRATION,
COUNTDOWN, COUNTDOWN,
SAFE_PHASE,
KILL_PHASE, KILL_PHASE,
FINALE, FINALE,
ENDED; ENDED;
@ -20,6 +21,7 @@ public enum ROUND_STATE {
return switch (this) { return switch (this) {
case PLAYER_REGISTRATION -> "Player Registration"; case PLAYER_REGISTRATION -> "Player Registration";
case COUNTDOWN -> "Countdown"; case COUNTDOWN -> "Countdown";
case SAFE_PHASE -> "Safe Phase";
case KILL_PHASE -> "Kill Phase"; case KILL_PHASE -> "Kill Phase";
case FINALE -> "Finale"; case FINALE -> "Finale";
case ENDED -> "Ended"; case ENDED -> "Ended";

View File

@ -19,7 +19,7 @@ public class PlayerService implements RoundListener {
public void stateChange(ROUND_STATE roundState) { public void stateChange(ROUND_STATE roundState) {
this.roundState = roundState; this.roundState = roundState;
switch (roundState) { switch (roundState) {
case PLAYER_REGISTRATION, KILL_PHASE -> { case PLAYER_REGISTRATION, KILL_PHASE, SAFE_PHASE -> {
//Nothing //Nothing
} }
case COUNTDOWN -> { case COUNTDOWN -> {
@ -94,7 +94,7 @@ public class PlayerService implements RoundListener {
} }
return switch (roundState) { return switch (roundState) {
case PLAYER_REGISTRATION, COUNTDOWN -> Optional.of(PLAYER_STATE.REGISTERED); case PLAYER_REGISTRATION, COUNTDOWN -> Optional.of(PLAYER_STATE.REGISTERED);
case KILL_PHASE, FINALE, ENDED -> Optional.of(PLAYER_STATE.SPECTATING); case KILL_PHASE, FINALE, ENDED, SAFE_PHASE -> Optional.of(PLAYER_STATE.SPECTATING);
}; };
} }
} }

View File

@ -0,0 +1,90 @@
package com.alttd.hunger_games.services;
import com.alttd.hunger_games.config.Config;
import com.alttd.hunger_games.data_objects.DESTINATION;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import lombok.NoArgsConstructor;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@NoArgsConstructor
public class PlayerTeleporterService implements RoundListener {
private static PlayerTeleporterService instance = null;
private final Map<DESTINATION, Integer> placementCount = new HashMap<>();
private final Map<DESTINATION, Set<Location>> usedLocations = new HashMap<>();
public static PlayerTeleporterService createSingletonInstance() {
if (instance != null) {
throw new IllegalStateException("PlayerTeleporterService is already initialized.");
}
instance = new PlayerTeleporterService();
return instance;
}
public void teleportPlayer(Player player, DESTINATION destination) {
player.teleport(getLocationFromDestination(destination));
}
private Location getLocationFromDestination(DESTINATION destination) {
return switch (destination) {
case START_AREA -> {
Location startCenter = Config.DESTINATION.START_CENTER;
if (startCenter == null) {
throw new IllegalStateException("Start center is not set.");
}
int startRadius = Config.DESTINATION.START_RADIUS;
yield calculateLocation(destination, startCenter, startRadius);
}
case FINALE_AREA -> {
Location finaleCenter = Config.DESTINATION.FINALE_CENTER;
if (finaleCenter == null) {
throw new IllegalStateException("Finale center is not set.");
}
int finaleRadius = Config.DESTINATION.FINALE_RADIUS;
yield calculateLocation(destination, finaleCenter, finaleRadius);
}
case SPECTATOR_AREA -> {
Location spectatorArea = Config.DESTINATION.SPECTATOR_AREA;
if (spectatorArea == null) {
throw new IllegalStateException("Spectator area is not set.");
}
yield spectatorArea;
}
};
}
private Location calculateLocation(DESTINATION destination, Location center, int radius) {
int count = placementCount.merge(destination, 1, Integer::sum);
double angleDegrees = vanDerCorput(count - 1) * 360.0;
double angleRadians = Math.toRadians(angleDegrees);
double x = center.getX() + radius * Math.cos(angleRadians);
double z = center.getZ() + radius * Math.sin(angleRadians);
Location location = new Location(center.getWorld(), x, center.getY(), z);
usedLocations.computeIfAbsent(destination, k -> new HashSet<>()).add(location);
return location;
}
private static double vanDerCorput(int n) {
double result = 0.0;
double denominator = 1.0;
while (n > 0) {
denominator *= 2.0;
result += (n % 2) / denominator;
n /= 2;
}
return result;
}
@Override
public void stateChange(ROUND_STATE roundState) {
placementCount.clear();
usedLocations.clear();
}
}

View File

@ -9,11 +9,21 @@ import java.util.stream.Collectors;
public class RoundService implements RoundListener { public class RoundService implements RoundListener {
private static RoundService instance = null;
@Getter @Getter
private ROUND_STATE roundState; private ROUND_STATE roundState;
private final HashMap<UUID, PLAYER_STATE> players = new HashMap<>(); private final HashMap<UUID, PLAYER_STATE> players = new HashMap<>();
public RoundService(Round round) { public static RoundService createSingletonInstance(Round round) {
if (instance != null) {
throw new IllegalStateException("RoundService is already initialized.");
}
instance = new RoundService(round);
return instance;
}
private RoundService(Round round) {
this.roundState = round.register(this); this.roundState = round.register(this);
} }