Adjusted the title message to simplify formatting and better align with gameplay context. Removed redundant text and ensured proper use of placeholders for dynamic content.
388 lines
16 KiB
Java
388 lines
16 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.minimessage.tag.resolver.TagResolver;
|
|
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 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 ItemStack flagItem = new ItemStack(Material.BLACK_BANNER);
|
|
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);
|
|
}
|
|
|
|
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, flagItem);
|
|
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()));
|
|
}
|
|
|
|
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();
|
|
resetFlag();
|
|
//TODO stop capture and let ppl know they can now capture the flag
|
|
}
|
|
}
|
|
|
|
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.getUniqueId()).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();
|
|
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();
|
|
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);
|
|
}
|
|
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(player -> gameManager.getTeamPlayer(player.getUniqueId()))
|
|
.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() {
|
|
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();
|
|
}
|
|
|
|
public void handleCarrierDeathOrDisconnect(Player player) {
|
|
if (flagCarrier == null) {
|
|
return;
|
|
}
|
|
if (!flagCarrier.getUniqueId().equals(player.getUniqueId())) {
|
|
return;
|
|
}
|
|
resetFlagCarrier();
|
|
spawnFlag();
|
|
gameManager.getTeam(player.getUniqueId())
|
|
.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"));
|
|
}
|
|
}
|