Compare commits

...

8 Commits

Author SHA1 Message Date
Teriuihi e62d0df9df Refactor potion healing to exclude non-teammates.
Split logic into smaller methods for clarity and maintainability. Added `shouldHeal` to determine team eligibility and `calculateActualHealing` to handle precise healing calculations. Updated intensity handling to exclude non-teammates during healing.
2025-02-23 01:22:52 +01:00
Teriuihi eeed5b4c54 Add player statistics tracking and integration.
Introduced a comprehensive system for tracking player stats, including kills, flags captured, damage dealt, and more. Integrated stat recording into related game events and ensured proper handling for both individual and team actions. This enhances gameplay analysis and future feature potential.
2025-02-23 01:14:27 +01:00
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
Teriuihi e5656f23ce Fix class change restriction and track player death state
Ensure players can change classes when they are dead or near spawn if alive. Added logic to track and update player death state, improving clarity and game mechanics around death handling.
2025-02-23 00:33:30 +01:00
Teriuihi 409a1aa596 Make addToScoreboard public and reset team scores.
Changed addToScoreboard visibility to public for external usage, allowing it to be called when a player joins a team. Added a reset for team scores in the Flag class to ensure scores are cleared when resetting the game state.
2025-02-23 00:27:49 +01:00
Teriuihi 57f2898451 Update OnPlayerOnlineStatus to assign a class to a joining player
The constructor of OnPlayerOnlineStatus was updated to include WorldBorderApi, and its functionality now integrates FighterCreator for enhanced player handling. This change ensures proper application of a class and world border logic during player teleport and initialization.
2025-02-16 00:13:05 +01:00
Teriuihi 2242fea737 Prevent block breaking during active game phases
Added an event handler to cancel block breaking unless the block is snow or the game phase is inactive. This ensures players cannot modify the environment during ongoing games.
2025-02-15 23:44:22 +01:00
Teriuihi e04892156c Gracefully handle null or offline players in game phases.
Added checks to ensure players are online and not null before interacting with them during the game phases. This prevents potential null pointer exceptions and improves overall code robustness. Also added a null check for the executorService in the CombatPhase end method.
2025-02-15 23:44:15 +01:00
14 changed files with 262 additions and 19 deletions

View File

@ -89,7 +89,7 @@ public class Main extends JavaPlugin {
pluginManager.registerEvents(new FlagTryCaptureEvent(flag), this);
pluginManager.registerEvents(new OnPlayerDeath(gameManager, worldBorderApi, this, flag), this);
pluginManager.registerEvents(new InventoryItemInteractionEvent(), this);
pluginManager.registerEvents(new OnPlayerOnlineStatus(gameManager, flag), this);
pluginManager.registerEvents(new OnPlayerOnlineStatus(gameManager, flag, worldBorderApi), this);
pluginManager.registerEvents(new GUIListener(), this);
}

View File

@ -46,7 +46,9 @@ public class SelectClass extends SubCommand {
return 0;
}
TeamPlayer teamPlayer = optionalTeamPlayer.get();
if (!gamePhase.equals(GamePhase.CLASS_SELECTION) && teamPlayer.getTeam().getSpawnLocation().distance(player.getLocation()) > 5) {
if (!teamPlayer.isDead()
&& !gamePhase.equals(GamePhase.CLASS_SELECTION)
&& teamPlayer.getTeam().getSpawnLocation().distance(player.getLocation()) > 5) {
commandSender.sendRichMessage("<red>You have to be near your spawn to change classes.</red>");
return 0;
}

View File

@ -5,12 +5,14 @@ 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.stats.Stat;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.damage.DamageEffect;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@ -44,7 +46,18 @@ public class OnPlayerDeath implements Listener {
Player player = event.getPlayer();
player.getInventory().clear();
player.updateInventory();
gameManager.getTeamPlayer(player)
.ifPresent(TeamPlayer::setDead);
flag.handleCarrierDeathOrDisconnect(player);
try {
if (event.getDamageSource().getDamageType().getDamageEffect().equals(DamageEffect.FREEZING)) {
gameManager.getTeamPlayer(player)
.ifPresent(teamPlayer -> teamPlayer.increaseStat(Stat.DEATHS_IN_POWDERED_SNOW));
}
} catch (Exception e) {
log.warn("Failed to check for death cause due to exception", e);
}
}
@EventHandler

View File

@ -5,8 +5,10 @@ import com.alttd.ctf.database.DiscordUserMapper;
import com.alttd.ctf.flag.Flag;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.creation.FighterCreator;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamPlayer;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.exceptions.PersistenceException;
import org.bukkit.attribute.Attribute;
@ -26,10 +28,12 @@ public class OnPlayerOnlineStatus implements Listener {
private final GameManager gameManager;
private final Flag flag;
private final WorldBorderApi worldBorderApi;
public OnPlayerOnlineStatus(GameManager gameManager, Flag flag) {
public OnPlayerOnlineStatus(GameManager gameManager, Flag flag, WorldBorderApi worldBorderApi) {
this.gameManager = gameManager;
this.flag = flag;
this.worldBorderApi = worldBorderApi;
}
@EventHandler
@ -60,8 +64,10 @@ public class OnPlayerOnlineStatus implements Listener {
teamPlayer = min.get().addPlayer(player);
} else {
teamPlayer = optionalTeamPlayer.get();
teamPlayer.getTeam().addToScoreboard(player);
}
player.teleportAsync(teamPlayer.getTeam().getSpawnLocation());
FighterCreator.createFighter(teamPlayer.getTeam().getColor())
.apply(teamPlayer, worldBorderApi, gamePhase, true);
}
private void resetPlayer(Player player) {

View File

@ -0,0 +1,108 @@
package com.alttd.ctf.events;
import com.alttd.ctf.game.GameManager;
import com.alttd.ctf.stats.Stat;
import com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Tag;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Player;
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.entity.PotionSplashEvent;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.util.Optional;
@Slf4j
public class OtherGameEvents implements Listener {
private final GameManager gameManager;
public OtherGameEvents(GameManager gameManager) {
this.gameManager = gameManager;
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
if (gameManager.getGamePhase().isEmpty()) {
return;
}
if (!Tag.SNOW.isTagged(event.getBlock().getType())) {
event.setCancelled(true);
return;
}
gameManager.getTeamPlayer(event.getPlayer())
.ifPresent(teamPlayer -> teamPlayer.increaseStat(Stat.SNOW_MINED));
}
@EventHandler
public void onBlockPlace(BlockBreakEvent event) {
if (gameManager.getGamePhase().isEmpty()) {
return;
}
if (!Tag.SNOW.isTagged(event.getBlock().getType())) {
event.setCancelled(true);
log.warn("Player {} placed a block that wasn't snow: {}",
event.getPlayer().getName(), event.getBlock().getType());
return;
}
gameManager.getTeamPlayer(event.getPlayer())
.ifPresent(teamPlayer -> teamPlayer.increaseStat(Stat.BLOCKS_PLACED));
}
@EventHandler
public void onPotionSplash(PotionSplashEvent event) {
ThrownPotion thrownPotion = event.getPotion();
if (!(thrownPotion.getShooter() instanceof Player player))
return;
Optional<TeamPlayer> optionalTeamPlayer = gameManager.getTeamPlayer(player);
if (optionalTeamPlayer.isEmpty())
return;
TeamPlayer teamPlayer = optionalTeamPlayer.get();
event.getAffectedEntities().stream()
.filter(livingEntity -> livingEntity instanceof Player)
.map(livingEntity -> (Player) livingEntity)
.forEach(target -> {
if (shouldHeal(teamPlayer, target)) {
return;
}
event.setIntensity(target, 0);
});
double totalHealing = thrownPotion.getEffects().stream()
.filter(effect -> effect.getType() == PotionEffectType.INSTANT_HEALTH)
.flatMapToDouble(effect -> event.getAffectedEntities().stream()
.filter(livingEntity -> livingEntity instanceof Player)
.map(livingEntity -> (Player) livingEntity)
.mapToDouble(target -> calculateActualHealing(target, effect, event.getIntensity(target))))
.sum();
teamPlayer.increaseStat(Stat.DAMAGE_HEALED, totalHealing);
}
private boolean shouldHeal(TeamPlayer healer, Player target) {
Optional<TeamPlayer> optionalTeamTarget = gameManager.getTeamPlayer(target);
return optionalTeamTarget.isPresent() && healer.getTeam() == optionalTeamTarget.get().getTeam();
}
private double calculateActualHealing(Player target, PotionEffect effect, double intensity) {
AttributeInstance playerMaxHealth = target.getAttribute(Attribute.GENERIC_MAX_HEALTH);
if (playerMaxHealth == null) {
return 0;
}
double missingHealth = playerMaxHealth.getValue() - target.getHealth();
//Only counts healing on teammates since intensity was set to 0 for non teammates
double potentialHealing = (effect.getAmplifier() + 1) * intensity;
return Math.min(potentialHealing, missingHealth);
}
}

View File

@ -3,6 +3,7 @@ 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.stats.Stat;
import com.alttd.ctf.team.TeamPlayer;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location;
@ -46,8 +47,13 @@ public class SnowballEvent implements Listener {
GameClass shooterClass = shooterTeamPlayer.getGameClass();
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
double newHealth = hitPlayer.getHealth() - shooterClass.getDamage();
hitPlayer.setHealth(Math.max(newHealth, 0));
double newHealth = Math.max(hitPlayer.getHealth() - shooterClass.getDamage(), 0);
hitPlayer.setHealth(newHealth);
shooterTeamPlayer.increaseStat(Stat.DAMAGE_DONE, shooterClass.getDamage());
if (newHealth <= 0) {
shooterTeamPlayer.increaseStat(Stat.KILLS);
}
log.debug("{} health was set to {} because of a snowball thrown by {}",
hitPlayer.getName(), Math.max(newHealth, 0), shooter.getName());
});
@ -58,6 +64,7 @@ public class SnowballEvent implements Listener {
handleSnowballThrown(event, (shooter, shooterTeamPlayer) -> {
GameClass shooterClass = shooterTeamPlayer.getGameClass();
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
shooterTeamPlayer.increaseStat(Stat.SNOWBALLS_THROWN);
});
}

View File

@ -3,6 +3,7 @@ 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.stats.Stat;
import com.alttd.ctf.team.Team;
import com.alttd.ctf.team.TeamColor;
import com.alttd.ctf.team.TeamPlayer;
@ -85,6 +86,7 @@ public class Flag implements Runnable {
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();
}
@ -165,7 +167,8 @@ public class Flag implements Runnable {
Placeholder.component("player", flagCarrier.displayName())));
Bukkit.getOnlinePlayers().forEach(player ->
gameManager.getTeam(player).ifPresent(team ->
player.showTitle(team.getId() == winningTeam.getId() ? capturingTeamTitle : huntingTeamTitle)));
player.showTitle(team.getId().intValue() == winningTeam.getId().intValue()
? capturingTeamTitle : huntingTeamTitle)));
}
private void spawnParticlesOnSquareBorder(Location center, double size) {
@ -221,7 +224,10 @@ public class Flag implements Runnable {
flagCarrier.getInventory().setItem(EquipmentSlot.HEAD, null);
gameManager.getTeamPlayer(flagCarrier)
.ifPresent(teamPlayer -> teamPlayer.getGameClass().setArmor(flagCarrier, teamPlayer));
.ifPresent(teamPlayer -> {
teamPlayer.getGameClass().setArmor(flagCarrier, teamPlayer);
teamPlayer.increaseStat(Stat.FLAGS_CAPTURED);
});
resetFlagCarrier();
}
@ -318,6 +324,7 @@ public class Flag implements Runnable {
return Math.max(updatedValue, 0);
});
});
nearbyPlayers.forEach(teamPlayer -> teamPlayer.increaseStat(Stat.TIME_SPEND_CAPTURING_FLAG));
return true;
}
@ -376,6 +383,7 @@ public class Flag implements Runnable {
resetFlagCarrier();
resetFlag();
wins.clear();
gameManager.getTeams().forEach(team -> team.setScore(0));
}
public void handleCarrierDeathOrDisconnect(Player player) {

View File

@ -32,6 +32,8 @@ public class CombatPhase implements GamePhaseExecutor {
@Override
public void end(GamePhase ignored) {
executorService.shutdown();
if (executorService != null) {
executorService.shutdown();
}
}
}

View File

@ -4,11 +4,11 @@ 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.game_class.GameClass;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@Slf4j
public class GatheringPhase implements GamePhaseExecutor {
@ -37,8 +37,12 @@ public class GatheringPhase implements GamePhaseExecutor {
return;
}
gameManager.getTeams().forEach(team -> {
team.getPlayers().forEach(player -> {
player.resetWorldBorder(Bukkit.getPlayer(player.getUuid()), worldBorderApi, nextPhase, flag.getFlagLocation());
team.getPlayers().forEach(teamPlayer -> {
Player player = Bukkit.getPlayer(teamPlayer.getUuid());
if (player == null || !player.isOnline()) {
return;
}
teamPlayer.resetWorldBorder(player, worldBorderApi, nextPhase, flag.getFlagLocation());
});
});
}

View File

@ -0,0 +1,58 @@
package com.alttd.ctf.stats;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.util.UUID;
@Getter
public final class PlayerStat {
private static final CommandSender commandSender = Bukkit.getConsoleSender();
private final UUID uuid;
private final String inGameName;
private boolean completedGame = false;
private int flagsCaptured = 0;
private int kills = 0;
private double damageDone = 0;
private double damageHealed = 0;
private int snowMined = 0;
private int blocksPlaced = 0;
private int snowballsThrown = 0;
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));
}
completedGame = true;
}
case FLAGS_CAPTURED -> flagsCaptured++;
case KILLS -> kills++;
case SNOW_MINED -> snowMined++;
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 DAMAGE_DONE, DAMAGE_HEALED -> throw new IllegalArgumentException(String.format("%s requires a number", stat.name()));
}
}
public void increaseStat(Stat stat, double value) throws IllegalArgumentException {
switch (stat) {
case DAMAGE_DONE -> damageDone += value;
case DAMAGE_HEALED -> damageHealed += value;
default -> throw new IllegalArgumentException(String.format("%s cannot be passed with a number", stat.name()));
}
}
}

View File

@ -0,0 +1,14 @@
package com.alttd.ctf.stats;
public enum Stat {
COMPLETED_GAME,
FLAGS_CAPTURED,
KILLS,
DAMAGE_DONE,
DAMAGE_HEALED,
SNOW_MINED,
BLOCKS_PLACED,
SNOWBALLS_THROWN,
TIME_SPEND_CAPTURING_FLAG,
DEATHS_IN_POWDERED_SNOW
}

View File

@ -76,7 +76,7 @@ public class Team {
public TeamPlayer addPlayer(Player player) {
removeFromScoreBoard(player);
UUID uuid = player.getUniqueId();
TeamPlayer teamPlayer = new TeamPlayer(uuid, this);
TeamPlayer teamPlayer = new TeamPlayer(player, this);
players.put(uuid, teamPlayer);
addToScoreboard(player);
if (discordTeam != null) {
@ -113,7 +113,7 @@ public class Team {
log.debug("Removed player {} from team with id {}", player.getName(), id);
}
private void addToScoreboard(Player player) {
public void addToScoreboard(Player player) {
org.bukkit.scoreboard.Team team = scoreboard.getTeam("ctf_" + id);
if (team == null) {
team = scoreboard.registerNewTeam("ctf_" + id);

View File

@ -7,6 +7,8 @@ import com.alttd.ctf.game.GamePhase;
import com.alttd.ctf.game_class.GameClass;
import com.alttd.ctf.game_class.GameClassRetrieval;
import com.alttd.ctf.gui.ClassSelectionGUI;
import com.alttd.ctf.stats.PlayerStat;
import com.alttd.ctf.stats.Stat;
import com.github.yannicklamprecht.worldborder.api.WorldBorderApi;
import lombok.Getter;
import lombok.Setter;
@ -21,14 +23,17 @@ import java.util.*;
@Getter
public class TeamPlayer {
private final PlayerStat playerStat;
private final UUID uuid;
private final Team team;
@Setter
private GameClass gameClass;
private boolean isDead = false;
protected TeamPlayer(UUID uuid, Team team) {
this.uuid = uuid;
protected TeamPlayer(Player player, Team team) {
this.uuid = player.getUniqueId();
this.team = team;
this.playerStat = new PlayerStat(uuid, player.getName());
}
public void respawn(@NotNull Player player, @NotNull WorldBorderApi worldBorderApi, @NotNull GamePhase gamePhase) {
@ -48,9 +53,17 @@ public class TeamPlayer {
}
player.teleportAsync(spawnLocation).thenAcceptAsync(unused ->
resetWorldBorder(player, worldBorderApi, gamePhase, worldBorderCenter));
isDead = false;
}
public void resetWorldBorder(Player player, WorldBorderApi worldBorderApi, GamePhase gamePhase, Location worldBorderCenter) {
public void setDead() {
isDead = true;
}
public void resetWorldBorder(@NotNull Player player, WorldBorderApi worldBorderApi, GamePhase gamePhase, Location worldBorderCenter) {
if (!player.isOnline()) {
return;
}
WorldBorderSettings worldBorderSettings = GameConfig.WORLD_BORDER.getGAME_PHASE_WORLD_BORDER().get(gamePhase);
if (worldBorderSettings == null) {
throw new IllegalStateException("All phases need to have world border settings");
@ -78,4 +91,12 @@ public class TeamPlayer {
public int hashCode() {
return Objects.hash(uuid);
}
public void increaseStat(Stat stat) {
playerStat.increaseStat(stat);
}
public void increaseStat(Stat stat, double amount) {
playerStat.increaseStat(stat, amount);
}
}

View File

@ -1,3 +1,3 @@
#Sat Feb 15 22:27:11 CET 2025
buildNumber=66
#Sun Feb 23 01:14:21 CET 2025
buildNumber=70
version=0.1