ctf/src/main/java/com/alttd/ctf/flag/Flag.java
Teriuihi 8386e773ce Add glowing effect to flag carrier
The flag carrier now receives a glowing effect to improve visibility and make them easier to track during gameplay. This change enhances player experience and adds a strategic element to capturing the flag.
2025-02-23 00:34:43 +01:00

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"));
}
}