Compare commits

...

5 Commits

Author SHA1 Message Date
Teriuihi 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
Teriuihi 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
Teriuihi 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
Teriuihi 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
Teriuihi 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
15 changed files with 145 additions and 52 deletions

View File

@ -58,7 +58,7 @@ public class ChangeTeam extends SubCommand {
}
private void changeTeam(CommandSender commandSender, Player player, Team team) {
Optional<Team> optionalOldTeam = gameManager.getTeam(player.getUniqueId());
Optional<Team> optionalOldTeam = gameManager.getTeam(player);
if (optionalOldTeam.isPresent()) {
moveBetweenTeams(commandSender, player, team, optionalOldTeam.get());
return;

View File

@ -12,6 +12,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@ -75,7 +76,8 @@ public class CreateTeam extends SubCommand {
int highestId = gameManager.getMaxTeamId();
Team team = new Team(MiniMessage.miniMessage().deserialize(String.format("<color:%s>%s</color>", color, name)),
highestId + 1, player.getLocation(), player.getLocation(), player.getLocation(), teamColor);
highestId + 1, player.getLocation(), player.getLocation(), player.getLocation(), teamColor,
Material.RED_BANNER, "§c");
return consumer.apply(team);
}

View File

@ -40,7 +40,7 @@ public class SelectClass extends SubCommand {
return 0;
}
GamePhase gamePhase = optionalGamePhase.get();
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty()) {
commandSender.sendRichMessage("<red>You have to be in a CTF team to select a class.</red>");
return 0;

View File

@ -3,6 +3,9 @@ package com.alttd.ctf.config;
import com.alttd.ctf.Main;
import com.alttd.ctf.game.GamePhase;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.HashMap;
@ -88,6 +91,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;
@SuppressWarnings("unused")
private static void load() {
@ -98,6 +102,7 @@ public class GameConfig extends AbstractConfig {
CAPTURE_RADIUS = config.getDouble(prefix, "capture-radius", CAPTURE_RADIUS);
CAPTURE_SCORE = config.getInt(prefix, "capture-score", CAPTURE_SCORE);
TURN_IN_RADIUS = config.getDouble(prefix, "turn-in-radius", TURN_IN_RADIUS);
MATERIAL = Material.valueOf(config.getString(prefix, "material", MATERIAL.toString()));
}
}

View File

@ -3,6 +3,9 @@ package com.alttd.ctf.events;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerAttemptPickupItemEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
@ -26,4 +29,20 @@ public class InventoryItemInteractionEvent implements Listener {
event.setCancelled(true);
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (event.getSlotType() != InventoryType.SlotType.ARMOR) {
return;
}
event.setCancelled(true);
}
@EventHandler
public void onInventoryDrag(InventoryDragEvent event) {
if (event.getRawSlots().stream().noneMatch(slot -> slot >= 5 && slot <= 8)) { // Slot numbers 5-8 for armor
return;
}
event.setCancelled(true);
}
}

View File

@ -55,7 +55,7 @@ public class OnPlayerDeath implements Listener {
log.warn("Player {} died while the game wasn't running", player.getName());
return;
}
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty()) {
return;
}

View File

@ -51,7 +51,7 @@ public class OnPlayerOnlineStatus implements Listener {
if (gamePhase.equals(GamePhase.ENDED)) {
return;
}
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
TeamPlayer teamPlayer;
if (optionalTeamPlayer.isEmpty()) {
Optional<Team> min = gameManager.getTeams().stream().min(Comparator.comparingInt(team -> team.getPlayers().size()));
@ -59,7 +59,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.getUniqueId());
teamPlayer = min.get().addPlayer(player);
} else {
teamPlayer = optionalTeamPlayer.get();
}

View File

@ -89,7 +89,7 @@ public class SnowballEvent implements Listener {
return;
}
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter.getUniqueId());
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(shooter);
if (teamPlayer.isEmpty()) {
log.debug("The shooter that threw a snowball was not a team player");
return;
@ -100,13 +100,14 @@ public class SnowballEvent implements Listener {
private void handleSnowballHit(EntityDamageByEntityEvent event, SnowballHitConsumer consumer) {
Optional<GamePhase> optionalGamePhase = gameManager.getGamePhase();
if (optionalGamePhase.isEmpty()) {
log.debug("No game is running but player was hit by snowball");
log.debug("No game is running but player was hit");
return;
}
GamePhase gamePhase = optionalGamePhase.get();
if (!gamePhase.equals(GamePhase.COMBAT)) {
log.debug("Not in combat phase but player was hit by snowball");
log.debug("Not in combat phase but player was hit, cancelling event");
event.setCancelled(true);
return;
}
@ -116,7 +117,8 @@ public class SnowballEvent implements Listener {
}
if (!(event.getDamager() instanceof org.bukkit.entity.Snowball snowball)) {
log.debug("The player was hit by something other than a snowball");
log.debug("The player was hit by something other than a snowball, canceling event");
event.setCancelled(true);
return;
}
@ -125,13 +127,13 @@ public class SnowballEvent implements Listener {
return;
}
Optional<TeamPlayer> teamPlayerShooter = gameManager.getTeamPlayer(shooter.getUniqueId());
Optional<TeamPlayer> teamPlayerShooter = gameManager.getTeamPlayer(shooter);
if (teamPlayerShooter.isEmpty()) {
log.debug("The shooter that hit a player with a snowball was not a team player");
return;
}
Optional<TeamPlayer> teamPlayerHit = gameManager.getTeamPlayer(hitPlayer.getUniqueId());
Optional<TeamPlayer> teamPlayerHit = gameManager.getTeamPlayer(hitPlayer);
if (teamPlayerHit.isEmpty()) {
log.debug("The shooter that hit a player with a snowball was not a team player");
return;

View File

@ -33,7 +33,6 @@ public class Flag implements Runnable {
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
private final HashMap<Integer, Integer> teamFlagPointCount = new HashMap<>();
private final ItemStack flagItem = new ItemStack(Material.BLACK_BANNER);
private final BossBar bossBar = createBossBar();
private final HashMap<Integer, Integer> wins = new HashMap<>();
private int lastWinningTeamId = -1;
@ -53,6 +52,7 @@ public class Flag implements Runnable {
throw new IllegalStateException(String.format("Tried to spawn flag in world [%s] that doesn't exist", GameConfig.FLAG.world));
}
this.flagLocation = new Location(world, GameConfig.FLAG.x, GameConfig.FLAG.y, GameConfig.FLAG.z);
gameManager.getTeams().forEach(team -> team.setScore(0));
}
private BossBar createBossBar() {
@ -80,15 +80,15 @@ public class Flag implements Runnable {
}
//TODO knockback enemies from flag location to create space for person who captured mayb short speed boost and heal?
//TODO add de-buffs and enable buffs for others?
player.getInventory().setItem(EquipmentSlot.HEAD, flagItem);
player.getInventory().setItem(EquipmentSlot.HEAD, new ItemStack(teamPlayer.getTeam().getFlagMaterial()));
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(Material.AIR));
flagCarrier = player;
notifyAboutCapture();
resetFlag();
}
public void spawnFlag() {
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(flagItem.getType()));
public void spawnFlag(Material material) {
Bukkit.getScheduler().runTask(main, () -> flagLocation.getBlock().setType(material));
}
private void spawnFlagParticleRing() {
@ -141,7 +141,7 @@ public class Flag implements Runnable {
updateDisplay();
} else {
winningTeam = optionalTeam.get();
resetFlag();
spawnFlag(winningTeam.getFlagMaterial());
//TODO stop capture and let ppl know they can now capture the flag
}
}
@ -159,7 +159,7 @@ public class Flag implements Runnable {
miniMessage.deserialize("<red>kill <player> before they bring it to their base.</red>",
Placeholder.component("player", flagCarrier.displayName())));
Bukkit.getOnlinePlayers().forEach(player ->
gameManager.getTeam(player.getUniqueId()).ifPresent(team ->
gameManager.getTeam(player).ifPresent(team ->
player.showTitle(team.getId() == winningTeam.getId() ? capturingTeamTitle : huntingTeamTitle)));
}
@ -194,9 +194,10 @@ public class Flag implements Runnable {
private void checkFlagCarrier() {
if (flagCarrier.isDead() || !flagCarrier.isOnline()) {
resetFlagCarrier();
spawnFlag();
spawnFlag(GameConfig.FLAG.MATERIAL);
return;
}
double distance = winningTeam.getFlagTurnInLocation().distance(flagCarrier.getLocation());
if (distance > GameConfig.FLAG.TURN_IN_RADIUS) {
Location location = flagCarrier.getLocation();
@ -206,18 +207,17 @@ public class Flag implements Runnable {
spawnParticlesOnSquareBorder(winningTeam.getFlagTurnInLocation(), GameConfig.FLAG.TURN_IN_RADIUS);
return;
}
notifyAboutTurnIn();
spawnFlag();
spawnFlag(GameConfig.FLAG.MATERIAL);
wins.merge(winningTeam.getId(), 1, Integer::sum);
winningTeam = null;
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(flagCarrier.getUniqueId());
if (optionalTeamPlayer.isEmpty()) {
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
} else {
TeamPlayer teamPlayer = optionalTeamPlayer.get();
teamPlayer.getGameClass().setArmor(flagCarrier, teamPlayer);
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
}
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));
resetFlagCarrier();
}
@ -276,7 +276,7 @@ public class Flag implements Runnable {
*/
private boolean updateScoreBasedOnNearbyPlayers(Collection<Player> players) {
List<TeamPlayer> nearbyPlayers = players.stream()
.map(player -> gameManager.getTeamPlayer(player.getUniqueId()))
.map(gameManager::getTeamPlayer)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
@ -377,8 +377,8 @@ public class Flag implements Runnable {
return;
}
resetFlagCarrier();
spawnFlag();
gameManager.getTeam(player.getUniqueId())
spawnFlag(GameConfig.FLAG.MATERIAL);
gameManager.getTeam(player)
.ifPresentOrElse(team -> Bukkit.broadcast(MiniMessage.miniMessage()
.deserialize("<red><team>'s flag carrier died! The flag has respawned",
Placeholder.component("team", team.getName()))),

View File

@ -37,7 +37,7 @@ public class FlagTryCaptureEvent implements Listener {
return;
}
Player player = event.getPlayer();
Optional<TeamPlayer> teamPlayer = winningTeam.getPlayer(player.getUniqueId());
Optional<TeamPlayer> teamPlayer = winningTeam.getPlayer(player);
if (teamPlayer.isEmpty()) {
return;
}

View File

@ -39,11 +39,11 @@ public class GameManager {
public void registerPlayer(Team team, Player player) {
unregisterPlayer(player);
teams.get(team.getId()).addPlayer(player.getUniqueId());
teams.get(team.getId()).addPlayer(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) {
@ -54,17 +54,17 @@ public class GameManager {
return teams.values();
}
public Optional<Team> getTeam(@NotNull UUID uuid) {
return getTeams().stream().filter(filterTeam -> filterTeam.getPlayer(uuid).isPresent()).findAny();
public Optional<Team> getTeam(@NotNull Player player) {
return getTeams().stream().filter(filterTeam -> filterTeam.getPlayer(player).isPresent()).findAny();
}
public Optional<Team> getTeam(int teamId) {
return getTeams().stream().filter(filterTeam -> filterTeam.getId() == teamId).findAny();
}
public Optional<TeamPlayer> getTeamPlayer(@NotNull UUID uuid) {
public Optional<TeamPlayer> getTeamPlayer(@NotNull Player player) {
return getTeams().stream()
.map(team -> team.getPlayer(uuid))
.map(team -> team.getPlayer(player))
.filter(Optional::isPresent)
.findFirst()
.orElseGet(Optional::empty);

View File

@ -21,6 +21,9 @@ import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Slf4j
@ -52,12 +55,14 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>Select your class with <gold>/ctf selectclass</gold></green>"));
CircularIterator<Team> teamCircularIterator = new CircularIterator<>(gameManager.getTeams());
if (teamCircularIterator.hasNext()) {
Bukkit.getOnlinePlayers().stream()
ArrayList<? extends Player> players = new ArrayList<>(Bukkit.getOnlinePlayers());
Collections.shuffle(players);
players.stream()
.filter(player -> !player.hasPermission("ctf.bypass"))
.filter(player -> gameManager.getTeamPlayer(player.getUniqueId()).isEmpty())
.filter(player -> gameManager.getTeamPlayer(player).isEmpty())
.forEach(player -> {
Team team = teamCircularIterator.next();
team.addPlayer(player.getUniqueId());
team.addPlayer(player);
player.sendRichMessage("You joined <team>!", Placeholder.component("team", team.getName()));
});
} else {
@ -68,7 +73,7 @@ public class ClassSelectionPhase implements GamePhaseExecutor {
private void teleportPlayersToStartingZone() {
Bukkit.getOnlinePlayers().forEach(player -> {
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(player.getUniqueId());
Optional<TeamPlayer> teamPlayer = gameManager.getTeamPlayer(player);
if (teamPlayer.isEmpty()) {
log.warn("{} is not a team player when teleporting to starting zone", player.getName());
return;

View File

@ -1,5 +1,6 @@
package com.alttd.ctf.game.phases;
import com.alttd.ctf.config.GameConfig;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game.GamePhaseExecutor;
@ -17,7 +18,7 @@ public class CombatPhase implements GamePhaseExecutor {
@Override
public synchronized void start(Flag flag) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<green>CAPTURE THE FLAG</green>"));
flag.spawnFlag();
flag.spawnFlag(GameConfig.FLAG.MATERIAL);
if (executorService == null) {
executorService = Executors.newSingleThreadScheduledExecutor();
} else if (executorService.isTerminated() || executorService.isShutdown()) {

View File

@ -7,7 +7,16 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
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.legacy.LegacyComponentSerializer;
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.*;
@ -17,6 +26,8 @@ import java.util.*;
@AllArgsConstructor
public class Team {
@JsonIgnore
private final static Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
@JsonIgnore
private final HashMap<UUID, TeamPlayer> players = new HashMap<>();
@JsonProperty("name")
@ -42,15 +53,27 @@ public class Team {
@NotNull
@Getter
private TeamColor color;
@JsonProperty("flagMaterial")
@NotNull
@Getter
private Material flagMaterial;
@JsonProperty("legacyTeamColor")
@NotNull
@Getter
private String legacyTeamColor;
public TeamPlayer addPlayer(UUID uuid) {
public TeamPlayer addPlayer(Player player) {
removeFromScoreBoard(player);
UUID uuid = player.getUniqueId();
TeamPlayer teamPlayer = new TeamPlayer(uuid, this);
players.put(uuid, teamPlayer);
log.debug("Added player with uuid {} to team with id {}", uuid, id);
addToScoreboard(player);
log.debug("Added player {} to team with id {}", player.getName(), id);
return teamPlayer;
}
public Optional<TeamPlayer> getPlayer(@NotNull UUID uuid) {
public Optional<TeamPlayer> getPlayer(@NotNull Player player) {
UUID uuid = player.getUniqueId();
if (!players.containsKey(uuid))
return Optional.empty();
return Optional.of(players.get(uuid));
@ -60,13 +83,49 @@ public class Team {
return players.values();
}
public void removePlayer(@NotNull UUID uuid) {
TeamPlayer remove = players.remove(uuid);
public void removePlayer(@NotNull Player player) {
removeFromScoreBoard(player);
TeamPlayer remove = players.remove(player.getUniqueId());
if (remove != null) {
log.debug("Removed player with uuid {} from team with id {}", uuid, id);
log.debug("Removed player {} from team with id {}", player.getName(), id);
}
}
private 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);
}
private void removeFromScoreBoard(Player player) {
scoreboard.getTeams().stream()
.filter(team -> team.getName().startsWith("ctf_"))
.filter(team -> team.hasPlayer(player))
.forEach(team -> team.removePlayer(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;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {

View File

@ -1,3 +1,3 @@
#Tue Feb 11 22:21:13 CET 2025
buildNumber=45
#Sat Feb 15 03:47:20 CET 2025
buildNumber=50
version=0.1