Compare commits

..
79 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
auto 56455b4c50 Fix executor handling in phase transitions and game ending
Ensure proper management of executor service during CombatPhase and shutdown when the game ends. Avoids potential resource leaks or invalid executor state during phase transitions.
2025-02-11 23:17:10 +01:00
auto 3b4beefb37 Remove redundant TODO comment in CombatPhase.java
The comment about adding players to the boss bar list was removed as it is completed.
2025-02-11 23:10:06 +01:00
auto 9ae63ba1e4 Refine title message for flag capture notification.
Adjusted the title message to simplify formatting and better align with gameplay context. Removed redundant text and ensured proper use of placeholders for dynamic content.
2025-02-11 23:08:59 +01:00
auto c3b11995c7 Update phase transition notifications with detailed timing
Introduced specific second-based notifications for better accuracy as a phase approaches. Added distinct messaging for the game ending phase to improve clarity for players. Simplified code by replacing hard-coded values with a predefined list of notification intervals.
2025-02-11 23:06:05 +01:00
auto 8b09e54106 Fix flag capture and game phase timing bugs
Adjusted the flag capture sequence to ensure proper update of the carrier before notifying about the capture. Fixed the phaseStartTime initialization to correctly track phase transitions. Updated the build number for the new changes.
2025-02-11 23:02:38 +01:00
auto 237cae37ca Refactor flag capture and turn-in notifications.
Extracted and separated notification logic for flag capture and turn-in events into distinct methods to improve clarity and maintainability. Updated messaging to better align with game events and streamlined title displays for players.
2025-02-11 22:12:31 +01:00
auto a3c22cf074 Refactor flag reset logic for clarity and reuse.
Consolidated flag reset responsibilities into `resetFlagCarrier` and `resetAll` methods to improve code readability and maintainability. Replaced redundant flag reset operations with the new methods across the codebase. Updated `EndedPhase` to use `resetAll` for consistent behavior.
2025-02-11 22:08:58 +01:00
auto 4cfc8f1572 Fix flag particle spawn location during turn-in
Updated the particle spawning method to use the flag's turn-in location instead of the world border center. This ensures particles are correctly displayed at the intended location, aligning with gameplay expectations.
2025-02-11 21:46:44 +01:00
auto 9563e9641f Add skip phase command to bypass game phases
Introduced a new `/ctf skipphase` command allowing admins to skip the current game phase. Updated relevant classes to handle phase-skipping logic and ensure proper game state transitions. Added corresponding message configuration and subcommand registration.
2025-02-11 21:45:14 +01:00
auto aad63f174e Refactor phase transitions and add particle effects for flags
Simplified phase transition logic by removing unnecessary parameters and added proper executor shutdown at the end of the game. Enhanced visuals by introducing particle effects around flags when a team wins. Also implemented a method to skip the current game phase.
2025-02-11 21:41:33 +01:00
auto 66c24d852d Refactor and enhance flag capture notification system.
Extracted the flag capture notification logic into a dedicated method for better readability and reusability. Added team-specific title messages to notify players of the flag capture, improving clarity and game immersion.
2025-02-11 21:27:34 +01:00
auto 7acf39b600 Fix armor color setting method from BGR to RGB.
The method for setting armor colors was incorrect and used `Color.fromBGR` instead of `Color.fromRGB`. This ensures proper color rendering and aligns with expected behavior.
2025-02-11 21:27:26 +01:00
auto 6f99402d16 Add TURN_IN_RADIUS config and spawn particles at turn in point as well
Introduced a new TURN_IN_RADIUS configuration parameter to manage flag turn-in distance. Refactored particle spawning logic to use configurable radii and adjusted related methods accordingly for better flexibility and maintainability.
2025-02-11 21:08:20 +01:00
auto 5f2fb8fe0a Send death message with respawn timer on player death
Players now receive a formatted message indicating their death and the respawn time remaining. This enhances feedback and improves the overall player experience.
2025-02-09 22:12:50 +01:00
auto dbcbb10079 Refactor player status handling and improve flag logic
Rename OnPlayerJoin to OnPlayerOnlineStatus, adding logic for player disconnects. Enhance OnPlayerDeath and Flag classes to handle flag reset and notifications when a flag carrier dies or disconnects.
2025-02-09 22:10:48 +01:00
auto 7070165a94 Rename WINNING_SCORE to CAPTURE_SCORE in GameConfig.
Updated variable and configuration references to reflect the renaming of WINNING_SCORE to CAPTURE_SCORE for clarity and consistency. Adjusted related usage in Flag class to align with the new naming convention.
2025-02-09 22:01:12 +01:00
auto 8411db57a1 Update flag capture radius to use configurable value
Replaced the hardcoded 10-block radius with a configurable value from GameConfig.FLAG.CAPTURE_RADIUS. This ensures the capture radius can be easily adjusted without code changes and improves flexibility.
2025-02-09 22:00:51 +01:00
auto 498ed774a4 Increase particle trail density and remove obsolete comment.
Updated the particle trail to spawn three particles instead of one, enhancing visual clarity. Also removed an outdated TODO comment that is no longer relevant to the functionality.
2025-02-09 21:59:12 +01:00
auto e95dabccac Set breakable items to unbreakable in class creators
Made items unbreakable for Engineer, Tank, and Fighter classes to improve durability and gameplay consistency. This change ensures tools and equipment don't wear out during use, aligning with game design goals.
2025-02-09 21:57:41 +01:00
auto 6ae2563d16 Set death message to empty Component on player death.
Replaced `null` with `Component.empty()` for the death message to comply with updated API standards and improve code clarity.
2025-02-09 21:57:27 +01:00
auto 91ee3cf8f9 Add blocking mechanics for snowball hits
Introduced a check to disable snowball damage if the target is blocking and the impact angle is within 80 degrees. Extended `SnowballHitConsumer` to include the `Snowball` object for more accurate handling of hits. This improves gameplay by allowing players to block snowball attacks with proper positioning.
2025-02-09 21:57:16 +01:00
auto 4af509eb5f Fix world border config loading and phase transition logic
Made DEFAULT_SIZE in GameConfig mutable and ensured it updates dynamically from the configuration. Corrected phase transition logic in RunningGame to prevent potential out-of-bounds errors. Updated capture radius and build version.
2025-02-09 04:30:19 +01:00
auto 4f8a7a0416 Clear player potion effects on class reset
Ensure active potion effects are removed when resetting a player's class. This prevents lingering effects from other classes, maintaining consistent game behavior.
2025-02-08 23:11:41 +01:00
auto 55b88a950d Refactor snowball event handling for better readability.
Renamed method for clarity and improved team player checks. Added debug logs for unsupported scenarios and ensured no actions occur when players hit teammates. These changes enhance code maintainability and prevent unintended behavior.
2025-02-08 23:09:09 +01:00
auto 172f9a29a2 Add throw speed to item lore in GameClass
This update enhances the item lore display by including the throw speed attribute alongside health and damage. It improves clarity for players by providing more comprehensive item stats directly on the display.
2025-02-08 23:06:51 +01:00
auto 8718ca0918 Refactor world border handling for game phases.
Removed world border size from `GamePhase` and centralized configuration in `GameConfig`. Updated logic to use `WorldBorderSettings` dynamically, ensuring all phases define valid world border behavior. Adjusted related methods to improve maintainability and clarity.
2025-02-08 23:06:10 +01:00
auto 728e8b7486 Add configurable respawn time to GameConfig
Introduced a new RESPAWN section in GameConfig for managing respawn time settings. Updated OnPlayerDeath to use the configurable value instead of a hardcoded duration. This allows greater flexibility for game configuration.
2025-02-08 23:01:43 +01:00
auto 74cf3589c0 Refactor configuration handling and add world border support
Moved FLAG settings from Config to GameConfig for better modularity. Introduced world border configuration with types and sizes for different game phases. Updated related classes to utilize new configuration structure and added error handling to prevent runtime failures.
2025-02-08 23:00:04 +01:00
auto d7432c9b89 Change flag color to black banner.
Updated the default flag item from a cyan banner to a black banner.
2025-02-08 21:56:56 +01:00
auto 7ed4c20d29 Update OnPlayerDeath to delay respawning
Modified the OnPlayerDeath event to schedule game class application with a 10-second delay after player death instead of immediately applying it.
2025-02-08 21:56:23 +01:00
auto 33578027d3 Refactor game class handling and fix GUI inventory usage.
Simplified game class retrieval by removing redundant data structures and combining logic. Updated GUI inventory to ensure tasks are run on the main thread using Bukkit's scheduler. Improved code organization by centralizing class selection logic in `TeamPlayer`.
2025-02-08 21:56:05 +01:00
auto a52efb9dbb Remove unnecessary logging in resetWorldBorder method
Removed a debug log statement in the resetWorldBorder method as it was redundant. This improves code cleanliness and avoids excessive log clutter during game phase transitions.
2025-02-08 21:06:22 +01:00
auto 72dc1af903 Add flag turn-in location to team functionality
Introduced a new `flagTurnInLocation` property to the `Team` class, used to determine where players return flags. Updated relevant logic in `Flag` and `CreateTeam` to utilize this new property, including reducing the required distance for flag turn-in.
2025-02-08 21:03:01 +01:00
auto e93580bea9 Automatically show class selection when the phase starts 2025-02-08 20:56:15 +01:00
auto 53de0fe217 Remove logging in TeamPlayer for world border
Removed redundant log messages in the `resetWorldBorder` method to reduce clutter and improve clarity.
2025-02-08 20:55:37 +01:00
auto b8afe209bc Remove unused variable in resetWorldBorder method
Deleted an unused `IWorldBorder` variable in the `resetWorldBorder` method to clean up the code. This change helps improve readability and eliminates unnecessary declarations.
2025-02-08 20:45:30 +01:00
auto be7b508667 Integrate WorldBorderAPI and enhance game phase functionality
Added WorldBorderAPI dependency to manage player boundaries dynamically during game phases. Updated game phases, respawn mechanics, and class selection to utilize the WorldBorderAPI. Reworked related components to improve boundary handling, ensuring consistent gameplay flow and preparation for future enhancements.
2025-02-08 20:45:06 +01:00
52 changed files with 1980 additions and 342 deletions
+26
View File
@@ -2,6 +2,7 @@ import java.util.Properties
plugins { plugins {
id("java") id("java")
id("com.gradleup.shadow") version "9.0.0-beta4"
} }
group = "com.alttd.ctf" group = "com.alttd.ctf"
@@ -31,6 +32,12 @@ dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.2") implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.2")
// End JSON config 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 { tasks.test {
@@ -41,6 +48,21 @@ tasks.jar {
archiveFileName.set("CaptureTheFlag.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 versionPropsFile = file("version.properties")
val versionProps = Properties().apply { val versionProps = Properties().apply {
if (versionPropsFile.exists()) { if (versionPropsFile.exists()) {
@@ -68,6 +90,10 @@ tasks.named("build") {
dependsOn(incrementBuildNumber) dependsOn(incrementBuildNumber)
} }
tasks.named("shadowJar") {
dependsOn(incrementBuildNumber)
}
tasks.withType<Jar> { tasks.withType<Jar> {
manifest { manifest {
attributes( attributes(
+4
View File
@@ -5,6 +5,10 @@ dependencyResolutionManagement {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
maven("https://repo.destro.xyz/snapshots") // Altitude - Galaxy maven("https://repo.destro.xyz/snapshots") // Altitude - Galaxy
maven { // WorldBorderAPI
name = "eldonexus"
url = uri("https://eldonexus.de/repository/maven-releases/")
}
} }
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
} }
+81 -20
View File
@@ -4,55 +4,58 @@ import com.alttd.ctf.commands.CommandManager;
import com.alttd.ctf.config.Config; import com.alttd.ctf.config.Config;
import com.alttd.ctf.config.GameConfig; import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.config.Messages; import com.alttd.ctf.config.Messages;
import com.alttd.ctf.events.InventoryItemInteractionEvent; import com.alttd.ctf.events.*;
import com.alttd.ctf.events.OnPlayerDeath;
import com.alttd.ctf.events.OnPlayerJoin;
import com.alttd.ctf.events.SnowballEvent;
import com.alttd.ctf.flag.Flag; import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.flag.FlagTryCaptureEvent; import com.alttd.ctf.flag.FlagTryCaptureEvent;
import com.alttd.ctf.game.GameManager; 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.gui.GUIListener;
import com.alttd.ctf.json_config.JacksonConfig; import com.alttd.ctf.json_config.JacksonConfig;
import com.alttd.ctf.json_config.JsonConfigManager; import com.alttd.ctf.json_config.JsonConfigManager;
import com.alttd.ctf.stats.PlayerStat;
import com.alttd.ctf.team.Team; import com.alttd.ctf.team.Team;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.GameRule; import org.bukkit.GameRule;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
public class Main extends JavaPlugin { public class Main extends JavaPlugin {
private GameManager gameManager = null; private GameManager gameManager = null;
private Flag flag;
@Override @Override
public void onEnable() { public void onEnable() {
PlayerStat.main = this;
GUIInventory.setMain(this); // sorry
ClassSelectionGUI.setMain(this); // sorry
Package pkg = Main.class.getPackage(); Package pkg = Main.class.getPackage();
String version = pkg.getImplementationVersion(); String version = pkg.getImplementationVersion();
log.info("Plugin enabled, version {}", version); log.info("Plugin enabled, version {}", version);
reloadConfigs(); reloadConfigs();
this.gameManager = new GameManager(); WorldBorderApi worldBorderApi = worldBorder();
this.gameManager = new GameManager(this, worldBorderApi);
registerTeams(); //Skipped in reloadConfig if gameManager is not created yet 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
CommandManager commandManager = new CommandManager(this, gameManager, flag); Flag flag = new Flag(this, gameManager);
new CommandManager(this, gameManager, flag, worldBorderApi);
//Ensuring immediate respawn is on in all worlds //Ensuring immediate respawn is on in all worlds
log.info("Enabling immediate respawn for {}.", Config.FLAG.world); enableImmediateRespawn();
World world = Bukkit.getWorld(Config.FLAG.world); registerEvents(flag, worldBorderApi);
if (world != null) {
world.setGameRule(GameRule.DO_IMMEDIATE_RESPAWN, true);
} else {
log.error("No valid flag world defined, unable to modify game rules");
}
registerEvents(flag);
} }
public void reloadConfigs() { public void reloadConfigs() {
@@ -61,17 +64,40 @@ public class Main extends JavaPlugin {
GameConfig.reload(this); GameConfig.reload(this);
if (gameManager != null) { if (gameManager != null) {
registerTeams(); registerTeams();
loadPlayerStats();
} }
} }
private void registerEvents(Flag flag) { private WorldBorderApi worldBorder() {
RegisteredServiceProvider<WorldBorderApi> worldBorderApiRegisteredServiceProvider = getServer().getServicesManager().getRegistration(WorldBorderApi.class);
if (worldBorderApiRegisteredServiceProvider == null) {
log.error("WorldBorder API not found, disabling plugin");
getServer().getPluginManager().disablePlugin(this);
return null;
}
return worldBorderApiRegisteredServiceProvider.getProvider();
}
private void 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 pluginManager = getServer().getPluginManager();
//TODO add event for player joining and clear their inv
pluginManager.registerEvents(new SnowballEvent(gameManager), this); pluginManager.registerEvents(new SnowballEvent(gameManager), this);
pluginManager.registerEvents(new OtherGameEvents(gameManager), this);
pluginManager.registerEvents(new FlagTryCaptureEvent(flag), this); pluginManager.registerEvents(new FlagTryCaptureEvent(flag), this);
pluginManager.registerEvents(new OnPlayerDeath(gameManager), this); pluginManager.registerEvents(new OnPlayerDeath(gameManager, worldBorderApi, this, flag), this);
pluginManager.registerEvents(new InventoryItemInteractionEvent(), this); pluginManager.registerEvents(new InventoryItemInteractionEvent(), this);
pluginManager.registerEvents(new OnPlayerJoin(gameManager, flag), this); pluginManager.registerEvents(new OnPlayerOnlineStatus(gameManager, flag, worldBorderApi), this);
pluginManager.registerEvents(new GUIListener(), this); pluginManager.registerEvents(new GUIListener(), this);
} }
@@ -101,4 +127,39 @@ public class Main extends JavaPlugin {
teams.forEach(gameManager::registerTeam); 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);
}
} }
@@ -5,6 +5,7 @@ import com.alttd.ctf.commands.subcommands.*;
import com.alttd.ctf.config.Messages; import com.alttd.ctf.config.Messages;
import com.alttd.ctf.flag.Flag; import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager; import com.alttd.ctf.game.GameManager;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@@ -22,7 +23,7 @@ import java.util.stream.Collectors;
public class CommandManager implements CommandExecutor, TabExecutor { public class CommandManager implements CommandExecutor, TabExecutor {
private final List<SubCommand> subCommands; private final List<SubCommand> subCommands;
public CommandManager(Main main, GameManager gameManager, Flag flag) { public CommandManager(Main main, GameManager gameManager, Flag flag, WorldBorderApi worldBorderApi) {
PluginCommand command = main.getCommand("ctf"); PluginCommand command = main.getCommand("ctf");
if (command == null) { if (command == null) {
subCommands = null; subCommands = null;
@@ -34,9 +35,11 @@ public class CommandManager implements CommandExecutor, TabExecutor {
subCommands = Arrays.asList( subCommands = Arrays.asList(
new ChangeTeam(gameManager), new ChangeTeam(gameManager),
new SkipPhase(gameManager),
new Start(gameManager, flag), new Start(gameManager, flag),
new CreateTeam(main, gameManager), new CreateTeam(main, gameManager),
new SelectClass(gameManager), new HighestStat(gameManager),
new SelectClass(gameManager, worldBorderApi),
new Reload(main) new Reload(main)
); );
} }
@@ -58,7 +58,7 @@ public class ChangeTeam extends SubCommand {
} }
private void changeTeam(CommandSender commandSender, Player player, Team team) { 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()) { if (optionalOldTeam.isPresent()) {
moveBetweenTeams(commandSender, player, team, optionalOldTeam.get()); moveBetweenTeams(commandSender, player, team, optionalOldTeam.get());
return; return;
@@ -12,6 +12,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Material;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -75,8 +76,8 @@ public class CreateTeam extends SubCommand {
int highestId = gameManager.getMaxTeamId(); int highestId = gameManager.getMaxTeamId();
Team team = new Team(MiniMessage.miniMessage().deserialize(String.format("<color:%s>%s</color>", color, name)), Team team = new Team(MiniMessage.miniMessage().deserialize(String.format("<color:%s>%s</color>", color, name)),
highestId + 1, player.getLocation(), player.getLocation(), teamColor); highestId + 1, player.getLocation(), player.getLocation(), player.getLocation(), teamColor,
Material.RED_BANNER, "§c", -1L);
return consumer.apply(team); 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;
}
}
@@ -5,15 +5,13 @@ import com.alttd.ctf.config.Messages;
import com.alttd.ctf.game.GameManager; import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase; import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass; import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.creation.EngineerCreator; import com.alttd.ctf.game_class.GameClassRetrieval;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.game_class.creation.TankCreator;
import com.alttd.ctf.gui.ClassSelectionGUI; import com.alttd.ctf.gui.ClassSelectionGUI;
import com.alttd.ctf.team.TeamPlayer; import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -22,15 +20,12 @@ public class SelectClass extends SubCommand {
private final GameManager gameManager; private final GameManager gameManager;
private final HashMap<Integer, List<GameClass>> gameClasses; private final HashMap<Integer, List<GameClass>> gameClasses;
private final WorldBorderApi worldBorderApi;
public SelectClass(GameManager gameManager) { public SelectClass(GameManager gameManager, WorldBorderApi worldBorderApi) {
this.gameManager = gameManager; this.gameManager = gameManager;
this.gameClasses = new HashMap<>(); this.worldBorderApi = worldBorderApi;
gameManager.getTeams().forEach(team -> { gameClasses = GameClassRetrieval.getGameClassesForAllTeams(gameManager);
gameClasses.computeIfAbsent(team.getId(), teamId -> new ArrayList<>()).add(FighterCreator.createFighter(team.getColor()));
gameClasses.computeIfAbsent(team.getId(), teamId -> new ArrayList<>()).add(TankCreator.createTank(team.getColor()));
gameClasses.computeIfAbsent(team.getId(), teamId -> new ArrayList<>()).add(EngineerCreator.createEngineer(team.getColor()));
});
} }
@Override @Override
@@ -45,17 +40,19 @@ public class SelectClass extends SubCommand {
return 0; return 0;
} }
GamePhase gamePhase = optionalGamePhase.get(); GamePhase gamePhase = optionalGamePhase.get();
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId()); Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty()) { if (optionalTeamPlayer.isEmpty()) {
commandSender.sendRichMessage("<red>You have to be in a CTF team to select a class.</red>"); commandSender.sendRichMessage("<red>You have to be in a CTF team to select a class.</red>");
return 0; return 0;
} }
TeamPlayer teamPlayer = optionalTeamPlayer.get(); 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>"); commandSender.sendRichMessage("<red>You have to be near your spawn to change classes.</red>");
return 0; return 0;
} }
new ClassSelectionGUI(gameClasses.get(teamPlayer.getTeam().getId()), teamPlayer) new ClassSelectionGUI(gameClasses.get(teamPlayer.getTeam().getId()), teamPlayer, worldBorderApi, gamePhase)
.open(player); .open(player);
return 0; return 0;
} }
@@ -0,0 +1,40 @@
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 lombok.AllArgsConstructor;
import org.bukkit.command.CommandSender;
import java.util.List;
@AllArgsConstructor
public class SkipPhase extends SubCommand {
private final GameManager gameManager;
@Override
public int onCommand(CommandSender commandSender, String[] args) {
if (!gameManager.skipPhase()) {
commandSender.sendRichMessage("<red>The phase was not skipped because there is no running game or there is no next phase</red>");
return 0;
}
commandSender.sendRichMessage("<green>The current phase was skipped!</green>");
return 0;
}
@Override
public String getName() {
return "skipphase";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return List.of();
}
@Override
public String getHelpMessage() {
return Messages.HELP.SKIP_PHASE;
}
}
@@ -102,6 +102,12 @@ abstract class AbstractConfig {
return yaml.getInt(path, yaml.getInt(path)); 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) { double getDouble(String prefix, String path, double def) {
path = prefix + path; path = prefix + path;
yaml.addDefault(path, def); yaml.addDefault(path, def);
+25 -10
View File
@@ -41,21 +41,36 @@ public class Config extends AbstractConfig{
} }
} }
public static class FLAG { @SuppressWarnings("unused")
private static final String prefix = "flag."; public static class DISCORD {
private static final String prefix = "discord.";
public static String world = "world"; public static long PLAYER_SERVER_ID = 0L;
public static double x = 0;
public static double y = 0;
public static double z = 0;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void load() { private static void load() {
world = config.getString(prefix, "world", world); PLAYER_SERVER_ID = config.getLong(prefix, "player-server-id", PLAYER_SERVER_ID);
x = config.getDouble(prefix, "x", x);
y = config.getDouble(prefix, "y", y);
z = config.getDouble(prefix, "z", z);
} }
} }
@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.Main;
import com.alttd.ctf.game.GamePhase; import com.alttd.ctf.game.GamePhase;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap; import java.util.HashMap;
@@ -45,4 +47,73 @@ public class GameConfig extends AbstractConfig{
} }
} }
@SuppressWarnings("unused")
public static class WORLD_BORDER {
private static final String prefix = "world-border.";
public static double DEFAULT_SIZE = 140;
private static final HashMap<GamePhase, WorldBorderSettings> GAME_PHASE_WORLD_BORDER = new HashMap<>();
public static HashMap<GamePhase, WorldBorderSettings> getGAME_PHASE_WORLD_BORDER() {
return new HashMap<>(GAME_PHASE_WORLD_BORDER);
}
@SuppressWarnings("unused")
private static void load() {
GAME_PHASE_WORLD_BORDER.clear();
for (GamePhase phase : GamePhase.values()) {
String stringType = config.getString(prefix, phase.name().toLowerCase() + ".type", WorldBorderType.PLAYER.name());
double size = config.getDouble(prefix, phase.name().toLowerCase() + ".size", 10);
WorldBorderType worldBorderType;
try {
worldBorderType = WorldBorderType.valueOf(stringType);
} catch (IllegalArgumentException e) {
log.error("Invalid world border type [{}] in game phase world border config", stringType);
continue;
}
GAME_PHASE_WORLD_BORDER.put(phase, new WorldBorderSettings(worldBorderType, size));
log.debug("Set {} phase world border type to {} blocks", phase.name(), size);
}
DEFAULT_SIZE = config.getDouble(prefix, "default-size", DEFAULT_SIZE);
}
}
public static class FLAG {
private static final String prefix = "flag.";
public static String world = "world";
public static double x = 0;
public static double y = 0;
public static double z = 0;
public static double CAPTURE_RADIUS = 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() {
world = config.getString(prefix, "world", world);
x = config.getDouble(prefix, "x", x);
y = config.getDouble(prefix, "y", y);
z = config.getDouble(prefix, "z", z);
CAPTURE_RADIUS = config.getDouble(prefix, "capture-radius", CAPTURE_RADIUS);
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()));
}
}
public static class RESPAWN {
private static final String prefix = "respawn.";
public static int TIME = 10;
@SuppressWarnings("unused")
private static void load() {
TIME = config.getInt(prefix, "time", TIME);
}
}
} }
@@ -8,7 +8,7 @@ public class Messages extends AbstractConfig {
static Messages config; static Messages config;
Messages(Main main) { Messages(Main main) {
super(main, "config.yml"); super(main, "messages.yml");
} }
public static void reload(Main main) { public static void reload(Main main) {
@@ -27,6 +27,8 @@ public class Messages extends AbstractConfig {
public static String CREATE_TEAM = "<green>Create a team: <gold>/ctf createteam <team_name> <hex_color></gold></green>"; public static String CREATE_TEAM = "<green>Create a team: <gold>/ctf createteam <team_name> <hex_color></gold></green>";
public static String START = "<green>Start a new game: <gold>/ctf start <time_in_minutes></gold></green>"; 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 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") @SuppressWarnings("unused")
private static void load() { private static void load() {
@@ -37,6 +39,8 @@ public class Messages extends AbstractConfig {
CREATE_TEAM = config.getString(prefix, "create-team", CREATE_TEAM); CREATE_TEAM = config.getString(prefix, "create-team", CREATE_TEAM);
START = config.getString(prefix, "start", START); START = config.getString(prefix, "start", START);
SELECT_CLASS = config.getString(prefix, "select-class", SELECT_CLASS); 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,4 @@
package com.alttd.ctf.config;
public record WorldBorderSettings(WorldBorderType type, double size) {
}
@@ -0,0 +1,6 @@
package com.alttd.ctf.config;
public enum WorldBorderType {
FLAG,
PLAYER
}
@@ -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.Material;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; 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.PlayerAttemptPickupItemEvent;
import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerDropItemEvent;
@@ -26,4 +29,20 @@ public class InventoryItemInteractionEvent implements Listener {
event.setCancelled(true); 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);
}
} }
@@ -1,7 +1,18 @@
package com.alttd.ctf.events; package com.alttd.ctf.events;
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.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.stats.Stat;
import com.alttd.ctf.team.TeamPlayer; 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.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -10,12 +21,19 @@ import org.bukkit.event.player.PlayerRespawnEvent;
import java.util.Optional; import java.util.Optional;
@Slf4j
public class OnPlayerDeath implements Listener { public class OnPlayerDeath implements Listener {
private final GameManager gameManager; private final GameManager gameManager;
private final WorldBorderApi worldBorderApi;
private final Main main;
private final Flag flag;
public OnPlayerDeath(GameManager gameManager) { public OnPlayerDeath(GameManager gameManager, WorldBorderApi worldBorderApi, Main main, Flag flag) {
this.gameManager = gameManager; this.gameManager = gameManager;
this.worldBorderApi = worldBorderApi;
this.main = main;
this.flag = flag;
} }
@EventHandler @EventHandler
@@ -23,23 +41,43 @@ public class OnPlayerDeath implements Listener {
if (gameManager.getGamePhase().isEmpty()) { if (gameManager.getGamePhase().isEmpty()) {
return; return;
} }
event.deathMessage(null); event.deathMessage(Component.empty());
event.setShouldDropExperience(false); event.setShouldDropExperience(false);
Player player = event.getPlayer(); Player player = event.getPlayer();
player.getInventory().clear(); player.getInventory().clear();
player.updateInventory(); 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 @EventHandler
public void onPlayerRespawn(PlayerRespawnEvent event) { public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId()); Optional<GamePhase> gamePhase = gameManager.getGamePhase();
if (gamePhase.isEmpty()) {
log.warn("Player {} died while the game wasn't running", player.getName());
return;
}
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty()) { if (optionalTeamPlayer.isEmpty()) {
return; return;
} }
TeamPlayer teamPlayer = optionalTeamPlayer.get(); TeamPlayer teamPlayer = optionalTeamPlayer.get();
event.setRespawnLocation(teamPlayer.getTeam().getSpawnLocation()); event.setRespawnLocation(player.getWorld().getSpawnLocation());
teamPlayer.getGameClass().apply(teamPlayer); player.sendRichMessage("<red>You died</red><nl><green>You will respawn in <seconds> seconds.</green>",
Placeholder.component("nl", Component.newline()),
Placeholder.parsed("seconds", String.valueOf(GameConfig.RESPAWN.TIME)));
Bukkit.getScheduler().runTaskLater(main, () -> teamPlayer.getGameClass().apply(teamPlayer, worldBorderApi, gamePhase.get(), true), GameConfig.RESPAWN.TIME * 20L);//10 x 20 ticks aka 10 seconds
} }
} }
@@ -1,67 +0,0 @@
package com.alttd.ctf.events;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import java.util.Comparator;
import java.util.Optional;
@Slf4j
public class OnPlayerJoin implements Listener {
private final GameManager gameManager;
private final Flag flag;
public OnPlayerJoin(GameManager gameManager, Flag flag) {
this.gameManager = gameManager;
this.flag = flag;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
AttributeInstance maxHealthAttribute = player.getAttribute(Attribute.GENERIC_MAX_HEALTH);
if (maxHealthAttribute == null) {
log.error("Player does not have max health attribute");
return;
}
maxHealthAttribute.setBaseValue(20);
player.setHealth(20);
flag.addPlayer(player);
player.getInventory().clear();
player.updateInventory();
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.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());
}
}
@@ -0,0 +1,114 @@
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;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.Optional;
@Slf4j
public class OnPlayerOnlineStatus implements Listener {
private final GameManager gameManager;
private final Flag flag;
private final WorldBorderApi worldBorderApi;
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");
return;
}
maxHealthAttribute.setBaseValue(20);
player.setHealth(20);
flag.addPlayer(player);
player.getInventory().clear();
player.updateInventory();
}
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;
}
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);
}
});
}
@EventHandler
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,17 +3,28 @@ package com.alttd.ctf.events;
import com.alttd.ctf.game.GameManager; import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase; import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass; 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 com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Snowball; import org.bukkit.entity.Snowball;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent; 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.Optional;
import java.util.UUID;
@Slf4j @Slf4j
public class SnowballEvent implements Listener { public class SnowballEvent implements Listener {
@@ -26,35 +37,127 @@ public class SnowballEvent implements Listener {
@FunctionalInterface @FunctionalInterface
private interface SnowballHitConsumer { private interface SnowballHitConsumer {
void apply(Player hitPlayer, Player shooter, TeamPlayer shooterTeamPlayer); void apply(Player hitPlayer, Player shooter, TeamPlayer shooterTeamPlayer, Snowball snowball);
} }
@FunctionalInterface @FunctionalInterface
private interface SnowballThrownConsumer { private interface SnowballThrownConsumer {
void apply(Player shooter, TeamPlayer shooterTeamPlayer); void apply(Player shooter, TeamPlayer shooterTeamPlayer, Snowball snowball);
} }
@EventHandler @EventHandler
public void onSnowballHit(EntityDamageByEntityEvent event) { public void onSnowballHit(EntityDamageByEntityEvent event) {
handleSnowballHit(event, (hitPlayer, shooter, shooterTeamPlayer) -> { handleSnowballHit(event, (hitPlayer, shooter, shooterTeamPlayer, snowball) -> {
if (blockedAttack(hitPlayer, shooter, snowball)) {
playBlockSounds(hitPlayer, shooter);
return;
}
GameClass shooterClass = shooterTeamPlayer.getGameClass(); GameClass shooterClass = shooterTeamPlayer.getGameClass();
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed()); shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
double newHealth = hitPlayer.getHealth() - shooterClass.getDamage(); double newHealth = Math.max(hitPlayer.getHealth() - shooterClass.getDamage(), 0);
hitPlayer.setHealth(Math.max(newHealth, 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 {}", log.debug("{} health was set to {} because of a snowball thrown by {}",
hitPlayer.getName(), Math.max(newHealth, 0), shooter.getName()); 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 @EventHandler
public void onSnowballHitBySnowball(ProjectileLaunchEvent event) { public void onSnowballThrown(ProjectileLaunchEvent event) {
handleSnowballThrown(event, (shooter, shooterTeamPlayer) -> { handleSnowballThrown(event, (shooter, shooterTeamPlayer, snowball) -> {
GameClass shooterClass = shooterTeamPlayer.getGameClass(); 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()); shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
shooterTeamPlayer.increaseStat(Stat.SNOWBALLS_THROWN);
if (shooterClass instanceof Mage mage) {
mage.duplicateSnowBalls(shooter, 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();
Vector impactDirection = snowballLocation.toVector().subtract(playerLocation.toVector()).normalize();
double angle = playerFacing.angle(impactDirection);
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) { private void handleSnowballThrown(ProjectileLaunchEvent event, SnowballThrownConsumer consumer) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase(); Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) { if (optionalGamePhase.isEmpty()) {
@@ -69,24 +172,25 @@ public class SnowballEvent implements Listener {
return; return;
} }
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter.getUniqueId()); Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter);
if (teamPlayer.isEmpty()) { if (teamPlayer.isEmpty()) {
log.debug("The shooter that threw a snowball was not a team player"); log.debug("The shooter that threw a snowball was not a team player");
return; return;
} }
consumer.apply(shooter, teamPlayer.get()); consumer.apply(shooter, teamPlayer.get(), snowball);
} }
private void handleSnowballHit(EntityDamageByEntityEvent event, SnowballHitConsumer consumer) { private void handleSnowballHit(EntityDamageByEntityEvent event, SnowballHitConsumer consumer) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase(); Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) { 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; return;
} }
GamePhase gamePhase = optionalGamePhase.get(); GamePhase gamePhase = optionalGamePhase.get();
if (!gamePhase.equals(GamePhase.COMBAT)) { 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; return;
} }
@@ -96,7 +200,8 @@ public class SnowballEvent implements Listener {
} }
if (!(event.getDamager() instanceof org.bukkit.entity.Snowball snowball)) { 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; return;
} }
@@ -105,11 +210,22 @@ public class SnowballEvent implements Listener {
return; return;
} }
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter.getUniqueId()); Optional<TeamPlayer> teamPlayerShooter = gameManager.getTeamPlayer(shooter);
if (teamPlayer.isEmpty()) { if (teamPlayerShooter.isEmpty()) {
log.debug("The shooter that hit a player with a snowball was not a team player"); log.debug("The shooter that hit a player with a snowball was not a team player");
return; return;
} }
consumer.apply(hitPlayer, shooter, teamPlayer.get());
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().equals(teamPlayerShooter.get().getTeam().getId())) {
log.debug("The shooter hit a member of their own team");
return;
}
consumer.apply(hitPlayer, shooter, teamPlayerShooter.get(), snowball);
} }
} }
+185 -84
View File
@@ -1,17 +1,19 @@
package com.alttd.ctf.flag; package com.alttd.ctf.flag;
import com.alttd.ctf.Main; import com.alttd.ctf.Main;
import com.alttd.ctf.config.Config; import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.game.GameManager; import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.stats.Stat;
import com.alttd.ctf.team.Team; import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamColor; import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer; import com.alttd.ctf.team.TeamPlayer;
import lombok.RequiredArgsConstructor; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; 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.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.title.Title;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.boss.BarColor; import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle; import org.bukkit.boss.BarStyle;
@@ -20,31 +22,42 @@ import org.bukkit.boss.KeyedBossBar;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.potion.PotionEffect;
import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@RequiredArgsConstructor
public class Flag implements Runnable { public class Flag implements Runnable {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
private final HashMap<Integer, Integer> teamFlagPointCount = new HashMap<>(); private final HashMap<Integer, Integer> teamFlagPointCount = new HashMap<>();
private final ItemStack flagItem = new ItemStack(Material.CYAN_BANNER);
private final BossBar bossBar = createBossBar(); private final BossBar bossBar = createBossBar();
private final HashMap<Integer, Integer> wins = new HashMap<>(); private final HashMap<Integer, Integer> wins = new HashMap<>();
private int lastWinningTeamId = -1; private int lastWinningTeamId = -1;
private Location flagLocation; @Getter
private final Location flagLocation;
private Team winningTeam; private Team winningTeam;
private Player flagCarrier; private Player flagCarrier;
private final Main main; private final Main main;
private final GameManager gameManager; private final GameManager gameManager;
public Flag(Main main, GameManager gameManager) {
this.main = main;
this.gameManager = gameManager;
World world = Bukkit.getWorld(GameConfig.FLAG.world);
if (world == null) {
throw new IllegalStateException(String.format("Tried to spawn flag in world [%s] that doesn't exist", GameConfig.FLAG.world));
}
this.flagLocation = new Location(world, GameConfig.FLAG.x, GameConfig.FLAG.y, GameConfig.FLAG.z);
gameManager.getTeams().forEach(team -> team.setScore(0));
}
private BossBar createBossBar() { private BossBar createBossBar() {
NamespacedKey namespacedKey = NamespacedKey.fromString("ctf_flag", main); NamespacedKey namespacedKey = NamespacedKey.fromString("ctf_flag", main);
if (namespacedKey == null) { if (namespacedKey == null) {
@@ -60,10 +73,6 @@ public class Flag implements Runnable {
return captureProgress; return captureProgress;
} }
protected Location getFlagLocation() {
return flagLocation;
}
public void addPlayer(Player player) { public void addPlayer(Player player) {
bossBar.addPlayer(player); bossBar.addPlayer(player);
} }
@@ -72,27 +81,57 @@ public class Flag implements Runnable {
if (flagCarrier != null) { if (flagCarrier != null) {
return; return;
} }
//TODO knockback enemies from flag location to create space for person who captured mayb short speed boost and heal? Team team = teamPlayer.getTeam();
//TODO add de-buffs and enable buffs for others? flagLocation.getNearbyPlayers(5).forEach(nearbyPlayer -> {
player.getInventory().setItem(EquipmentSlot.HEAD, flagItem); if (nearbyPlayer.getUniqueId().equals(player.getUniqueId())) {
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(Material.AIR)); return;
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green><player> from <team> captured the flag!", TagResolver.resolver( }
Placeholder.component("player", player.displayName()), Optional<TeamPlayer> nearByTeamPlayer = gameManager.getTeamPlayer(nearbyPlayer);
Placeholder.component("team", teamPlayer.getTeam().getName()) if (nearByTeamPlayer.isEmpty()) {
))); return;
flagCarrier = player; } else if (nearByTeamPlayer.get().getTeam().equals(team)) {
teamFlagPointCount.clear(); return;
} }
public void spawnFlag() { Vector direction = nearbyPlayer.getLocation().toVector().subtract(flagLocation.toVector()).normalize();
//TODO disable flag capture for a minute or so, and maybe have it slowly drop down? direction.setY(0.2);
World world = Bukkit.getWorld(Config.FLAG.world); nearbyPlayer.setVelocity(direction.multiply(5));
if (world == null) { });
throw new IllegalStateException(String.format("Tried to spawn flag in world [%s] that doesn't exist", Config.FLAG.world)); 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(Material material) {
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(material));
}
private void spawnFlagParticleRing() {
Location center = flagLocation.clone();
World world = center.getWorld();
double radius = 0.7;
double gap = 0.2;
double circumference = 2 * Math.PI * radius;
int particleCount = (int) (circumference / gap);
Particle particle = Particle.DUST;
TeamColor color = winningTeam.getColor();
// Generate particle positions
for (double heightOffset = 0; heightOffset < 2; heightOffset += 0.5) {
center.setY(center.getY() + 0.5);
for (int i = 0; i < particleCount; i++) {
double angle = 2 * Math.PI * i / particleCount;
double x = center.getX() + radius * Math.cos(angle);
double z = center.getZ() + radius * Math.sin(angle);
double y = center.getY();
world.spawnParticle(particle, x, y, z, 1, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(color.r(), color.g(), color.b()), 1));
}
} }
this.flagLocation = new Location(world, Config.FLAG.x, Config.FLAG.y, Config.FLAG.z);
//Place block on main thread
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(flagItem.getType()));
} }
@Override @Override
@@ -101,11 +140,15 @@ public class Flag implements Runnable {
checkFlagCarrier(); checkFlagCarrier();
return; return;
} }
if (winningTeam != null) {
spawnFlagParticleRing();
return;
}
if (flagLocation == null) { if (flagLocation == null) {
log.warn("Tried to run Flag without a flag location, spawn it first"); log.warn("Tried to run Flag without a flag location, spawn it first");
return; return;
} }
spawnParticlesOnSquareBorder(flagLocation.getWorld(), flagLocation); spawnParticlesOnSquareBorder(flagLocation, GameConfig.FLAG.CAPTURE_RADIUS);
if (!updateScoreBasedOnNearbyPlayers().join()) { if (!updateScoreBasedOnNearbyPlayers().join()) {
return; //Score didn't change return; //Score didn't change
} }
@@ -117,36 +160,54 @@ public class Flag implements Runnable {
updateDisplay(); updateDisplay();
} else { } else {
winningTeam = optionalTeam.get(); winningTeam = optionalTeam.get();
bossBar.setVisible(false); spawnFlag(winningTeam.getFlagMaterial());
bossBar.setProgress(0); updateDisplay();
//TODO stop capture and let ppl know they can now capture the flag 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));
} }
} }
private void spawnParticlesOnSquareBorder(World world, Location center) { private void notifyAboutCapture() {
double size = 10; Bukkit.broadcast(miniMessage.deserialize("<player> is carrying the flag for <team>!",
Placeholder.component("player", flagCarrier.displayName()),
Placeholder.component("team", winningTeam.getName())));
Title capturingTeamTitle = Title.title(miniMessage.deserialize("<green><team> obtained the flag!</green>",
Placeholder.component("team", winningTeam.getName())),
miniMessage.deserialize("<green>protect <player> while they bring it to your base.</green>",
Placeholder.component("player", flagCarrier.displayName())));
Title huntingTeamTitle = Title.title(miniMessage.deserialize("<red><team> obtained the flag!</red>",
Placeholder.component("team", winningTeam.getName())),
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).ifPresent(team ->
player.showTitle(team.getId().intValue() == winningTeam.getId().intValue()
? capturingTeamTitle : huntingTeamTitle)));
}
private void spawnParticlesOnSquareBorder(Location center, double size) {
double step = 0.2; double step = 0.2;
center.add(0, 0.5, 0); World world = center.getWorld();
Location finalCenter = center.clone().add(0, 0.5, 0);
Bukkit.getScheduler().runTask(main, () -> { Bukkit.getScheduler().runTask(main, () -> {
// Top and Bottom (Z varies, X constant)
for (double z = -size; z <= size; z += step) { for (double z = -size; z <= size; z += step) {
world.spawnParticle(Particle.FLAME, center.getX() + size, center.getY(), center.getZ() + z, 1, 0, 0, 0, 0); world.spawnParticle(Particle.FLAME, finalCenter.getX() + size, finalCenter.getY(), finalCenter.getZ() + z, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, center.getX() - size, center.getY(), center.getZ() + z, 1, 0, 0, 0, 0); world.spawnParticle(Particle.FLAME, finalCenter.getX() - size, finalCenter.getY(), finalCenter.getZ() + z, 1, 0, 0, 0, 0);
} }
// Left and Right (X varies, Z constant)
for (double x = -size; x <= size; x += step) { for (double x = -size; x <= size; x += step) {
world.spawnParticle(Particle.FLAME, center.getX() + x, center.getY(), center.getZ() + size, 1, 0, 0, 0, 0); world.spawnParticle(Particle.FLAME, finalCenter.getX() + x, finalCenter.getY(), finalCenter.getZ() + size, 1, 0, 0, 0, 0);
world.spawnParticle(Particle.FLAME, center.getX() + x, center.getY(), center.getZ() - size, 1, 0, 0, 0, 0); world.spawnParticle(Particle.FLAME, finalCenter.getX() + x, finalCenter.getY(), finalCenter.getZ() - size, 1, 0, 0, 0, 0);
} }
}); });
} }
LinkedList<Location> particleTrail = new LinkedList<>(); private final LinkedList<Location> particleTrail = new LinkedList<>();
private void spawnTrail() { private void spawnTrail() {
TeamColor color = winningTeam.getColor(); TeamColor color = winningTeam.getColor();
particleTrail.forEach(location -> location.getWorld().spawnParticle(Particle.DUST, location, 1, 0, 0, 0, particleTrail.forEach(location -> location.getWorld().spawnParticle(Particle.DUST, location, 3, 0, 0, 0,
new Particle.DustOptions(Color.fromRGB(color.r(), color.g(), color.b()), 1))); new Particle.DustOptions(Color.fromRGB(color.r(), color.g(), color.b()), 1)));
if (particleTrail.size() > 15) { if (particleTrail.size() > 15) {
particleTrail.removeFirst(); particleTrail.removeFirst();
@@ -155,37 +216,46 @@ public class Flag implements Runnable {
private void checkFlagCarrier() { private void checkFlagCarrier() {
if (flagCarrier.isDead() || !flagCarrier.isOnline()) { if (flagCarrier.isDead() || !flagCarrier.isOnline()) {
flagCarrier = null; resetFlagCarrier();
particleTrail.clear(); spawnFlag(GameConfig.FLAG.MATERIAL);
spawnFlag();
return; return;
} }
double distance = winningTeam.getSpawnLocation().distance(flagCarrier.getLocation());
if (distance > 5) { double distance = winningTeam.getFlagTurnInLocation().distance(flagCarrier.getLocation());
if (distance > GameConfig.FLAG.TURN_IN_RADIUS) {
Location location = flagCarrier.getLocation(); Location location = flagCarrier.getLocation();
location.setY(location.getY() + 1); location.setY(location.getY() + 1);
particleTrail.add(location); particleTrail.add(location);
spawnTrail(); spawnTrail();
//TODO spawn some particles or something so a trail is made for specific classes to follow? spawnParticlesOnSquareBorder(winningTeam.getFlagTurnInLocation(), GameConfig.FLAG.TURN_IN_RADIUS);
return; return;
} }
//TODO better message? mayb with a text thing on the screen?
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<player> captured the flag for <team>!", notifyAboutTurnIn();
spawnFlag(GameConfig.FLAG.MATERIAL);
wins.merge(winningTeam.getId(), 1, Integer::sum);
wins.forEach((id, score) -> gameManager.getTeam(id).ifPresent(team -> team.setScore(score)));
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
gameManager.getTeamPlayer(flagCarrier)
.ifPresent(teamPlayer -> {
teamPlayer.getGameClass().setArmor(flagCarrier, teamPlayer);
teamPlayer.increaseStat(Stat.FLAGS_CAPTURED);
});
resetFlagCarrier();
}
private void notifyAboutTurnIn() {
Bukkit.broadcast(miniMessage.deserialize("<player> captured the flag for <team>!",
Placeholder.component("player", flagCarrier.displayName()), Placeholder.component("player", flagCarrier.displayName()),
Placeholder.component("team", winningTeam.getName()))); Placeholder.component("team", winningTeam.getName())));
spawnFlag(); Title title = Title.title(Component.empty(),
wins.merge(winningTeam.getId(), 1, Integer::sum); miniMessage.deserialize("<green><player> captured the flag for <team> team</green>",
winningTeam = null; Placeholder.component("player", flagCarrier.displayName()),
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(flagCarrier.getUniqueId()); Placeholder.component("team", winningTeam.getName())));
if (optionalTeamPlayer.isEmpty()) { Bukkit.getOnlinePlayers().forEach(player -> player.showTitle(title));
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
} else {
TeamPlayer teamPlayer = optionalTeamPlayer.get();
teamPlayer.getGameClass().setArmor(flagCarrier, teamPlayer);
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
}
flagCarrier = null;
particleTrail.clear();
} }
private Optional<Team> winnerExists() { private Optional<Team> winnerExists() {
@@ -194,7 +264,7 @@ public class Flag implements Runnable {
if (max.isEmpty()) { if (max.isEmpty()) {
return Optional.empty(); return Optional.empty();
} }
if (max.get().getValue() < 100) { if (max.get().getValue() < GameConfig.FLAG.CAPTURE_SCORE) {
return Optional.empty(); return Optional.empty();
} }
return gameManager.getTeam(max.get().getKey()); return gameManager.getTeam(max.get().getKey());
@@ -209,7 +279,7 @@ public class Flag implements Runnable {
private CompletableFuture<Boolean> updateScoreBasedOnNearbyPlayers() { private CompletableFuture<Boolean> updateScoreBasedOnNearbyPlayers() {
CompletableFuture<Boolean> future = new CompletableFuture<>(); CompletableFuture<Boolean> future = new CompletableFuture<>();
Bukkit.getScheduler().runTask(main, () -> { Bukkit.getScheduler().runTask(main, () -> {
Collection<Player> nearbyPlayers = flagLocation.getNearbyPlayers(10); Collection<Player> nearbyPlayers = flagLocation.getNearbyPlayers(GameConfig.FLAG.CAPTURE_RADIUS);
Bukkit.getScheduler().runTaskAsynchronously(main, () -> { Bukkit.getScheduler().runTaskAsynchronously(main, () -> {
boolean result = updateScoreBasedOnNearbyPlayers(nearbyPlayers); boolean result = updateScoreBasedOnNearbyPlayers(nearbyPlayers);
future.complete(result); future.complete(result);
@@ -220,7 +290,7 @@ public class Flag implements Runnable {
/** /**
* Updates the score of teams based on the nearby players within a specified range. * Updates the score of teams based on the nearby players within a specified range.
* This method identifies nearby players around the current location within a 10-block radius, * This method identifies nearby players around the current location within a CAPTURE_RADIUS-block radius,
* determines their respective teams, and calculates the team with the maximum number of players * determines their respective teams, and calculates the team with the maximum number of players
* in proximity. If there is a tie for the maximum count, the method exits without updating scores. * in proximity. If there is a tie for the maximum count, the method exits without updating scores.
* If a single team has the highest number of nearby players, scores are updated accordingly: * If a single team has the highest number of nearby players, scores are updated accordingly:
@@ -232,7 +302,7 @@ public class Flag implements Runnable {
*/ */
private boolean updateScoreBasedOnNearbyPlayers(Collection<Player> players) { private boolean updateScoreBasedOnNearbyPlayers(Collection<Player> players) {
List<TeamPlayer> nearbyPlayers = players.stream() List<TeamPlayer> nearbyPlayers = players.stream()
.map(player -> gameManager.getTeamPlayer(player.getUniqueId())) .map(gameManager::getTeamPlayer)
.filter(Optional::isPresent) .filter(Optional::isPresent)
.map(Optional::get) .map(Optional::get)
.toList(); .toList();
@@ -262,13 +332,15 @@ public class Flag implements Runnable {
} }
Team winningTeam = teamLongEntry.getKey(); Team winningTeam = teamLongEntry.getKey();
teamCounts.forEach((team, count) -> { teamFlagPointCount.putIfAbsent(winningTeam.getId(), 0);
teamFlagPointCount.merge(team.getId(), team.equals(winningTeam) ? 1 : -1, (oldValue, delta) -> { teamFlagPointCount.entrySet().forEach(entry -> {
int updatedValue = oldValue + delta; if (entry.getKey().equals(winningTeam.getId())) {
log.debug("Set count to {} for team {}", updatedValue, team.getId()); entry.setValue(entry.getValue() + 1);
return Math.max(updatedValue, 0); } else {
}); entry.setValue(Math.max(0, entry.getValue() - 1));
}
}); });
nearbyPlayers.forEach(teamPlayer -> teamPlayer.increaseStat(Stat.TIME_SPEND_CAPTURING_FLAG));
return true; return true;
} }
@@ -288,7 +360,7 @@ public class Flag implements Runnable {
bossBar.setTitle(String.format("Team %s is capturing the flag", PlainTextComponentSerializer.plainText().serialize(team.get().getName()))); bossBar.setTitle(String.format("Team %s is capturing the flag", PlainTextComponentSerializer.plainText().serialize(team.get().getName())));
lastWinningTeamId = highestKey; lastWinningTeamId = highestKey;
} }
bossBar.setProgress(Math.min(100, teamFlagPointCount.get(highestKey)) / 100.0); bossBar.setProgress(Math.min(GameConfig.FLAG.CAPTURE_SCORE, teamFlagPointCount.get(highestKey)) / (double) GameConfig.FLAG.CAPTURE_SCORE);
bossBar.setVisible(teamFlagPointCount.get(highestKey) > 0); bossBar.setVisible(teamFlagPointCount.get(highestKey) > 0);
} }
@@ -306,14 +378,43 @@ public class Flag implements Runnable {
return winsByTeam; return winsByTeam;
} }
public void reset() { public void resetFlagCarrier() {
final Player player = flagCarrier;
if (player != null) {
Bukkit.getScheduler().runTask(main, player::clearActivePotionEffects);
}
flagCarrier = null;
winningTeam = null;
particleTrail.clear();
}
public void resetFlag() {
bossBar.setVisible(false); bossBar.setVisible(false);
teamFlagPointCount.clear(); teamFlagPointCount.clear();
bossBar.setProgress(0); bossBar.setProgress(0);
winningTeam = null;
flagCarrier = null;
particleTrail.clear();
wins.clear();
lastWinningTeamId = -1; lastWinningTeamId = -1;
} }
public void resetAll() {
resetFlagCarrier();
resetFlag();
wins.clear();
gameManager.getTeams().forEach(team -> team.setScore(0));
}
public void handleCarrierDeathOrDisconnect(Player player) {
if (flagCarrier == null) {
return;
}
if (!flagCarrier.getUniqueId().equals(player.getUniqueId())) {
return;
}
resetFlagCarrier();
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()))),
() -> log.warn("A flag carrier died who was not part of a team"));
}
} }
@@ -9,7 +9,6 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.util.Optional; import java.util.Optional;
@@ -37,7 +36,7 @@ public class FlagTryCaptureEvent implements Listener {
return; return;
} }
Player player = event.getPlayer(); Player player = event.getPlayer();
Optional<TeamPlayer> teamPlayer = winningTeam.getPlayer(player.getUniqueId()); Optional<TeamPlayer> teamPlayer = winningTeam.getPlayer(player);
if (teamPlayer.isEmpty()) { if (teamPlayer.isEmpty()) {
return; return;
} }
@@ -1,13 +1,16 @@
package com.alttd.ctf.game; package com.alttd.ctf.game;
import com.alttd.ctf.Main;
import com.alttd.ctf.flag.Flag; import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.phases.ClassSelectionPhase; import com.alttd.ctf.game.phases.ClassSelectionPhase;
import com.alttd.ctf.game.phases.CombatPhase; import com.alttd.ctf.game.phases.CombatPhase;
import com.alttd.ctf.game.phases.EndedPhase; import com.alttd.ctf.game.phases.EndedPhase;
import com.alttd.ctf.game.phases.GatheringPhase; import com.alttd.ctf.game.phases.GatheringPhase;
import com.alttd.ctf.game_class.creation.FighterCreator; 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.Team;
import com.alttd.ctf.team.TeamPlayer; import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -23,29 +26,34 @@ public class GameManager {
private final HashMap<GamePhase, GamePhaseExecutor> phases; private final HashMap<GamePhase, GamePhaseExecutor> phases;
private RunningGame runningGame; private RunningGame runningGame;
private final HashMap<Integer, Team> teams = new HashMap<>(); private final HashMap<Integer, Team> teams = new HashMap<>();
private final HashMap<UUID, PlayerStat> playerStats = new HashMap<>();
public GameManager() { public GameManager(Main main, WorldBorderApi worldBorderApi) {
phases = new HashMap<>(); phases = new HashMap<>();
phases.put(GamePhase.CLASS_SELECTION, new ClassSelectionPhase(this, (FighterCreator::createFighter))); phases.put(GamePhase.CLASS_SELECTION, new ClassSelectionPhase(this, FighterCreator::createFighter, worldBorderApi));
phases.put(GamePhase.GATHERING, new GatheringPhase()); phases.put(GamePhase.GATHERING, new GatheringPhase(this, worldBorderApi));
phases.put(GamePhase.COMBAT, new CombatPhase()); phases.put(GamePhase.COMBAT, new CombatPhase());
phases.put(GamePhase.ENDED, new EndedPhase()); phases.put(GamePhase.ENDED, new EndedPhase(main, this));
} }
public Optional<GamePhase> getGamePhase() { public Optional<GamePhase> getGamePhase() {
return runningGame == null ? Optional.empty() : Optional.of(runningGame.getCurrentPhase()); 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); 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) { 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) { public void registerTeam(Team team) {
team.registerGameManger(this);
teams.put(team.getId(), team); teams.put(team.getId(), team);
} }
@@ -53,17 +61,17 @@ public class GameManager {
return teams.values(); return teams.values();
} }
public Optional<Team> getTeam(@NotNull UUID uuid) { public Optional<Team> getTeam(@NotNull Player player) {
return getTeams().stream().filter(filterTeam -> filterTeam.getPlayer(uuid).isPresent()).findAny(); return getTeams().stream().filter(filterTeam -> filterTeam.getPlayer(player).isPresent()).findAny();
} }
public Optional<Team> getTeam(int teamId) { public Optional<Team> getTeam(int teamId) {
return getTeams().stream().filter(filterTeam -> filterTeam.getId() == teamId).findAny(); 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() return getTeams().stream()
.map(team -> team.getPlayer(uuid)) .map(team -> team.getPlayer(player))
.filter(Optional::isPresent) .filter(Optional::isPresent)
.findFirst() .findFirst()
.orElseGet(Optional::empty); .orElseGet(Optional::empty);
@@ -76,7 +84,7 @@ public class GameManager {
executorService.shutdown(); executorService.shutdown();
executorService = Executors.newSingleThreadScheduledExecutor(); executorService = Executors.newSingleThreadScheduledExecutor();
} }
runningGame = new RunningGame(this, duration, flag); runningGame = new RunningGame(this, duration, flag, executorService);
executorService.scheduleAtFixedRate(runningGame, 0, 1, TimeUnit.SECONDS); executorService.scheduleAtFixedRate(runningGame, 0, 1, TimeUnit.SECONDS);
} }
@@ -98,4 +106,27 @@ public class GameManager {
.max() .max()
.orElse(0); .orElse(0);
} }
public boolean skipPhase() {
if (runningGame == null) {
return false;
}
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());
}
} }
@@ -6,5 +6,5 @@ public interface GamePhaseExecutor {
void start(Flag flag); void start(Flag flag);
void end(); void end(GamePhase nextGamePhase);
} }
@@ -2,6 +2,7 @@ package com.alttd.ctf.game;
import com.alttd.ctf.config.GameConfig; import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag; import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.team.TeamScoreboard;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -13,6 +14,8 @@ import javax.annotation.Nullable;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
@Slf4j @Slf4j
public class RunningGame implements Runnable { public class RunningGame implements Runnable {
@@ -20,60 +23,78 @@ public class RunningGame implements Runnable {
private final HashMap<GamePhase, Duration> phaseDurations = GameConfig.PHASES.getGAME_PHASE_DURATION(); private final HashMap<GamePhase, Duration> phaseDurations = GameConfig.PHASES.getGAME_PHASE_DURATION();
private final GameManager gameManager; private final GameManager gameManager;
private final Flag flag; private final Flag flag;
private final ScheduledExecutorService executorService;
@Getter @Getter
private GamePhase currentPhase = GamePhase.values()[0]; private GamePhase currentPhase = GamePhase.values()[0];
private Instant phaseStartTime = null; private Instant phaseStartTime = null;
private int lastMinuteBroadcast = 0; private int lastMinuteBroadcast = 0;
public RunningGame(GameManager gameManager, Duration gameDuration, Flag flag) { public RunningGame(GameManager gameManager, Duration gameDuration, Flag flag, ScheduledExecutorService executorService) {
this.gameManager = gameManager; this.gameManager = gameManager;
this.flag = flag; this.flag = flag;
this.executorService = executorService;
phaseDurations.put(GamePhase.COMBAT, gameDuration); phaseDurations.put(GamePhase.COMBAT, gameDuration);
} }
@Override @Override
public void run() { public void run() {
GamePhase nextPhase = GamePhase.values().length < currentPhase.ordinal() ? null : GamePhase.values()[currentPhase.ordinal() + 1]; try {
GamePhase nextPhase = (currentPhase.ordinal() + 1 < GamePhase.values().length) ? GamePhase.values()[currentPhase.ordinal() + 1] : null;
if (phaseStartTime == null) { if (phaseStartTime == null) {
phaseStartTime = Instant.now(); nextPhaseActions(null, currentPhase);
nextPhaseActions(null, currentPhase, nextPhase);
} }
if (Duration.between(phaseStartTime, Instant.now()).compareTo(phaseDurations.get(currentPhase)) >= 0) { if (nextPhase != null && Duration.between(phaseStartTime, Instant.now()).compareTo(phaseDurations.get(currentPhase)) >= 0) {
GamePhase previousPhase = currentPhase; nextPhaseActions(currentPhase, nextPhase);
currentPhase = GamePhase.values()[currentPhase.ordinal() + 1];
nextPhaseActions(previousPhase, currentPhase, nextPhase);
phaseStartTime = Instant.now(); phaseStartTime = Instant.now();
} else if (nextPhase != null) { } else if (nextPhase != null) {
broadcastNextPhaseStartTime(currentPhase, nextPhase); broadcastNextPhaseStartTime(currentPhase, nextPhase);
} else {
executorService.shutdown();
}
TeamScoreboard.refreshScoreboard(gameManager);
} catch (Exception e) {
log.error("Unexpected error in running game", e);
throw new RuntimeException(e);
} }
} }
private void nextPhaseActions(@Nullable GamePhase previousPhase, @NotNull GamePhase phase, @Nullable GamePhase nextPhase) { private void nextPhaseActions(@Nullable GamePhase previousPhase, @NotNull GamePhase phase) {
//TODO command to go to next phase this.currentPhase = phase;
this.phaseStartTime = Instant.now();
if (previousPhase != null) { if (previousPhase != null) {
gameManager.getPhaseExecutor(previousPhase).end(); gameManager.getPhaseExecutor(previousPhase).end(phase);
} }
gameManager.getPhaseExecutor(phase).start(flag); gameManager.getPhaseExecutor(phase).start(flag);
if (nextPhase != null) { if (phase.equals(GamePhase.ENDED)) {
broadcastNextPhaseStartTime(phase, nextPhase); executorService.shutdown();
} }
} }
private void broadcastNextPhaseStartTime(GamePhase currentPhase, GamePhase nextPhase) {//TODO check how this works/what it should do private static final List<Integer> NOTIFY_SECONDS = List.of(45, 30, 15, 10, 5, 3, 2, 1);
private void broadcastNextPhaseStartTime(GamePhase currentPhase, GamePhase nextPhase) {
//Remaining time for this phase //Remaining time for this phase
Duration duration = phaseDurations.get(currentPhase).minus(Duration.between(phaseStartTime, Instant.now())); 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 ((duration.toMinutes() > 1 && (duration.toMinutes() % 15 == 0 || duration.toMinutes() <= 5)) && duration.toSecondsPart() < 2) {
if (lastMinuteBroadcast == duration.toMinutes()) { if (lastMinuteBroadcast == duration.toMinutes()) {
return; return;
} }
lastMinuteBroadcast = (int) duration.toMinutes(); lastMinuteBroadcast = (int) duration.toMinutes();
if (nextPhase.equals(GamePhase.ENDED)) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(
"<green>The game will end in <bold><red>" + duration.toMinutes() + "</red> minutes</bold></green>"));
return;
}
Bukkit.broadcast(MiniMessage.miniMessage().deserialize( Bukkit.broadcast(MiniMessage.miniMessage().deserialize(
"<green>The <phase> will start in <bold><red>" + duration.toMinutes() + "</red> minutes</bold></green>", "<green>The <phase> will start in <bold><red>" + duration.toMinutes() + "</red> minutes</bold></green>",
Placeholder.component("phase", nextPhase.getDisplayName()) Placeholder.component("phase", nextPhase.getDisplayName())
)); ));
} else if (duration.toMinutes() < 1 && duration.toSeconds() % 15 == 0) { } else if (duration.toMinutes() < 1 && NOTIFY_SECONDS.contains(duration.toSecondsPart())) {
if (nextPhase.equals(GamePhase.ENDED)) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(
"<green>The game will end in <bold><red>" + duration.toSeconds() + "</red> seconds</bold></green>"));
return;
}
Bukkit.broadcast(MiniMessage.miniMessage().deserialize( Bukkit.broadcast(MiniMessage.miniMessage().deserialize(
"<green>The <phase> will start in <bold><red>" + duration.toSeconds() + "</red> seconds</bold></green>", "<green>The <phase> will start in <bold><red>" + duration.toSeconds() + "</red> seconds</bold></green>",
Placeholder.component("phase", nextPhase.getDisplayName()) Placeholder.component("phase", nextPhase.getDisplayName())
@@ -81,9 +102,23 @@ public class RunningGame implements Runnable {
} }
} }
public boolean skipCurrentPhase() {
GamePhase nextPhase = (currentPhase.ordinal() + 1 < GamePhase.values().length) ? GamePhase.values()[currentPhase.ordinal() + 1] : null;
if (nextPhase == null) {
log.warn("Tried to skip phase {} but there is no next phase", currentPhase);
return false;
}
nextPhaseActions(currentPhase, nextPhase);
return true;
}
public void end() { public void end() {
//TODO say the phase ended early? //TODO say the phase ended early?
currentPhase = GamePhase.ENDED; nextPhaseActions(currentPhase, GamePhase.ENDED);
nextPhaseActions(null, currentPhase, null); }
public Duration getRemainingTime() {
Duration duration = phaseDurations.get(currentPhase);
return duration.minus(Duration.between(phaseStartTime, Instant.now()));
} }
} }
@@ -1,19 +1,28 @@
package com.alttd.ctf.game.phases; package com.alttd.ctf.game.phases;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag; import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager; import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor; import com.alttd.ctf.game.GamePhaseExecutor;
import com.alttd.ctf.game_class.GameClass; import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.Team; import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamColor; import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer;
import com.alttd.ctf.util.CircularIterator; import com.alttd.ctf.util.CircularIterator;
import com.github.yannicklamprecht.worldborder.api.IWorldBorder;
import com.github.yannicklamprecht.worldborder.api.Position;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Optional; import java.util.Optional;
@Slf4j @Slf4j
@@ -21,61 +30,78 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
private final GameManager gameManager; private final GameManager gameManager;
private final DefaultClassCreator defaultClassCreator; private final DefaultClassCreator defaultClassCreator;
private final WorldBorderApi worldBorderApi;
@FunctionalInterface @FunctionalInterface
public interface DefaultClassCreator { public interface DefaultClassCreator {
@NotNull GameClass apply(TeamColor teamColor); @NotNull GameClass apply(TeamColor teamColor);
} }
public ClassSelectionPhase(@NotNull GameManager gameManager, DefaultClassCreator defaultClassCreator) { public ClassSelectionPhase(@NotNull GameManager gameManager, DefaultClassCreator defaultClassCreator,
@NotNull WorldBorderApi worldBorderApi) {
this.gameManager = gameManager; this.gameManager = gameManager;
this.defaultClassCreator = defaultClassCreator; this.defaultClassCreator = defaultClassCreator;
this.worldBorderApi = worldBorderApi;
} }
@Override @Override
public void start(Flag flag) { public void start(Flag flag) {
teleportPlayersToStartingZone(); Location flagLocation = flag.getFlagLocation();
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Select your class</green>")); IWorldBorder worldBorder = worldBorderApi.getWorldBorder(flagLocation.getWorld());
worldBorder.setCenter(Position.of(flagLocation));
worldBorder.setSize(GameConfig.WORLD_BORDER.DEFAULT_SIZE);
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Select your class with <gold>/ctf selectclass</gold></green>"));
CircularIterator<Team> teamCircularIterator = new CircularIterator<>(gameManager.getTeams()); CircularIterator<Team> teamCircularIterator = new CircularIterator<>(gameManager.getTeams());
if (teamCircularIterator.hasNext()) { if (teamCircularIterator.hasNext()) {
Bukkit.getOnlinePlayers().stream() ArrayList<? extends Player> players = new ArrayList<>(Bukkit.getOnlinePlayers());
.filter(player -> gameManager.getTeamPlayer(player.getUniqueId()).isEmpty()) Collections.shuffle(players);
players.stream()
.filter(player -> !player.hasPermission("ctf.bypass"))
.filter(player -> gameManager.getTeamPlayer(player).isEmpty())
.forEach(player -> { .forEach(player -> {
Team team = teamCircularIterator.next(); Team team = teamCircularIterator.next();
team.addPlayer(player.getUniqueId()); gameManager.registerPlayer(team, player);
player.sendRichMessage("You joined <team>!", Placeholder.component("team", team.getName())); player.sendRichMessage("You joined <team>!", Placeholder.component("team", team.getName()));
}); });
} else { } else {
log.warn("No teams to add players to"); log.warn("No teams to add players to");
} }
//TODO let players select classes teleportPlayersToStartingZone();
// They should always be able to do this when in their starting zone
// They should be locked into their starting zone until the next phase starts
// That phase should handle opening the zone
} }
private void teleportPlayersToStartingZone() { private void teleportPlayersToStartingZone() {
Bukkit.getOnlinePlayers().forEach(player -> { Bukkit.getOnlinePlayers().forEach(player -> {
Optional<Team> team = gameManager.getTeam(player.getUniqueId()); Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(player);
if (team.isEmpty()) { if (teamPlayer.isEmpty()) {
log.warn("{} is not a team player when teleporting to starting zone", player.getName());
return; return;
} }
Location spawnLocation = team.get().getSpawnLocation(); Optional<GamePhase> gamePhase = gameManager.getGamePhase();
player.teleportAsync(spawnLocation); if (gamePhase.isEmpty()) {
log.warn("Game phase is empty when teleporting to starting zone");
return;
}
teamPlayer.get().respawn(player, worldBorderApi, gamePhase.get());
}); });
} }
@Override @Override
public void end() { public void end(GamePhase nextPhase) {
gameManager.getTeams().forEach(team -> { gameManager.getTeams().forEach(team -> {
GameClass defaultClass = defaultClassCreator.apply(team.getColor()); GameClass defaultClass = defaultClassCreator.apply(team.getColor());
team.getPlayers().forEach(player -> { team.getPlayers().forEach(teamPlayer -> {
if (player.getGameClass() != null) { Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) {
log.debug("Tried to reset world border for offline player");
return; return;
} }
defaultClass.apply(player); teamPlayer.resetWorldBorder(player, worldBorderApi, nextPhase, team.getWorldBorderCenter());
if (teamPlayer.getGameClass() != null) {
return;
}
defaultClass.apply(teamPlayer, worldBorderApi, nextPhase, false);
}); });
}); });
//TODO expand world border so ppl can gather things but not get near the flag yet
} }
} }
@@ -1,6 +1,8 @@
package com.alttd.ctf.game.phases; package com.alttd.ctf.game.phases;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag; import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor; import com.alttd.ctf.game.GamePhaseExecutor;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -14,21 +16,24 @@ public class CombatPhase implements GamePhaseExecutor {
private ScheduledExecutorService executorService = null; private ScheduledExecutorService executorService = null;
@Override @Override
public void start(Flag flag) { public synchronized void start(Flag flag) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>CAPTURE THE FLAG</green>")); Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>CAPTURE THE FLAG</green>"));
flag.spawnFlag(); flag.spawnFlag(GameConfig.FLAG.MATERIAL);
if (executorService == null || !executorService.isShutdown()) { if (executorService == null) {
if (executorService != null) { executorService = Executors.newSingleThreadScheduledExecutor();
} else if (executorService.isTerminated() || executorService.isShutdown()) {
executorService = Executors.newSingleThreadScheduledExecutor();
} else {
executorService.shutdown(); executorService.shutdown();
}
executorService = Executors.newSingleThreadScheduledExecutor(); executorService = Executors.newSingleThreadScheduledExecutor();
} }
executorService.scheduleAtFixedRate(flag, 0, 1, TimeUnit.SECONDS); executorService.scheduleAtFixedRate(flag, 0, 1, TimeUnit.SECONDS);
// TODO Add players to bossbar list for capture progress(or maybe only when any progress is made)
} }
@Override @Override
public void end() { public void end(GamePhase ignored) {
if (executorService != null) {
executorService.shutdown(); executorService.shutdown();
} }
} }
}
@@ -1,7 +1,10 @@
package com.alttd.ctf.game.phases; package com.alttd.ctf.game.phases;
import com.alttd.ctf.config.Config; import com.alttd.ctf.Main;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag; 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.GamePhaseExecutor;
import com.alttd.ctf.team.Team; import com.alttd.ctf.team.Team;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -11,9 +14,11 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.attribute.Attribute; import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeInstance;
import org.bukkit.command.ConsoleCommandSender;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -24,6 +29,13 @@ import java.util.Map;
public class EndedPhase implements GamePhaseExecutor { public class EndedPhase implements GamePhaseExecutor {
private final MiniMessage miniMessage = MiniMessage.miniMessage(); 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 @Override
public void start(Flag flag) { public void start(Flag flag) {
@@ -31,7 +43,7 @@ public class EndedPhase implements GamePhaseExecutor {
HashMap<Team, Integer> wins = flag.getWins(); HashMap<Team, Integer> wins = flag.getWins();
Bukkit.broadcast(Component.join(JoinConfiguration.separator(Component.newline()), getWinnerMessages(wins))); Bukkit.broadcast(Component.join(JoinConfiguration.separator(Component.newline()), getWinnerMessages(wins)));
new Thread(() -> { new Thread(() -> {
World world = Bukkit.getWorld(Config.FLAG.world); World world = Bukkit.getWorld(GameConfig.FLAG.world);
if (world == null) { if (world == null) {
log.error("Invalid flag world defined"); log.error("Invalid flag world defined");
return; return;
@@ -49,9 +61,9 @@ public class EndedPhase implements GamePhaseExecutor {
maxHealthAttribute.setBaseValue(20); maxHealthAttribute.setBaseValue(20);
player.setHealth(20); player.setHealth(20);
}); });
flag.reset(); flag.resetAll();
gameManager.getTeams().forEach(team -> Bukkit.getOnlinePlayers().forEach(team::removePlayer));
}).start(); }).start();
// TODO reset world (coreprotect) to prep for next round
} }
private List<Component> getWinnerMessages(HashMap<Team, Integer> wins) { private List<Component> getWinnerMessages(HashMap<Team, Integer> wins) {
@@ -70,11 +82,10 @@ public class EndedPhase implements GamePhaseExecutor {
return messages; return messages;
} else if (topTeams.size() > 1) { // Draw scenario, multiple teams have the same top score } 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>")); messages.add(miniMessage.deserialize("<yellow>It's a draw! Top teams:</yellow>"));
topTeams.forEach(team -> { topTeams.forEach(team -> messages.add(
messages.add(miniMessage.deserialize("<team> had <score> captures.", miniMessage.deserialize("<team> had <score> captures.",
Placeholder.component("team", team.getName()), Placeholder.component("team", team.getName()),
Placeholder.parsed("score", String.valueOf(highestScore)))); Placeholder.parsed("score", String.valueOf(highestScore)))));
});
addOtherTeamsScore(wins, highestScore, messages); addOtherTeamsScore(wins, highestScore, messages);
return messages; return messages;
} else { // Single winner } else { // Single winner
@@ -82,6 +93,18 @@ public class EndedPhase implements GamePhaseExecutor {
messages.add(miniMessage.deserialize("<green><team> has won with <score> captures!</green>", messages.add(miniMessage.deserialize("<green><team> has won with <score> captures!</green>",
Placeholder.component("team", winner.getName()), Placeholder.component("team", winner.getName()),
Placeholder.parsed("score", String.valueOf(highestScore)))); 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) { if (wins.size() <= 1) {
return messages; return messages;
} }
@@ -101,7 +124,7 @@ public class EndedPhase implements GamePhaseExecutor {
} }
@Override @Override
public void end() { public void end(GamePhase ignored) {
} }
} }
@@ -1,20 +1,48 @@
package com.alttd.ctf.game.phases; package com.alttd.ctf.game.phases;
import com.alttd.ctf.flag.Flag; 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.GamePhaseExecutor;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@Slf4j
public class GatheringPhase implements GamePhaseExecutor { public class GatheringPhase implements GamePhaseExecutor {
private final GameManager gameManager;
private final WorldBorderApi worldBorderApi;
private Flag flag;
public GatheringPhase(GameManager gameManager, WorldBorderApi worldBorderApi) {
this.gameManager = gameManager;
this.worldBorderApi = worldBorderApi;
}
@Override @Override
public void start(Flag flag) { public void start(Flag flag) {
this.flag = flag;
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Gather materials and prepare for combat!</green>")); Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Gather materials and prepare for combat!</green>"));
//TODO give everyone haste or something so they can mine faster till this game phase ends //TODO give everyone haste or something so they can mine faster till this game phase ends
// Let them store things at base during this phase, after only one class can do this? // Let them store things at base during this phase, after only one class can do this?
} }
@Override @Override
public void end() { public void end(GamePhase nextPhase) {
//TODO Remove team area barrier if (flag == null) {
log.error("Unable to update world border due to missing Flag");
return;
}
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,7 +1,9 @@
package com.alttd.ctf.game_class; package com.alttd.ctf.game_class;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.team.TeamColor; import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer; import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@@ -60,15 +62,16 @@ public abstract class GameClass {
throw new IllegalArgumentException("Display item has no lore"); throw new IllegalArgumentException("Display item has no lore");
} }
ArrayList<Component> loreList = new ArrayList<>(lore); ArrayList<Component> loreList = new ArrayList<>(lore);
loreList.add(miniMessage.deserialize("<gold>Health: <health>, Damage: <damage></gold>", loreList.add(miniMessage.deserialize("<gold>Health: <health>, Damage: <damage> Throw speed: <throw_speed></gold>",
Placeholder.parsed("health", String.valueOf(health)), Placeholder.parsed("health", String.valueOf(health)),
Placeholder.parsed("damage", String.valueOf(damage)) Placeholder.parsed("damage", String.valueOf(damage)),
Placeholder.parsed("throw_speed", String.valueOf(throwTickSpeed))
)); ));
itemMeta.lore(loreList); itemMeta.lore(loreList);
displayItem.setItemMeta(itemMeta); displayItem.setItemMeta(itemMeta);
} }
public void apply(TeamPlayer teamPlayer) { public void apply(TeamPlayer teamPlayer, WorldBorderApi worldBorderApi, GamePhase gamePhase, boolean teleport) {
Player player = Bukkit.getPlayer(teamPlayer.getUuid()); Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) { if (player == null || !player.isOnline()) {
log.warn("Tried to give class to offline player {}", player == null ? teamPlayer.getUuid() : player.getName()); log.warn("Tried to give class to offline player {}", player == null ? teamPlayer.getUuid() : player.getName());
@@ -83,6 +86,7 @@ public abstract class GameClass {
player.setHealth(health); player.setHealth(health);
//Always reset the player inventory since other classes might have had them get items //Always reset the player inventory since other classes might have had them get items
player.getInventory().clear(); player.getInventory().clear();
player.clearActivePotionEffects();
player.getInventory().setContents(tools.toArray(ItemStack[]::new)); player.getInventory().setContents(tools.toArray(ItemStack[]::new));
TeamColor color = teamPlayer.getTeam().getColor(); TeamColor color = teamPlayer.getTeam().getColor();
@@ -91,7 +95,7 @@ public abstract class GameClass {
player.updateInventory(); player.updateInventory();
teamPlayer.setGameClass(this); teamPlayer.setGameClass(this);
player.sendRichMessage("You selected the <class_name> class", Placeholder.component("class_name", className)); player.sendRichMessage("You selected the <class_name> class", Placeholder.component("class_name", className));
player.teleportAsync(teamPlayer.getTeam().getSpawnLocation()); teamPlayer.respawn(player, worldBorderApi, gamePhase, teleport);
} }
private void setArmor(Player player, int r, int g, int b) { private void setArmor(Player player, int r, int g, int b) {
@@ -99,7 +103,7 @@ public abstract class GameClass {
ItemStack itemStack = new ItemStack(material); ItemStack itemStack = new ItemStack(material);
ItemMeta itemMeta = itemStack.getItemMeta(); ItemMeta itemMeta = itemStack.getItemMeta();
if (itemMeta instanceof LeatherArmorMeta leatherArmorMeta) { if (itemMeta instanceof LeatherArmorMeta leatherArmorMeta) {
leatherArmorMeta.setColor(Color.fromBGR(r, g, b)); leatherArmorMeta.setColor(Color.fromRGB(r, g, b));
itemStack.setItemMeta(leatherArmorMeta); itemStack.setItemMeta(leatherArmorMeta);
} }
return itemStack; return itemStack;
@@ -0,0 +1,29 @@
package com.alttd.ctf.game_class;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game_class.creation.*;
import com.alttd.ctf.team.Team;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class GameClassRetrieval {
public static HashMap<Integer, List<GameClass>> getGameClassesForAllTeams(GameManager gameManager) {
final HashMap<Integer, List<GameClass>> gameClasses = new HashMap<>();
gameManager.getTeams().forEach(team -> gameClasses.put(team.getId(), getGameClassesForTeam(team)));
return gameClasses;
}
public static List<GameClass> getGameClassesForTeam(Team team) {
final List<GameClass> gameClasses = new ArrayList<>();
gameClasses.add(FighterCreator.createFighter(team.getColor()));
gameClasses.add(TankCreator.createTank(team.getColor()));
gameClasses.add(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.GameClass;
import com.alttd.ctf.game_class.implementations.Engineer; import com.alttd.ctf.game_class.implementations.Engineer;
import com.alttd.ctf.game_class.implementations.Fighter;
import com.alttd.ctf.team.TeamColor; import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -57,6 +56,7 @@ public class EngineerCreator {
ItemMeta meta = shovel.getItemMeta(); ItemMeta meta = shovel.getItemMeta();
meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow excavator</color>", teamColor.hex()))); meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow excavator</color>", teamColor.hex())));
meta.addEnchant(Enchantment.EFFICIENCY, 4, false); meta.addEnchant(Enchantment.EFFICIENCY, 4, false);
meta.setUnbreakable(true);
shovel.setItemMeta(meta); shovel.setItemMeta(meta);
return shovel; return shovel;
} }
@@ -56,6 +56,7 @@ public class FighterCreator {
ItemMeta meta = shovel.getItemMeta(); ItemMeta meta = shovel.getItemMeta();
meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex()))); meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex())));
meta.addEnchant(Enchantment.EFFICIENCY, 1, false); meta.addEnchant(Enchantment.EFFICIENCY, 1, false);
meta.setUnbreakable(true);
shovel.setItemMeta(meta); shovel.setItemMeta(meta);
return shovel; return shovel;
} }
@@ -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; package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass; 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.game_class.implementations.Tank;
import com.alttd.ctf.team.TeamColor; import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -22,7 +21,7 @@ public class TankCreator {
private static final MiniMessage miniMessage = MiniMessage.miniMessage(); private static final MiniMessage miniMessage = MiniMessage.miniMessage();
@Contract("_ -> new") @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), return new Tank(getArmor(), getTools(teamColor), getDisplayItem(teamColor),
30, 7, 4); 30, 7, 4);
} }
@@ -43,6 +42,7 @@ public class TankCreator {
itemMeta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); itemMeta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE);
itemMeta.itemName(MiniMessage.miniMessage().deserialize( itemMeta.itemName(MiniMessage.miniMessage().deserialize(
String.format("<color:%s>Shield</color>", teamColor.hex()))); String.format("<color:%s>Shield</color>", teamColor.hex())));
itemMeta.setUnbreakable(true);
shield.setItemMeta(itemMeta); shield.setItemMeta(itemMeta);
return shield; return shield;
} }
@@ -51,6 +51,7 @@ public class TankCreator {
ItemStack shovel = new ItemStack(Material.WOODEN_SHOVEL); ItemStack shovel = new ItemStack(Material.WOODEN_SHOVEL);
ItemMeta meta = shovel.getItemMeta(); ItemMeta meta = shovel.getItemMeta();
meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex()))); meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex())));
meta.setUnbreakable(true);
shovel.setItemMeta(meta); shovel.setItemMeta(meta);
return shovel; return shovel;
} }
@@ -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; package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass; import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List; 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);
}
}
@@ -1,8 +1,13 @@
package com.alttd.ctf.gui; package com.alttd.ctf.gui;
import com.alttd.ctf.Main;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass; import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.TeamPlayer; import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Setter;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -11,17 +16,21 @@ import java.util.List;
public class ClassSelectionGUI extends GUIInventory { public class ClassSelectionGUI extends GUIInventory {
public ClassSelectionGUI(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer) { @Setter
private static Main main;
public ClassSelectionGUI(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer,
@NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
super(InventoryType.CHEST, teamPlayer.getTeam().getName().append(MiniMessage.miniMessage().deserialize(" - class selection"))); super(InventoryType.CHEST, teamPlayer.getTeam().getName().append(MiniMessage.miniMessage().deserialize(" - class selection")));
createClassSelection(gameClasses, teamPlayer); createClassSelection(gameClasses, teamPlayer, worldBorderApi, gamePhase);
} }
private void createClassSelection(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer) { private void createClassSelection(@NotNull List<GameClass> gameClasses, @NotNull TeamPlayer teamPlayer, @NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
int pos = (9 + (9 - gameClasses.size()) / 2); int pos = (9 + (9 - gameClasses.size()) / 2);
for (GameClass gameClass : gameClasses) { for (GameClass gameClass : gameClasses) {
setItem(pos++, gameClass.getDisplayItem(), player -> { setItem(pos++, gameClass.getDisplayItem(), player -> {
gameClass.apply(teamPlayer); gameClass.apply(teamPlayer, worldBorderApi, gamePhase, true);
player.closeInventory(InventoryCloseEvent.Reason.PLUGIN); Bukkit.getScheduler().runTask(main, () -> player.closeInventory(InventoryCloseEvent.Reason.PLUGIN));
}); });
} }
} }
-2
View File
@@ -2,8 +2,6 @@ package com.alttd.ctf.gui;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.MerchantInventory;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
@@ -1,19 +1,22 @@
package com.alttd.ctf.gui; package com.alttd.ctf.gui;
import com.alttd.ctf.Main;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.MerchantInventory;
import java.util.HashMap; import java.util.HashMap;
public abstract class GUIInventory implements GUI { public abstract class GUIInventory implements GUI {
@Setter
private static Main main;
@Getter @Getter
protected final Inventory inventory; protected final Inventory inventory;
protected final HashMap<Integer, GUIAction> guiActions; protected final HashMap<Integer, GUIAction> guiActions;
@@ -35,7 +38,7 @@ public abstract class GUIInventory implements GUI {
} }
public void open(Player player) { public void open(Player player) {
player.openInventory(inventory); Bukkit.getScheduler().runTask(main, () -> player.openInventory(inventory));
GUIByUUID.put(player.getUniqueId(), this); GUIByUUID.put(player.getUniqueId(), this);
} }
@@ -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();
});
}
}
+69 -12
View File
@@ -1,22 +1,28 @@
package com.alttd.ctf.team; 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.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
@Slf4j @Slf4j
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @RequiredArgsConstructor
public class Team { public class Team {
@JsonIgnore
private final TeamScoreboard scoreboard = new TeamScoreboard(this);
@JsonIgnore @JsonIgnore
private final HashMap<UUID, TeamPlayer> players = new HashMap<>(); private final HashMap<UUID, TeamPlayer> players = new HashMap<>();
@JsonProperty("name") @JsonProperty("name")
@@ -25,7 +31,8 @@ public class Team {
private Component name; private Component name;
@JsonProperty("id") @JsonProperty("id")
@Getter @Getter
private int id; @NotNull
private Integer id;
@JsonProperty("spawnLocation") @JsonProperty("spawnLocation")
@NotNull @NotNull
@Getter @Getter
@@ -33,20 +40,51 @@ public class Team {
@JsonProperty("worldBorderCenter") @JsonProperty("worldBorderCenter")
@NotNull @NotNull
@Getter @Getter
private Location worldBorderCenter; //TODO https://github.com/yannicklamprecht/WorldBorderAPI/blob/main/how-to-use.md private Location worldBorderCenter; // https://github.com/yannicklamprecht/WorldBorderAPI/blob/main/how-to-use.md
@JsonProperty("flagTurnInLocation")
@NotNull
@Getter
private Location flagTurnInLocation;
@JsonProperty("teamColor") @JsonProperty("teamColor")
@NotNull @NotNull
@Getter @Getter
private TeamColor color; 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) { public void registerGameManger(GameManager gameManager) {
TeamPlayer teamPlayer = new TeamPlayer(uuid, this); 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); 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; return teamPlayer;
} }
public Optional<TeamPlayer> getPlayer(@NotNull UUID uuid) { public Optional<TeamPlayer> getPlayer(@NotNull Player player) {
UUID uuid = player.getUniqueId();
if (!players.containsKey(uuid)) if (!players.containsKey(uuid))
return Optional.empty(); return Optional.empty();
return Optional.of(players.get(uuid)); return Optional.of(players.get(uuid));
@@ -56,11 +94,30 @@ public class Team {
return players.values(); return players.values();
} }
public void removePlayer(@NotNull UUID uuid) { public void removePlayer(@NotNull Player player) {
TeamPlayer remove = players.remove(uuid); removeFromScoreboard(player);
if (remove != null) { TeamPlayer remove = players.remove(player.getUniqueId());
log.debug("Removed player with uuid {} from team with id {}", uuid, id); 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 @Override
@@ -1,23 +1,78 @@
package com.alttd.ctf.team; package com.alttd.ctf.team;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.config.WorldBorderSettings;
import com.alttd.ctf.config.WorldBorderType;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass; import com.alttd.ctf.game_class.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.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.*;
import java.util.UUID;
@Slf4j
@Getter @Getter
public class TeamPlayer { public class TeamPlayer {
private final PlayerStat playerStat;
private final UUID uuid; private final UUID uuid;
private final Team team; private final Team team;
@Setter @Setter
private GameClass gameClass; private GameClass gameClass;
private boolean isDead = false;
protected TeamPlayer(UUID uuid, Team team) { protected TeamPlayer(Player player, Team team, PlayerStat playerStat) {
this.uuid = uuid; this.uuid = player.getUniqueId();
this.team = team; this.team = team;
this.playerStat = playerStat;
}
public void respawn(@NotNull Player player, @NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
respawn(player, worldBorderApi, gamePhase, true);
}
public void respawn(Player player, WorldBorderApi worldBorderApi, GamePhase gamePhase, boolean teleport) {
Location spawnLocation = team.getSpawnLocation();
Location worldBorderCenter = team.getWorldBorderCenter();
List<GameClass> gameClasses = GameClassRetrieval.getGameClassesForTeam(team);
new ClassSelectionGUI(gameClasses, this, worldBorderApi, GamePhase.CLASS_SELECTION).open(player);
if (!teleport) {
resetWorldBorder(player, worldBorderApi, gamePhase, worldBorderCenter);
return;
}
player.teleportAsync(spawnLocation).thenAcceptAsync(unused ->
resetWorldBorder(player, worldBorderApi, gamePhase, worldBorderCenter));
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");
}
if (worldBorderSettings.type().equals(WorldBorderType.FLAG)) {
worldBorderApi.resetWorldBorderToGlobal(player);
return;
}
worldBorderApi.setBorder(player, worldBorderSettings.size(), worldBorderCenter);
} }
@Override @Override
@@ -36,4 +91,12 @@ public class TeamPlayer {
public int hashCode() { public int hashCode() {
return Objects.hash(uuid); 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 @@
#Sat Feb 08 00:30:51 CET 2025 #Sun Mar 02 00:42:30 CET 2025
buildNumber=11 buildNumber=112
version=0.1 version=0.1