Add initial Capture the Flag plugin implementation

Implemented the core structure for a Capture the Flag (CTF) plugin. This includes team management, game phases, player classes, command handling, and configuration support. The project is set up with Gradle for dependency management and provides placeholders for future feature expansion.
This commit is contained in:
Teriuihi 2025-01-24 17:49:17 +01:00
commit 6e38d42f2d
29 changed files with 1625 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
#.idea/modules.xml
#.idea/jarRepositories.xml
#.idea/compiler.xml
#.idea/libraries/
.idea/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

44
TODO.md Normal file
View File

@ -0,0 +1,44 @@
- [ ] Game manager
- [ ] Stores/creates/modifies teams
- [ ] Allows for rollback of world through CoreProtect
- [ ] Creates teams (exclusively) (load from config?)
- [ ] Stores all players
- [ ] Allows starting, restarting, and ending game
- [ ] Limit playing area (x, y, and z) (could be done through WorldGuard)
- [ ] Stores location for ppl to watch/wait in or handles putting them in gmsp
- [ ] Team
- [x] Stores members
- [ ] Stores score
- [ ] Stores respawn location
- [ ] Respawn player on team by calling respawn function
- [ ] Functions as starting point for team
- [ ] Check if player is in respawn point (for class changes
- [ ] Stores team colors (for armor)
- [ ] Allows creation of Team player object (exclusively)
- [ ] Manages respawns (config timer)
- [ ] Tracks flag
- [ ] Handles team losing
- [ ] Stops respawns after team loses flag
- [ ] Buff/Debuff for flag carrier and dead teams and chases?
- [ ] Flag location indicator (beacon beams, compass, particles)
- [ ] Snowball storage
- [ ] Team player
- [ ] Must be member of team
- [ ] Stores health
- [ ] Stores individual score
- [ ] Class manager
- [ ] Stores list of all classes
- [ ] Allows Team member to select class
- [ ] Classes
- [ ] Fighter: lower health, higher dmg/throwing speed
- [ ] Tank: Has shield, invincibility effect, slower (short + long cooldown)?
- [ ] Engineer: Better shovel, lower health, lower dmg, (can drop snowballs to team members?) store snow at base, can build?
- [ ] HARD Mage: Drops snowballs in area (casting cost)
- [ ] HARD Scout: low dmg, high speed, (invisible sometimes?) can see flag carrier through walls or maybe see their path in particles?
- [ ] Game events
- [ ] Blocks dropping items (could be done through WorldGuard flag)
- [ ] Hit by snowball (handles damage) (no friendly fire)
- [ ] Only allows breaking snow (for snowballs)
- [ ] Blocks building with anything other than snow (could be done through WorldGuard flag)
- [ ] Blocks breaking anything other than snow (could be done through WorldGuard flag)
- [ ] OPTIONAL: Wind to move players a bit if they are high up to discourage towering

22
build.gradle.kts Normal file
View File

@ -0,0 +1,22 @@
plugins {
id("java")
}
group = "com.alttd.ctf"
version = "1.0-SNAPSHOT"
dependencies {
compileOnly("com.alttd:Galaxy-API:1.21-R0.1-SNAPSHOT") {
isChanging = true
}
compileOnly("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
tasks.test {
useJUnitPlatform()
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Fri Dec 20 21:17:54 CET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

16
settings.gradle.kts Normal file
View File

@ -0,0 +1,16 @@
rootProject.name = "CaptureTheFlag"
dependencyResolutionManagement {
repositories {
mavenLocal()
mavenCentral()
maven("https://repo.destro.xyz/snapshots") // Altitude - Galaxy
}
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
}
pluginManagement {
repositories {
gradlePluginPortal()
}
}

View File

@ -0,0 +1,32 @@
package com.alttd.ctf;
import com.alttd.ctf.commands.CommandManager;
import com.alttd.ctf.config.Config;
import com.alttd.ctf.events.OnSnowballHit;
import com.alttd.ctf.game.GameManager;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@Slf4j
public class Main extends JavaPlugin {
@Override
public void onEnable() {
log.info("Plugin enabled!");
reloadConfigs();
GameManager gameManager = new GameManager();
CommandManager commandManager = new CommandManager(this, gameManager);
registerEvents(gameManager);
}
public void reloadConfigs() {
Config.reload(this);
}
private void registerEvents(GameManager gameManager) {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new OnSnowballHit(gameManager), this);
}
}

View File

@ -0,0 +1,118 @@
package com.alttd.ctf.commands;
import com.alttd.ctf.Main;
import com.alttd.ctf.commands.subcommands.ChangeTeam;
import com.alttd.ctf.commands.subcommands.Start;
import com.alttd.ctf.config.Messages;
import com.alttd.ctf.game.GameManager;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.command.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Getter
@Slf4j
public class CommandManager implements CommandExecutor, TabExecutor {
private final List<SubCommand> subCommands;
public CommandManager(Main main, GameManager gameManager) {
PluginCommand command = main.getCommand("ctf");
if (command == null) {
subCommands = null;
log.error("Unable to find transfer command.");
return;
}
command.setExecutor(this);
command.setTabCompleter(this);
subCommands = Arrays.asList(
new ChangeTeam(gameManager),
new Start(gameManager)
);
}
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String cmd, @NotNull String[] args) {
if (args.length == 0) {
commandSender.sendRichMessage(Messages.HELP.HELP_MESSAGE_WRAPPER.replaceAll("<commands>", subCommands.stream()
.filter(subCommand -> commandSender.hasPermission(subCommand.getPermission()))
.map(SubCommand::getHelpMessage)
.collect(Collectors.joining("\n"))));
return true;
}
SubCommand subCommand = getSubCommand(args[0]);
if (subCommand == null)
return false;
if (!commandSender.hasPermission(subCommand.getPermission())) {
commandSender.sendRichMessage(Messages.GENERIC.NO_PERMISSION, Placeholder.parsed("permission", subCommand.getPermission()));
return true;
}
int failedPos = subCommand.onCommand(commandSender, args);
if (failedPos > 0) {
commandSender.sendRichMessage(String.format("<hover:show_text:'%s'>%s</hover>",
getHoverText(command.getName(), args, failedPos),
subCommand.getHelpMessage()));
} else if (failedPos < 0) {
commandSender.sendRichMessage(subCommand.getHelpMessage());
}
return true;
}
private String getHoverText(String commandName, String[] args, int failedPos) {
StringBuilder hoverText = new StringBuilder();
hoverText.append("<green>/").append(commandName);
for (int i = 0; i < failedPos; i++) {
hoverText.append(" ").append(args[i]);
}
hoverText.append("</green>");
if (failedPos < args.length) {
hoverText.append(" <red>").append(args[failedPos]).append("</red>");
}
for (int i = failedPos + 1; i < args.length; i++) {
hoverText.append(" <yellow>").append(args[i]).append("</yellow>");
}
return hoverText.toString();
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String cmd, @NotNull String[] args) {
List<String> res = new ArrayList<>();
if (args.length <= 1) {
res.addAll(subCommands.stream()
.filter(subCommand -> commandSender.hasPermission(subCommand.getPermission()))
.map(SubCommand::getName)
.filter(name -> args.length == 0 || name.startsWith(args[0]))
.toList()
);
} else {
SubCommand subCommand = getSubCommand(args[0]);
if (subCommand != null && commandSender.hasPermission(subCommand.getPermission()))
res.addAll(subCommand.getTabComplete(commandSender, args).stream()
.filter(str -> str.toLowerCase().startsWith(args[args.length - 1].toLowerCase()))
.toList());
}
return res;
}
private SubCommand getSubCommand(String cmdName) {
return subCommands.stream()
.filter(subCommand -> subCommand.getName().equals(cmdName))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,25 @@
package com.alttd.ctf.commands;
import org.bukkit.command.CommandSender;
import java.util.List;
public abstract class SubCommand {
public SubCommand() {}
//A return value other than 0 means the command failed
// If the value is more than 0 it should be the position in the array of the argument it failed on
// If the value is less than 0 it means something else failed (like the amount of arguments)
public abstract int onCommand(CommandSender commandSender, String[] args);
public abstract String getName();
public String getPermission() {
return "ctf." + getName();
}
public abstract List<String> getTabComplete(CommandSender commandSender, String[] args);
public abstract String getHelpMessage();
}

View File

@ -0,0 +1,109 @@
package com.alttd.ctf.commands.subcommands;
import com.alttd.ctf.commands.SubCommand;
import com.alttd.ctf.config.Messages;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.team.Team;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Optional;
public class ChangeTeam extends SubCommand {
private final GameManager gameManager;
public ChangeTeam(GameManager gameManager) {
this.gameManager = gameManager;
}
@FunctionalInterface
private interface ChangeTeamConsumer {
int apply(Player player, Team team);
}
@Override
public int onCommand(CommandSender commandSender, String[] args) {
return handle(commandSender, args, (player, team) -> {
changeTeam(commandSender, player, team);
return 0;
});
}
private int handle(CommandSender commandSender, String[] args, ChangeTeamConsumer consumer) {
if (args.length != 3) {
return -1;
}
Player player = Bukkit.getPlayer(args[1]);
if (player == null) {
commandSender.sendRichMessage("<red>Please provide a valid player</red>");
return 1;
}
int teamId;
try {
teamId = Integer.parseInt(args[2]);
} catch (NumberFormatException e) {
commandSender.sendRichMessage(String.format("<red>Please enter a valid integer, %s is not a valid integer</red>", args[2]));
return 2;
}
Optional<Team> optionalTeam = gameManager.getTeams().stream().filter(team -> team.getId() == teamId).findFirst();
if (optionalTeam.isEmpty()) {
commandSender.sendRichMessage(String.format("<red>Please provide a valid team id %d is not a valid team id</red>", teamId));
return 3;
}
return consumer.apply(player, optionalTeam.get());
}
private void changeTeam(CommandSender commandSender, Player player, Team team) {
Optional<Team> optionalOldTeam = gameManager.getTeam(player.getUniqueId());
if (optionalOldTeam.isPresent()) {
moveBetweenTeams(commandSender, player, team, optionalOldTeam.get());
return;
}
gameManager.registerPlayer(team, player);
commandSender.sendRichMessage("<green><player> has been placed in<team>.</green>",
TagResolver.resolver(
Placeholder.component("player", player.displayName()),
Placeholder.component("team", team.getName())));
}
private void moveBetweenTeams(CommandSender commandSender, Player player, Team newTeam, Team oldTeam) {
if (oldTeam.equals(newTeam)) {
commandSender.sendRichMessage("<green><player> was already in <team>, nothing has changed.</green>",
TagResolver.resolver(
Placeholder.component("player", player.displayName()),
Placeholder.component("team", newTeam.getName())));
return;
}
gameManager.registerPlayer(newTeam, player);
commandSender.sendRichMessage("<green><player> has been moved from <old_team> to <new_team>.</green>",
TagResolver.resolver(
Placeholder.component("player", player.displayName()),
Placeholder.component("old_team", oldTeam.getName())),
Placeholder.component("new_team", newTeam.getName()));
}
@Override
public String getName() {
return "changeteam";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return switch (args.length) {
case 2 -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toList();
case 3 -> gameManager.getTeams().stream().map(Team::getId).map(Object::toString).toList();
default -> List.of();
};
}
@Override
public String getHelpMessage() {
return Messages.HELP.CHANGE_TEAM;
}
}

View File

@ -0,0 +1,64 @@
package com.alttd.ctf.commands.subcommands;
import com.alttd.ctf.commands.SubCommand;
import com.alttd.ctf.config.Messages;
import com.alttd.ctf.game.GameManager;
import org.bukkit.command.CommandSender;
import java.time.Duration;
import java.util.List;
public class Start extends SubCommand {
private final GameManager gameManager;
public Start(GameManager gameManager) {
this.gameManager = gameManager;
}
@FunctionalInterface
private interface StartConsumer {
int apply(Duration combatTime);
}
@Override
public int onCommand(CommandSender commandSender, String[] args) {
return handle(commandSender, args, combatTime -> {
gameManager.start(combatTime);
return 0;
});
}
private int handle(CommandSender commandSender, String[] args, StartConsumer consumer) {
if (args.length != 2) {
return -1;
}
Duration combatTime;
try {
combatTime = Duration.ofSeconds(Integer.parseInt(args[1]));
} catch (NumberFormatException e) {
commandSender.sendRichMessage("<red>Please enter a valid integer</red>");
return 1;
}
return consumer.apply(combatTime);
}
@Override
public String getName() {
return "start";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
//noinspection SwitchStatementWithTooFewBranches
return switch (args.length) {
case 2 -> List.of("30", "45", "60");
default -> List.of();
};
}
@Override
public String getHelpMessage() {
return Messages.HELP.START;
}
}

View File

@ -0,0 +1,148 @@
package com.alttd.ctf.config;
import com.alttd.ctf.Main;
import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@SuppressWarnings({"unused", "SameParameterValue"})
abstract class AbstractConfig {
File file;
YamlConfiguration yaml;
AbstractConfig(Main main, String filename) {
init(new File(main.getDataFolder(), filename), filename);
}
AbstractConfig(File file, String filename) {
init(new File(file.getPath() + File.separator + filename), filename);
}
private void init(File file, String filename) {
this.file = file;
this.yaml = new YamlConfiguration();
try {
yaml.load(file);
} catch (IOException ignore) {
} catch (InvalidConfigurationException ex) {
log.error("Could not load {}, please correct your syntax errors", filename, ex);
throw new RuntimeException(ex);
}
yaml.options().copyDefaults(true);
}
void readConfig(Class<?> clazz, Object instance) {
for (Class<?> declaredClass : clazz.getDeclaredClasses()) {
for (Method method : declaredClass.getDeclaredMethods()) {
if (!Modifier.isPrivate(method.getModifiers())) {
continue;
}
if (method.getParameterTypes().length != 0 || method.getReturnType() != Void.TYPE) {
continue;
}
try {
method.setAccessible(true);
method.invoke(instance);
} catch (InvocationTargetException ex) {
throw new RuntimeException(ex.getCause());
} catch (Exception ex) {
log.error("Error invoking {}.", method, ex);
}
}
}
save();
}
private void save() {
try {
yaml.save(file);
} catch (IOException ex) {
log.error("Could not save {}.", file.toString(), ex);
}
}
void set(String prefix, String path, Object val) {
path = prefix + path;
yaml.addDefault(path, val);
yaml.set(path, val);
save();
}
String getString(String prefix, String path, String def) {
path = prefix + path;
yaml.addDefault(path, def);
return yaml.getString(path, yaml.getString(path));
}
boolean getBoolean(String prefix, String path, boolean def) {
path = prefix + path;
yaml.addDefault(path, def);
return yaml.getBoolean(path, yaml.getBoolean(path));
}
int getInt(String prefix, String path, int def) {
path = prefix + path;
yaml.addDefault(path, def);
return yaml.getInt(path, yaml.getInt(path));
}
double getDouble(String prefix, String path, double def) {
path = prefix + path;
yaml.addDefault(path, def);
return yaml.getDouble(path, yaml.getDouble(path));
}
<T> List<String> getList(String prefix, String path, T def) {
path = prefix + path;
yaml.addDefault(path, def);
List<?> list = yaml.getList(path, yaml.getList(path));
return list == null ? null : list.stream().map(Object::toString).collect(Collectors.toList());
}
List<String> getStringList(String prefix, String path, List<String> def) {
path = prefix + path;
yaml.addDefault(path, def);
return yaml.getStringList(path);
}
@NonNull
<T> Map<String, T> getMap(String prefix, @NonNull String path, final @Nullable Map<String, T> def) {
path = prefix + path;
final ImmutableMap.Builder<String, T> builder = ImmutableMap.builder();
if (def != null && yaml.getConfigurationSection(path) == null) {
yaml.addDefault(path, def.isEmpty() ? new HashMap<>() : def);
return def;
}
final ConfigurationSection section = yaml.getConfigurationSection(path);
if (section != null) {
for (String key : section.getKeys(false)) {
@SuppressWarnings("unchecked")
final T val = (T) section.get(key);
if (val != null) {
builder.put(key, val);
}
}
}
return builder.build();
}
ConfigurationSection getConfigurationSection(String path) {
return yaml.getConfigurationSection(path);
}
}

View File

@ -0,0 +1,44 @@
package com.alttd.ctf.config;
import com.alttd.ctf.Main;
import lombok.extern.slf4j.Slf4j;
import java.util.logging.Level;
@Slf4j
public class Config extends AbstractConfig{
static Config config;
private final Main main;
Config(Main main) {
super(main, "config.yml");
this.main = main;
}
public static void reload(Main main) {
log.info("Reloading config");
config = new Config(main);
config.readConfig(Config.class, null);
}
@SuppressWarnings("unused")
public static class SETTINGS {
private static final String prefix = "settings.";
@SuppressWarnings("unused")
private static void load() {
if (config.getBoolean(prefix, "debug", false)) {
config.main.getLogger().setLevel(Level.FINE);
} else if (config.getBoolean(prefix, "warnings", true)) {
config.main.getLogger().setLevel(Level.WARNING);
} else {
config.main.getLogger().setLevel(Level.INFO);
}
log.debug("Debug logging is enabled");
log.warn("Warning logging is enabled");
log.info("Info logging is enabled");
}
}
}

View File

@ -0,0 +1,48 @@
package com.alttd.ctf.config;
import com.alttd.ctf.Main;
import com.alttd.ctf.game.GamePhase;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.HashMap;
@Slf4j
public class GameConfig extends AbstractConfig{
static GameConfig config;
GameConfig(Main main) {
super(main, "game-config.yml");
}
public static void reload(Main main) {
log.info("Reloading config");
config = new GameConfig(main);
config.readConfig(GameConfig.class, null);
}
@SuppressWarnings("unused")
public static class PHASES {
private static final String prefix = "phases.";
private static final HashMap<GamePhase, Duration> GAME_PHASE_DURATION = new HashMap<>();
public static HashMap<GamePhase, Duration> getGAME_PHASE_DURATION() {
return new HashMap<>(GAME_PHASE_DURATION);
}
@SuppressWarnings("unused")
private static void load() {
GAME_PHASE_DURATION.clear();
for (GamePhase phase : GamePhase.values()) {
if (phase.equals(GamePhase.COMBAT))
continue;
int phaseDurationInMinutes = config.getInt(prefix, phase.name().toLowerCase(), 5);
GAME_PHASE_DURATION.put(phase, Duration.ofMinutes(phaseDurationInMinutes));
log.debug("Set {} phase duration to {} minutes", phase.name(), phaseDurationInMinutes);
}
}
}
}

View File

@ -0,0 +1,52 @@
package com.alttd.ctf.config;
import com.alttd.ctf.Main;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Messages extends AbstractConfig {
static Messages config;
Messages(Main main) {
super(main, "config.yml");
}
public static void reload(Main main) {
log.info("Reloading messages config");
config = new Messages(main);
config.readConfig(Messages.class, null);
}
public static class HELP {
private static final String prefix = "help.";
public static String HELP_MESSAGE_WRAPPER = "<gold>Main help:\n<commands></gold>";
public static String HELP_MESSAGE = "<green>Show this menu: <gold>/ctf help</gold></green>";
public static String CHANGE_TEAM = "<green>Change a players team: <gold>/ctf changeteam <player> <team></gold></green>";
public static String START = "<green>Start a new game: <gold>/ctf start <time_in_minutes></gold></green>";
@SuppressWarnings("unused")
private static void load() {
HELP_MESSAGE_WRAPPER = config.getString(prefix, "help-wrapper", HELP_MESSAGE_WRAPPER);
HELP_MESSAGE = config.getString(prefix, "help", HELP_MESSAGE);
CHANGE_TEAM = config.getString(prefix, "change-team", CHANGE_TEAM);
START = config.getString(prefix, "start", START);
}
}
public static class GENERIC {
private static final String prefix = "generic.";
public static String NO_PERMISSION = "<red><hover:show_text:'<red><permission></red>'>You don't have permission for this command</hover></red>";
public static String PLAYER_ONLY = "<red>This command can only be executed as a player</red>";
public static String PLAYER_NOT_FOUND = "<red>Unable to find online player <player></red>";
@SuppressWarnings("unused")
private static void load() {
NO_PERMISSION = config.getString(prefix, "no-permission", NO_PERMISSION);
PLAYER_ONLY = config.getString(prefix, "player-only", PLAYER_ONLY);
PLAYER_NOT_FOUND = config.getString(prefix, "player-only", PLAYER_NOT_FOUND);
}
}
}

View File

@ -0,0 +1,78 @@
package com.alttd.ctf.events;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import java.util.Optional;
@Slf4j
public class OnSnowballHit implements Listener {
private final GameManager gameManager;
public OnSnowballHit(GameManager gameManager) {
this.gameManager = gameManager;
}
@FunctionalInterface
private interface SnowballHitConsumer {
void apply(Player hitPlayer, Player shooter, TeamPlayer shooterTeamPlayer);
}
@EventHandler
public void onSnowballHit(EntityDamageByEntityEvent event) {
handle(event, (hitPlayer, shooter, shooterTeamPlayer) -> {
GameClass shooterClass = shooterTeamPlayer.getGameClass();
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
double newHealth = hitPlayer.getHealth() - shooterClass.getDamage();
hitPlayer.setHealth(Math.max(newHealth, 0));
log.debug("{} health was set to {} because of a snowball thrown by {}",
hitPlayer.getName(), Math.max(newHealth, 0), shooter.getName());
});
}
private void handle(EntityDamageByEntityEvent event, SnowballHitConsumer consumer) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) {
log.debug("No game is running but player was hit by snowball");
return;
}
GamePhase gamePhase = optionalGamePhase.get();
if (!gamePhase.equals(GamePhase.COMBAT)) {
log.debug("Not in combat phase but player was hit by snowball");
return;
}
if (!(event.getEntity() instanceof Player hitPlayer)) {
log.debug("An entity other than a player was hit by a snowball");
return;
}
if (!(event.getDamager() instanceof org.bukkit.entity.Snowball snowball)) {
log.debug("The player was hit by something other than a snowball");
return;
}
if (!(snowball.getShooter() instanceof Player shooter)) {
log.debug("The shooter that hit a player with a snowball was not a player");
return;
}
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter.getUniqueId());
if (teamPlayer.isEmpty()) {
log.debug("The shooter that hit a player with a snowball was not a team player");
return;
}
consumer.apply(hitPlayer, shooter, teamPlayer.get());
}
}

View File

@ -0,0 +1,77 @@
package com.alttd.ctf.game;
import com.alttd.ctf.game.phases.ClassSelectionPhase;
import com.alttd.ctf.game_class.implementations.Fighter;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamPlayer;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class GameManager {
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private final HashMap<GamePhase, GamePhaseExecutor> phases;
private RunningGame runningGame;
private final HashMap<Integer, Team> teams = new HashMap<>();
public GameManager() {
phases = new HashMap<>();
//TODO initialize this somewhere else (maybe load it from a json file?)
phases.put(GamePhase.CLASS_SELECTION, new ClassSelectionPhase(this, new Fighter(List.of(Material.LEATHER_CHESTPLATE), List.of(), new ItemStack(Material.STONE), 15, 3, 5)));
}
public Optional<GamePhase> getGamePhase() {
return runningGame == null ? Optional.empty() : Optional.of(runningGame.getCurrentPhase());
}
public void registerPlayer(Team team, Player player) {
unregisterPlayer(player);
teams.get(team.getId()).addPlayer(player.getUniqueId());
}
public void unregisterPlayer(Player player) {
teams.values().forEach(team -> team.removePlayer(player.getUniqueId()));
}
public Collection<Team> getTeams() {
return teams.values();
}
public Optional<Team> getTeam(@NotNull UUID uuid) {
return getTeams().stream().filter(filterTeam -> filterTeam.getPlayer(uuid).isPresent()).findAny();
}
public Optional<TeamPlayer> getTeamPlayer(@NotNull UUID uuid) {
return getTeams().stream()
.map(team -> team.getPlayer(uuid))
.filter(Optional::isPresent)
.findFirst()
.orElseGet(Optional::empty);
}
public void start(Duration duration) {
if (runningGame != null) {
runningGame.end();
executorService.shutdown();
executorService = Executors.newSingleThreadScheduledExecutor();
}
runningGame = new RunningGame(this, duration);
executorService.scheduleAtFixedRate(runningGame, 0, 1, TimeUnit.SECONDS);
}
protected GamePhaseExecutor getPhaseExecutor(GamePhase gamePhase) {
GamePhaseExecutor gamePhaseExecutor = phases.get(gamePhase);
if (gamePhaseExecutor == null) {
throw new IllegalArgumentException("No phase executor found for phase " + gamePhase);
}
return gamePhaseExecutor;
}
}

View File

@ -0,0 +1,19 @@
package com.alttd.ctf.game;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
@Getter
public enum GamePhase {
CLASS_SELECTION(MiniMessage.miniMessage().deserialize("<green>Class selection phase</green>")),
GATHERING(MiniMessage.miniMessage().deserialize("<green>Gathering phase</green>")),
COMBAT(MiniMessage.miniMessage().deserialize("<green>Combat phase</green>")),
ENDED(MiniMessage.miniMessage().deserialize("<green>Game end phase</green>"));
private final Component displayName;
GamePhase(Component displayName) {
this.displayName = displayName;
}
}

View File

@ -0,0 +1,8 @@
package com.alttd.ctf.game;
public interface GamePhaseExecutor {
void start();
void end();
}

View File

@ -0,0 +1,71 @@
package com.alttd.ctf.game;
import com.alttd.ctf.config.GameConfig;
import lombok.Getter;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
public class RunningGame implements Runnable {
private final HashMap<GamePhase, Duration> phaseDurations = GameConfig.PHASES.getGAME_PHASE_DURATION();
private final GameManager gameManager;
@Getter
private GamePhase currentPhase = GamePhase.values()[0];
private Instant phaseStartTime = null;
public RunningGame(GameManager gameManager, Duration gameDuration) {
this.gameManager = gameManager;
phaseDurations.put(GamePhase.COMBAT, gameDuration);
}
@Override
public void run() {
if (phaseStartTime == null) {
phaseStartTime = Instant.now();
GamePhase nextPhase = GamePhase.values()[currentPhase.ordinal() + 1];
nextPhaseActions(currentPhase, nextPhase);
}
if (Duration.between(phaseStartTime, Instant.now()).compareTo(phaseDurations.get(currentPhase)) >= 0) {
GamePhase nextPhase = GamePhase.values()[currentPhase.ordinal() + 1];
nextPhaseActions(currentPhase, nextPhase);
currentPhase = nextPhase;
phaseStartTime = Instant.now();
}
}
private void nextPhaseActions(@NotNull GamePhase phase, @Nullable GamePhase nextPhase) {
//TODO class/functions for each phase that they run at the start of that phase
// These should notify the player of what the phase is and that it started as well
gameManager.getPhaseExecutor(phase).start();
if (nextPhase != null) {
broadcastNextPhaseStartTime(phase, nextPhase);
}
}
private void broadcastNextPhaseStartTime(GamePhase currentPhase, GamePhase nextPhase) {//TODO check how this works/what it should do
//Remaining time for this phase
Duration duration = phaseDurations.get(currentPhase).minus(Duration.between(phaseStartTime, Instant.now()));
if (duration.toMinutes() > 5 && duration.toMinutes() % 15 == 0) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(
"<green>The <phase> will start in <bold><red>" + duration.toMinutes() + "</red> minutes</bold></green>",
Placeholder.component("phase", nextPhase.getDisplayName())
));
} else if (duration.toMinutes() < 5) {
//TODO start minute countdown -> second countdown
}
}
public void end() {
//TODO say the phase ended early?
currentPhase = GamePhase.ENDED;
nextPhaseActions(currentPhase, null);
}
}

View File

@ -0,0 +1,54 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhaseExecutor;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.Team;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public class ClassSelectionPhase implements GamePhaseExecutor {
private final GameManager gameManager;
private final GameClass defaultClass;
public ClassSelectionPhase(@NotNull GameManager gameManager, @NotNull GameClass defaultClass) {
this.gameManager = gameManager;
this.defaultClass = defaultClass;
}
@Override
public void start() {
teleportPlayersToStartingZone();
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Select your class</green>"));
//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
}
private void teleportPlayersToStartingZone() {
Bukkit.getOnlinePlayers().forEach(player -> {
Optional<Team> team = gameManager.getTeam(player.getUniqueId());
if (team.isEmpty()) {
return;
}
Location spawnLocation = team.get().getSpawnLocation();
player.teleportAsync(spawnLocation);
});
}
@Override
public void end() {
gameManager.getTeams().forEach(team -> team.getPlayers().forEach(player -> {
if (player.getGameClass() != null) {
return;
}
defaultClass.apply(player);
}));
}
}

View File

@ -0,0 +1,81 @@
package com.alttd.ctf.game_class;
import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import java.util.List;
@Slf4j
public abstract class GameClass {
private final List<Material> armor;
private final List<ItemStack> tools;
private final ItemStack displayItem;
private final double health;
//Delay in ticks between throws (prevents auto clickers)
@Getter
private final int throwTickSpeed;
//Damage done when hitting a player with a snowball
@Getter
private final int damage;
protected GameClass(List<Material> armor, List<ItemStack> tools, ItemStack displayItem, double health, int throwTickSpeed, int damage) {
this.armor = armor;
this.tools = tools;
this.displayItem = displayItem;
this.health = health;
this.throwTickSpeed = throwTickSpeed;
this.damage = damage;
}
public void apply(TeamPlayer teamPlayer) {
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());
return;
}
AttributeInstance maxHealthAttribute = player.getAttribute(Attribute.GENERIC_MAX_HEALTH);
if (maxHealthAttribute == null) {
log.error("Player does not have max health attribute");
return;
}
//Always reset the player inventory since other classes might have had them get items
player.getInventory().clear();
player.getInventory().setContents(tools.toArray(ItemStack[]::new));
TeamColor color = teamPlayer.getTeam().getColor();
setArmor(player, color.r(), color.g(), color.b());
player.updateInventory();
maxHealthAttribute.setBaseValue(health);
teamPlayer.setGameClass(this);
}
private void setArmor(Player player, int r, int g, int b) {
player.getInventory().setArmorContents(armor.stream().map(material -> {
ItemStack itemStack = new ItemStack(material);
ItemMeta itemMeta = itemStack.getItemMeta();
if (itemMeta instanceof LeatherArmorMeta leatherArmorMeta) {
leatherArmorMeta.setColor(Color.fromBGR(r, g, b));
itemStack.setItemMeta(leatherArmorMeta);
}
return new ItemStack(material);
}).toArray(ItemStack[]::new));
}
ItemStack getDisplayItem() {
return displayItem;
}
}

View File

@ -0,0 +1,17 @@
package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.List;
@Slf4j
public class Fighter extends GameClass {
public Fighter(List<Material> armor, List<ItemStack> tools, ItemStack displayItem, double health, int tickThrowSpeed, int damage) {
super(armor, tools, displayItem, health, tickThrowSpeed, damage);
}
}

View File

@ -0,0 +1,74 @@
package com.alttd.ctf.team;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@Slf4j
public class Team {
private final HashMap<UUID, TeamPlayer> players = new HashMap<>();
@Getter
private Component name;
@Getter
private final int id;
@Getter
private Location spawnLocation;
@Getter
private Location worldBorderCenter; //TODO https://github.com/yannicklamprecht/WorldBorderAPI/blob/main/how-to-use.md
//TODO store team color to be used for kits and chat colors (in rgb?)
@Getter
private TeamColor color;
public Team(int id) {
this.id = id;
reloadTeamData();
}
private void reloadTeamData() {
this.color = new TeamColor(255, 0, 0, "#FF0000");
this.name = MiniMessage.miniMessage().deserialize(String.format("<color:#%s>Test Team</color>", color.hex()));
this.spawnLocation = Bukkit.getWorld("world").getSpawnLocation();
//TODO load team data from config
}
public void addPlayer(UUID uuid) {
players.put(uuid, new TeamPlayer(uuid, this));
log.debug("Added player with uuid {} to team with id {}", uuid, id);
}
public Optional<TeamPlayer> getPlayer(@NotNull UUID uuid) {
if (!players.containsKey(uuid))
return Optional.empty();
return Optional.of(players.get(uuid));
}
public Collection<TeamPlayer> getPlayers() {
return players.values();
}
public void removePlayer(@NotNull UUID uuid) {
TeamPlayer remove = players.remove(uuid);
if (remove != null) {
log.debug("Removed player with uuid {} from team with id {}", uuid, id);
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Team other = (Team) obj;
return Objects.equals(id, other.getId());
}
}

View File

@ -0,0 +1,4 @@
package com.alttd.ctf.team;
public record TeamColor(int r, int g, int b, String hex) {
}

View File

@ -0,0 +1,39 @@
package com.alttd.ctf.team;
import com.alttd.ctf.game_class.GameClass;
import lombok.Getter;
import lombok.Setter;
import java.util.Objects;
import java.util.UUID;
@Getter
public class TeamPlayer {
private final UUID uuid;
private final Team team;
@Setter
private GameClass gameClass;
protected TeamPlayer(UUID uuid, Team team) {
this.uuid = uuid;
this.team = team;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TeamPlayer other = (TeamPlayer) obj;
return Objects.equals(uuid, other.uuid);
}
@Override
public int hashCode() {
return Objects.hash(uuid);
}
}

View File

@ -0,0 +1,9 @@
name: CTF
version: '${version}'
main: com.alttd.ctf.Main
api-version: '1.21'
commands:
ctf:
description: Base command for capture the flag
permission: ctf.use
usage: /ctf