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.
399 lines
17 KiB
Java
399 lines
17 KiB
Java
package com.alttd.ctf.flag;
|
|
|
|
import com.alttd.ctf.Main;
|
|
import com.alttd.ctf.config.GameConfig;
|
|
import com.alttd.ctf.game.GameManager;
|
|
import com.alttd.ctf.team.Team;
|
|
import com.alttd.ctf.team.TeamColor;
|
|
import com.alttd.ctf.team.TeamPlayer;
|
|
import lombok.Getter;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import net.kyori.adventure.text.Component;
|
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
|
import net.kyori.adventure.title.Title;
|
|
import org.bukkit.*;
|
|
import org.bukkit.boss.BarColor;
|
|
import org.bukkit.boss.BarStyle;
|
|
import org.bukkit.boss.BossBar;
|
|
import org.bukkit.boss.KeyedBossBar;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.inventory.EquipmentSlot;
|
|
import org.bukkit.inventory.ItemStack;
|
|
import org.bukkit.potion.PotionEffect;
|
|
import org.bukkit.potion.PotionEffectType;
|
|
|
|
import java.util.*;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.stream.Collectors;
|
|
|
|
@Slf4j
|
|
public class Flag implements Runnable {
|
|
|
|
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
|
|
|
|
private final HashMap<Integer, Integer> teamFlagPointCount = new HashMap<>();
|
|
private final BossBar bossBar = createBossBar();
|
|
private final HashMap<Integer, Integer> wins = new HashMap<>();
|
|
private int lastWinningTeamId = -1;
|
|
@Getter
|
|
private final Location flagLocation;
|
|
private Team winningTeam;
|
|
private Player flagCarrier;
|
|
|
|
private final Main main;
|
|
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() {
|
|
NamespacedKey namespacedKey = NamespacedKey.fromString("ctf_flag", main);
|
|
if (namespacedKey == null) {
|
|
throw new IllegalStateException("No NamespaceKey could be created for the bossbar");
|
|
}
|
|
if (Bukkit.getBossBar(namespacedKey) != null) {
|
|
Bukkit.removeBossBar(namespacedKey);
|
|
}
|
|
KeyedBossBar captureProgress = Bukkit.createBossBar(namespacedKey, "Capture progress", BarColor.GREEN, BarStyle.SEGMENTED_20);
|
|
captureProgress.removeAll();
|
|
captureProgress.setProgress(0);
|
|
captureProgress.setVisible(false);
|
|
return captureProgress;
|
|
}
|
|
|
|
public void addPlayer(Player player) {
|
|
bossBar.addPlayer(player);
|
|
}
|
|
|
|
public synchronized void capture(TeamPlayer teamPlayer, Player player) {
|
|
if (flagCarrier != null) {
|
|
return;
|
|
}
|
|
//TODO knockback enemies from flag location to create space for person who captured mayb short speed boost and heal?
|
|
//TODO add de-buffs and enable buffs for others?
|
|
player.getInventory().setItem(EquipmentSlot.HEAD, 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
if (flagCarrier != null) {
|
|
checkFlagCarrier();
|
|
return;
|
|
}
|
|
if (winningTeam != null) {
|
|
spawnFlagParticleRing();
|
|
return;
|
|
}
|
|
if (flagLocation == null) {
|
|
log.warn("Tried to run Flag without a flag location, spawn it first");
|
|
return;
|
|
}
|
|
spawnParticlesOnSquareBorder(flagLocation, GameConfig.FLAG.CAPTURE_RADIUS);
|
|
if (!updateScoreBasedOnNearbyPlayers().join()) {
|
|
return; //Score didn't change
|
|
}
|
|
if (teamFlagPointCount.isEmpty()) {
|
|
return;
|
|
}
|
|
Optional<Team> optionalTeam = winnerExists();
|
|
if (optionalTeam.isEmpty()) {
|
|
updateDisplay();
|
|
} else {
|
|
winningTeam = optionalTeam.get();
|
|
spawnFlag(winningTeam.getFlagMaterial());
|
|
updateDisplay();
|
|
Title title = Title.title(MiniMessage.miniMessage().deserialize("Team <team> can capture the flag",
|
|
Placeholder.component("team", winningTeam.getName())), Component.empty());
|
|
Bukkit.getOnlinePlayers().forEach(player -> player.showTitle(title));
|
|
}
|
|
}
|
|
|
|
private void notifyAboutCapture() {
|
|
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() == winningTeam.getId() ? capturingTeamTitle : huntingTeamTitle)));
|
|
}
|
|
|
|
private void spawnParticlesOnSquareBorder(Location center, double size) {
|
|
double step = 0.2;
|
|
World world = center.getWorld();
|
|
Location finalCenter = center.clone().add(0, 0.5, 0);
|
|
Bukkit.getScheduler().runTask(main, () -> {
|
|
for (double z = -size; z <= size; z += step) {
|
|
world.spawnParticle(Particle.FLAME, finalCenter.getX() + size, finalCenter.getY(), finalCenter.getZ() + z, 1, 0, 0, 0, 0);
|
|
world.spawnParticle(Particle.FLAME, finalCenter.getX() - size, finalCenter.getY(), finalCenter.getZ() + z, 1, 0, 0, 0, 0);
|
|
}
|
|
|
|
for (double x = -size; x <= size; x += step) {
|
|
world.spawnParticle(Particle.FLAME, finalCenter.getX() + x, finalCenter.getY(), finalCenter.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 void spawnTrail() {
|
|
TeamColor color = winningTeam.getColor();
|
|
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)));
|
|
if (particleTrail.size() > 15) {
|
|
particleTrail.removeFirst();
|
|
}
|
|
}
|
|
|
|
private void checkFlagCarrier() {
|
|
if (flagCarrier.isDead() || !flagCarrier.isOnline()) {
|
|
resetFlagCarrier();
|
|
spawnFlag(GameConfig.FLAG.MATERIAL);
|
|
return;
|
|
}
|
|
|
|
double distance = winningTeam.getFlagTurnInLocation().distance(flagCarrier.getLocation());
|
|
if (distance > GameConfig.FLAG.TURN_IN_RADIUS) {
|
|
Location location = flagCarrier.getLocation();
|
|
location.setY(location.getY() + 1);
|
|
particleTrail.add(location);
|
|
spawnTrail();
|
|
spawnParticlesOnSquareBorder(winningTeam.getFlagTurnInLocation(), GameConfig.FLAG.TURN_IN_RADIUS);
|
|
return;
|
|
}
|
|
|
|
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));
|
|
|
|
resetFlagCarrier();
|
|
}
|
|
|
|
private void notifyAboutTurnIn() {
|
|
Bukkit.broadcast(miniMessage.deserialize("<player> captured the flag for <team>!",
|
|
Placeholder.component("player", flagCarrier.displayName()),
|
|
Placeholder.component("team", winningTeam.getName())));
|
|
Title title = Title.title(Component.empty(),
|
|
miniMessage.deserialize("<green><player> captured the flag for <team> team</green>",
|
|
Placeholder.component("player", flagCarrier.displayName()),
|
|
Placeholder.component("team", winningTeam.getName())));
|
|
Bukkit.getOnlinePlayers().forEach(player -> player.showTitle(title));
|
|
}
|
|
|
|
private Optional<Team> winnerExists() {
|
|
Optional<Map.Entry<Integer, Integer>> max = teamFlagPointCount.entrySet().stream()
|
|
.max(Map.Entry.comparingByValue());
|
|
if (max.isEmpty()) {
|
|
return Optional.empty();
|
|
}
|
|
if (max.get().getValue() < GameConfig.FLAG.CAPTURE_SCORE) {
|
|
return Optional.empty();
|
|
}
|
|
return gameManager.getTeam(max.get().getKey());
|
|
}
|
|
|
|
/**
|
|
* Updates the score based on nearby players within a specified radius of the flag's location.
|
|
* This method utilizes asynchronous and synchronous tasks to handle operations efficiently.
|
|
*
|
|
* @return A CompletableFuture that resolves to a Boolean indicating whether the score was successfully updated.
|
|
*/
|
|
private CompletableFuture<Boolean> updateScoreBasedOnNearbyPlayers() {
|
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
|
Bukkit.getScheduler().runTask(main, () -> {
|
|
Collection<Player> nearbyPlayers = flagLocation.getNearbyPlayers(GameConfig.FLAG.CAPTURE_RADIUS);
|
|
Bukkit.getScheduler().runTaskAsynchronously(main, () -> {
|
|
boolean result = updateScoreBasedOnNearbyPlayers(nearbyPlayers);
|
|
future.complete(result);
|
|
});
|
|
});
|
|
return future;
|
|
}
|
|
|
|
/**
|
|
* 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 CAPTURE_RADIUS-block radius,
|
|
* 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.
|
|
* If a single team has the highest number of nearby players, scores are updated accordingly:
|
|
* - Increment the score for the team with the majority presence.
|
|
* - Decrement the scores for other teams.
|
|
*
|
|
* @return true if a single team has the largest number of nearby players and scores were updated,
|
|
* false if there is a tie or no players are nearby.
|
|
*/
|
|
private boolean updateScoreBasedOnNearbyPlayers(Collection<Player> players) {
|
|
List<TeamPlayer> nearbyPlayers = players.stream()
|
|
.map(gameManager::getTeamPlayer)
|
|
.filter(Optional::isPresent)
|
|
.map(Optional::get)
|
|
.toList();
|
|
|
|
if (nearbyPlayers.isEmpty()) {
|
|
teamFlagPointCount.forEach((teamId, count) -> teamFlagPointCount.put(teamId, Math.max(0, count - 1)));
|
|
return true; //No players in the area, decrease score
|
|
}
|
|
|
|
Map<Team, Long> teamCounts = nearbyPlayers.stream()
|
|
.collect(Collectors.groupingBy(TeamPlayer::getTeam, Collectors.counting()));
|
|
|
|
Optional<Map.Entry<Team, Long>> maxEntry = teamCounts.entrySet()
|
|
.stream()
|
|
.max(Map.Entry.comparingByValue());
|
|
|
|
if (maxEntry.isEmpty()) {
|
|
return false; //No players in the area
|
|
}
|
|
Map.Entry<Team, Long> teamLongEntry = maxEntry.get();
|
|
long maxCount = teamLongEntry.getValue();
|
|
|
|
long maxCountTeams = teamCounts.values().stream().filter(count -> count == maxCount).count();
|
|
|
|
if (maxCountTeams != 1) {
|
|
return false; //There are multiple teams that have the most players in the area
|
|
}
|
|
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);
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
private void updateDisplay() {
|
|
Integer highestKey = teamFlagPointCount.entrySet().stream()
|
|
.max(Map.Entry.comparingByValue())
|
|
.map(Map.Entry::getKey)
|
|
.orElse(null);
|
|
if (highestKey == null) {
|
|
throw new IllegalStateException("Updating display without any teams existing in the score");
|
|
}
|
|
Optional<Team> team = gameManager.getTeam(highestKey);
|
|
if (team.isEmpty()) {
|
|
throw new IllegalStateException(String.format("Team %s in point list doesnt exist", highestKey));
|
|
}
|
|
if (lastWinningTeamId != highestKey) {
|
|
bossBar.setTitle(String.format("Team %s is capturing the flag", PlainTextComponentSerializer.plainText().serialize(team.get().getName())));
|
|
lastWinningTeamId = highestKey;
|
|
}
|
|
bossBar.setProgress(Math.min(GameConfig.FLAG.CAPTURE_SCORE, teamFlagPointCount.get(highestKey)) / (double) GameConfig.FLAG.CAPTURE_SCORE);
|
|
bossBar.setVisible(teamFlagPointCount.get(highestKey) > 0);
|
|
}
|
|
|
|
protected Optional<Team> getWinningTeam() {
|
|
return winningTeam == null ? Optional.empty() : Optional.of(winningTeam);
|
|
}
|
|
|
|
public HashMap<Team, Integer> getWins() {
|
|
HashMap<Team, Integer> winsByTeam = new HashMap<>();
|
|
wins.keySet().stream()
|
|
.map(gameManager::getTeam)
|
|
.filter(Optional::isPresent)
|
|
.map(Optional::get)
|
|
.forEach(team -> winsByTeam.put(team, wins.get(team.getId())));
|
|
return winsByTeam;
|
|
}
|
|
|
|
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);
|
|
teamFlagPointCount.clear();
|
|
bossBar.setProgress(0);
|
|
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"));
|
|
}
|
|
}
|