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.
This commit is contained in:
Teriuihi 2025-02-23 01:14:27 +01:00
parent 8386e773ce
commit eeed5b4c54
9 changed files with 203 additions and 21 deletions

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;
@ -47,6 +49,15 @@ public class OnPlayerDeath implements Listener {
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

@ -0,0 +1,86 @@
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.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;
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 -> {
AttributeInstance playerMaxHealth = target.getAttribute(Attribute.GENERIC_MAX_HEALTH);
if (playerMaxHealth == null) {
return 0;
}
double missingHealth = playerMaxHealth.getValue() - target.getHealth();
double potentialHealing = (effect.getAmplifier() + 1) * event.getIntensity(target);
return Math.min(potentialHealing, missingHealth);
}))
.sum();
optionalTeamPlayer.get().increaseStat(Stat.DAMAGE_HEALED, totalHealing);
}
}

View File

@ -3,16 +3,15 @@ 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;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.entity.Snowball;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.util.Vector;
@ -48,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());
});
@ -60,19 +64,10 @@ public class SnowballEvent implements Listener {
handleSnowballThrown(event, (shooter, shooterTeamPlayer) -> {
GameClass shooterClass = shooterTeamPlayer.getGameClass();
shooter.setCooldown(Material.SNOWBALL, shooterClass.getThrowTickSpeed());
shooterTeamPlayer.increaseStat(Stat.SNOWBALLS_THROWN);
});
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
if (gameManager.getGamePhase().isEmpty()) {
return;
}
if (Tag.SNOW.isTagged(event.getBlock().getType()))
return;
event.setCancelled(true);
}
private boolean blockedAttack(@NotNull Player hitPlayer, @NotNull Snowball snowball) {
if (!hitPlayer.isBlocking()) {
return false;

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;
@ -166,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) {
@ -222,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();
}
@ -319,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;
}

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) {

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,15 +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) {
@ -87,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