Compare commits

...

12 Commits

Author SHA1 Message Date
Teriuihi 8718ca0918 Refactor world border handling for game phases.
Removed world border size from `GamePhase` and centralized configuration in `GameConfig`. Updated logic to use `WorldBorderSettings` dynamically, ensuring all phases define valid world border behavior. Adjusted related methods to improve maintainability and clarity.
2025-02-08 23:06:10 +01:00
Teriuihi 728e8b7486 Add configurable respawn time to GameConfig
Introduced a new RESPAWN section in GameConfig for managing respawn time settings. Updated OnPlayerDeath to use the configurable value instead of a hardcoded duration. This allows greater flexibility for game configuration.
2025-02-08 23:01:43 +01:00
Teriuihi 74cf3589c0 Refactor configuration handling and add world border support
Moved FLAG settings from Config to GameConfig for better modularity. Introduced world border configuration with types and sizes for different game phases. Updated related classes to utilize new configuration structure and added error handling to prevent runtime failures.
2025-02-08 23:00:04 +01:00
Teriuihi d7432c9b89 Change flag color to black banner.
Updated the default flag item from a cyan banner to a black banner.
2025-02-08 21:56:56 +01:00
Teriuihi 7ed4c20d29 Update OnPlayerDeath to delay respawning
Modified the OnPlayerDeath event to schedule game class application with a 10-second delay after player death instead of immediately applying it.
2025-02-08 21:56:23 +01:00
Teriuihi 33578027d3 Refactor game class handling and fix GUI inventory usage.
Simplified game class retrieval by removing redundant data structures and combining logic. Updated GUI inventory to ensure tasks are run on the main thread using Bukkit's scheduler. Improved code organization by centralizing class selection logic in `TeamPlayer`.
2025-02-08 21:56:05 +01:00
Teriuihi a52efb9dbb Remove unnecessary logging in resetWorldBorder method
Removed a debug log statement in the resetWorldBorder method as it was redundant. This improves code cleanliness and avoids excessive log clutter during game phase transitions.
2025-02-08 21:06:22 +01:00
Teriuihi 72dc1af903 Add flag turn-in location to team functionality
Introduced a new `flagTurnInLocation` property to the `Team` class, used to determine where players return flags. Updated relevant logic in `Flag` and `CreateTeam` to utilize this new property, including reducing the required distance for flag turn-in.
2025-02-08 21:03:01 +01:00
Teriuihi e93580bea9 Automatically show class selection when the phase starts 2025-02-08 20:56:15 +01:00
Teriuihi 53de0fe217 Remove logging in TeamPlayer for world border
Removed redundant log messages in the `resetWorldBorder` method to reduce clutter and improve clarity.
2025-02-08 20:55:37 +01:00
Teriuihi b8afe209bc Remove unused variable in resetWorldBorder method
Deleted an unused `IWorldBorder` variable in the `resetWorldBorder` method to clean up the code. This change helps improve readability and eliminates unnecessary declarations.
2025-02-08 20:45:30 +01:00
Teriuihi be7b508667 Integrate WorldBorderAPI and enhance game phase functionality
Added WorldBorderAPI dependency to manage player boundaries dynamically during game phases. Updated game phases, respawn mechanics, and class selection to utilize the WorldBorderAPI. Reworked related components to improve boundary handling, ensuring consistent gameplay flow and preparation for future enhancements.
2025-02-08 20:45:06 +01:00
27 changed files with 363 additions and 129 deletions

View File

@ -31,6 +31,8 @@ dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.2")
// End JSON config dependencies
// WorldBorderAPI
compileOnly("com.github.yannicklamprecht:worldborderapi:1.210.0:dev")
}
tasks.test {

View File

@ -5,6 +5,10 @@ dependencyResolutionManagement {
mavenLocal()
mavenCentral()
maven("https://repo.destro.xyz/snapshots") // Altitude - Galaxy
maven { // WorldBorderAPI
name = "eldonexus"
url = uri("https://eldonexus.de/repository/maven-releases/")
}
}
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
}

View File

@ -11,15 +11,19 @@ import com.alttd.ctf.events.SnowballEvent;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.flag.FlagTryCaptureEvent;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.gui.ClassSelectionGUI;
import com.alttd.ctf.gui.GUIInventory;
import com.alttd.ctf.gui.GUIListener;
import com.alttd.ctf.json_config.JacksonConfig;
import com.alttd.ctf.json_config.JsonConfigManager;
import com.alttd.ctf.team.Team;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.World;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
@ -35,24 +39,27 @@ public class Main extends JavaPlugin {
@Override
public void onEnable() {
GUIInventory.setMain(this); // sorry
ClassSelectionGUI.setMain(this); // sorry
Package pkg = Main.class.getPackage();
String version = pkg.getImplementationVersion();
log.info("Plugin enabled, version {}", version);
reloadConfigs();
this.gameManager = new GameManager();
WorldBorderApi worldBorderApi = worldBorder();
this.gameManager = new GameManager(worldBorderApi);
registerTeams(); //Skipped in reloadConfig if gameManager is not created yet
flag = new Flag(this, gameManager);
CommandManager commandManager = new CommandManager(this, gameManager, flag);
new CommandManager(this, gameManager, flag, worldBorderApi);
//Ensuring immediate respawn is on in all worlds
log.info("Enabling immediate respawn for {}.", Config.FLAG.world);
World world = Bukkit.getWorld(Config.FLAG.world);
log.info("Enabling immediate respawn for {}.", GameConfig.FLAG.world);
World world = Bukkit.getWorld(GameConfig.FLAG.world);
if (world != null) {
world.setGameRule(GameRule.DO_IMMEDIATE_RESPAWN, true);
} else {
log.error("No valid flag world defined, unable to modify game rules");
}
registerEvents(flag);
registerEvents(flag, worldBorderApi);
}
public void reloadConfigs() {
@ -64,12 +71,23 @@ public class Main extends JavaPlugin {
}
}
private void registerEvents(Flag flag) {
private WorldBorderApi worldBorder() {
RegisteredServiceProvider<WorldBorderApi> worldBorderApiRegisteredServiceProvider = getServer().getServicesManager().getRegistration(WorldBorderApi.class);
if (worldBorderApiRegisteredServiceProvider == null) {
log.error("WorldBorder API not found, disabling plugin");
getServer().getPluginManager().disablePlugin(this);
return null;
}
return worldBorderApiRegisteredServiceProvider.getProvider();
}
private void registerEvents(Flag flag, WorldBorderApi worldBorderApi) {
PluginManager pluginManager = getServer().getPluginManager();
//TODO add event for player joining and clear their inv
pluginManager.registerEvents(new SnowballEvent(gameManager), this);
pluginManager.registerEvents(new FlagTryCaptureEvent(flag), this);
pluginManager.registerEvents(new OnPlayerDeath(gameManager), this);
pluginManager.registerEvents(new OnPlayerDeath(gameManager, worldBorderApi, this), this);
pluginManager.registerEvents(new InventoryItemInteractionEvent(), this);
pluginManager.registerEvents(new OnPlayerJoin(gameManager, flag), this);
pluginManager.registerEvents(new GUIListener(), this);

View File

@ -5,6 +5,7 @@ import com.alttd.ctf.commands.subcommands.*;
import com.alttd.ctf.config.Messages;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@ -22,7 +23,7 @@ import java.util.stream.Collectors;
public class CommandManager implements CommandExecutor, TabExecutor {
private final List<SubCommand> subCommands;
public CommandManager(Main main, GameManager gameManager, Flag flag) {
public CommandManager(Main main, GameManager gameManager, Flag flag, WorldBorderApi worldBorderApi) {
PluginCommand command = main.getCommand("ctf");
if (command == null) {
subCommands = null;
@ -36,7 +37,7 @@ public class CommandManager implements CommandExecutor, TabExecutor {
new ChangeTeam(gameManager),
new Start(gameManager, flag),
new CreateTeam(main, gameManager),
new SelectClass(gameManager),
new SelectClass(gameManager, worldBorderApi),
new Reload(main)
);
}

View File

@ -75,7 +75,7 @@ public class CreateTeam extends SubCommand {
int highestId = gameManager.getMaxTeamId();
Team team = new Team(MiniMessage.miniMessage().deserialize(String.format("<color:%s>%s</color>", color, name)),
highestId + 1, player.getLocation(), player.getLocation(), teamColor);
highestId + 1, player.getLocation(), player.getLocation(), player.getLocation(), teamColor);
return consumer.apply(team);
}

View File

@ -5,15 +5,13 @@ import com.alttd.ctf.config.Messages;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.creation.EngineerCreator;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.game_class.creation.TankCreator;
import com.alttd.ctf.game_class.GameClassRetrieval;
import com.alttd.ctf.gui.ClassSelectionGUI;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
@ -22,15 +20,12 @@ public class SelectClass extends SubCommand {
private final GameManager gameManager;
private final HashMap<Integer, List<GameClass>> gameClasses;
private final WorldBorderApi worldBorderApi;
public SelectClass(GameManager gameManager) {
public SelectClass(GameManager gameManager, WorldBorderApi worldBorderApi) {
this.gameManager = gameManager;
this.gameClasses = new HashMap<>();
gameManager.getTeams().forEach(team -> {
gameClasses.computeIfAbsent(team.getId(), teamId -> new ArrayList<>()).add(FighterCreator.createFighter(team.getColor()));
gameClasses.computeIfAbsent(team.getId(), teamId -> new ArrayList<>()).add(TankCreator.createTank(team.getColor()));
gameClasses.computeIfAbsent(team.getId(), teamId -> new ArrayList<>()).add(EngineerCreator.createEngineer(team.getColor()));
});
this.worldBorderApi = worldBorderApi;
gameClasses = GameClassRetrieval.getGameClassesForAllTeams(gameManager);
}
@Override
@ -55,7 +50,7 @@ public class SelectClass extends SubCommand {
commandSender.sendRichMessage("<red>You have to be near your spawn to change classes.</red>");
return 0;
}
new ClassSelectionGUI(gameClasses.get(teamPlayer.getTeam().getId()), teamPlayer)
new ClassSelectionGUI(gameClasses.get(teamPlayer.getTeam().getId()), teamPlayer, worldBorderApi, gamePhase)
.open(player);
return 0;
}

View File

@ -41,21 +41,4 @@ public class Config extends AbstractConfig{
}
}
public static class FLAG {
private static final String prefix = "flag.";
public static String world = "world";
public static double x = 0;
public static double y = 0;
public static double z = 0;
@SuppressWarnings("unused")
private static void load() {
world = config.getString(prefix, "world", world);
x = config.getDouble(prefix, "x", x);
y = config.getDouble(prefix, "y", y);
z = config.getDouble(prefix, "z", z);
}
}
}

View File

@ -8,7 +8,7 @@ import java.time.Duration;
import java.util.HashMap;
@Slf4j
public class GameConfig extends AbstractConfig{
public class GameConfig extends AbstractConfig {
static GameConfig config;
@ -45,4 +45,67 @@ public class GameConfig extends AbstractConfig{
}
}
@SuppressWarnings("unused")
public static class WORLD_BORDER {
private static final String prefix = "world-border.";
public static final double DEFAULT_SIZE = 140;
private static final HashMap<GamePhase, WorldBorderSettings> GAME_PHASE_WORLD_BORDER = new HashMap<>();
public static HashMap<GamePhase, WorldBorderSettings> getGAME_PHASE_WORLD_BORDER() {
return new HashMap<>(GAME_PHASE_WORLD_BORDER);
}
@SuppressWarnings("unused")
private static void load() {
GAME_PHASE_WORLD_BORDER.clear();
for (GamePhase phase : GamePhase.values()) {
String stringType = config.getString(prefix, phase.name().toLowerCase() + ".type", WorldBorderType.PLAYER.name());
double size = config.getDouble(prefix, phase.name().toLowerCase() + ".size", 10);
WorldBorderType worldBorderType;
try {
worldBorderType = WorldBorderType.valueOf(stringType);
} catch (IllegalArgumentException e) {
log.error("Invalid world border type [{}] in game phase world border config", stringType);
continue;
}
GAME_PHASE_WORLD_BORDER.put(phase, new WorldBorderSettings(worldBorderType, size));
log.debug("Set {} phase world border type to {} blocks", phase.name(), size);
}
}
}
public static class FLAG {
private static final String prefix = "flag.";
public static String world = "world";
public static double x = 0;
public static double y = 0;
public static double z = 0;
public static double CAPTURE_RADIUS = 7;
public static int WINNING_SCORE = 50;
@SuppressWarnings("unused")
private static void load() {
world = config.getString(prefix, "world", world);
x = config.getDouble(prefix, "x", x);
y = config.getDouble(prefix, "y", y);
z = config.getDouble(prefix, "z", z);
CAPTURE_RADIUS = config.getDouble(prefix, "capture-radius", CAPTURE_RADIUS);
WINNING_SCORE = config.getInt(prefix, "winning-score", WINNING_SCORE);
}
}
public static class RESPAWN {
private static final String prefix = "respawn.";
public static int TIME = 10;
@SuppressWarnings("unused")
private static void load() {
TIME = config.getInt(prefix, "time", TIME);
}
}
}

View File

@ -8,7 +8,7 @@ public class Messages extends AbstractConfig {
static Messages config;
Messages(Main main) {
super(main, "config.yml");
super(main, "messages.yml");
}
public static void reload(Main main) {

View File

@ -0,0 +1,4 @@
package com.alttd.ctf.config;
public record WorldBorderSettings(WorldBorderType type, double size) {
}

View File

@ -0,0 +1,6 @@
package com.alttd.ctf.config;
public enum WorldBorderType {
FLAG,
PLAYER
}

View File

@ -1,7 +1,14 @@
package com.alttd.ctf.events;
import com.alttd.ctf.Main;
import com.alttd.ctf.config.Config;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@ -10,12 +17,17 @@ import org.bukkit.event.player.PlayerRespawnEvent;
import java.util.Optional;
@Slf4j
public class OnPlayerDeath implements Listener {
private final GameManager gameManager;
private final WorldBorderApi worldBorderApi;
private final Main main;
public OnPlayerDeath(GameManager gameManager) {
public OnPlayerDeath(GameManager gameManager, WorldBorderApi worldBorderApi, Main main) {
this.gameManager = gameManager;
this.worldBorderApi = worldBorderApi;
this.main = main;
}
@EventHandler
@ -33,13 +45,18 @@ public class OnPlayerDeath implements Listener {
@EventHandler
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
Optional<GamePhase> gamePhase = gameManager.getGamePhase();
if (gamePhase.isEmpty()) {
log.warn("Player {} died while the game wasn't running", player.getName());
return;
}
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
if (optionalTeamPlayer.isEmpty()) {
return;
}
TeamPlayer teamPlayer = optionalTeamPlayer.get();
event.setRespawnLocation(teamPlayer.getTeam().getSpawnLocation());
teamPlayer.getGameClass().apply(teamPlayer);
event.setRespawnLocation(player.getWorld().getSpawnLocation());
Bukkit.getScheduler().runTaskLater(main, () -> teamPlayer.getGameClass().apply(teamPlayer, worldBorderApi, gamePhase.get(), true), GameConfig.RESPAWN.TIME * 20L);//10 x 20 ticks aka 10 seconds
}
}

View File

@ -1,12 +1,12 @@
package com.alttd.ctf.flag;
import com.alttd.ctf.Main;
import com.alttd.ctf.config.Config;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer;
import lombok.RequiredArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@ -20,31 +20,37 @@ import org.bukkit.boss.KeyedBossBar;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitScheduler;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
public class Flag implements Runnable {
private final HashMap<Integer, Integer> teamFlagPointCount = new HashMap<>();
private final ItemStack flagItem = new ItemStack(Material.CYAN_BANNER);
private final ItemStack flagItem = new ItemStack(Material.BLACK_BANNER);
private final BossBar bossBar = createBossBar();
private final HashMap<Integer, Integer> wins = new HashMap<>();
private int lastWinningTeamId = -1;
private Location flagLocation;
@Getter
private final Location flagLocation;
private Team winningTeam;
private Player flagCarrier;
private final Main main;
private final GameManager gameManager;
public Flag(Main main, GameManager gameManager) {
this.main = main;
this.gameManager = gameManager;
World world = Bukkit.getWorld(GameConfig.FLAG.world);
if (world == null) {
throw new IllegalStateException(String.format("Tried to spawn flag in world [%s] that doesn't exist", GameConfig.FLAG.world));
}
this.flagLocation = new Location(world, GameConfig.FLAG.x, GameConfig.FLAG.y, GameConfig.FLAG.z);
}
private BossBar createBossBar() {
NamespacedKey namespacedKey = NamespacedKey.fromString("ctf_flag", main);
if (namespacedKey == null) {
@ -60,10 +66,6 @@ public class Flag implements Runnable {
return captureProgress;
}
protected Location getFlagLocation() {
return flagLocation;
}
public void addPlayer(Player player) {
bossBar.addPlayer(player);
}
@ -85,13 +87,6 @@ public class Flag implements Runnable {
}
public void spawnFlag() {
//TODO disable flag capture for a minute or so, and maybe have it slowly drop down?
World world = Bukkit.getWorld(Config.FLAG.world);
if (world == null) {
throw new IllegalStateException(String.format("Tried to spawn flag in world [%s] that doesn't exist", Config.FLAG.world));
}
this.flagLocation = new Location(world, Config.FLAG.x, Config.FLAG.y, Config.FLAG.z);
//Place block on main thread
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(flagItem.getType()));
}
@ -126,18 +121,18 @@ public class Flag implements Runnable {
private void spawnParticlesOnSquareBorder(World world, Location center) {
double size = 10;
double step = 0.2;
center.add(0, 0.5, 0);
Location finalCenter = center.clone().add(0, 0.5, 0);
Bukkit.getScheduler().runTask(main, () -> {
// Top and Bottom (Z varies, X constant)
for (double z = -size; z <= size; z += step) {
world.spawnParticle(Particle.FLAME, center.getX() + size, center.getY(), center.getZ() + z, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, center.getX() - size, center.getY(), center.getZ() + z, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, finalCenter.getX() + size, finalCenter.getY(), finalCenter.getZ() + z, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, finalCenter.getX() - size, finalCenter.getY(), finalCenter.getZ() + z, 1, 0, 0, 0, 0);
}
// Left and Right (X varies, Z constant)
for (double x = -size; x <= size; x += step) {
world.spawnParticle(Particle.FLAME, center.getX() + x, center.getY(), center.getZ() + size, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, center.getX() + x, center.getY(), center.getZ() - size, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, finalCenter.getX() + x, finalCenter.getY(), finalCenter.getZ() + size, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, finalCenter.getX() + x, finalCenter.getY(), finalCenter.getZ() - size, 1, 0, 0, 0, 0);
}
});
}
@ -160,8 +155,8 @@ public class Flag implements Runnable {
spawnFlag();
return;
}
double distance = winningTeam.getSpawnLocation().distance(flagCarrier.getLocation());
if (distance > 5) {
double distance = winningTeam.getFlagTurnInLocation().distance(flagCarrier.getLocation());
if (distance > 2) {
Location location = flagCarrier.getLocation();
location.setY(location.getY() + 1);
particleTrail.add(location);
@ -194,7 +189,7 @@ public class Flag implements Runnable {
if (max.isEmpty()) {
return Optional.empty();
}
if (max.get().getValue() < 100) {
if (max.get().getValue() < GameConfig.FLAG.WINNING_SCORE) {
return Optional.empty();
}
return gameManager.getTeam(max.get().getKey());
@ -209,7 +204,7 @@ public class Flag implements Runnable {
private CompletableFuture<Boolean> updateScoreBasedOnNearbyPlayers() {
CompletableFuture<Boolean> future = new CompletableFuture<>();
Bukkit.getScheduler().runTask(main, () -> {
Collection<Player> nearbyPlayers = flagLocation.getNearbyPlayers(10);
Collection<Player> nearbyPlayers = flagLocation.getNearbyPlayers(GameConfig.FLAG.CAPTURE_RADIUS);
Bukkit.getScheduler().runTaskAsynchronously(main, () -> {
boolean result = updateScoreBasedOnNearbyPlayers(nearbyPlayers);
future.complete(result);
@ -288,7 +283,7 @@ public class Flag implements Runnable {
bossBar.setTitle(String.format("Team %s is capturing the flag", PlainTextComponentSerializer.plainText().serialize(team.get().getName())));
lastWinningTeamId = highestKey;
}
bossBar.setProgress(Math.min(100, teamFlagPointCount.get(highestKey)) / 100.0);
bossBar.setProgress(Math.min(GameConfig.FLAG.WINNING_SCORE, teamFlagPointCount.get(highestKey)) / (double) GameConfig.FLAG.WINNING_SCORE);
bossBar.setVisible(teamFlagPointCount.get(highestKey) > 0);
}

View File

@ -8,6 +8,7 @@ import com.alttd.ctf.game.phases.GatheringPhase;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@ -24,10 +25,10 @@ public class GameManager {
private RunningGame runningGame;
private final HashMap<Integer, Team> teams = new HashMap<>();
public GameManager() {
public GameManager(WorldBorderApi worldBorderApi) {
phases = new HashMap<>();
phases.put(GamePhase.CLASS_SELECTION, new ClassSelectionPhase(this, (FighterCreator::createFighter)));
phases.put(GamePhase.GATHERING, new GatheringPhase());
phases.put(GamePhase.CLASS_SELECTION, new ClassSelectionPhase(this, FighterCreator::createFighter, worldBorderApi));
phases.put(GamePhase.GATHERING, new GatheringPhase(this, worldBorderApi));
phases.put(GamePhase.COMBAT, new CombatPhase());
phases.put(GamePhase.ENDED, new EndedPhase());
}

View File

@ -6,5 +6,5 @@ public interface GamePhaseExecutor {
void start(Flag flag);
void end();
void end(GamePhase nextGamePhase);
}

View File

@ -33,26 +33,31 @@ public class RunningGame implements Runnable {
@Override
public void run() {
GamePhase nextPhase = GamePhase.values().length < currentPhase.ordinal() ? null : GamePhase.values()[currentPhase.ordinal() + 1];
if (phaseStartTime == null) {
phaseStartTime = Instant.now();
nextPhaseActions(null, currentPhase, nextPhase);
}
try {
GamePhase nextPhase = GamePhase.values().length < currentPhase.ordinal() ? null : GamePhase.values()[currentPhase.ordinal() + 1];
if (phaseStartTime == null) {
phaseStartTime = Instant.now();
nextPhaseActions(null, currentPhase, nextPhase);
}
if (Duration.between(phaseStartTime, Instant.now()).compareTo(phaseDurations.get(currentPhase)) >= 0) {
GamePhase previousPhase = currentPhase;
currentPhase = GamePhase.values()[currentPhase.ordinal() + 1];
nextPhaseActions(previousPhase, currentPhase, nextPhase);
phaseStartTime = Instant.now();
} else if (nextPhase != null) {
broadcastNextPhaseStartTime(currentPhase, nextPhase);
if (Duration.between(phaseStartTime, Instant.now()).compareTo(phaseDurations.get(currentPhase)) >= 0) {
GamePhase previousPhase = currentPhase;
currentPhase = GamePhase.values()[currentPhase.ordinal() + 1];
nextPhaseActions(previousPhase, currentPhase, nextPhase);
phaseStartTime = Instant.now();
} else if (nextPhase != null) {
broadcastNextPhaseStartTime(currentPhase, nextPhase);
}
} catch (Exception e) {
log.error("Unexpected error in running game", e);
throw new RuntimeException(e);
}
}
private void nextPhaseActions(@Nullable GamePhase previousPhase, @NotNull GamePhase phase, @Nullable GamePhase nextPhase) {
//TODO command to go to next phase
if (previousPhase != null) {
gameManager.getPhaseExecutor(previousPhase).end();
gameManager.getPhaseExecutor(previousPhase).end(phase);
}
gameManager.getPhaseExecutor(phase).start(flag);
if (nextPhase != null) {

View File

@ -1,17 +1,24 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer;
import com.alttd.ctf.util.CircularIterator;
import com.github.yannicklamprecht.worldborder.api.IWorldBorder;
import com.github.yannicklamprecht.worldborder.api.Position;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
@ -21,24 +28,32 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
private final GameManager gameManager;
private final DefaultClassCreator defaultClassCreator;
private final WorldBorderApi worldBorderApi;
@FunctionalInterface
public interface DefaultClassCreator {
@NotNull GameClass apply(TeamColor teamColor);
}
public ClassSelectionPhase(@NotNull GameManager gameManager, DefaultClassCreator defaultClassCreator) {
public ClassSelectionPhase(@NotNull GameManager gameManager, DefaultClassCreator defaultClassCreator,
@NotNull WorldBorderApi worldBorderApi) {
this.gameManager = gameManager;
this.defaultClassCreator = defaultClassCreator;
this.worldBorderApi = worldBorderApi;
}
@Override
public void start(Flag flag) {
teleportPlayersToStartingZone();
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Select your class</green>"));
Location flagLocation = flag.getFlagLocation();
IWorldBorder worldBorder = worldBorderApi.getWorldBorder(flagLocation.getWorld());
worldBorder.setCenter(Position.of(flagLocation));
worldBorder.setSize(GameConfig.WORLD_BORDER.DEFAULT_SIZE);
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Select your class with <gold>/ctf selectclass</gold></green>"));
CircularIterator<Team> teamCircularIterator = new CircularIterator<>(gameManager.getTeams());
if (teamCircularIterator.hasNext()) {
Bukkit.getOnlinePlayers().stream()
.filter(player -> !player.hasPermission("ctf.bypass"))
.filter(player -> gameManager.getTeamPlayer(player.getUniqueId()).isEmpty())
.forEach(player -> {
Team team = teamCircularIterator.next();
@ -48,34 +63,41 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
} else {
log.warn("No teams to add players to");
}
//TODO let players select classes
// They should always be able to do this when in their starting zone
// They should be locked into their starting zone until the next phase starts
// That phase should handle opening the zone
teleportPlayersToStartingZone();
}
private void teleportPlayersToStartingZone() {
Bukkit.getOnlinePlayers().forEach(player -> {
Optional<Team> team = gameManager.getTeam(player.getUniqueId());
if (team.isEmpty()) {
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
if (teamPlayer.isEmpty()) {
log.warn("{} is not a team player when teleporting to starting zone", player.getName());
return;
}
Location spawnLocation = team.get().getSpawnLocation();
player.teleportAsync(spawnLocation);
Optional<GamePhase> gamePhase = gameManager.getGamePhase();
if (gamePhase.isEmpty()) {
log.warn("Game phase is empty when teleporting to starting zone");
return;
}
teamPlayer.get().respawn(player, worldBorderApi, gamePhase.get());
});
}
@Override
public void end() {
public void end(GamePhase nextPhase) {
gameManager.getTeams().forEach(team -> {
GameClass defaultClass = defaultClassCreator.apply(team.getColor());
team.getPlayers().forEach(player -> {
if (player.getGameClass() != null) {
team.getPlayers().forEach(teamPlayer -> {
Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) {
log.debug("Tried to reset world border for offline player");
return;
}
defaultClass.apply(player);
teamPlayer.resetWorldBorder(player, worldBorderApi, nextPhase, team.getWorldBorderCenter());
if (teamPlayer.getGameClass() != null) {
return;
}
defaultClass.apply(teamPlayer, worldBorderApi, nextPhase, false);
});
});
//TODO expand world border so ppl can gather things but not get near the flag yet
}
}

View File

@ -1,6 +1,7 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
@ -28,7 +29,7 @@ public class CombatPhase implements GamePhaseExecutor {
}
@Override
public void end() {
public void end(GamePhase ignored) {
executorService.shutdown();
}
}

View File

@ -1,7 +1,8 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.config.Config;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor;
import com.alttd.ctf.team.Team;
import lombok.extern.slf4j.Slf4j;
@ -31,7 +32,7 @@ public class EndedPhase implements GamePhaseExecutor {
HashMap<Team, Integer> wins = flag.getWins();
Bukkit.broadcast(Component.join(JoinConfiguration.separator(Component.newline()), getWinnerMessages(wins)));
new Thread(() -> {
World world = Bukkit.getWorld(Config.FLAG.world);
World world = Bukkit.getWorld(GameConfig.FLAG.world);
if (world == null) {
log.error("Invalid flag world defined");
return;
@ -101,7 +102,7 @@ public class EndedPhase implements GamePhaseExecutor {
}
@Override
public void end() {
public void end(GamePhase ignored) {
}
}

View File

@ -1,20 +1,45 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor;
import com.alttd.ctf.game_class.GameClass;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
@Slf4j
public class GatheringPhase implements GamePhaseExecutor {
private final GameManager gameManager;
private final WorldBorderApi worldBorderApi;
private Flag flag;
public GatheringPhase(GameManager gameManager, WorldBorderApi worldBorderApi) {
this.gameManager = gameManager;
this.worldBorderApi = worldBorderApi;
}
@Override
public void start(Flag flag) {
this.flag = flag;
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Gather materials and prepare for combat!</green>"));
//TODO give everyone haste or something so they can mine faster till this game phase ends
// Let them store things at base during this phase, after only one class can do this?
}
@Override
public void end() {
//TODO Remove team area barrier
public void end(GamePhase nextPhase) {
if (flag == null) {
log.error("Unable to update world border due to missing Flag");
return;
}
gameManager.getTeams().forEach(team -> {
team.getPlayers().forEach(player -> {
player.resetWorldBorder(Bukkit.getPlayer(player.getUuid()), worldBorderApi, nextPhase, flag.getFlagLocation());
});
});
}
}

View File

@ -1,7 +1,9 @@
package com.alttd.ctf.game_class;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
@ -68,7 +70,7 @@ public abstract class GameClass {
displayItem.setItemMeta(itemMeta);
}
public void apply(TeamPlayer teamPlayer) {
public void apply(TeamPlayer teamPlayer, WorldBorderApi worldBorderApi, GamePhase gamePhase, boolean teleport) {
Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) {
log.warn("Tried to give class to offline player {}", player == null ? teamPlayer.getUuid() : player.getName());
@ -91,7 +93,7 @@ public abstract class GameClass {
player.updateInventory();
teamPlayer.setGameClass(this);
player.sendRichMessage("You selected the <class_name> class", Placeholder.component("class_name", className));
player.teleportAsync(teamPlayer.getTeam().getSpawnLocation());
teamPlayer.respawn(player, worldBorderApi, gamePhase, teleport);
}
private void setArmor(Player player, int r, int g, int b) {

View File

@ -0,0 +1,31 @@
package com.alttd.ctf.game_class;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game_class.creation.EngineerCreator;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.game_class.creation.TankCreator;
import com.alttd.ctf.team.Team;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class GameClassRetrieval {
public static HashMap<Integer, List<GameClass>> getGameClassesForAllTeams(GameManager gameManager) {
final HashMap<Integer, List<GameClass>> gameClasses = new HashMap<>();
gameManager.getTeams().forEach(team -> {
gameClasses.put(team.getId(), getGameClassesForTeam(team));
});
return gameClasses;
}
public static List<GameClass> getGameClassesForTeam(Team team) {
final List<GameClass> gameClasses = new ArrayList<>();
gameClasses.add(FighterCreator.createFighter(team.getColor()));
gameClasses.add(TankCreator.createTank(team.getColor()));
gameClasses.add(EngineerCreator.createEngineer(team.getColor()));
return gameClasses;
}
}

View File

@ -1,8 +1,13 @@
package com.alttd.ctf.gui;
import com.alttd.ctf.Main;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Setter;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
@ -11,17 +16,21 @@ import java.util.List;
public class ClassSelectionGUI extends GUIInventory {
public ClassSelectionGUI(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer) {
@Setter
private static Main main;
public ClassSelectionGUI(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer,
@NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
super(InventoryType.CHEST, teamPlayer.getTeam().getName().append(MiniMessage.miniMessage().deserialize(" - class selection")));
createClassSelection(gameClasses, teamPlayer);
createClassSelection(gameClasses, teamPlayer, worldBorderApi, gamePhase);
}
private void createClassSelection(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer) {
private void createClassSelection(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer, @NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
int pos = (9 + (9 - gameClasses.size()) / 2);
for (GameClass gameClass : gameClasses) {
setItem(pos++, gameClass.getDisplayItem(), player -> {
gameClass.apply(teamPlayer);
player.closeInventory(InventoryCloseEvent.Reason.PLUGIN);
gameClass.apply(teamPlayer, worldBorderApi, gamePhase, true);
Bukkit.getScheduler().runTask(main, () -> player.closeInventory(InventoryCloseEvent.Reason.PLUGIN));
});
}
}

View File

@ -1,19 +1,22 @@
package com.alttd.ctf.gui;
import com.alttd.ctf.Main;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.MerchantInventory;
import java.util.HashMap;
public abstract class GUIInventory implements GUI {
@Setter
private static Main main;
@Getter
protected final Inventory inventory;
protected final HashMap<Integer, GUIAction> guiActions;
@ -35,7 +38,7 @@ public abstract class GUIInventory implements GUI {
}
public void open(Player player) {
player.openInventory(inventory);
Bukkit.getScheduler().runTask(main, () -> player.openInventory(inventory));
GUIByUUID.put(player.getUniqueId(), this);
}

View File

@ -33,7 +33,11 @@ public class Team {
@JsonProperty("worldBorderCenter")
@NotNull
@Getter
private Location worldBorderCenter; //TODO https://github.com/yannicklamprecht/WorldBorderAPI/blob/main/how-to-use.md
private Location worldBorderCenter; // https://github.com/yannicklamprecht/WorldBorderAPI/blob/main/how-to-use.md
@JsonProperty("flagTurnInLocation")
@NotNull
@Getter
private Location flagTurnInLocation;
@JsonProperty("teamColor")
@NotNull
@Getter

View File

@ -1,12 +1,23 @@
package com.alttd.ctf.team;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.config.WorldBorderSettings;
import com.alttd.ctf.config.WorldBorderType;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.GameClassRetrieval;
import com.alttd.ctf.gui.ClassSelectionGUI;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.UUID;
import java.util.*;
@Slf4j
@Getter
public class TeamPlayer {
@ -20,6 +31,37 @@ public class TeamPlayer {
this.team = team;
}
public void respawn(@NotNull Player player, @NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
respawn(player, worldBorderApi, gamePhase, true);
}
public void respawn(Player player, WorldBorderApi worldBorderApi, GamePhase gamePhase, boolean teleport) {
Location spawnLocation = team.getSpawnLocation();
Location worldBorderCenter = team.getWorldBorderCenter();
List<GameClass> gameClasses = GameClassRetrieval.getGameClassesForTeam(team);
new ClassSelectionGUI(gameClasses, this, worldBorderApi, GamePhase.CLASS_SELECTION).open(player);
if (!teleport) {
resetWorldBorder(player, worldBorderApi, gamePhase, worldBorderCenter);
return;
}
player.teleportAsync(spawnLocation).thenAcceptAsync(unused ->
resetWorldBorder(player, worldBorderApi, gamePhase, worldBorderCenter));
}
public void resetWorldBorder(Player player, WorldBorderApi worldBorderApi, GamePhase gamePhase, Location worldBorderCenter) {
WorldBorderSettings worldBorderSettings = GameConfig.WORLD_BORDER.getGAME_PHASE_WORLD_BORDER().get(gamePhase);
if (worldBorderSettings == null) {
throw new IllegalStateException("All phases need to have world border settings");
}
if (worldBorderSettings.type().equals(WorldBorderType.FLAG)) {
worldBorderApi.resetWorldBorderToGlobal(player);
return;
}
worldBorderApi.setBorder(player, worldBorderSettings.size(), worldBorderCenter);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {

View File

@ -1,3 +1,3 @@
#Sat Feb 08 00:30:51 CET 2025
buildNumber=11
#Sat Feb 08 21:57:04 CET 2025
buildNumber=30
version=0.1