Skip to content

Commit

Permalink
Make assists part of stats (#1469)
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Herrera <[email protected]>
  • Loading branch information
Pablete1234 authored Dec 31, 2024
1 parent 4c7eb75 commit df8e82e
Show file tree
Hide file tree
Showing 11 changed files with 360 additions and 291 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public final PlayerRelation getRelation() {
return PlayerRelation.get(getVictim().getParticipantState(), getKiller());
}

/** Get the relationship between the victim and assister */
public final PlayerRelation getAssistRelation() {
return PlayerRelation.get(getVictim().getParticipantState(), getAssister());
}

/**
* Get whether the death was caused by a teammate.
*
Expand All @@ -140,6 +145,15 @@ public final boolean isEnemyKill() {
return PlayerRelation.ENEMY == getRelation();
}

/**
* Get whether the assist was caused by an enemy.
*
* @return Whether the assist was from an enemy.
*/
public final boolean isEnemyAssist() {
return PlayerRelation.ENEMY == getAssistRelation();
}

/**
* Get whether the {@link MatchPlayer} killed themselves.
*
Expand All @@ -149,24 +163,33 @@ public final boolean isSuicide() {
return PlayerRelation.SELF == getRelation();
}

/**
* Get whether the victim and killer are the same {@link MatchPlayer}.
*
* @return
*/
/** Get whether the victim and killer are the same {@link MatchPlayer}. */
public final boolean isSelfKill() {
return getKiller() != null && getKiller().isPlayer(getVictim());
}

public final boolean isSelfAssist() {
return getAssister() != null && getAssister().isPlayer(getVictim());
}

/**
* Get whether the death was from an enemy and it was no caused by suicide.
* Get whether the death was from an enemy and it was not caused by suicide.
*
* @return Whether the death was actually "challenging."
*/
public final boolean isChallengeKill() {
return isEnemyKill() && !isSelfKill();
}

/**
* Get whether the assist was from an enemy and it was not caused by suicide.
*
* @return Whether the assist was actually "challenging."
*/
public final boolean isChallengeAssist() {
return isEnemyAssist() && !isSelfAssist();
}

private static final HandlerList handlers = new HandlerList();

@Override
Expand Down
11 changes: 5 additions & 6 deletions core/src/main/java/tc/oc/pgm/command/StatsCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ public void stats(
if (match.isFinished()
&& PGM.get().getConfiguration().showVerboseStats()
&& match.hasModule(TeamMatchModule.class)) { // Should not try to trigger on FFA
stats.giveVerboseStatsItem(player, true);
stats.openStatsMenu(player);
} else if (player.getSettings().getValue(SettingKey.STATS).equals(SettingValue.STATS_ON)) {
audience.sendMessage(
TextFormatter.horizontalLineHeading(
sender,
translatable("match.stats.you", NamedTextColor.DARK_GREEN),
NamedTextColor.WHITE));
audience.sendMessage(TextFormatter.horizontalLineHeading(
sender,
translatable("match.stats.you", NamedTextColor.DARK_GREEN),
NamedTextColor.WHITE));
audience.sendMessage(stats.getBasicStatsMessage(player.getId()));
} else {
throw exception("match.stats.disabled");
Expand Down
97 changes: 74 additions & 23 deletions core/src/main/java/tc/oc/pgm/stats/PlayerStats.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package tc.oc.pgm.stats;

import static net.kyori.adventure.text.Component.translatable;
import static tc.oc.pgm.util.text.NumberComponent.number;
import static tc.oc.pgm.stats.StatType.ASSISTS;
import static tc.oc.pgm.stats.StatType.DEATHS;
import static tc.oc.pgm.stats.StatType.KILLS;
import static tc.oc.pgm.stats.StatType.KILL_DEATH_RATIO;
import static tc.oc.pgm.stats.StatType.KILL_STREAK;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import tc.oc.pgm.api.match.MatchScope;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.api.setting.SettingKey;
import tc.oc.pgm.api.setting.SettingValue;
import tc.oc.pgm.util.Audience;

/** A wrapper for stat info belonging to a {@link tc.oc.pgm.api.player.MatchPlayer} */
public class PlayerStats {
public class PlayerStats implements StatHolder {

// A reference to the players global stats to be incremented along with team based stats
private final PlayerStats parent;
Expand All @@ -22,6 +31,7 @@ public class PlayerStats {
// K/D
private int kills;
private int deaths;
private int assists;
private int killstreak; // Current killstreak
private int killstreakMax; // The highest killstreak reached this match

Expand Down Expand Up @@ -53,7 +63,7 @@ public class PlayerStats {

// The task responsible for displaying the stats over the hotbar
// See StatsMatchModule#sendLongHotbarMessage
private Future<?> hotbarTaskCache;
private Future<?> hotbarTask;

public PlayerStats() {
this(null, null);
Expand All @@ -68,17 +78,25 @@ public PlayerStats(PlayerStats parent, Component component) {

// Methods to update the stats, should only be accessed by StatsMatchModule

protected void onMurder() {
protected void onMurder(MatchPlayer player) {
kills++;
killstreak++;
if (killstreak > killstreakMax) killstreakMax = killstreak;
if (parent != null) parent.onMurder();
if (parent != null) parent.onMurder(null);
sendPlayerStats(player);
}

protected void onDeath() {
protected void onDeath(MatchPlayer player) {
deaths++;
killstreak = 0;
if (parent != null) parent.onDeath();
if (parent != null) parent.onDeath(null);
sendPlayerStats(player);
}

protected void onAssist(MatchPlayer player) {
assists++;
if (parent != null) parent.onAssist(null);
sendPlayerStats(player);
}

protected void onDamage(double damage, boolean bow) {
Expand Down Expand Up @@ -148,9 +166,7 @@ protected void onWoolTouch() {
}

protected void setLongestBowKill(double distance) {
if (distance > longestBowKill) {
longestBowKill = (int) Math.round(distance);
}
if (distance > longestBowKill) longestBowKill = (int) Math.round(distance);
if (parent != null) parent.setLongestBowKill(distance);
}

Expand All @@ -167,16 +183,24 @@ protected void onTeamSwitch() {
// Makes a simple stat message for this player that fits in one line

public Component getBasicStatsMessage() {
return translatable(
"match.stats",
NamedTextColor.GRAY,
number(kills, NamedTextColor.GREEN),
number(killstreak, NamedTextColor.GREEN),
number(deaths, NamedTextColor.RED),
number(getKD(), NamedTextColor.GREEN));
return pipeSeparated(KILLS, DEATHS, ASSISTS, KILL_STREAK, KILL_DEATH_RATIO)
.color(NamedTextColor.GRAY);
}

// Getters, both raw stats and some handy calculations
@Override
public Number getStat(StatType type) {
return switch (type) {
case KILLS -> kills;
case DEATHS -> deaths;
case ASSISTS -> assists;
case KILL_STREAK -> killstreak;
case BEST_KILL_STREAK -> killstreakMax;
case KILL_DEATH_RATIO -> getKD();
case LONGEST_BOW_SHOT -> longestBowKill;
case DAMAGE -> damageDone;
};
}

public double getKD() {
return kills / Math.max(1d, deaths);
Expand All @@ -196,6 +220,10 @@ public int getDeaths() {
return deaths;
}

public int getAssists() {
return assists;
}

public int getKillstreak() {
return killstreak;
}
Expand Down Expand Up @@ -264,12 +292,18 @@ public Duration getLongestFlagHold() {
return longestFlagHold;
}

public Future<?> getHotbarTask() {
return hotbarTaskCache;
private void setHotbarTask(Future<?> task) {
if (hotbarTask != null && !hotbarTask.isDone()) hotbarTask.cancel(true);
hotbarTask = task;
}

public void putHotbarTaskCache(Future<?> task) {
hotbarTaskCache = task;
public void sendPlayerStats(MatchPlayer player) {
if (player == null) return;
if (player.getSettings().getValue(SettingKey.STATS) == SettingValue.STATS_OFF) return;
setHotbarTask(player
.getMatch()
.getExecutor(MatchScope.LOADED)
.scheduleWithFixedDelay(new HotbarStatsRunner(player), 0, 1, TimeUnit.SECONDS));
}

public Component getPlayerComponent() {
Expand Down Expand Up @@ -297,6 +331,23 @@ public Duration getTimePlayed() {
}

public Duration getActiveSessionDuration() {
return (inTime == null) ? Duration.ZERO : Duration.between(inTime, Instant.now());
return inTime == null ? Duration.ZERO : Duration.between(inTime, Instant.now());
}

protected class HotbarStatsRunner implements Runnable {
private final Audience audience;
private final Component message;
private int remaining = 4;

private HotbarStatsRunner(Audience audience) {
this.audience = audience;
this.message = getBasicStatsMessage();
}

@Override
public void run() {
audience.sendActionBar(message);
if (remaining-- < 0) setHotbarTask(null);
}
}
}
29 changes: 29 additions & 0 deletions core/src/main/java/tc/oc/pgm/stats/StatHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package tc.oc.pgm.stats;

import static net.kyori.adventure.text.Component.join;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.JoinConfiguration.separator;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;

public interface StatHolder {
JoinConfiguration PIPE = separator(text(" | "));
JoinConfiguration SPACES = separator(text(" "));

Number getStat(StatType type);

default Component pipeSeparated(StatType... types) {
return getComponent(PIPE, types);
}

default Component spaceSeparated(StatType... types) {
return getComponent(SPACES, types);
}

default Component getComponent(JoinConfiguration joining, StatType... types) {
Component[] children = new Component[types.length];
for (int i = 0; i < types.length; i++) children[i] = types[i].component(this);
return join(joining, children);
}
}
43 changes: 43 additions & 0 deletions core/src/main/java/tc/oc/pgm/stats/StatType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package tc.oc.pgm.stats;

import static net.kyori.adventure.text.Component.translatable;
import static net.kyori.adventure.text.format.NamedTextColor.*;
import static tc.oc.pgm.stats.StatsMatchModule.damageComponent;
import static tc.oc.pgm.util.text.NumberComponent.number;

import java.util.Locale;
import net.kyori.adventure.text.Component;

public enum StatType {
KILLS,
DEATHS,
ASSISTS,
KILL_STREAK,
BEST_KILL_STREAK,
KILL_DEATH_RATIO,
LONGEST_BOW_SHOT {
private final String blocks = key + ".blocks";

@Override
public Component makeNumber(Number number) {
return translatable(blocks, number(number, YELLOW));
}
},
DAMAGE {
@Override
public Component makeNumber(Number number) {
return damageComponent(number.doubleValue(), GREEN);
}
};

public final String key = "match.stats.type." + name().toLowerCase(Locale.ROOT);
public static final StatType[] VALUES = values();

public Component makeNumber(Number number) {
return number(number, this == DEATHS ? RED : GREEN);
}

public Component component(StatHolder stats) {
return translatable(key, makeNumber(stats.getStat(this)));
}
}
Loading

0 comments on commit df8e82e

Please sign in to comment.