Compare commits

..
25 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
32 changed files with 733 additions and 162 deletions
+58 -15
View File
@@ -4,18 +4,17 @@ import com.alttd.ctf.commands.CommandManager;
import com.alttd.ctf.config.Config;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.config.Messages;
import com.alttd.ctf.events.InventoryItemInteractionEvent;
import com.alttd.ctf.events.OnPlayerDeath;
import com.alttd.ctf.events.OnPlayerOnlineStatus;
import com.alttd.ctf.events.SnowballEvent;
import com.alttd.ctf.events.*;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.flag.FlagTryCaptureEvent;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.phases.EndedPhase;
import com.alttd.ctf.gui.ClassSelectionGUI;
import com.alttd.ctf.gui.GUIInventory;
import com.alttd.ctf.gui.GUIListener;
import com.alttd.ctf.json_config.JacksonConfig;
import com.alttd.ctf.json_config.JsonConfigManager;
import com.alttd.ctf.stats.PlayerStat;
import com.alttd.ctf.team.Team;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
@@ -29,16 +28,18 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
@Slf4j
public class Main extends JavaPlugin {
private GameManager gameManager = null;
private Flag flag;
@Override
public void onEnable() {
PlayerStat.main = this;
GUIInventory.setMain(this); // sorry
ClassSelectionGUI.setMain(this); // sorry
Package pkg = Main.class.getPackage();
@@ -47,18 +48,13 @@ public class Main extends JavaPlugin {
reloadConfigs();
WorldBorderApi worldBorderApi = worldBorder();
this.gameManager = new GameManager(worldBorderApi);
this.gameManager = new GameManager(this, worldBorderApi);
registerTeams(); //Skipped in reloadConfig if gameManager is not created yet
flag = new Flag(this, gameManager);
loadPlayerStats(); //Skipped in reloadConfig if gameManager is not created yet
Flag flag = new Flag(this, gameManager);
new CommandManager(this, gameManager, flag, worldBorderApi);
//Ensuring immediate respawn is on in all worlds
log.info("Enabling immediate respawn for {}.", GameConfig.FLAG.world);
World world = Bukkit.getWorld(GameConfig.FLAG.world);
if (world != null) {
world.setGameRule(GameRule.DO_IMMEDIATE_RESPAWN, true);
} else {
log.error("No valid flag world defined, unable to modify game rules");
}
enableImmediateRespawn();
registerEvents(flag, worldBorderApi);
}
@@ -68,6 +64,7 @@ public class Main extends JavaPlugin {
GameConfig.reload(this);
if (gameManager != null) {
registerTeams();
loadPlayerStats();
}
}
@@ -83,9 +80,20 @@ public class Main extends JavaPlugin {
return worldBorderApiRegisteredServiceProvider.getProvider();
}
private void enableImmediateRespawn() {
log.info("Enabling immediate respawn for {}.", GameConfig.FLAG.world);
World world = Bukkit.getWorld(GameConfig.FLAG.world);
if (world != null) {
world.setGameRule(GameRule.DO_IMMEDIATE_RESPAWN, true);
} else {
log.error("No valid flag world defined, unable to modify game rules");
}
}
private void registerEvents(Flag flag, WorldBorderApi worldBorderApi) {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new SnowballEvent(gameManager), this);
pluginManager.registerEvents(new OtherGameEvents(gameManager), this);
pluginManager.registerEvents(new FlagTryCaptureEvent(flag), this);
pluginManager.registerEvents(new OnPlayerDeath(gameManager, worldBorderApi, this, flag), this);
pluginManager.registerEvents(new InventoryItemInteractionEvent(), this);
@@ -119,4 +127,39 @@ public class Main extends JavaPlugin {
teams.forEach(gameManager::registerTeam);
}
}
private void loadPlayerStats() {
JsonConfigManager<PlayerStat> config = new JsonConfigManager<>(JacksonConfig.configureMapper());
List<PlayerStat> playerStats;
try {
File playerStatsDirectory = new File(getDataFolder(), "player_stats");
if (!playerStatsDirectory.exists() && !playerStatsDirectory.mkdirs()) {
log.error("Unable to make playerStats directory at {} shutting down plugin", playerStatsDirectory.getAbsolutePath());
}
playerStats = config.loadConfigs(PlayerStat.class, playerStatsDirectory);
} catch (IOException e) {
log.error("Unable to load teams, shutting down plugin", e);
getServer().getPluginManager().disablePlugin(this);
return;
}
gameManager.setPlayerStats(playerStats);
final JsonConfigManager<PlayerStat> jsonConfigManager = new JsonConfigManager<>(JacksonConfig.configureMapper());
Runnable runnable = () -> {
log.info("Saving player stats");
File playerStatsDirectory = new File(getDataFolder(), "player_stats");
gameManager.getPlayerStats().stream().filter(PlayerStat::isTouched).forEach(playerStat -> {
try {
jsonConfigManager.saveConfig(playerStat, playerStatsDirectory, playerStat.getUuid().toString());
playerStat.setUnTouched();
} catch (IOException e) {
log.error("Failed to save player stats for [{}].", playerStat.getUuid(), e);
}
});
};
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(runnable, 0, 1, java.util.concurrent.TimeUnit.MINUTES);
}
}
@@ -38,6 +38,7 @@ public class CommandManager implements CommandExecutor, TabExecutor {
new SkipPhase(gameManager),
new Start(gameManager, flag),
new CreateTeam(main, gameManager),
new HighestStat(gameManager),
new SelectClass(gameManager, worldBorderApi),
new Reload(main)
);
@@ -120,4 +121,4 @@ public class CommandManager implements CommandExecutor, TabExecutor {
.findFirst()
.orElse(null);
}
}
}
@@ -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;
}
}
@@ -48,7 +48,7 @@ public class SelectClass extends SubCommand {
TeamPlayer teamPlayer = optionalTeamPlayer.get();
if (!teamPlayer.isDead()
&& !gamePhase.equals(GamePhase.CLASS_SELECTION)
&& teamPlayer.getTeam().getSpawnLocation().distance(player.getLocation()) > 5) {
&& teamPlayer.getTeam().getSpawnLocation().distance(player.getLocation()) > 10) {
commandSender.sendRichMessage("<red>You have to be near your spawn to change classes.</red>");
return 0;
}
@@ -3,16 +3,10 @@ package com.alttd.ctf.commands.subcommands;
import com.alttd.ctf.commands.SubCommand;
import com.alttd.ctf.config.Messages;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.team.Team;
import lombok.AllArgsConstructor;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Optional;
@AllArgsConstructor
public class SkipPhase extends SubCommand {
@@ -4,7 +4,6 @@ import com.alttd.ctf.Main;
import com.alttd.ctf.game.GamePhase;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
@@ -91,7 +90,7 @@ public class GameConfig extends AbstractConfig {
public static double CAPTURE_RADIUS = 5;
public static int CAPTURE_SCORE = 50;
public static double TURN_IN_RADIUS = 3;
public static @NotNull Material MATERIAL = Material.RED_BANNER;
public static @NotNull Material MATERIAL = Material.BLACK_BANNER;
@SuppressWarnings("unused")
private static void load() {
@@ -28,6 +28,7 @@ public class Messages extends AbstractConfig {
public static String START = "<green>Start a new game: <gold>/ctf start <time_in_minutes></gold></green>";
public static String SELECT_CLASS = "<green>Open class selection: <gold>/ctf selectclass</gold></green>";
public static String SKIP_PHASE = "<green>Skip the current phase: <gold>/ctf skipphase</gold></green>";
public static String HIGHEST_STAT = "<green>Display the highest stat and the player: <gold>/ctf higheststat</gold></green>";
@SuppressWarnings("unused")
private static void load() {
@@ -39,6 +40,7 @@ public class Messages extends AbstractConfig {
START = config.getString(prefix, "start", START);
SELECT_CLASS = config.getString(prefix, "select-class", SELECT_CLASS);
SKIP_PHASE = config.getString(prefix, "skip-phase", SKIP_PHASE);
HIGHEST_STAT = config.getString(prefix, "highest-stat", HIGHEST_STAT);
}
}
@@ -40,8 +40,12 @@ public class OnPlayerOnlineStatus implements Listener {
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
resetPlayer(player);
handleRunningGame(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) {
@@ -61,7 +65,7 @@ public class OnPlayerOnlineStatus implements Listener {
log.error("No team found when attempting to add freshly joined player to a team");
return;
}
teamPlayer = min.get().addPlayer(player);
teamPlayer = gameManager.registerPlayer(min.get(), player);
} else {
teamPlayer = optionalTeamPlayer.get();
teamPlayer.getTeam().addToScoreboard(player);
@@ -1,6 +1,7 @@
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;
@@ -12,6 +13,7 @@ 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;
@@ -29,7 +31,8 @@ public class OtherGameEvents implements Listener {
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
if (gameManager.getGamePhase().isEmpty()) {
Optional<GamePhase> gamePhase = gameManager.getGamePhase();
if (gamePhase.isEmpty() || gamePhase.get() == GamePhase.ENDED) {
return;
}
if (!Tag.SNOW.isTagged(event.getBlock().getType())) {
@@ -41,8 +44,9 @@ public class OtherGameEvents implements Listener {
}
@EventHandler
public void onBlockPlace(BlockBreakEvent event) {
if (gameManager.getGamePhase().isEmpty()) {
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())) {
@@ -3,11 +3,14 @@ package com.alttd.ctf.events;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Mage;
import com.alttd.ctf.stats.Stat;
import com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.entity.Snowball;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -17,7 +20,11 @@ import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Optional;
import java.util.UUID;
@Slf4j
public class SnowballEvent implements Listener {
@@ -35,13 +42,14 @@ public class SnowballEvent implements Listener {
@FunctionalInterface
private interface SnowballThrownConsumer {
void apply(Player shooter, TeamPlayer shooterTeamPlayer);
void apply(Player shooter, TeamPlayer shooterTeamPlayer, Snowball snowball);
}
@EventHandler
public void onSnowballHit(EntityDamageByEntityEvent event) {
handleSnowballHit(event, (hitPlayer, shooter, shooterTeamPlayer, snowball) -> {
if (blockedAttack(hitPlayer, snowball)) { //Disable damage when it is blocked
if (blockedAttack(hitPlayer, shooter, snowball)) {
playBlockSounds(hitPlayer, shooter);
return;
}
GameClass shooterClass = shooterTeamPlayer.getGameClass();
@@ -56,22 +64,59 @@ public class SnowballEvent implements Listener {
}
log.debug("{} health was set to {} because of a snowball thrown by {}",
hitPlayer.getName(), Math.max(newHealth, 0), shooter.getName());
applyDamageEffects(hitPlayer, shooter);
});
}
private void playBlockSounds(Player hitPlayer, Player shooter) {
hitPlayer.playSound(hitPlayer.getLocation(), Sound.ITEM_SHIELD_BLOCK, 1.0f, 1.5f);
shooter.playSound(shooter.getLocation(), Sound.ITEM_SHIELD_BLOCK, 1.0f, 1.5f);
}
private void applyDamageEffects(Player hitPlayer, Player shooter) {
hitPlayer.getWorld().playSound(hitPlayer.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 1.0f);
hitPlayer.getWorld().spawnParticle(
Particle.DAMAGE_INDICATOR,
hitPlayer.getLocation().add(0, 1, 0), 10, 0.5, 0.5, 0.5, 0.1
);
shooter.playSound(shooter.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 1.5f);
}
@EventHandler
public void onSnowballThrown(ProjectileLaunchEvent event) {
handleSnowballThrown(event, (shooter, shooterTeamPlayer) -> {
handleSnowballThrown(event, (shooter, shooterTeamPlayer, snowball) -> {
GameClass shooterClass = shooterTeamPlayer.getGameClass();
if (shooterClass == null) {
shooter.sendRichMessage("<red>You appear to not have selected a class, please do so by going to spawn and using /ctf selectclass</red>");
event.setCancelled(true);
return;
}
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
shooterTeamPlayer.increaseStat(Stat.SNOWBALLS_THROWN);
if (shooterClass instanceof Mage mage) {
mage.duplicateSnowBalls(shooter, snowball);
}
});
}
private boolean blockedAttack(@NotNull Player hitPlayer, @NotNull Snowball snowball) {
private final HashMap<UUID, LinkedList<Instant>> lastTankHits = new HashMap<>();
private boolean blockedAttack(@NotNull Player hitPlayer, @NotNull Player shooter, @NotNull Snowball snowball) {
if (!hitPlayer.isBlocking()) {
return false;
}
if (reachedMaxHits(hitPlayer)) {
hitPlayer.setCooldown(Material.SHIELD, 40);
if (hitPlayer.getInventory().getItemInMainHand().getType().equals(Material.SHIELD)) {
hitPlayer.swingMainHand();
} else if (hitPlayer.getInventory().getItemInOffHand().getType().equals(Material.SHIELD)) {
hitPlayer.swingOffHand();
}
applyBlockBrokenEffects(hitPlayer, shooter);
return false;
}
Location playerLocation = hitPlayer.getLocation();
Vector playerFacing = playerLocation.getDirection().normalize();
Location snowballLocation = snowball.getLocation();
@@ -82,6 +127,37 @@ public class SnowballEvent implements Listener {
return !(Math.toDegrees(angle) > 80); //Blocked if the angle was <= 80
}
private void applyBlockBrokenEffects(@NotNull Player hitPlayer, @NotNull Player shooter) {
hitPlayer.getWorld().playSound(hitPlayer.getLocation(), Sound.ITEM_SHIELD_BREAK, 1.0f, 1.0f);
Location shieldLocation = hitPlayer.getLocation()
.add(hitPlayer.getLocation().getDirection().normalize().multiply(0.5))
.add(0, 0.5, 0);
hitPlayer.getWorld().spawnParticle(
Particle.CRIT,
shieldLocation, 10, 0.5, 0.5, 0.5, 0.1
);
shooter.playSound(shooter.getLocation(), Sound.ITEM_SHIELD_BREAK, 1.0f, 1.5f);
}
private boolean reachedMaxHits(@NotNull Player hitPlayer) {
boolean reachedMaxHits = false;
Instant now = Instant.now();
LinkedList<Instant> lastHits = lastTankHits.getOrDefault(hitPlayer.getUniqueId(), new LinkedList<>());
Instant cutoffTime = now.minusMillis(300);
while (!lastHits.isEmpty() && lastHits.peek().isBefore(cutoffTime)) {
lastHits.poll();
}
lastHits.addLast(now);
if (lastHits.size() >= 3) {
lastHits.clear();
reachedMaxHits = true;
}
lastTankHits.put(hitPlayer.getUniqueId(), lastHits);
return reachedMaxHits;
}
private void handleSnowballThrown(ProjectileLaunchEvent event, SnowballThrownConsumer consumer) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) {
@@ -101,7 +177,7 @@ public class SnowballEvent implements Listener {
log.debug("The shooter that threw a snowball was not a team player");
return;
}
consumer.apply(shooter, teamPlayer.get());
consumer.apply(shooter, teamPlayer.get(), snowball);
}
private void handleSnowballHit(EntityDamageByEntityEvent event, SnowballHitConsumer consumer) {
@@ -146,10 +222,10 @@ public class SnowballEvent implements Listener {
return;
}
if (teamPlayerHit.get().getTeam().getId() == teamPlayerShooter.get().getTeam().getId()) {
if (teamPlayerHit.get().getTeam().getId().equals(teamPlayerShooter.get().getTeam().getId())) {
log.debug("The shooter hit a member of their own team");
return;
}
consumer.apply(hitPlayer, shooter, teamPlayerHit.get(), snowball);
consumer.apply(hitPlayer, shooter, teamPlayerShooter.get(), snowball);
}
}
+25 -9
View File
@@ -24,6 +24,7 @@ import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@@ -80,8 +81,22 @@ public class Flag implements Runnable {
if (flagCarrier != null) {
return;
}
//TODO knockback enemies from flag location to create space for person who captured mayb short speed boost and heal?
//TODO add de-buffs and enable buffs for others?
Team team = teamPlayer.getTeam();
flagLocation.getNearbyPlayers(5).forEach(nearbyPlayer -> {
if (nearbyPlayer.getUniqueId().equals(player.getUniqueId())) {
return;
}
Optional<TeamPlayer> nearByTeamPlayer = gameManager.getTeamPlayer(nearbyPlayer);
if (nearByTeamPlayer.isEmpty()) {
return;
} else if (nearByTeamPlayer.get().getTeam().equals(team)) {
return;
}
Vector direction = nearbyPlayer.getLocation().toVector().subtract(flagLocation.toVector()).normalize();
direction.setY(0.2);
nearbyPlayer.setVelocity(direction.multiply(5));
});
player.getInventory().setItem(EquipmentSlot.HEAD, new ItemStack(teamPlayer.getTeam().getFlagMaterial()));
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(Material.AIR));
flagCarrier = player;
@@ -188,7 +203,7 @@ public class Flag implements Runnable {
});
}
LinkedList<Location> particleTrail = new LinkedList<>();
private final LinkedList<Location> particleTrail = new LinkedList<>();
private void spawnTrail() {
TeamColor color = winningTeam.getColor();
@@ -317,12 +332,13 @@ public class Flag implements Runnable {
}
Team winningTeam = teamLongEntry.getKey();
teamCounts.forEach((team, count) -> {
teamFlagPointCount.merge(team.getId(), team.equals(winningTeam) ? 1 : -1, (oldValue, delta) -> {
int updatedValue = oldValue + delta;
log.debug("Set count to {} for team {}", updatedValue, team.getId());
return Math.max(updatedValue, 0);
});
teamFlagPointCount.putIfAbsent(winningTeam.getId(), 0);
teamFlagPointCount.entrySet().forEach(entry -> {
if (entry.getKey().equals(winningTeam.getId())) {
entry.setValue(entry.getValue() + 1);
} else {
entry.setValue(Math.max(0, entry.getValue() - 1));
}
});
nearbyPlayers.forEach(teamPlayer -> teamPlayer.increaseStat(Stat.TIME_SPEND_CAPTURING_FLAG));
return true;
@@ -9,7 +9,6 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.util.Optional;
@@ -1,11 +1,13 @@
package com.alttd.ctf.game;
import com.alttd.ctf.Main;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.phases.ClassSelectionPhase;
import com.alttd.ctf.game.phases.CombatPhase;
import com.alttd.ctf.game.phases.EndedPhase;
import com.alttd.ctf.game.phases.GatheringPhase;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.stats.PlayerStat;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
@@ -24,22 +26,26 @@ public class GameManager {
private final HashMap<GamePhase, GamePhaseExecutor> phases;
private RunningGame runningGame;
private final HashMap<Integer, Team> teams = new HashMap<>();
private final HashMap<UUID, PlayerStat> playerStats = new HashMap<>();
public GameManager(WorldBorderApi worldBorderApi) {
public GameManager(Main main, WorldBorderApi worldBorderApi) {
phases = new HashMap<>();
phases.put(GamePhase.CLASS_SELECTION, new ClassSelectionPhase(this, FighterCreator::createFighter, worldBorderApi));
phases.put(GamePhase.GATHERING, new GatheringPhase(this, worldBorderApi));
phases.put(GamePhase.COMBAT, new CombatPhase());
phases.put(GamePhase.ENDED, new EndedPhase());
phases.put(GamePhase.ENDED, new EndedPhase(main, this));
}
public Optional<GamePhase> getGamePhase() {
return runningGame == null ? Optional.empty() : Optional.of(runningGame.getCurrentPhase());
}
public void registerPlayer(Team team, Player player) {
public TeamPlayer registerPlayer(Team team, Player player) {
unregisterPlayer(player);
teams.get(team.getId()).addPlayer(player);
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) {
@@ -107,4 +113,20 @@ public class GameManager {
}
return runningGame.skipCurrentPhase();
}
public void setPlayerStats(List<PlayerStat> playerStats) {
this.playerStats.clear();
playerStats.forEach(playerStat -> this.playerStats.put(playerStat.getUuid(), playerStat));
}
public Collection<PlayerStat> getPlayerStats() {
return this.playerStats.values();
}
public Optional<Duration> getRemainingTime() {
if (runningGame == null || runningGame.getCurrentPhase().equals(GamePhase.ENDED)) {
return Optional.empty();
}
return Optional.of(runningGame.getRemainingTime());
}
}
@@ -2,6 +2,7 @@ package com.alttd.ctf.game;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.team.TeamScoreboard;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -51,6 +52,7 @@ public class RunningGame implements Runnable {
} else {
executorService.shutdown();
}
TeamScoreboard.refreshScoreboard(gameManager);
} catch (Exception e) {
log.error("Unexpected error in running game", e);
throw new RuntimeException(e);
@@ -73,7 +75,6 @@ public class RunningGame implements Runnable {
private void broadcastNextPhaseStartTime(GamePhase currentPhase, GamePhase nextPhase) {
//Remaining time for this phase
Duration duration = phaseDurations.get(currentPhase).minus(Duration.between(phaseStartTime, Instant.now()));
log.debug(duration.toString());//TODO remove debug
if ((duration.toMinutes() > 1 && (duration.toMinutes() % 15 == 0 || duration.toMinutes() <= 5)) && duration.toSecondsPart() < 2) {
if (lastMinuteBroadcast == duration.toMinutes()) {
return;
@@ -115,4 +116,9 @@ public class RunningGame implements Runnable {
//TODO say the phase ended early?
nextPhaseActions(currentPhase, GamePhase.ENDED);
}
public Duration getRemainingTime() {
Duration duration = phaseDurations.get(currentPhase);
return duration.minus(Duration.between(phaseStartTime, Instant.now()));
}
}
@@ -23,7 +23,6 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Slf4j
@@ -62,7 +61,7 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
.filter(player -> gameManager.getTeamPlayer(player).isEmpty())
.forEach(player -> {
Team team = teamCircularIterator.next();
team.addPlayer(player);
gameManager.registerPlayer(team, player);
player.sendRichMessage("You joined <team>!", Placeholder.component("team", team.getName()));
});
} else {
@@ -1,7 +1,9 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.Main;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor;
import com.alttd.ctf.team.Team;
@@ -12,9 +14,11 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.command.ConsoleCommandSender;
import java.util.ArrayList;
import java.util.HashMap;
@@ -25,6 +29,13 @@ import java.util.Map;
public class EndedPhase implements GamePhaseExecutor {
private final MiniMessage miniMessage = MiniMessage.miniMessage();
private final Main main;
private final GameManager gameManager;
public EndedPhase(Main main, GameManager gameManager) {
this.main = main;
this.gameManager = gameManager;
}
@Override
public void start(Flag flag) {
@@ -51,8 +62,8 @@ public class EndedPhase implements GamePhaseExecutor {
player.setHealth(20);
});
flag.resetAll();
gameManager.getTeams().forEach(team -> Bukkit.getOnlinePlayers().forEach(team::removePlayer));
}).start();
// TODO reset world (coreprotect) to prep for next round
}
private List<Component> getWinnerMessages(HashMap<Team, Integer> wins) {
@@ -71,18 +82,29 @@ public class EndedPhase implements GamePhaseExecutor {
return messages;
} else if (topTeams.size() > 1) { // Draw scenario, multiple teams have the same top score
messages.add(miniMessage.deserialize("<yellow>It's a draw! Top teams:</yellow>"));
topTeams.forEach(team -> {
messages.add(miniMessage.deserialize("<team> had <score> captures.",
Placeholder.component("team", team.getName()),
Placeholder.parsed("score", String.valueOf(highestScore))));
});
topTeams.forEach(team -> messages.add(
miniMessage.deserialize("<team> had <score> captures.",
Placeholder.component("team", team.getName()),
Placeholder.parsed("score", String.valueOf(highestScore)))));
addOtherTeamsScore(wins, highestScore, messages);
return messages;
} else { // Single winner
Team winner = topTeams.getFirst();
messages.add(miniMessage.deserialize("<green><team> has won with <score> captures!</green>",
Placeholder.component("team", winner.getName()),
Placeholder.parsed("score", String.valueOf(highestScore))));
Placeholder.component("team", winner.getName()),
Placeholder.parsed("score", String.valueOf(highestScore))));
ConsoleCommandSender consoleSender = Bukkit.getConsoleSender();
Bukkit.getScheduler().runTaskAsynchronously(main, () -> {
winner.getPlayers().forEach(teamPlayer -> {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(teamPlayer.getUuid());
String name = offlinePlayer.getName();
if (name == null) {
return;
}
Bukkit.dispatchCommand(consoleSender, String.format("lp user %s permission settemp ctf.victory.%s true 14d", name, winner.getId()));
});
});
if (wins.size() <= 1) {
return messages;
}
@@ -97,8 +119,8 @@ public class EndedPhase implements GamePhaseExecutor {
wins.entrySet().stream()
.filter(entry -> entry.getValue() < winningScore)
.forEach(entry -> messages.add(miniMessage.deserialize("<yellow><team> had <score> captures.</yellow>",
Placeholder.component("team", entry.getKey().getName()),
Placeholder.parsed("score", String.valueOf(entry.getValue())))));
Placeholder.component("team", entry.getKey().getName()),
Placeholder.parsed("score", String.valueOf(entry.getValue())))));
}
@Override
@@ -36,14 +36,13 @@ public class GatheringPhase implements GamePhaseExecutor {
log.error("Unable to update world border due to missing Flag");
return;
}
gameManager.getTeams().forEach(team -> {
team.getPlayers().forEach(teamPlayer -> {
Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) {
return;
}
teamPlayer.resetWorldBorder(player, worldBorderApi, nextPhase, flag.getFlagLocation());
});
});
gameManager.getTeams()
.forEach(team -> team.getPlayers().forEach(teamPlayer -> {
Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) {
return;
}
teamPlayer.resetWorldBorder(player, worldBorderApi, nextPhase, flag.getFlagLocation());
}));
}
}
@@ -1,9 +1,7 @@
package com.alttd.ctf.game_class;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game_class.creation.EngineerCreator;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.game_class.creation.TankCreator;
import com.alttd.ctf.game_class.creation.*;
import com.alttd.ctf.team.Team;
import java.util.ArrayList;
@@ -14,9 +12,7 @@ public class GameClassRetrieval {
public static HashMap<Integer, List<GameClass>> getGameClassesForAllTeams(GameManager gameManager) {
final HashMap<Integer, List<GameClass>> gameClasses = new HashMap<>();
gameManager.getTeams().forEach(team -> {
gameClasses.put(team.getId(), getGameClassesForTeam(team));
});
gameManager.getTeams().forEach(team -> gameClasses.put(team.getId(), getGameClassesForTeam(team)));
return gameClasses;
}
@@ -24,7 +20,9 @@ public class GameClassRetrieval {
final List<GameClass> gameClasses = new ArrayList<>();
gameClasses.add(FighterCreator.createFighter(team.getColor()));
gameClasses.add(TankCreator.createTank(team.getColor()));
gameClasses.add(TrapperCreator.createTrapper(team.getColor()));
gameClasses.add(EngineerCreator.createEngineer(team.getColor()));
gameClasses.add(MageCreator.createMage(team.getColor()));
return gameClasses;
}
@@ -2,7 +2,6 @@ package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Engineer;
import com.alttd.ctf.game_class.implementations.Fighter;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -0,0 +1,60 @@
package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Mage;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import java.util.List;
@Slf4j
public class MageCreator {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
@Contract("_ -> new")
public static @NotNull GameClass createMage(@NotNull TeamColor teamColor) {
return new Mage(getArmor(), getTools(teamColor), getDisplayItem(teamColor),
20, 100, 1);
}
@Contract(value = " -> new", pure = true)
private static @NotNull @Unmodifiable List<Material> getArmor() {
return (List.of(Material.CHAINMAIL_BOOTS, Material.AIR, Material.AIR, Material.LEATHER_HELMET));
}
@Contract("_ -> new")
private static @NotNull @Unmodifiable List<ItemStack> getTools(@NotNull TeamColor teamColor) {
return (List.of(getShovel(teamColor)));
}
private static @NotNull ItemStack getShovel(@NotNull TeamColor teamColor) {
ItemStack shovel = new ItemStack(Material.WOODEN_SHOVEL);
ItemMeta meta = shovel.getItemMeta();
meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex())));
meta.setUnbreakable(true);
meta.addEnchant(Enchantment.EFFICIENCY, 1, false);
shovel.setItemMeta(meta);
return shovel;
}
private static @NotNull ItemStack getDisplayItem(@NotNull TeamColor teamColor) {
ItemStack itemStack = new ItemStack(Material.SNOWBALL);
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.displayName(miniMessage.deserialize(String.format("<color:%s>Mage</color>", teamColor.hex())));
itemMeta.lore(List.of(
miniMessage.deserialize("<gold>The Mage can throw many snowballs at once</gold>"),
miniMessage.deserialize("<gold>But it has a long cooldown.</gold>")
));
itemStack.setItemMeta(itemMeta);
return itemStack;
}
}
@@ -1,7 +1,6 @@
package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Fighter;
import com.alttd.ctf.game_class.implementations.Tank;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
@@ -22,7 +21,7 @@ public class TankCreator {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
@Contract("_ -> new")
public static @NotNull GameClass createTank(@NotNull TeamColor teamColor) {//TODO add ability to become temp invulnerable (with some particle effects mayb?)
public static @NotNull GameClass createTank(@NotNull TeamColor teamColor) {
return new Tank(getArmor(), getTools(teamColor), getDisplayItem(teamColor),
30, 7, 4);
}
@@ -0,0 +1,74 @@
package com.alttd.ctf.game_class.creation;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.implementations.Trapper;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class TrapperCreator {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
@Contract("_ -> new")
public static @NotNull GameClass createTrapper(@NotNull TeamColor teamColor) {
return new Trapper(getArmor(), getTools(teamColor), getDisplayItem(teamColor),
20, 5, 4);
}
@Contract(value = " -> new", pure = true)
private static @Unmodifiable List<Material> getArmor() {
return (List.of(Material.LEATHER_BOOTS, Material.LEATHER_LEGGINGS, Material.LEATHER_CHESTPLATE, Material.AIR));
}
@Contract("_ -> new")
private static @NotNull @Unmodifiable List<ItemStack> getTools(TeamColor teamColor) {
List<ItemStack> itemStacks = new ArrayList<>(List.of(getShovel(teamColor)));
itemStacks.addAll(getPowderedSnow(teamColor));
return itemStacks;
}
private static @NotNull List<ItemStack> getPowderedSnow(@NotNull TeamColor teamColor) {
ItemStack powderedSnow = new ItemStack(Material.POWDER_SNOW_BUCKET);
ItemMeta itemMeta = powderedSnow.getItemMeta();
itemMeta.itemName(MiniMessage.miniMessage().deserialize(
String.format("<color:%s>Snow Trap</color>", teamColor.hex())));
powderedSnow.setItemMeta(itemMeta);
List<ItemStack> snowBuckets = new ArrayList<>();
for (int i = 0; i < 16; i++) {
snowBuckets.add(powderedSnow.clone());
}
return snowBuckets;
}
private static @NotNull ItemStack getShovel(@NotNull TeamColor teamColor) {
ItemStack shovel = new ItemStack(Material.WOODEN_SHOVEL);
ItemMeta meta = shovel.getItemMeta();
meta.setUnbreakable(true);
meta.itemName(miniMessage.deserialize(String.format("<color:%s>Snow shovel</color>", teamColor.hex())));
shovel.setItemMeta(meta);
return shovel;
}
private static @NotNull ItemStack getDisplayItem(@NotNull TeamColor teamColor) {
ItemStack itemStack = new ItemStack(Material.POWDER_SNOW_BUCKET);
ItemMeta itemMeta = itemStack.getItemMeta();
itemMeta.displayName(miniMessage.deserialize(String.format("<color:%s>Trapper</color>", teamColor.hex())));
itemMeta.lore(List.of(
miniMessage.deserialize("<gold>The Trapper is a normal class, but it has a trick up it's sleeve</gold>"),
miniMessage.deserialize("<gold>They can place their powdered snow to trap the enemy team.</gold>")
));
itemStack.setItemMeta(itemMeta);
return itemStack;
}
}
@@ -1,11 +1,9 @@
package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.team.TeamColor;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -0,0 +1,62 @@
package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.entity.Snowball;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@Slf4j
public class Mage extends GameClass {
public Mage(@NotNull List<Material> armor, @NotNull List<ItemStack> tools, @NotNull ItemStack displayItem,
double health, int throwTickSpeed, int damage) {
super(armor, tools, displayItem, health, throwTickSpeed, damage);
}
public void duplicateSnowBalls(Player shooter, Snowball snowball) {
Location location = snowball.getLocation();
PlayerInventory inventory = shooter.getInventory();
ItemStack itemStack = inventory.getItemInMainHand();
if (itemStack.getType() != Material.SNOWBALL) {
ItemStack itemInOffHand = inventory.getItemInOffHand();
if (itemInOffHand.getType() != Material.SNOWBALL) {
log.warn("Unable to find snowballs in main/off hand");
return;
}
itemStack = itemInOffHand;
}
if (itemStack.getAmount() <= 1) {
return;
}
duplicate(shooter, location, snowball.getVelocity(), itemStack.getAmount() - 1);
itemStack.setAmount(0);
}
private void duplicate(@NotNull Player shooter, @NotNull Location location, @NotNull Vector velocity, int count) {
for (int i = 0; i < count; i++) {
double offsetX = (Math.random() - 0.5) * 2;
double offsetY = (Math.random() - 0.5) * 2;
double offsetZ = (Math.random() - 0.5) * 2;
Location newLocation = location.clone().add(offsetX, offsetY, offsetZ);
Snowball snowball = location.getWorld().spawn(newLocation, Snowball.class);
snowball.setShooter(shooter);
double velocityChangeX = (Math.random() - 0.5) * 0.5;
double velocityChangeY = (Math.random() - 0.5) * 0.5;
double velocityChangeZ = (Math.random() - 0.5) * 0.5;
Vector newVelocity = velocity.clone().add(new Vector(velocityChangeX, velocityChangeY, velocityChangeZ));
snowball.setVelocity(newVelocity);
}
}
}
@@ -0,0 +1,15 @@
package com.alttd.ctf.game_class.implementations;
import com.alttd.ctf.game_class.GameClass;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Trapper extends GameClass {
public Trapper(@NotNull List<Material> armor, @NotNull List<ItemStack> tools, @NotNull ItemStack displayItem,
double health, int throwTickSpeed, int damage) {
super(armor, tools, displayItem, health, throwTickSpeed, damage);
}
}
-2
View File
@@ -2,8 +2,6 @@ package com.alttd.ctf.gui;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.MerchantInventory;
import java.util.HashMap;
import java.util.UUID;
@@ -1,16 +1,42 @@
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();
private final UUID uuid;
private final String inGameName;
@JsonIgnore
private static boolean someoneDiedInPowderedSnow = false;
@JsonProperty
@NotNull
private UUID uuid;
@JsonProperty
@NotNull
private String inGameName;
private boolean completedGame = false;
private int flagsCaptured = 0;
@@ -23,16 +49,16 @@ public final class PlayerStat {
private long timeSpendCapturingFlag = 0;
private int deathsInPowderedSnow = 0;
public PlayerStat(UUID uuid, String inGameName) {
this.uuid = uuid;
this.inGameName = inGameName;
}
public void increaseStat(Stat stat) throws IllegalArgumentException {
switch (stat) {
case COMPLETED_GAME -> {
if (!completedGame) {
Bukkit.dispatchCommand(commandSender, String.format("lp user %s permission set ctf.game.completed", inGameName));
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;
}
@@ -42,9 +68,30 @@ public final class PlayerStat {
case BLOCKS_PLACED -> blocksPlaced++;
case SNOWBALLS_THROWN -> snowballsThrown++;
case TIME_SPEND_CAPTURING_FLAG -> timeSpendCapturingFlag++;
case DEATHS_IN_POWDERED_SNOW -> deathsInPowderedSnow++; //TODO announce if they are the first person to do this and save they are the first
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 {
@@ -53,6 +100,25 @@ public final class PlayerStat {
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;
};
}
}
@@ -5,7 +5,6 @@ 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.Permission;
import net.dv8tion.jda.api.entities.*;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -56,9 +55,7 @@ public class DiscordTeam {
}
Member nullableMember = guild.getMemberById(player.getDiscordID());
if (nullableMember == null) {
guild.retrieveMemberById(player.getDiscordID()).queue(member -> {
consumer.apply(role, member);
});
guild.retrieveMemberById(player.getDiscordID()).queue(member -> consumer.apply(role, member));
} else {
consumer.apply(role, nullableMember);
}
@@ -70,7 +67,7 @@ public class DiscordTeam {
log.info("Unable to remove team role from server owner");
return;
}
member.getGuild().removeRoleFromMember(member, role).queue(ignored -> kickFromVoiceIfNeeded(member));
member.getGuild().removeRoleFromMember(member, role).queue();
});
}
@@ -83,24 +80,8 @@ public class DiscordTeam {
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(ignored -> kickFromVoiceIfNeeded(member));
.forEach(otherRole -> member.getGuild().removeRoleFromMember(member, otherRole).queue()));
member.getGuild().addRoleToMember(member, role).queue();
});
}
private void kickFromVoiceIfNeeded(@NotNull Member member) {
GuildVoiceState voiceState = member.getVoiceState();
if (voiceState == null || !voiceState.inAudioChannel()) {
return;
}
AudioChannel channel = voiceState.getChannel();
if (channel == null) {
return;
}
if (!member.hasPermission(channel, Permission.VOICE_CONNECT)) {
member.getGuild().kickVoiceMember(member).queue();
}
}
}
+10 -38
View File
@@ -1,6 +1,7 @@
package com.alttd.ctf.team;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.stats.PlayerStat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
@@ -8,15 +9,9 @@ import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.*;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@@ -27,7 +22,7 @@ import java.util.*;
public class Team {
@JsonIgnore
private final static Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
private final TeamScoreboard scoreboard = new TeamScoreboard(this);
@JsonIgnore
private final HashMap<UUID, TeamPlayer> players = new HashMap<>();
@JsonProperty("name")
@@ -73,10 +68,10 @@ public class Team {
discordTeam = new DiscordTeam(gameManager);
}
public TeamPlayer addPlayer(Player player) {
removeFromScoreBoard(player);
public TeamPlayer addPlayer(Player player, PlayerStat playerStat) {
removeFromScoreboard(player);
UUID uuid = player.getUniqueId();
TeamPlayer teamPlayer = new TeamPlayer(player, this);
TeamPlayer teamPlayer = new TeamPlayer(player, this, playerStat);
players.put(uuid, teamPlayer);
addToScoreboard(player);
if (discordTeam != null) {
@@ -100,7 +95,7 @@ public class Team {
}
public void removePlayer(@NotNull Player player) {
removeFromScoreBoard(player);
removeFromScoreboard(player);
TeamPlayer remove = players.remove(player.getUniqueId());
if (remove == null) {
return;
@@ -114,38 +109,15 @@ public class Team {
}
public void addToScoreboard(Player player) {
org.bukkit.scoreboard.Team team = scoreboard.getTeam("ctf_" + id);
if (team == null) {
team = scoreboard.registerNewTeam("ctf_" + id);
team.displayName(name);
NamedTextColor namedTextColor = NamedTextColor.nearestTo(TextColor.color(color.r(), color.g(), color.b()));
team.color(namedTextColor);
}
team.addPlayer(player);
player.setScoreboard(scoreboard);
scoreboard.addToScoreboard(player);
}
private void removeFromScoreBoard(Player player) {
scoreboard.getTeams().stream()
.filter(team -> team.getName().startsWith("ctf_"))
.filter(team -> team.hasPlayer(player))
.forEach(team -> team.removePlayer(player));
private void removeFromScoreboard(Player player) {
scoreboard.removeFromScoreboard(player);
}
public void setScore(int newScore) {
Objective objective = getOrCreateObjective();
Score score = objective.getScore(legacyTeamColor + PlainTextComponentSerializer.plainText().serialize(name));
score.setScore(newScore);
}
private 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;
scoreboard.setScore(newScore);
}
@Override
@@ -30,10 +30,10 @@ public class TeamPlayer {
private GameClass gameClass;
private boolean isDead = false;
protected TeamPlayer(Player player, Team team) {
protected TeamPlayer(Player player, Team team, PlayerStat playerStat) {
this.uuid = player.getUniqueId();
this.team = team;
this.playerStat = new PlayerStat(uuid, player.getName());
this.playerStat = playerStat;
}
public void respawn(@NotNull Player player, @NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
@@ -60,8 +60,8 @@ public class TeamPlayer {
isDead = true;
}
public void resetWorldBorder(@NotNull Player player, WorldBorderApi worldBorderApi, GamePhase gamePhase, Location worldBorderCenter) {
if (!player.isOnline()) {
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);
@@ -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 @@
#Sun Feb 23 01:14:21 CET 2025
buildNumber=70
#Sun Mar 02 00:42:30 CET 2025
buildNumber=112
version=0.1