Compare commits

..
42 Commits
Author SHA1 Message Date
auto 4953cec215 Make game logic adjustments and improve async command handling
Added permission checks for players bypassing game actions and refined block event handling based on game phase. Introduced asynchronous handling for permission commands to improve server performance. Updated constructors and dependencies to streamline game phase management.
2025-03-06 19:47:28 +01:00
auto 92f9e732a1 Remove redundant code and add victory reward commands
Removed unused methods and logging, such as `kickFromVoiceIfNeeded` and unnecessary permission checks. Added a feature to grant temporary permissions to winning players using LuckPerms. Updated the build number to reflect the changes.
2025-03-01 22:40:37 +01:00
auto 95b54d1a03 Prevent same-team players from being knocked back by flags.
Added a check to ensure players on the same team as the flag carrier are not affected by knockback when near the flag. Updated the build number to reflect the new changes.
2025-03-01 20:17:41 +01:00
auto 05a63b9a52 Prevent snowball throw if player hasn't selected a class
Added
2025-03-01 20:04:48 +01:00
auto 137e5a5125 Remove unused log, add null check, and update player teleport.
Removed a redundant logging statement from `OtherGameEvents`. Added a null check for shooter class in `SnowballEvent` to avoid potential issues. Enhanced player handling in `OnPlayerOnlineStatus` by teleporting players to their team’s spawn location if applicable.
2025-03-01 20:03:40 +01:00
auto 3c5842a92c Implement shield mechanics and enhance game end reset logic
Added shield blocking, cooldown, and breaking mechanics with sound and particle effects. Improved game reset functionality to properly remove players from teams at the end. Updated capture material and refactored imports for cleaner structure.
2025-03-01 19:29:46 +01:00
auto f5bb1564ae Increase spawn proximity threshold for class change checks.
Previously, players needed to be within 5 blocks of their spawn to switch classes. This threshold has been increased to 10 blocks, providing a better balance between gameplay flexibility and restrictions.
2025-03-01 18:29:09 +01:00
auto fa8d320da9 Refactor and optimize redundant code across the project
Removed unused imports and variables to streamline the codebase. Consolidated lambda expressions and improved formatting for better readability. Simplified methods by removing unnecessary parameters and redundant logic.
2025-03-01 01:22:13 +01:00
auto 816c429346 Remove leftover debug logging in broadcastNextPhaseStartTime
The debug log statement was unnecessary and likely left in by mistake. This change cleans up the code by removing it, ensuring the method only contains relevant functionality.
2025-03-01 01:17:58 +01:00
auto afbd1a9f09 Todo removed from tank class since the shield is strong enough now 2025-03-01 01:16:39 +01:00
auto 5430af2149 Remove completed TODO comments
Cleaned up obsolete TODO comments related to powdered snow mechanics and mage behavior.
2025-03-01 01:16:07 +01:00
auto ce7297afb8 Refactor flag point update logic for winning team.
Fixed the logic for updating flag point. Ensures correct handling of winning and losing teams by directly iterating through entries. This way points are reduced if a team is no longer "winning".
2025-03-01 01:15:48 +01:00
auto a10607092b Refactor snow bucket creation to return a list of snow buckets.
Updated `getPowderedSnow` to generate a list of 16 individual snow bucket items instead of modifying the amount of a single item. Adjusted `getTools` to handle the list returned by this method, ensuring proper item handling and consistency.
2025-03-01 00:49:01 +01:00
auto fe39cdf5a1 Change Mage class display item to Snowball 2025-03-01 00:46:07 +01:00
auto 6008378258 Fix mage class damage and improve mage snowball throw logic
Updated Mage health regeneration from 3 to 1 and adjusted snowball velocity randomness for better gameplay balance. Fixed comparison in SnowballEvent to use `equals` and corrected parameter assignment in `consumer.apply`. Incremented build number to reflect the changes.
2025-03-01 00:45:07 +01:00
auto 18b211f681 Reduce Mage class default damage from 5 to 3. 2025-03-01 00:26:28 +01:00
auto b5c59cf173 Implement knockback effect for players near captured flag
Added a feature to knock back nearby players when a flag is captured, creating space for the flag bearer.
2025-03-01 00:25:52 +01:00
auto 436b462ffe Removed TODO that was done 2025-03-01 00:09:30 +01:00
auto d11cf25fc8 Add Mage character class with snowball duplication ability
Introduced the Mage class as a playable character along with its creation logic, tools, and armor setup. Mages can duplicate snowballs upon throwing, creating multiple projectiles with varying directions and velocities to enhance gameplay dynamics. Updated relevant systems to accommodate this new class.
2025-03-01 00:05:57 +01:00
auto e8c015790f Fix Trapper using Tank constructor.
Replaced the use of Tank with Trapper in the TrapperCreator class and adjusted associated parameters. Modified Trapper's constructor to be public, ensuring proper accessibility for instantiation.
2025-03-01 00:05:45 +01:00
auto ba8fbe83e0 Remove TODO comment in TrapperCreator class.
The comment about adding temporary invulnerability was removed, it was meant for the tank class.
2025-02-28 23:12:45 +01:00
auto d56e8eaea5 Add Trapper game class to the CTF system
Introduced the new "Trapper" class with unique armor, tools, and abilities to enhance gameplay. The Trapper specializes in utilizing powdered snow traps to hinder the enemy team. This includes creating necessary implementations and updating retrieval logic.
2025-02-28 23:12:13 +01:00
auto 3550d75634 Introduce TeamScoreboard and highest stat tracking functionality.
This commit refactors scoreboard management into a dedicated TeamScoreboard class, improving modularity and readability. It also adds a new command to track and display the highest player stats across all game metrics, enhancing gameplay insights.
2025-02-28 22:21:23 +01:00
auto 14158f73b8 Handle null player check in resetWorldBorder method.
Previously, the method assumed the player object was never null, which could cause potential issues. This update adds a null check to ensure stability and prevent unexpected errors when invoking the resetWorldBorder method.
2025-02-26 22:40:58 +01:00
auto c738f02d17 Add persistent player statistics tracking
Introduced the `PlayerStat` system to persist player statistics across sessions. Enhanced player management by linking `PlayerStat` with teams and saving/updating stats periodically. Refactored related methods to support the new functionality and ensure data integrity.
2025-02-26 22:00:46 +01:00
auto e62d0df9df Refactor potion healing to exclude non-teammates.
Split logic into smaller methods for clarity and maintainability. Added `shouldHeal` to determine team eligibility and `calculateActualHealing` to handle precise healing calculations. Updated intensity handling to exclude non-teammates during healing.
2025-02-23 01:22:52 +01:00
auto eeed5b4c54 Add player statistics tracking and integration.
Introduced a comprehensive system for tracking player stats, including kills, flags captured, damage dealt, and more. Integrated stat recording into related game events and ensured proper handling for both individual and team actions. This enhances gameplay analysis and future feature potential.
2025-02-23 01:14:27 +01:00
auto 8386e773ce Add glowing effect to flag carrier
The flag carrier now receives a glowing effect to improve visibility and make them easier to track during gameplay. This change enhances player experience and adds a strategic element to capturing the flag.
2025-02-23 00:34:43 +01:00
auto e5656f23ce Fix class change restriction and track player death state
Ensure players can change classes when they are dead or near spawn if alive. Added logic to track and update player death state, improving clarity and game mechanics around death handling.
2025-02-23 00:33:30 +01:00
auto 409a1aa596 Make addToScoreboard public and reset team scores.
Changed addToScoreboard visibility to public for external usage, allowing it to be called when a player joins a team. Added a reset for team scores in the Flag class to ensure scores are cleared when resetting the game state.
2025-02-23 00:27:49 +01:00
auto 57f2898451 Update OnPlayerOnlineStatus to assign a class to a joining player
The constructor of OnPlayerOnlineStatus was updated to include WorldBorderApi, and its functionality now integrates FighterCreator for enhanced player handling. This change ensures proper application of a class and world border logic during player teleport and initialization.
2025-02-16 00:13:05 +01:00
auto 2242fea737 Prevent block breaking during active game phases
Added an event handler to cancel block breaking unless the block is snow or the game phase is inactive. This ensures players cannot modify the environment during ongoing games.
2025-02-15 23:44:22 +01:00
auto e04892156c Gracefully handle null or offline players in game phases.
Added checks to ensure players are online and not null before interacting with them during the game phases. This prevents potential null pointer exceptions and improves overall code robustness. Also added a null check for the executorService in the CombatPhase end method.
2025-02-15 23:44:15 +01:00
auto c09c55ed7a Add Discord integration for teams and user linking
Introduced database connectivity and Discord role management via MyBatis and JDA. Enhanced `Team` functionality to support Discord roles and integrated player-Discord linking during server join. Updated Gradle build to include required dependencies.
2025-02-15 22:31:35 +01:00
auto 2c80b2d474 Handle null player in resetFlagCarrier method
Added a null check for the flagCarrier player before clearing active potion effects to prevent potential NullPointerExceptions. This ensures stable behavior when resetting the flag carrier.
2025-02-15 04:18:08 +01:00
auto 4f0a4eff6f Update flag capture flow to notify players visually
Added a title notification for players when a team can capture the flag, improving communication during gameplay.
2025-02-15 04:10:52 +01:00
auto c02bda6ea7 Add slowness effect to flag carriers and clear it on reset
Flag carriers now receive a slowness effect to hinder movement while holding the flag. The effect is cleared when the flag carrier is reset to ensure consistent gameplay behavior. Updated build number to reflect these changes.
2025-02-15 04:07:07 +01:00
auto 400032a94c Switch from UUID to Player & add scoreboard for teams
Replaced UUID-based logic with Player objects across all game systems. Added team scorebaord sfor colors in tab and clarity for players as to who is in the laed
2025-02-15 03:53:35 +01:00
auto 6893e07619 Prevent armor manipulation with InventoryClick and Drag events
Implemented handling for InventoryClick and InventoryDrag events to block interactions with armor slots. This ensures that players cannot equip or remove armor through inventory actions, maintaining consistency with existing item interaction restrictions.
2025-02-15 02:54:02 +01:00
auto f27038f91d Ensure snowball hit events are properly canceled outside combat phase.
Previously, events where players were hit outside the combat phase or by non-snowball entities were not being canceled appropriately. This commit adds `event.setCancelled(true)` to prevent unintended behaviors in these scenarios.
2025-02-15 02:48:20 +01:00
auto 07b700bc32 Shuffle player list before assigning teams
Added logic to randomize the order of players before assigning them to teams. This ensures a fairer distribution when teams are being populated.
2025-02-15 02:47:04 +01:00
auto 7ae91bcf06 Add customizable flag material for Capture the Flag
Introduced a configurable `Material` property for flags to enhance flexibility. Updated relevant classes and methods to support dynamic flag material. Default material is set to `RED_BANNER`, with the option to override via configuration or team settings.
2025-02-15 02:42:27 +01:00
43 changed files with 1355 additions and 161 deletions
+24
View File
@@ -2,6 +2,7 @@ import java.util.Properties
plugins {
id("java")
id("com.gradleup.shadow") version "9.0.0-beta4"
}
group = "com.alttd.ctf"
@@ -33,6 +34,10 @@ dependencies {
// End JSON config dependencies
// WorldBorderAPI
compileOnly("com.github.yannicklamprecht:worldborderapi:1.210.0:dev")
//Database
implementation("org.mybatis:mybatis:3.5.13")
implementation("mysql:mysql-connector-java:8.0.33")
}
tasks.test {
@@ -43,6 +48,21 @@ tasks.jar {
archiveFileName.set("CaptureTheFlag.jar")
}
tasks {
shadowJar {
archiveFileName.set("CaptureTheFlag.jar")
listOf(
"org.apache.ibatis"
).forEach { relocate(it, "${rootProject.group}.lib.$it") }
}
build {
dependsOn(shadowJar)
}
}
val versionPropsFile = file("version.properties")
val versionProps = Properties().apply {
if (versionPropsFile.exists()) {
@@ -70,6 +90,10 @@ tasks.named("build") {
dependsOn(incrementBuildNumber)
}
tasks.named("shadowJar") {
dependsOn(incrementBuildNumber)
}
tasks.withType<Jar> {
manifest {
attributes(
+58 -15
View File
@@ -4,18 +4,17 @@ import com.alttd.ctf.commands.CommandManager;
import com.alttd.ctf.config.Config;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.config.Messages;
import com.alttd.ctf.events.InventoryItemInteractionEvent;
import com.alttd.ctf.events.OnPlayerDeath;
import com.alttd.ctf.events.OnPlayerOnlineStatus;
import com.alttd.ctf.events.SnowballEvent;
import com.alttd.ctf.events.*;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.flag.FlagTryCaptureEvent;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.phases.EndedPhase;
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.stats.PlayerStat;
import com.alttd.ctf.team.Team;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
@@ -29,16 +28,18 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
@Slf4j
public class Main extends JavaPlugin {
private GameManager gameManager = null;
private Flag flag;
@Override
public void onEnable() {
PlayerStat.main = this;
GUIInventory.setMain(this); // sorry
ClassSelectionGUI.setMain(this); // sorry
Package pkg = Main.class.getPackage();
@@ -47,18 +48,13 @@ public class Main extends JavaPlugin {
reloadConfigs();
WorldBorderApi worldBorderApi = worldBorder();
this.gameManager = new GameManager(worldBorderApi);
this.gameManager = new GameManager(this, worldBorderApi);
registerTeams(); //Skipped in reloadConfig if gameManager is not created yet
flag = new Flag(this, gameManager);
loadPlayerStats(); //Skipped in reloadConfig if gameManager is not created yet
Flag flag = new Flag(this, gameManager);
new CommandManager(this, gameManager, flag, worldBorderApi);
//Ensuring immediate respawn is on in all worlds
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");
}
enableImmediateRespawn();
registerEvents(flag, worldBorderApi);
}
@@ -68,6 +64,7 @@ public class Main extends JavaPlugin {
GameConfig.reload(this);
if (gameManager != null) {
registerTeams();
loadPlayerStats();
}
}
@@ -83,13 +80,24 @@ public class Main extends JavaPlugin {
return worldBorderApiRegisteredServiceProvider.getProvider();
}
private void enableImmediateRespawn() {
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");
}
}
private void registerEvents(Flag flag, WorldBorderApi worldBorderApi) {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new SnowballEvent(gameManager), this);
pluginManager.registerEvents(new OtherGameEvents(gameManager), this);
pluginManager.registerEvents(new FlagTryCaptureEvent(flag), this);
pluginManager.registerEvents(new OnPlayerDeath(gameManager, worldBorderApi, this, flag), this);
pluginManager.registerEvents(new InventoryItemInteractionEvent(), this);
pluginManager.registerEvents(new OnPlayerOnlineStatus(gameManager, flag), this);
pluginManager.registerEvents(new OnPlayerOnlineStatus(gameManager, flag, worldBorderApi), this);
pluginManager.registerEvents(new GUIListener(), this);
}
@@ -119,4 +127,39 @@ public class Main extends JavaPlugin {
teams.forEach(gameManager::registerTeam);
}
private void loadPlayerStats() {
JsonConfigManager<PlayerStat> config = new JsonConfigManager<>(JacksonConfig.configureMapper());
List<PlayerStat> playerStats;
try {
File playerStatsDirectory = new File(getDataFolder(), "player_stats");
if (!playerStatsDirectory.exists() && !playerStatsDirectory.mkdirs()) {
log.error("Unable to make playerStats directory at {} shutting down plugin", playerStatsDirectory.getAbsolutePath());
}
playerStats = config.loadConfigs(PlayerStat.class, playerStatsDirectory);
} catch (IOException e) {
log.error("Unable to load teams, shutting down plugin", e);
getServer().getPluginManager().disablePlugin(this);
return;
}
gameManager.setPlayerStats(playerStats);
final JsonConfigManager<PlayerStat> jsonConfigManager = new JsonConfigManager<>(JacksonConfig.configureMapper());
Runnable runnable = () -> {
log.info("Saving player stats");
File playerStatsDirectory = new File(getDataFolder(), "player_stats");
gameManager.getPlayerStats().stream().filter(PlayerStat::isTouched).forEach(playerStat -> {
try {
jsonConfigManager.saveConfig(playerStat, playerStatsDirectory, playerStat.getUuid().toString());
playerStat.setUnTouched();
} catch (IOException e) {
log.error("Failed to save player stats for [{}].", playerStat.getUuid(), e);
}
});
};
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(runnable, 0, 1, java.util.concurrent.TimeUnit.MINUTES);
}
}
@@ -38,6 +38,7 @@ public class CommandManager implements CommandExecutor, TabExecutor {
new SkipPhase(gameManager),
new Start(gameManager, flag),
new CreateTeam(main, gameManager),
new HighestStat(gameManager),
new SelectClass(gameManager, worldBorderApi),
new Reload(main)
);
@@ -58,7 +58,7 @@ public class ChangeTeam extends SubCommand {
}
private void changeTeam(CommandSender commandSender, Player player, Team team) {
Optional<Team> optionalOldTeam = gameManager.getTeam(player.getUniqueId());
Optional<Team> optionalOldTeam = gameManager.getTeam(player);
if (optionalOldTeam.isPresent()) {
moveBetweenTeams(commandSender, player, team, optionalOldTeam.get());
return;
@@ -12,6 +12,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -75,8 +76,8 @@ 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(), player.getLocation(), teamColor);
highestId + 1, player.getLocation(), player.getLocation(), player.getLocation(), teamColor,
Material.RED_BANNER, "§c", -1L);
return consumer.apply(team);
}
@@ -0,0 +1,79 @@
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.stats.PlayerStat;
import com.alttd.ctf.stats.Stat;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.*;
@Slf4j
public class HighestStat extends SubCommand {
private final GameManager gameManager;
private final MiniMessage miniMessage = MiniMessage.miniMessage();
public HighestStat(GameManager gameManager) {
this.gameManager = gameManager;
}
private record HighestStatValue(Stat stat, UUID uuid, double value) {
}
@Override
public int onCommand(CommandSender commandSender, String[] args) {
ArrayList<HighestStatValue> highestStatValues = new ArrayList<>();
Collection<PlayerStat> playerStats = gameManager.getPlayerStats();
for (Stat stat : Stat.values()) {
Optional<PlayerStat> max = playerStats.stream().max(Comparator.comparingDouble(playerStat -> playerStat.getStat(stat)));
if (max.isEmpty()) {
log.warn("No max stat found for {}", stat.toString());
continue;
}
PlayerStat playerStat = max.get();
highestStatValues.add(new HighestStatValue(stat, playerStat.getUuid(), playerStat.getStat(stat)));
}
Component message = highestStatValues.stream().map(highestStatValue -> {
TextComponent messageBuilder = Component.empty();
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(highestStatValue.uuid);
Player player = offlinePlayer.getPlayer();
if (!offlinePlayer.isOnline() || player == null) {
messageBuilder = messageBuilder.append(miniMessage.deserialize(offlinePlayer.getName() == null ? highestStatValue.uuid.toString() : offlinePlayer.getName()));
} else {
messageBuilder = messageBuilder.append(player.name());
}
messageBuilder = messageBuilder.append(Component.text(": "));
messageBuilder = messageBuilder.append(Component.text(highestStatValue.stat.toString()));
messageBuilder = messageBuilder.append(Component.text(": "));
messageBuilder = messageBuilder.append(Component.text(highestStatValue.value()));
return messageBuilder;
}).reduce(Component.empty(), (a, b) -> a.append(Component.newline()).append(b));
commandSender.sendMessage(message);
return 0;
}
@Override
public String getName() {
return "higheststat";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return List.of();
}
@Override
public String getHelpMessage() {
return Messages.HELP.HIGHEST_STAT;
}
}
@@ -40,13 +40,15 @@ public class SelectClass extends SubCommand {
return 0;
}
GamePhase gamePhase = optionalGamePhase.get();
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty()) {
commandSender.sendRichMessage("<red>You have to be in a CTF team to select a class.</red>");
return 0;
}
TeamPlayer teamPlayer = optionalTeamPlayer.get();
if (!gamePhase.equals(GamePhase.CLASS_SELECTION) && teamPlayer.getTeam().getSpawnLocation().distance(player.getLocation()) > 5) {
if (!teamPlayer.isDead()
&& !gamePhase.equals(GamePhase.CLASS_SELECTION)
&& teamPlayer.getTeam().getSpawnLocation().distance(player.getLocation()) > 10) {
commandSender.sendRichMessage("<red>You have to be near your spawn to change classes.</red>");
return 0;
}
@@ -3,16 +3,10 @@ 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 lombok.AllArgsConstructor;
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;
@AllArgsConstructor
public class SkipPhase extends SubCommand {
@@ -102,6 +102,12 @@ abstract class AbstractConfig {
return yaml.getInt(path, yaml.getInt(path));
}
public long getLong(String prefix, String path, long def) {
path = prefix + path;
yaml.addDefault(path, def);
return yaml.getLong(path, yaml.getLong(path));
}
double getDouble(String prefix, String path, double def) {
path = prefix + path;
yaml.addDefault(path, def);
@@ -41,4 +41,36 @@ public class Config extends AbstractConfig{
}
}
@SuppressWarnings("unused")
public static class DISCORD {
private static final String prefix = "discord.";
public static long PLAYER_SERVER_ID = 0L;
@SuppressWarnings("unused")
private static void load() {
PLAYER_SERVER_ID = config.getLong(prefix, "player-server-id", PLAYER_SERVER_ID);
}
}
@SuppressWarnings("unused")
public static class DISCORD_DATABASE {
private static final String prefix = "discord-database.";
public static String HOST = "localhost";
public static int PORT = 3306;
public static String DATABASE = "discordLink";
public static String USERNAME = "username";
public static String PASSWORD = "password";
@SuppressWarnings("unused")
private static void load() {
HOST = config.getString(prefix, "host", HOST);
DATABASE = config.getString(prefix, "database", DATABASE);
PORT = config.getInt(prefix, "port", PORT);
USERNAME = config.getString(prefix, "username", USERNAME);
PASSWORD = config.getString(prefix, "password", PASSWORD);
}
}
}
@@ -3,6 +3,8 @@ package com.alttd.ctf.config;
import com.alttd.ctf.Main;
import com.alttd.ctf.game.GamePhase;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.HashMap;
@@ -88,6 +90,7 @@ public class GameConfig extends AbstractConfig {
public static double CAPTURE_RADIUS = 5;
public static int CAPTURE_SCORE = 50;
public static double TURN_IN_RADIUS = 3;
public static @NotNull Material MATERIAL = Material.BLACK_BANNER;
@SuppressWarnings("unused")
private static void load() {
@@ -98,6 +101,7 @@ public class GameConfig extends AbstractConfig {
CAPTURE_RADIUS = config.getDouble(prefix, "capture-radius", CAPTURE_RADIUS);
CAPTURE_SCORE = config.getInt(prefix, "capture-score", CAPTURE_SCORE);
TURN_IN_RADIUS = config.getDouble(prefix, "turn-in-radius", TURN_IN_RADIUS);
MATERIAL = Material.valueOf(config.getString(prefix, "material", MATERIAL.toString()));
}
}
@@ -28,6 +28,7 @@ public class Messages extends AbstractConfig {
public static String START = "<green>Start a new game: <gold>/ctf start <time_in_minutes></gold></green>";
public static String SELECT_CLASS = "<green>Open class selection: <gold>/ctf selectclass</gold></green>";
public static String SKIP_PHASE = "<green>Skip the current phase: <gold>/ctf skipphase</gold></green>";
public static String HIGHEST_STAT = "<green>Display the highest stat and the player: <gold>/ctf higheststat</gold></green>";
@SuppressWarnings("unused")
private static void load() {
@@ -39,6 +40,7 @@ public class Messages extends AbstractConfig {
START = config.getString(prefix, "start", START);
SELECT_CLASS = config.getString(prefix, "select-class", SELECT_CLASS);
SKIP_PHASE = config.getString(prefix, "skip-phase", SKIP_PHASE);
HIGHEST_STAT = config.getString(prefix, "highest-stat", HIGHEST_STAT);
}
}
@@ -0,0 +1,49 @@
package com.alttd.ctf.database;
import com.alttd.ctf.config.Config;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import java.util.function.Consumer;
@Slf4j
public class Connections {
private static SqlSessionFactory discordSqlSessionFactory = null;
public static void runDiscordQuery(Consumer<SqlSession> consumer) {
new Thread(() -> {
if (discordSqlSessionFactory == null) {
discordSqlSessionFactory = createDiscordSqlSessionFactory();
}
try (SqlSession session = discordSqlSessionFactory.openSession()) {
consumer.accept(session);
} catch (Exception e) {
log.error("Failed to run discord query", e);
}
}).start();
}
private static SqlSessionFactory createDiscordSqlSessionFactory() {
PooledDataSource dataSource = new PooledDataSource();
dataSource.setDriver("com.mysql.cj.jdbc.Driver");
dataSource.setUrl(String.format("jdbc:mysql://%s:%d/%s", Config.DISCORD_DATABASE.HOST,
Config.DISCORD_DATABASE.PORT, Config.DISCORD_DATABASE.DATABASE));
dataSource.setUsername(Config.DISCORD_DATABASE.USERNAME);
dataSource.setPassword(Config.DISCORD_DATABASE.PASSWORD);
Environment environment = new Environment("production", new JdbcTransactionFactory(), dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(DiscordUserMapper.class);
return new SqlSessionFactoryBuilder().build(configuration);
}
}
@@ -0,0 +1,9 @@
package com.alttd.ctf.database;
import org.apache.ibatis.annotations.Select;
public interface DiscordUserMapper {
@Select("SELECT discord_id FROM linked_accounts WHERE player_uuid = #{uuid}")
Long getDiscordId(String uuid);
}
@@ -3,6 +3,9 @@ package com.alttd.ctf.events;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerAttemptPickupItemEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
@@ -26,4 +29,20 @@ public class InventoryItemInteractionEvent implements Listener {
event.setCancelled(true);
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (event.getSlotType() != InventoryType.SlotType.ARMOR) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onInventoryDrag(InventoryDragEvent event) {
if (event.getRawSlots().stream().noneMatch(slot -> slot >= 5 && slot <= 8)) { // Slot numbers 5-8 for armor
return;
}
event.setCancelled(true);
}
}
@@ -5,12 +5,14 @@ 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.stats.Stat;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.damage.DamageEffect;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -44,7 +46,18 @@ public class OnPlayerDeath implements Listener {
Player player = event.getPlayer();
player.getInventory().clear();
player.updateInventory();
gameManager.getTeamPlayer(player)
.ifPresent(TeamPlayer::setDead);
flag.handleCarrierDeathOrDisconnect(player);
try {
if (event.getDamageSource().getDamageType().getDamageEffect().equals(DamageEffect.FREEZING)) {
gameManager.getTeamPlayer(player)
.ifPresent(teamPlayer -> teamPlayer.increaseStat(Stat.DEATHS_IN_POWDERED_SNOW));
}
} catch (Exception e) {
log.warn("Failed to check for death cause due to exception", e);
}
}
@EventHandler
@@ -55,7 +68,7 @@ public class OnPlayerDeath implements Listener {
log.warn("Player {} died while the game wasn't running", player.getName());
return;
}
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty()) {
return;
}
@@ -1,11 +1,16 @@
package com.alttd.ctf.events;
import com.alttd.ctf.database.Connections;
import com.alttd.ctf.database.DiscordUserMapper;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
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 lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.exceptions.PersistenceException;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Player;
@@ -23,15 +28,53 @@ public class OnPlayerOnlineStatus implements Listener {
private final GameManager gameManager;
private final Flag flag;
private final WorldBorderApi worldBorderApi;
public OnPlayerOnlineStatus(GameManager gameManager, Flag flag) {
public OnPlayerOnlineStatus(GameManager gameManager, Flag flag, WorldBorderApi worldBorderApi) {
this.gameManager = gameManager;
this.flag = flag;
this.worldBorderApi = worldBorderApi;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
resetPlayer(player);
if (!player.hasPermission("ctf.bypass")) {
handleRunningGame(player);
}
handleDiscordLink(player);
gameManager.getTeamPlayer(player).ifPresent(teamPlayer ->
player.teleportAsync(teamPlayer.getTeam().getSpawnLocation()));
}
private void handleRunningGame(Player player) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) {
return;
}
GamePhase gamePhase = optionalGamePhase.get();
if (gamePhase.equals(GamePhase.ENDED)) {
return;
}
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
TeamPlayer teamPlayer;
if (optionalTeamPlayer.isEmpty()) {
Optional<Team> min = gameManager.getTeams().stream().min(Comparator.comparingInt(team -> team.getPlayers().size()));
if (min.isEmpty()) {
log.error("No team found when attempting to add freshly joined player to a team");
return;
}
teamPlayer = gameManager.registerPlayer(min.get(), player);
} else {
teamPlayer = optionalTeamPlayer.get();
teamPlayer.getTeam().addToScoreboard(player);
}
FighterCreator.createFighter(teamPlayer.getTeam().getColor())
.apply(teamPlayer, worldBorderApi, gamePhase, true);
}
private void resetPlayer(Player player) {
AttributeInstance maxHealthAttribute = player.getAttribute(Attribute.GENERIC_MAX_HEALTH);
if (maxHealthAttribute == null) {
log.error("Player does not have max health attribute");
@@ -43,32 +86,29 @@ public class OnPlayerOnlineStatus implements Listener {
flag.addPlayer(player);
player.getInventory().clear();
player.updateInventory();
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) {
}
private void handleDiscordLink(Player player) {
Connections.runDiscordQuery(sqlSession -> {
try {
DiscordUserMapper mapper = sqlSession.getMapper(DiscordUserMapper.class);
Long discordId = mapper.getDiscordId(player.getUniqueId().toString());
if (discordId == null) {
log.info("Player {} is not linked", player.getName());
return;
}
GamePhase gamePhase = optionalGamePhase.get();
if (gamePhase.equals(GamePhase.ENDED)) {
return;
log.info("Set discord id to {} for {}", discordId, player.getName());
player.setDiscordId(discordId);
} catch (PersistenceException e) {
log.error("Failed to set discord id for {}", player.getName(), e);
}
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
TeamPlayer teamPlayer;
if (optionalTeamPlayer.isEmpty()) {
Optional<Team> min = gameManager.getTeams().stream().min(Comparator.comparingInt(team -> team.getPlayers().size()));
if (min.isEmpty()) {
log.error("No team found when attempting to add freshly joined player to a team");
return;
}
teamPlayer = min.get().addPlayer(player.getUniqueId());
} else {
teamPlayer = optionalTeamPlayer.get();
}
player.teleportAsync(teamPlayer.getTeam().getSpawnLocation());
});
}
@EventHandler
public void onPlayerJoin(@NotNull PlayerQuitEvent event) {
flag.handleCarrierDeathOrDisconnect(event.getPlayer());
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
Player player = event.getPlayer();
flag.handleCarrierDeathOrDisconnect(player);
}
}
@@ -0,0 +1,112 @@
package com.alttd.ctf.events;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.stats.Stat;
import com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Tag;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Player;
import org.bukkit.entity.ThrownPotion;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.PotionSplashEvent;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.util.Optional;
@Slf4j
public class OtherGameEvents implements Listener {
private final GameManager gameManager;
public OtherGameEvents(GameManager gameManager) {
this.gameManager = gameManager;
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Optional<GamePhase> gamePhase = gameManager.getGamePhase();
if (gamePhase.isEmpty() || gamePhase.get() == GamePhase.ENDED) {
return;
}
if (!Tag.SNOW.isTagged(event.getBlock().getType())) {
event.setCancelled(true);
return;
}
gameManager.getTeamPlayer(event.getPlayer())
.ifPresent(teamPlayer -> teamPlayer.increaseStat(Stat.SNOW_MINED));
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
Optional<GamePhase> gamePhase = gameManager.getGamePhase();
if (gamePhase.isEmpty() || gamePhase.get() == GamePhase.ENDED) {
return;
}
if (!Tag.SNOW.isTagged(event.getBlock().getType())) {
event.setCancelled(true);
log.warn("Player {} placed a block that wasn't snow: {}",
event.getPlayer().getName(), event.getBlock().getType());
return;
}
gameManager.getTeamPlayer(event.getPlayer())
.ifPresent(teamPlayer -> teamPlayer.increaseStat(Stat.BLOCKS_PLACED));
}
@EventHandler
public void onPotionSplash(PotionSplashEvent event) {
ThrownPotion thrownPotion = event.getPotion();
if (!(thrownPotion.getShooter() instanceof Player player))
return;
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty())
return;
TeamPlayer teamPlayer = optionalTeamPlayer.get();
event.getAffectedEntities().stream()
.filter(livingEntity -> livingEntity instanceof Player)
.map(livingEntity -> (Player) livingEntity)
.forEach(target -> {
if (shouldHeal(teamPlayer, target)) {
return;
}
event.setIntensity(target, 0);
});
double totalHealing = thrownPotion.getEffects().stream()
.filter(effect -> effect.getType() == PotionEffectType.INSTANT_HEALTH)
.flatMapToDouble(effect -> event.getAffectedEntities().stream()
.filter(livingEntity -> livingEntity instanceof Player)
.map(livingEntity -> (Player) livingEntity)
.mapToDouble(target -> calculateActualHealing(target, effect, event.getIntensity(target))))
.sum();
teamPlayer.increaseStat(Stat.DAMAGE_HEALED, totalHealing);
}
private boolean shouldHeal(TeamPlayer healer, Player target) {
Optional<TeamPlayer> optionalTeamTarget = gameManager.getTeamPlayer(target);
return optionalTeamTarget.isPresent() && healer.getTeam() == optionalTeamTarget.get().getTeam();
}
private double calculateActualHealing(Player target, PotionEffect effect, double intensity) {
AttributeInstance playerMaxHealth = target.getAttribute(Attribute.GENERIC_MAX_HEALTH);
if (playerMaxHealth == null) {
return 0;
}
double missingHealth = playerMaxHealth.getValue() - target.getHealth();
//Only counts healing on teammates since intensity was set to 0 for non teammates
double potentialHealing = (effect.getAmplifier() + 1) * intensity;
return Math.min(potentialHealing, missingHealth);
}
}
@@ -3,10 +3,14 @@ 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.game_class.implementations.Mage;
import com.alttd.ctf.stats.Stat;
import com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Snowball;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -16,7 +20,11 @@ import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Optional;
import java.util.UUID;
@Slf4j
public class SnowballEvent implements Listener {
@@ -34,37 +42,81 @@ public class SnowballEvent implements Listener {
@FunctionalInterface
private interface SnowballThrownConsumer {
void apply(Player shooter, TeamPlayer shooterTeamPlayer);
void apply(Player shooter, TeamPlayer shooterTeamPlayer, Snowball snowball);
}
@EventHandler
public void onSnowballHit(EntityDamageByEntityEvent event) {
handleSnowballHit(event, (hitPlayer, shooter, shooterTeamPlayer, snowball) -> {
if (blockedAttack(hitPlayer, snowball)) { //Disable damage when it is blocked
if (blockedAttack(hitPlayer, shooter, snowball)) {
playBlockSounds(hitPlayer, shooter);
return;
}
GameClass shooterClass = shooterTeamPlayer.getGameClass();
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
double newHealth = hitPlayer.getHealth() - shooterClass.getDamage();
hitPlayer.setHealth(Math.max(newHealth, 0));
double newHealth = Math.max(hitPlayer.getHealth() - shooterClass.getDamage(), 0);
hitPlayer.setHealth(newHealth);
shooterTeamPlayer.increaseStat(Stat.DAMAGE_DONE, shooterClass.getDamage());
if (newHealth <= 0) {
shooterTeamPlayer.increaseStat(Stat.KILLS);
}
log.debug("{} health was set to {} because of a snowball thrown by {}",
hitPlayer.getName(), Math.max(newHealth, 0), shooter.getName());
applyDamageEffects(hitPlayer, shooter);
});
}
private void playBlockSounds(Player hitPlayer, Player shooter) {
hitPlayer.playSound(hitPlayer.getLocation(), Sound.ITEM_SHIELD_BLOCK, 1.0f, 1.5f);
shooter.playSound(shooter.getLocation(), Sound.ITEM_SHIELD_BLOCK, 1.0f, 1.5f);
}
private void applyDamageEffects(Player hitPlayer, Player shooter) {
hitPlayer.getWorld().playSound(hitPlayer.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 1.0f);
hitPlayer.getWorld().spawnParticle(
Particle.DAMAGE_INDICATOR,
hitPlayer.getLocation().add(0, 1, 0), 10, 0.5, 0.5, 0.5, 0.1
);
shooter.playSound(shooter.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 1.5f);
}
@EventHandler
public void onSnowballThrown(ProjectileLaunchEvent event) {
handleSnowballThrown(event, (shooter, shooterTeamPlayer) -> {
handleSnowballThrown(event, (shooter, shooterTeamPlayer, snowball) -> {
GameClass shooterClass = shooterTeamPlayer.getGameClass();
if (shooterClass == null) {
shooter.sendRichMessage("<red>You appear to not have selected a class, please do so by going to spawn and using /ctf selectclass</red>");
event.setCancelled(true);
return;
}
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
shooterTeamPlayer.increaseStat(Stat.SNOWBALLS_THROWN);
if (shooterClass instanceof Mage mage) {
mage.duplicateSnowBalls(shooter, snowball);
}
});
}
private boolean blockedAttack(@NotNull Player hitPlayer, @NotNull Snowball snowball) {
private final HashMap<UUID, LinkedList<Instant>> lastTankHits = new HashMap<>();
private boolean blockedAttack(@NotNull Player hitPlayer, @NotNull Player shooter, @NotNull Snowball snowball) {
if (!hitPlayer.isBlocking()) {
return false;
}
if (reachedMaxHits(hitPlayer)) {
hitPlayer.setCooldown(Material.SHIELD, 40);
if (hitPlayer.getInventory().getItemInMainHand().getType().equals(Material.SHIELD)) {
hitPlayer.swingMainHand();
} else if (hitPlayer.getInventory().getItemInOffHand().getType().equals(Material.SHIELD)) {
hitPlayer.swingOffHand();
}
applyBlockBrokenEffects(hitPlayer, shooter);
return false;
}
Location playerLocation = hitPlayer.getLocation();
Vector playerFacing = playerLocation.getDirection().normalize();
Location snowballLocation = snowball.getLocation();
@@ -75,6 +127,37 @@ public class SnowballEvent implements Listener {
return !(Math.toDegrees(angle) > 80); //Blocked if the angle was <= 80
}
private void applyBlockBrokenEffects(@NotNull Player hitPlayer, @NotNull Player shooter) {
hitPlayer.getWorld().playSound(hitPlayer.getLocation(), Sound.ITEM_SHIELD_BREAK, 1.0f, 1.0f);
Location shieldLocation = hitPlayer.getLocation()
.add(hitPlayer.getLocation().getDirection().normalize().multiply(0.5))
.add(0, 0.5, 0);
hitPlayer.getWorld().spawnParticle(
Particle.CRIT,
shieldLocation, 10, 0.5, 0.5, 0.5, 0.1
);
shooter.playSound(shooter.getLocation(), Sound.ITEM_SHIELD_BREAK, 1.0f, 1.5f);
}
private boolean reachedMaxHits(@NotNull Player hitPlayer) {
boolean reachedMaxHits = false;
Instant now = Instant.now();
LinkedList<Instant> lastHits = lastTankHits.getOrDefault(hitPlayer.getUniqueId(), new LinkedList<>());
Instant cutoffTime = now.minusMillis(300);
while (!lastHits.isEmpty() && lastHits.peek().isBefore(cutoffTime)) {
lastHits.poll();
}
lastHits.addLast(now);
if (lastHits.size() >= 3) {
lastHits.clear();
reachedMaxHits = true;
}
lastTankHits.put(hitPlayer.getUniqueId(), lastHits);
return reachedMaxHits;
}
private void handleSnowballThrown(ProjectileLaunchEvent event, SnowballThrownConsumer consumer) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) {
@@ -89,24 +172,25 @@ public class SnowballEvent implements Listener {
return;
}
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter.getUniqueId());
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter);
if (teamPlayer.isEmpty()) {
log.debug("The shooter that threw a snowball was not a team player");
return;
}
consumer.apply(shooter, teamPlayer.get());
consumer.apply(shooter, teamPlayer.get(), snowball);
}
private void handleSnowballHit(EntityDamageByEntityEvent event, SnowballHitConsumer consumer) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) {
log.debug("No game is running but player was hit by snowball");
log.debug("No game is running but player was hit");
return;
}
GamePhase gamePhase = optionalGamePhase.get();
if (!gamePhase.equals(GamePhase.COMBAT)) {
log.debug("Not in combat phase but player was hit by snowball");
log.debug("Not in combat phase but player was hit, cancelling event");
event.setCancelled(true);
return;
}
@@ -116,7 +200,8 @@ public class SnowballEvent implements Listener {
}
if (!(event.getDamager() instanceof org.bukkit.entity.Snowball snowball)) {
log.debug("The player was hit by something other than a snowball");
log.debug("The player was hit by something other than a snowball, canceling event");
event.setCancelled(true);
return;
}
@@ -125,22 +210,22 @@ public class SnowballEvent implements Listener {
return;
}
Optional<TeamPlayer> teamPlayerShooter = gameManager.getTeamPlayer(shooter.getUniqueId());
Optional<TeamPlayer> teamPlayerShooter = gameManager.getTeamPlayer(shooter);
if (teamPlayerShooter.isEmpty()) {
log.debug("The shooter that hit a player with a snowball was not a team player");
return;
}
Optional<TeamPlayer> teamPlayerHit = gameManager.getTeamPlayer(hitPlayer.getUniqueId());
Optional<TeamPlayer> teamPlayerHit = gameManager.getTeamPlayer(hitPlayer);
if (teamPlayerHit.isEmpty()) {
log.debug("The shooter that hit a player with a snowball was not a team player");
return;
}
if (teamPlayerHit.get().getTeam().getId() == teamPlayerShooter.get().getTeam().getId()) {
if (teamPlayerHit.get().getTeam().getId().equals(teamPlayerShooter.get().getTeam().getId())) {
log.debug("The shooter hit a member of their own team");
return;
}
consumer.apply(hitPlayer, shooter, teamPlayerHit.get(), snowball);
consumer.apply(hitPlayer, shooter, teamPlayerShooter.get(), snowball);
}
}
+63 -30
View File
@@ -3,6 +3,7 @@ package com.alttd.ctf.flag;
import com.alttd.ctf.Main;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.stats.Stat;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer;
@@ -11,7 +12,6 @@ import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.title.Title;
import org.bukkit.*;
@@ -22,6 +22,9 @@ import org.bukkit.boss.KeyedBossBar;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@@ -33,7 +36,6 @@ public class Flag implements Runnable {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
private final HashMap<Integer, Integer> teamFlagPointCount = new HashMap<>();
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;
@@ -53,6 +55,7 @@ public class Flag implements Runnable {
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);
gameManager.getTeams().forEach(team -> team.setScore(0));
}
private BossBar createBossBar() {
@@ -78,17 +81,33 @@ public class Flag implements Runnable {
if (flagCarrier != null) {
return;
}
//TODO knockback enemies from flag location to create space for person who captured mayb short speed boost and heal?
//TODO add de-buffs and enable buffs for others?
player.getInventory().setItem(EquipmentSlot.HEAD, flagItem);
Team team = teamPlayer.getTeam();
flagLocation.getNearbyPlayers(5).forEach(nearbyPlayer -> {
if (nearbyPlayer.getUniqueId().equals(player.getUniqueId())) {
return;
}
Optional<TeamPlayer> nearByTeamPlayer = gameManager.getTeamPlayer(nearbyPlayer);
if (nearByTeamPlayer.isEmpty()) {
return;
} else if (nearByTeamPlayer.get().getTeam().equals(team)) {
return;
}
Vector direction = nearbyPlayer.getLocation().toVector().subtract(flagLocation.toVector()).normalize();
direction.setY(0.2);
nearbyPlayer.setVelocity(direction.multiply(5));
});
player.getInventory().setItem(EquipmentSlot.HEAD, new ItemStack(teamPlayer.getTeam().getFlagMaterial()));
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(Material.AIR));
flagCarrier = player;
player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, PotionEffect.INFINITE_DURATION, 0, false, false));
player.addPotionEffect(new PotionEffect(PotionEffectType.GLOWING, PotionEffect.INFINITE_DURATION, 0, false, false));
notifyAboutCapture();
resetFlag();
}
public void spawnFlag() {
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(flagItem.getType()));
public void spawnFlag(Material material) {
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(material));
}
private void spawnFlagParticleRing() {
@@ -141,8 +160,11 @@ public class Flag implements Runnable {
updateDisplay();
} else {
winningTeam = optionalTeam.get();
resetFlag();
//TODO stop capture and let ppl know they can now capture the flag
spawnFlag(winningTeam.getFlagMaterial());
updateDisplay();
Title title = Title.title(MiniMessage.miniMessage().deserialize("Team <team> can capture the flag",
Placeholder.component("team", winningTeam.getName())), Component.empty());
Bukkit.getOnlinePlayers().forEach(player -> player.showTitle(title));
}
}
@@ -159,8 +181,9 @@ public class Flag implements Runnable {
miniMessage.deserialize("<red>kill <player> before they bring it to their base.</red>",
Placeholder.component("player", flagCarrier.displayName())));
Bukkit.getOnlinePlayers().forEach(player ->
gameManager.getTeam(player.getUniqueId()).ifPresent(team ->
player.showTitle(team.getId() == winningTeam.getId() ? capturingTeamTitle : huntingTeamTitle)));
gameManager.getTeam(player).ifPresent(team ->
player.showTitle(team.getId().intValue() == winningTeam.getId().intValue()
? capturingTeamTitle : huntingTeamTitle)));
}
private void spawnParticlesOnSquareBorder(Location center, double size) {
@@ -180,7 +203,7 @@ public class Flag implements Runnable {
});
}
LinkedList<Location> particleTrail = new LinkedList<>();
private final LinkedList<Location> particleTrail = new LinkedList<>();
private void spawnTrail() {
TeamColor color = winningTeam.getColor();
@@ -194,9 +217,10 @@ public class Flag implements Runnable {
private void checkFlagCarrier() {
if (flagCarrier.isDead() || !flagCarrier.isOnline()) {
resetFlagCarrier();
spawnFlag();
spawnFlag(GameConfig.FLAG.MATERIAL);
return;
}
double distance = winningTeam.getFlagTurnInLocation().distance(flagCarrier.getLocation());
if (distance > GameConfig.FLAG.TURN_IN_RADIUS) {
Location location = flagCarrier.getLocation();
@@ -206,18 +230,20 @@ public class Flag implements Runnable {
spawnParticlesOnSquareBorder(winningTeam.getFlagTurnInLocation(), GameConfig.FLAG.TURN_IN_RADIUS);
return;
}
notifyAboutTurnIn();
spawnFlag();
spawnFlag(GameConfig.FLAG.MATERIAL);
wins.merge(winningTeam.getId(), 1, Integer::sum);
winningTeam = null;
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(flagCarrier.getUniqueId());
if (optionalTeamPlayer.isEmpty()) {
wins.forEach((id, score) -> gameManager.getTeam(id).ifPresent(team -> team.setScore(score)));
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
} else {
TeamPlayer teamPlayer = optionalTeamPlayer.get();
gameManager.getTeamPlayer(flagCarrier)
.ifPresent(teamPlayer -> {
teamPlayer.getGameClass().setArmor(flagCarrier, teamPlayer);
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
}
teamPlayer.increaseStat(Stat.FLAGS_CAPTURED);
});
resetFlagCarrier();
}
@@ -276,7 +302,7 @@ public class Flag implements Runnable {
*/
private boolean updateScoreBasedOnNearbyPlayers(Collection<Player> players) {
List<TeamPlayer> nearbyPlayers = players.stream()
.map(player -> gameManager.getTeamPlayer(player.getUniqueId()))
.map(gameManager::getTeamPlayer)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
@@ -306,13 +332,15 @@ public class Flag implements Runnable {
}
Team winningTeam = teamLongEntry.getKey();
teamCounts.forEach((team, count) -> {
teamFlagPointCount.merge(team.getId(), team.equals(winningTeam) ? 1 : -1, (oldValue, delta) -> {
int updatedValue = oldValue + delta;
log.debug("Set count to {} for team {}", updatedValue, team.getId());
return Math.max(updatedValue, 0);
});
teamFlagPointCount.putIfAbsent(winningTeam.getId(), 0);
teamFlagPointCount.entrySet().forEach(entry -> {
if (entry.getKey().equals(winningTeam.getId())) {
entry.setValue(entry.getValue() + 1);
} else {
entry.setValue(Math.max(0, entry.getValue() - 1));
}
});
nearbyPlayers.forEach(teamPlayer -> teamPlayer.increaseStat(Stat.TIME_SPEND_CAPTURING_FLAG));
return true;
}
@@ -351,6 +379,10 @@ public class Flag implements Runnable {
}
public void resetFlagCarrier() {
final Player player = flagCarrier;
if (player != null) {
Bukkit.getScheduler().runTask(main, player::clearActivePotionEffects);
}
flagCarrier = null;
winningTeam = null;
particleTrail.clear();
@@ -367,6 +399,7 @@ public class Flag implements Runnable {
resetFlagCarrier();
resetFlag();
wins.clear();
gameManager.getTeams().forEach(team -> team.setScore(0));
}
public void handleCarrierDeathOrDisconnect(Player player) {
@@ -377,8 +410,8 @@ public class Flag implements Runnable {
return;
}
resetFlagCarrier();
spawnFlag();
gameManager.getTeam(player.getUniqueId())
spawnFlag(GameConfig.FLAG.MATERIAL);
gameManager.getTeam(player)
.ifPresentOrElse(team -> Bukkit.broadcast(MiniMessage.miniMessage()
.deserialize("<red><team>'s flag carrier died! The flag has respawned",
Placeholder.component("team", team.getName()))),
@@ -9,7 +9,6 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.util.Optional;
@@ -37,7 +36,7 @@ public class FlagTryCaptureEvent implements Listener {
return;
}
Player player = event.getPlayer();
Optional<TeamPlayer> teamPlayer = winningTeam.getPlayer(player.getUniqueId());
Optional<TeamPlayer> teamPlayer = winningTeam.getPlayer(player);
if (teamPlayer.isEmpty()) {
return;
}
@@ -1,11 +1,13 @@
package com.alttd.ctf.game;
import com.alttd.ctf.Main;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.phases.ClassSelectionPhase;
import com.alttd.ctf.game.phases.CombatPhase;
import com.alttd.ctf.game.phases.EndedPhase;
import com.alttd.ctf.game.phases.GatheringPhase;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.stats.PlayerStat;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
@@ -24,29 +26,34 @@ public class GameManager {
private final HashMap<GamePhase, GamePhaseExecutor> phases;
private RunningGame runningGame;
private final HashMap<Integer, Team> teams = new HashMap<>();
private final HashMap<UUID, PlayerStat> playerStats = new HashMap<>();
public GameManager(WorldBorderApi worldBorderApi) {
public GameManager(Main main, WorldBorderApi worldBorderApi) {
phases = new HashMap<>();
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());
phases.put(GamePhase.ENDED, new EndedPhase(main, this));
}
public Optional<GamePhase> getGamePhase() {
return runningGame == null ? Optional.empty() : Optional.of(runningGame.getCurrentPhase());
}
public void registerPlayer(Team team, Player player) {
public TeamPlayer registerPlayer(Team team, Player player) {
unregisterPlayer(player);
teams.get(team.getId()).addPlayer(player.getUniqueId());
UUID uuid = player.getUniqueId();
PlayerStat playerStat = playerStats
.computeIfAbsent(uuid, (ignored) -> new PlayerStat(uuid, player.getName()));
return teams.get(team.getId()).addPlayer(player, playerStat);
}
public void unregisterPlayer(Player player) {
teams.values().forEach(team -> team.removePlayer(player.getUniqueId()));
teams.values().forEach(team -> team.removePlayer(player));
}
public void registerTeam(Team team) {
team.registerGameManger(this);
teams.put(team.getId(), team);
}
@@ -54,17 +61,17 @@ public class GameManager {
return teams.values();
}
public Optional<Team> getTeam(@NotNull UUID uuid) {
return getTeams().stream().filter(filterTeam -> filterTeam.getPlayer(uuid).isPresent()).findAny();
public Optional<Team> getTeam(@NotNull Player player) {
return getTeams().stream().filter(filterTeam -> filterTeam.getPlayer(player).isPresent()).findAny();
}
public Optional<Team> getTeam(int teamId) {
return getTeams().stream().filter(filterTeam -> filterTeam.getId() == teamId).findAny();
}
public Optional<TeamPlayer> getTeamPlayer(@NotNull UUID uuid) {
public Optional<TeamPlayer> getTeamPlayer(@NotNull Player player) {
return getTeams().stream()
.map(team -> team.getPlayer(uuid))
.map(team -> team.getPlayer(player))
.filter(Optional::isPresent)
.findFirst()
.orElseGet(Optional::empty);
@@ -106,4 +113,20 @@ public class GameManager {
}
return runningGame.skipCurrentPhase();
}
public void setPlayerStats(List<PlayerStat> playerStats) {
this.playerStats.clear();
playerStats.forEach(playerStat -> this.playerStats.put(playerStat.getUuid(), playerStat));
}
public Collection<PlayerStat> getPlayerStats() {
return this.playerStats.values();
}
public Optional<Duration> getRemainingTime() {
if (runningGame == null || runningGame.getCurrentPhase().equals(GamePhase.ENDED)) {
return Optional.empty();
}
return Optional.of(runningGame.getRemainingTime());
}
}
@@ -2,6 +2,7 @@ package com.alttd.ctf.game;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.team.TeamScoreboard;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -51,6 +52,7 @@ public class RunningGame implements Runnable {
} else {
executorService.shutdown();
}
TeamScoreboard.refreshScoreboard(gameManager);
} catch (Exception e) {
log.error("Unexpected error in running game", e);
throw new RuntimeException(e);
@@ -73,7 +75,6 @@ public class RunningGame implements Runnable {
private void broadcastNextPhaseStartTime(GamePhase currentPhase, GamePhase nextPhase) {
//Remaining time for this phase
Duration duration = phaseDurations.get(currentPhase).minus(Duration.between(phaseStartTime, Instant.now()));
log.debug(duration.toString());//TODO remove debug
if ((duration.toMinutes() > 1 && (duration.toMinutes() % 15 == 0 || duration.toMinutes() <= 5)) && duration.toSecondsPart() < 2) {
if (lastMinuteBroadcast == duration.toMinutes()) {
return;
@@ -115,4 +116,9 @@ public class RunningGame implements Runnable {
//TODO say the phase ended early?
nextPhaseActions(currentPhase, GamePhase.ENDED);
}
public Duration getRemainingTime() {
Duration duration = phaseDurations.get(currentPhase);
return duration.minus(Duration.between(phaseStartTime, Instant.now()));
}
}
@@ -21,6 +21,8 @@ import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Optional;
@Slf4j
@@ -52,12 +54,14 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
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()
ArrayList<? extends Player> players = new ArrayList<>(Bukkit.getOnlinePlayers());
Collections.shuffle(players);
players.stream()
.filter(player -> !player.hasPermission("ctf.bypass"))
.filter(player -> gameManager.getTeamPlayer(player.getUniqueId()).isEmpty())
.filter(player -> gameManager.getTeamPlayer(player).isEmpty())
.forEach(player -> {
Team team = teamCircularIterator.next();
team.addPlayer(player.getUniqueId());
gameManager.registerPlayer(team, player);
player.sendRichMessage("You joined <team>!", Placeholder.component("team", team.getName()));
});
} else {
@@ -68,7 +72,7 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
private void teleportPlayersToStartingZone() {
Bukkit.getOnlinePlayers().forEach(player -> {
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(player);
if (teamPlayer.isEmpty()) {
log.warn("{} is not a team player when teleporting to starting zone", player.getName());
return;
@@ -1,5 +1,6 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor;
@@ -17,7 +18,7 @@ public class CombatPhase implements GamePhaseExecutor {
@Override
public synchronized void start(Flag flag) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>CAPTURE THE FLAG</green>"));
flag.spawnFlag();
flag.spawnFlag(GameConfig.FLAG.MATERIAL);
if (executorService == null) {
executorService = Executors.newSingleThreadScheduledExecutor();
} else if (executorService.isTerminated() || executorService.isShutdown()) {
@@ -31,6 +32,8 @@ public class CombatPhase implements GamePhaseExecutor {
@Override
public void end(GamePhase ignored) {
if (executorService != null) {
executorService.shutdown();
}
}
}
@@ -1,7 +1,9 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.Main;
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.team.Team;
@@ -12,9 +14,11 @@ 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.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.command.ConsoleCommandSender;
import java.util.ArrayList;
import java.util.HashMap;
@@ -25,6 +29,13 @@ import java.util.Map;
public class EndedPhase implements GamePhaseExecutor {
private final MiniMessage miniMessage = MiniMessage.miniMessage();
private final Main main;
private final GameManager gameManager;
public EndedPhase(Main main, GameManager gameManager) {
this.main = main;
this.gameManager = gameManager;
}
@Override
public void start(Flag flag) {
@@ -51,8 +62,8 @@ public class EndedPhase implements GamePhaseExecutor {
player.setHealth(20);
});
flag.resetAll();
gameManager.getTeams().forEach(team -> Bukkit.getOnlinePlayers().forEach(team::removePlayer));
}).start();
// TODO reset world (coreprotect) to prep for next round
}
private List<Component> getWinnerMessages(HashMap<Team, Integer> wins) {
@@ -71,11 +82,10 @@ public class EndedPhase implements GamePhaseExecutor {
return messages;
} else if (topTeams.size() > 1) { // Draw scenario, multiple teams have the same top score
messages.add(miniMessage.deserialize("<yellow>It's a draw! Top teams:</yellow>"));
topTeams.forEach(team -> {
messages.add(miniMessage.deserialize("<team> had <score> captures.",
topTeams.forEach(team -> messages.add(
miniMessage.deserialize("<team> had <score> captures.",
Placeholder.component("team", team.getName()),
Placeholder.parsed("score", String.valueOf(highestScore))));
});
Placeholder.parsed("score", String.valueOf(highestScore)))));
addOtherTeamsScore(wins, highestScore, messages);
return messages;
} else { // Single winner
@@ -83,6 +93,18 @@ public class EndedPhase implements GamePhaseExecutor {
messages.add(miniMessage.deserialize("<green><team> has won with <score> captures!</green>",
Placeholder.component("team", winner.getName()),
Placeholder.parsed("score", String.valueOf(highestScore))));
ConsoleCommandSender consoleSender = Bukkit.getConsoleSender();
Bukkit.getScheduler().runTaskAsynchronously(main, () -> {
winner.getPlayers().forEach(teamPlayer -> {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(teamPlayer.getUuid());
String name = offlinePlayer.getName();
if (name == null) {
return;
}
Bukkit.dispatchCommand(consoleSender, String.format("lp user %s permission settemp ctf.victory.%s true 14d", name, winner.getId()));
});
});
if (wins.size() <= 1) {
return messages;
}
@@ -4,11 +4,11 @@ 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;
import org.bukkit.entity.Player;
@Slf4j
public class GatheringPhase implements GamePhaseExecutor {
@@ -36,10 +36,13 @@ public class GatheringPhase implements GamePhaseExecutor {
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());
});
});
gameManager.getTeams()
.forEach(team -> team.getPlayers().forEach(teamPlayer -> {
Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) {
return;
}
teamPlayer.resetWorldBorder(player, worldBorderApi, nextPhase, flag.getFlagLocation());
}));
}
}
@@ -1,9 +1,7 @@
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.game_class.creation.*;
import com.alttd.ctf.team.Team;
import java.util.ArrayList;
@@ -14,9 +12,7 @@ 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));
});
gameManager.getTeams().forEach(team -> gameClasses.put(team.getId(), getGameClassesForTeam(team)));
return gameClasses;
}
@@ -24,7 +20,9 @@ public class GameClassRetrieval {
final List<GameClass> gameClasses = new ArrayList<>();
gameClasses.add(FighterCreator.createFighter(team.getColor()));
gameClasses.add(TankCreator.createTank(team.getColor()));
gameClasses.add(TrapperCreator.createTrapper(team.getColor()));
gameClasses.add(EngineerCreator.createEngineer(team.getColor()));
gameClasses.add(MageCreator.createMage(team.getColor()));
return gameClasses;
}
@@ -2,7 +2,6 @@ package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Engineer;
import com.alttd.ctf.game_class.implementations.Fighter;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -0,0 +1,60 @@
package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Mage;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import java.util.List;
@Slf4j
public class MageCreator {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
@Contract("_ -> new")
public static @NotNull GameClass createMage(@NotNull TeamColor teamColor) {
return new Mage(getArmor(), getTools(teamColor), getDisplayItem(teamColor),
20, 100, 1);
}
@Contract(value = " -> new", pure = true)
private static @NotNull @Unmodifiable List<Material> getArmor() {
return (List.of(Material.CHAINMAIL_BOOTS, Material.AIR, Material.AIR, Material.LEATHER_HELMET));
}
@Contract("_ -> new")
private static @NotNull @Unmodifiable List<ItemStack> getTools(@NotNull TeamColor teamColor) {
return (List.of(getShovel(teamColor)));
}
private static @NotNull ItemStack getShovel(@NotNull TeamColor teamColor) {
ItemStack shovel = new ItemStack(Material.WOODEN_SHOVEL);
ItemMeta meta = shovel.getItemMeta();
meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex())));
meta.setUnbreakable(true);
meta.addEnchant(Enchantment.EFFICIENCY, 1, false);
shovel.setItemMeta(meta);
return shovel;
}
private static @NotNull ItemStack getDisplayItem(@NotNull TeamColor teamColor) {
ItemStack itemStack = new ItemStack(Material.SNOWBALL);
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.displayName(miniMessage.deserialize(String.format("<color:%s>Mage</color>", teamColor.hex())));
itemMeta.lore(List.of(
miniMessage.deserialize("<gold>The Mage can throw many snowballs at once</gold>"),
miniMessage.deserialize("<gold>But it has a long cooldown.</gold>")
));
itemStack.setItemMeta(itemMeta);
return itemStack;
}
}
@@ -1,7 +1,6 @@
package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Fighter;
import com.alttd.ctf.game_class.implementations.Tank;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
@@ -22,7 +21,7 @@ public class TankCreator {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
@Contract("_ -> new")
public static @NotNull GameClass createTank(@NotNull TeamColor teamColor) {//TODO add ability to become temp invulnerable (with some particle effects mayb?)
public static @NotNull GameClass createTank(@NotNull TeamColor teamColor) {
return new Tank(getArmor(), getTools(teamColor), getDisplayItem(teamColor),
30, 7, 4);
}
@@ -0,0 +1,74 @@
package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Trapper;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class TrapperCreator {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
@Contract("_ -> new")
public static @NotNull GameClass createTrapper(@NotNull TeamColor teamColor) {
return new Trapper(getArmor(), getTools(teamColor), getDisplayItem(teamColor),
20, 5, 4);
}
@Contract(value = " -> new", pure = true)
private static @Unmodifiable List<Material> getArmor() {
return (List.of(Material.LEATHER_BOOTS, Material.LEATHER_LEGGINGS, Material.LEATHER_CHESTPLATE, Material.AIR));
}
@Contract("_ -> new")
private static @NotNull @Unmodifiable List<ItemStack> getTools(TeamColor teamColor) {
List<ItemStack> itemStacks = new ArrayList<>(List.of(getShovel(teamColor)));
itemStacks.addAll(getPowderedSnow(teamColor));
return itemStacks;
}
private static @NotNull List<ItemStack> getPowderedSnow(@NotNull TeamColor teamColor) {
ItemStack powderedSnow = new ItemStack(Material.POWDER_SNOW_BUCKET);
ItemMeta itemMeta = powderedSnow.getItemMeta();
itemMeta.itemName(MiniMessage.miniMessage().deserialize(
String.format("<color:%s>Snow Trap</color>", teamColor.hex())));
powderedSnow.setItemMeta(itemMeta);
List<ItemStack> snowBuckets = new ArrayList<>();
for (int i = 0; i < 16; i++) {
snowBuckets.add(powderedSnow.clone());
}
return snowBuckets;
}
private static @NotNull ItemStack getShovel(@NotNull TeamColor teamColor) {
ItemStack shovel = new ItemStack(Material.WOODEN_SHOVEL);
ItemMeta meta = shovel.getItemMeta();
meta.setUnbreakable(true);
meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex())));
shovel.setItemMeta(meta);
return shovel;
}
private static @NotNull ItemStack getDisplayItem(@NotNull TeamColor teamColor) {
ItemStack itemStack = new ItemStack(Material.POWDER_SNOW_BUCKET);
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.displayName(miniMessage.deserialize(String.format("<color:%s>Trapper</color>", teamColor.hex())));
itemMeta.lore(List.of(
miniMessage.deserialize("<gold>The Trapper is a normal class, but it has a trick up it's sleeve</gold>"),
miniMessage.deserialize("<gold>They can place their powdered snow to trap the enemy team.</gold>")
));
itemStack.setItemMeta(itemMeta);
return itemStack;
}
}
@@ -1,11 +1,9 @@
package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -0,0 +1,62 @@
package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.entity.Snowball;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@Slf4j
public class Mage extends GameClass {
public Mage(@NotNull List<Material> armor, @NotNull List<ItemStack> tools, @NotNull ItemStack displayItem,
double health, int throwTickSpeed, int damage) {
super(armor, tools, displayItem, health, throwTickSpeed, damage);
}
public void duplicateSnowBalls(Player shooter, Snowball snowball) {
Location location = snowball.getLocation();
PlayerInventory inventory = shooter.getInventory();
ItemStack itemStack = inventory.getItemInMainHand();
if (itemStack.getType() != Material.SNOWBALL) {
ItemStack itemInOffHand = inventory.getItemInOffHand();
if (itemInOffHand.getType() != Material.SNOWBALL) {
log.warn("Unable to find snowballs in main/off hand");
return;
}
itemStack = itemInOffHand;
}
if (itemStack.getAmount() <= 1) {
return;
}
duplicate(shooter, location, snowball.getVelocity(), itemStack.getAmount() - 1);
itemStack.setAmount(0);
}
private void duplicate(@NotNull Player shooter, @NotNull Location location, @NotNull Vector velocity, int count) {
for (int i = 0; i < count; i++) {
double offsetX = (Math.random() - 0.5) * 2;
double offsetY = (Math.random() - 0.5) * 2;
double offsetZ = (Math.random() - 0.5) * 2;
Location newLocation = location.clone().add(offsetX, offsetY, offsetZ);
Snowball snowball = location.getWorld().spawn(newLocation, Snowball.class);
snowball.setShooter(shooter);
double velocityChangeX = (Math.random() - 0.5) * 0.5;
double velocityChangeY = (Math.random() - 0.5) * 0.5;
double velocityChangeZ = (Math.random() - 0.5) * 0.5;
Vector newVelocity = velocity.clone().add(new Vector(velocityChangeX, velocityChangeY, velocityChangeZ));
snowball.setVelocity(newVelocity);
}
}
}
@@ -0,0 +1,15 @@
package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Trapper extends GameClass {
public Trapper(@NotNull List<Material> armor, @NotNull List<ItemStack> tools, @NotNull ItemStack displayItem,
double health, int throwTickSpeed, int damage) {
super(armor, tools, displayItem, health, throwTickSpeed, damage);
}
}
-2
View File
@@ -2,8 +2,6 @@ package com.alttd.ctf.gui;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.MerchantInventory;
import java.util.HashMap;
import java.util.UUID;
@@ -0,0 +1,124 @@
package com.alttd.ctf.stats;
import com.alttd.ctf.Main;
import com.alttd.ctf.config.GameConfig;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@NoArgsConstructor
@RequiredArgsConstructor
@Getter
public final class PlayerStat {
@JsonIgnore
public static Main main;
@JsonIgnore
private boolean touched;
@JsonIgnore
private static final CommandSender commandSender = Bukkit.getConsoleSender();
@JsonIgnore
private static boolean someoneDiedInPowderedSnow = false;
@JsonProperty
@NotNull
private UUID uuid;
@JsonProperty
@NotNull
private String inGameName;
private boolean completedGame = false;
private int flagsCaptured = 0;
private int kills = 0;
private double damageDone = 0;
private double damageHealed = 0;
private int snowMined = 0;
private int blocksPlaced = 0;
private int snowballsThrown = 0;
private long timeSpendCapturingFlag = 0;
private int deathsInPowderedSnow = 0;
public void increaseStat(Stat stat) throws IllegalArgumentException {
switch (stat) {
case COMPLETED_GAME -> {
if (!completedGame) {
Bukkit.getScheduler().runTask(main, () -> {
Bukkit.getScheduler().runTaskAsynchronously(main, () -> {
Bukkit.dispatchCommand(commandSender, String.format("lp user %s permission settemp ctf.game.completed true 14d", inGameName));
});
});
}
completedGame = true;
}
case FLAGS_CAPTURED -> flagsCaptured++;
case KILLS -> kills++;
case SNOW_MINED -> snowMined++;
case BLOCKS_PLACED -> blocksPlaced++;
case SNOWBALLS_THROWN -> snowballsThrown++;
case TIME_SPEND_CAPTURING_FLAG -> timeSpendCapturingFlag++;
case DEATHS_IN_POWDERED_SNOW -> {
if (someoneDiedInPowderedSnow) {
deathsInPowderedSnow++;
return;
}
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
if (!offlinePlayer.isOnline()) {
return;
}
Player player = offlinePlayer.getPlayer();
if (player == null) {
return;
}
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<red><player> will receive a consolation prize for being the first to die in powdered snow.</red>",
Placeholder.component("player",player.displayName())));
Bukkit.getScheduler().runTaskAsynchronously(main, () -> {
Bukkit.dispatchCommand(commandSender, String.format("lp user %s permission settemp ctf.game.first_powdered_snow_death true 14d", inGameName));
});
someoneDiedInPowderedSnow = true;
}
case DAMAGE_DONE, DAMAGE_HEALED -> throw new IllegalArgumentException(String.format("%s requires a number", stat.name()));
}
touched = true;
}
public void increaseStat(Stat stat, double value) throws IllegalArgumentException {
switch (stat) {
case DAMAGE_DONE -> damageDone += value;
case DAMAGE_HEALED -> damageHealed += value;
default -> throw new IllegalArgumentException(String.format("%s cannot be passed with a number", stat.name()));
}
touched = true;
}
public void setUnTouched() {
touched = false;
}
public double getStat(Stat stat) {
return switch (stat) {
case COMPLETED_GAME -> completedGame ? 1 : 0;
case FLAGS_CAPTURED -> flagsCaptured;
case KILLS -> kills;
case DAMAGE_DONE -> damageDone;
case DAMAGE_HEALED -> damageHealed;
case SNOW_MINED -> snowMined;
case BLOCKS_PLACED -> blocksPlaced;
case SNOWBALLS_THROWN -> snowballsThrown;
case TIME_SPEND_CAPTURING_FLAG -> timeSpendCapturingFlag;
case DEATHS_IN_POWDERED_SNOW -> deathsInPowderedSnow;
};
}
}
@@ -0,0 +1,14 @@
package com.alttd.ctf.stats;
public enum Stat {
COMPLETED_GAME,
FLAGS_CAPTURED,
KILLS,
DAMAGE_DONE,
DAMAGE_HEALED,
SNOW_MINED,
BLOCKS_PLACED,
SNOWBALLS_THROWN,
TIME_SPEND_CAPTURING_FLAG,
DEATHS_IN_POWDERED_SNOW
}
@@ -0,0 +1,87 @@
package com.alttd.ctf.team;
import com.alttd.ctf.config.Config;
import com.alttd.ctf.game.GameManager;
import com.alttd.galaxy.discord.Bot;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.*;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@Slf4j
public class DiscordTeam {
private final GameManager gameManager;
public DiscordTeam(GameManager gameManager) {
this.gameManager = gameManager;
}
@FunctionalInterface
private interface Validate {
void apply(Role role, Member member);
}
private void validateModifyRole(Team team, Player player, Validate consumer) {
if (!player.isDiscordLinked()) {
log.info("{} is not discord linked", player.getName());
return;
}
if (team.getDiscordRole() == -1) {
log.warn("No valid discord role set for team {}", team.getId());
return;
}
Bot bot = Bukkit.getBot();
if (bot == null) {
log.error("Unable to get bot");
return;
}
JDA jda = bot.getJDA();
if (jda == null) {
log.error("Unable to get JDA");
return;
}
Guild guild = jda.getGuildById(Config.DISCORD.PLAYER_SERVER_ID);
if (guild == null) {
log.warn("Unable to get guild {}", Config.DISCORD.PLAYER_SERVER_ID);
return;
}
Role role = guild.getRoleById(team.getDiscordRole());
if (role == null) {
log.warn("Unable to get role {} for team {}", team.getDiscordRole(), team.getId());
return;
}
Member nullableMember = guild.getMemberById(player.getDiscordID());
if (nullableMember == null) {
guild.retrieveMemberById(player.getDiscordID()).queue(member -> consumer.apply(role, member));
} else {
consumer.apply(role, nullableMember);
}
}
public void removeRole(Team team, @NotNull Player player) {
validateModifyRole(team, player, (role, member) -> {
if (member.isOwner()) {
log.info("Unable to remove team role from server owner");
return;
}
member.getGuild().removeRoleFromMember(member, role).queue();
});
}
public void addRole(Team team, Player player) {
validateModifyRole(team, player, (role, member) -> {
if (member.isOwner()) {
log.info("Unable to add team role to server owner");
return;
}
gameManager.getTeams().forEach(otherTeam ->
member.getRoles().stream()
.filter(otherRole -> otherRole.getIdLong() == otherTeam.getDiscordRole())
.forEach(otherRole -> member.getGuild().removeRoleFromMember(member, otherRole).queue()));
member.getGuild().addRoleToMember(member, role).queue();
});
}
}
+64 -11
View File
@@ -1,22 +1,28 @@
package com.alttd.ctf.team;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.stats.PlayerStat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
public class Team {
@JsonIgnore
private final TeamScoreboard scoreboard = new TeamScoreboard(this);
@JsonIgnore
private final HashMap<UUID, TeamPlayer> players = new HashMap<>();
@JsonProperty("name")
@@ -25,7 +31,8 @@ public class Team {
private Component name;
@JsonProperty("id")
@Getter
private int id;
@NotNull
private Integer id;
@JsonProperty("spawnLocation")
@NotNull
@Getter
@@ -42,15 +49,42 @@ public class Team {
@NotNull
@Getter
private TeamColor color;
@JsonProperty("flagMaterial")
@NotNull
@Getter
private Material flagMaterial;
@JsonProperty("legacyTeamColor")
@NotNull
@Getter
private String legacyTeamColor;
@JsonProperty("discordRole")
@Getter
@NotNull
private Long discordRole;
@JsonIgnore
private DiscordTeam discordTeam;
public TeamPlayer addPlayer(UUID uuid) {
TeamPlayer teamPlayer = new TeamPlayer(uuid, this);
public void registerGameManger(GameManager gameManager) {
discordTeam = new DiscordTeam(gameManager);
}
public TeamPlayer addPlayer(Player player, PlayerStat playerStat) {
removeFromScoreboard(player);
UUID uuid = player.getUniqueId();
TeamPlayer teamPlayer = new TeamPlayer(player, this, playerStat);
players.put(uuid, teamPlayer);
log.debug("Added player with uuid {} to team with id {}", uuid, id);
addToScoreboard(player);
if (discordTeam != null) {
discordTeam.addRole(this, player);
} else {
log.warn("No discord team set for {} to add role", id);
}
log.debug("Added player {} to team with id {}", player.getName(), id);
return teamPlayer;
}
public Optional<TeamPlayer> getPlayer(@NotNull UUID uuid) {
public Optional<TeamPlayer> getPlayer(@NotNull Player player) {
UUID uuid = player.getUniqueId();
if (!players.containsKey(uuid))
return Optional.empty();
return Optional.of(players.get(uuid));
@@ -60,11 +94,30 @@ public class Team {
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);
public void removePlayer(@NotNull Player player) {
removeFromScoreboard(player);
TeamPlayer remove = players.remove(player.getUniqueId());
if (remove == null) {
return;
}
if (discordTeam != null) {
discordTeam.removeRole(this, player);
} else {
log.warn("No discord team set for {} to remove role", id);
}
log.debug("Removed player {} from team with id {}", player.getName(), id);
}
public void addToScoreboard(Player player) {
scoreboard.addToScoreboard(player);
}
private void removeFromScoreboard(Player player) {
scoreboard.removeFromScoreboard(player);
}
public void setScore(int newScore) {
scoreboard.setScore(newScore);
}
@Override
@@ -7,6 +7,8 @@ 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.alttd.ctf.stats.PlayerStat;
import com.alttd.ctf.stats.Stat;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Getter;
import lombok.Setter;
@@ -21,14 +23,17 @@ import java.util.*;
@Getter
public class TeamPlayer {
private final PlayerStat playerStat;
private final UUID uuid;
private final Team team;
@Setter
private GameClass gameClass;
private boolean isDead = false;
protected TeamPlayer(UUID uuid, Team team) {
this.uuid = uuid;
protected TeamPlayer(Player player, Team team, PlayerStat playerStat) {
this.uuid = player.getUniqueId();
this.team = team;
this.playerStat = playerStat;
}
public void respawn(@NotNull Player player, @NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
@@ -48,9 +53,17 @@ public class TeamPlayer {
}
player.teleportAsync(spawnLocation).thenAcceptAsync(unused ->
resetWorldBorder(player, worldBorderApi, gamePhase, worldBorderCenter));
isDead = false;
}
public void setDead() {
isDead = true;
}
public void resetWorldBorder(Player player, WorldBorderApi worldBorderApi, GamePhase gamePhase, Location worldBorderCenter) {
if (player == null || !player.isOnline()) {
return;
}
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");
@@ -78,4 +91,12 @@ public class TeamPlayer {
public int hashCode() {
return Objects.hash(uuid);
}
public void increaseStat(Stat stat) {
playerStat.increaseStat(stat);
}
public void increaseStat(Stat stat, double amount) {
playerStat.increaseStat(stat, amount);
}
}
@@ -0,0 +1,84 @@
package com.alttd.ctf.team;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import net.kyori.adventure.text.format.*;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.*;
import java.time.Duration;
public class TeamScoreboard {
private final static Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
private final Team team;
public TeamScoreboard(Team team) {
this.team = team;
}
public void addToScoreboard(Player player) {
org.bukkit.scoreboard.Team scoreboardTeam = scoreboard.getTeam("ctf_" + team.getId());
if (scoreboardTeam == null) {
scoreboardTeam = scoreboard.registerNewTeam("ctf_" + team.getId());
scoreboardTeam.displayName(team.getName());
TeamColor color = team.getColor();
NamedTextColor namedTextColor = NamedTextColor.nearestTo(TextColor.color(color.r(), color.g(), color.b()));
scoreboardTeam.color(namedTextColor);
}
scoreboardTeam.addPlayer(player);
player.setScoreboard(scoreboard);
}
protected void removeFromScoreboard(Player player) {
scoreboard.getTeams().stream()
.filter(team -> team.getName().startsWith("ctf_"))
.filter(team -> team.hasPlayer(player))
.forEach(team -> team.removePlayer(player));
}
protected void setScore(int newScore) {
Objective objective = getOrCreateObjective();
Score score = objective.getScore(team.getLegacyTeamColor() +
PlainTextComponentSerializer.plainText().serialize(team.getName()));
score.setScore(newScore);
}
private static Objective getOrCreateObjective() {
Objective objective = scoreboard.getObjective("teamScores");
if (objective == null) {
objective = scoreboard.registerNewObjective("teamScores", Criteria.DUMMY,
MiniMessage.miniMessage().deserialize("<gold>CTF score</gold>"));
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
}
return objective;
}
private record PhaseScore(GamePhase gamePhase, Score score) {}
private static PhaseScore phaseScore = null;
private static void updateTime(GamePhase gamePhase, Duration duration) {
if (phaseScore == null) {
phaseScore = new PhaseScore(gamePhase, getOrCreateObjective()
.getScore(ChatColor.GREEN + PlainTextComponentSerializer.plainText().serialize(gamePhase.getDisplayName())));
} else if (phaseScore.gamePhase() != gamePhase) {
phaseScore.score.resetScore();
phaseScore = new PhaseScore(gamePhase, getOrCreateObjective()
.getScore(ChatColor.GREEN + PlainTextComponentSerializer.plainText().serialize(gamePhase.getDisplayName())));
}
phaseScore.score.setScore(duration.toMinutesPart() == 0 ? duration.toSecondsPart() : duration.toMinutesPart());
}
public static void refreshScoreboard(GameManager gameManager) {
gameManager.getGamePhase().ifPresent(gamePhase ->
gameManager.getRemainingTime().ifPresent(duration ->
updateTime(gamePhase, duration)));
// TODO if game is active show time + game phase
// if someone has flag show that
// show how many of each team in circle?
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
#Tue Feb 11 22:21:13 CET 2025
buildNumber=45
#Sun Mar 02 00:42:30 CET 2025
buildNumber=112
version=0.1