feat: replace ELO system by a bounty system

This commit is contained in:
HerozDotExe 2026-02-13 23:11:33 +01:00
parent 16bbb741c8
commit 034cb40967
16 changed files with 328 additions and 169 deletions

View file

@ -4,9 +4,7 @@ This is a plugin used on one of my minecraft server.
## Features
* ELO Ranking system (like chess)
* You gain ELO when you kill a player
* YOu lose ELO when you get killed by a player
* Leaderboard command to see the top ELO (/rankings)
* Command to force set ranking to your liking (/setranking <player> <ranking>)
* ELO is displayer under the name of players
* Bounty system
* Your bounty increase with time
* Your bounty gets reset when you get kill
* Your bounty increase faster if you have a high KDR

View file

@ -1,46 +0,0 @@
package dev.hrz0.smp4;
import java.lang.Math;
public class ELO {
// Function to calculate the Probability
public static double Probability(double rating1, double rating2) {
// Calculate and return the expected score
return 1.0 / (1 + Math.pow(10, (rating1 - rating2) / 400.0));
}
// Function to calculate Elo rating
// K is a constant.
// outcome determines the outcome: 1 for Player A win, 0 for Player B win, 0.5 for draw.
public static int[] EloRating(double Ra, double Rb, int K, double outcome) {
System.out.println("Original Ratings:-");
System.out.println("Ra = " + Ra + " Rb = " + Rb);
// Calculate the Winning Probability of Player B
double Pb = Probability(Ra, Rb);
// Calculate the Winning Probability of Player A
double Pa = Probability(Rb, Ra);
// Update the Elo Ratings
Ra = Ra + K * (outcome - Pa);
Rb = Rb + K * ((1 - outcome) - Pb);
return new int[] {
(int) Ra, (int) Rb
};
}
// public static void main(String[] args) {
// // Current ELO ratings
// double Ra = 1200, Rb = 1000;
//
// // K is a constant
// int K = 30;
//
// // Outcome: 1 for Player A win, 0 for Player B win, 0.5 for draw
// double outcome = 1;
//
// // Function call
// EloRating(Ra, Rb, K, outcome);
// }
}

View file

@ -24,7 +24,7 @@ public class LeaderboardCommand implements BasicCommand {
StringBuilder leaderboard = new StringBuilder("<yellow><white>[</white><dark_red>ELO</dark_red><white>]</white> <u>ELO Leaderboards:</u>\n");
ArrayList<Map.Entry<String, Integer>> players = new ArrayList<>(states.rankings.entrySet());
ArrayList<Map.Entry<String, Integer>> players = new ArrayList<>(states.worths.entrySet());
players.sort(Collections.reverseOrder(Map.Entry.comparingByValue()));
@ -44,6 +44,6 @@ public class LeaderboardCommand implements BasicCommand {
@Override
public @Nullable String permission() {
return "smp4.rankings";
return "smp4.worths";
}
}

View file

@ -1,10 +1,7 @@
package dev.hrz0.smp4.Listeners;
import dev.hrz0.smp4.ELO;
import dev.hrz0.smp4.States.States;
import dev.hrz0.smp4.Utilities;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.black_ixx.playerpoints.PlayerPoints;
import org.black_ixx.playerpoints.PlayerPointsAPI;
import org.bukkit.Bukkit;
@ -16,21 +13,14 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DeathListener implements Listener {
private final States states;
private final Logger logger;
private final FileConfiguration config;
private PlayerPointsAPI pointsAPI;
public DeathListener(States states, Logger logger, FileConfiguration config) {
this.states = states;
this.logger = logger;
this.config = config;
}
@ -43,55 +33,87 @@ public class DeathListener implements Listener {
Player killer = event.getEntity().getKiller();
if (killed != null && killer != null && killer.getType().equals(EntityType.PLAYER)) {
logger.log(Level.INFO, String.format("%s killed %s", killer, killed));
int killedRanking = states.rankings.getPlayerRanking(killed);
int killerRanking = states.rankings.getPlayerRanking(killer);
int[] newRankings = ELO.EloRating(killedRanking, killerRanking, 200, 0);
states.rankings.setPlayerRanking(killed, newRankings[0]);
states.rankings.setPlayerRanking(killer, newRankings[1]);
MiniMessage mm = MiniMessage.miniMessage();
Component msg = mm.deserialize(String.format("""
<yellow><white>[</white><dark_red>ELO</dark_red><white>]</white> <u>Updated rankings:</u>
%s's ranking increased by %s (%s->%s)
%s's ranking decreased by %s (%s->%s)</yellow>""",
killer.getName(), newRankings[1] - killerRanking, killerRanking, newRankings[1], killed.getName(), killedRanking - newRankings[0], killedRanking, newRankings[0]));
Utilities.setScore(killed, newRankings[0]);
Utilities.setScore(killer, newRankings[1]);
Utilities.announce(msg);
List<String> commands = config.getStringList("killCommands");
Map<String, String> toFill = new HashMap<String, String>();
toFill.put("<killer>", killer.getName());
toFill.put("<killed>", killed.getName());
Utilities.runCommands(commands, toFill);
if (Bukkit.getPluginManager().isPluginEnabled("PlayerPoints")) {
this.pointsAPI = PlayerPoints.getInstance().getAPI();
PlayerPointsAPI pointsAPI = PlayerPoints.getInstance().getAPI();
if (this.pointsAPI != null) {
// Take money
double money = pointsAPI.look(killed.getUniqueId());
double newMoney = money * ((double) (100 - config.getInt("percentOfMoneyToTakeOnKills")) / 100);
pointsAPI.set(killed.getUniqueId(), (int) newMoney);
int killedWorth = states.worths.getPlayerWorth(killed);
pointsAPI.give(killer.getUniqueId(), killedWorth);
killer.sendRichMessage(String.format("[SMP4] <gold>You got %s points from killing %s.</gold>", killedWorth, killed.getName()));
states.worths.setPlayerWorth(killed, 0);
killed.sendRichMessage("[SMP4] <gold>Your worth got reset to 0.</gold>");
killed.sendRichMessage(String.format("[SMP4] <dark_purple>You lost %s points by getting killed by %s (%s->%s).</dark_purple>", money - newMoney, killed.getName(), money, newMoney));
// Take money
double money = pointsAPI.look(killed.getUniqueId());
double newMoney = money * ((double) (100 - config.getInt("percentOfMoneyToTakeOnKills")) / 100);
pointsAPI.set(killed.getUniqueId(), (int) newMoney);
killed.sendRichMessage(String.format("[SMP4] <gold>You lost %s%% points from dying to %s.</gold>", money-newMoney, killer));
// Give money
int moneyGain = newRankings[1] - killerRanking;
int multiplier = config.getInt("moneyMultiplier");
pointsAPI.give(killer.getUniqueId(), moneyGain*multiplier);
states.kdr.increasePlayerDeaths(killed);
killed.sendRichMessage("[SMP4] <gold>Your kdr has been updated.</gold>");
states.kdr.increasePlayerKills(killer);
killer.sendRichMessage("[SMP4] <gold>Your kdr has been updated.</gold>");
killer.sendRichMessage(String.format("[SMP4] <dark_purple>You received %s points for killing %s.</dark_purple>", moneyGain*multiplier, killed.getName()));
}
Utilities.setScore(killed, 0);
}
}
}
// @EventHandler(priority = EventPriority.HIGHEST)
// public void onPlayerDeath(PlayerDeathEvent event) {
// Player killed = event.getEntity().getPlayer();
// Player killer = event.getEntity().getKiller();
//
// if (killed != null && killer != null && killer.getType().equals(EntityType.PLAYER)) {
// logger.log(Level.INFO, String.format("%s killed %s", killer, killed));
//
// int killedRanking = states.rankings.getPlayerRanking(killed);
// int killerRanking = states.rankings.getPlayerRanking(killer);
//
// int[] newRankings = ELO.EloRating(killedRanking, killerRanking, 200, 0);
//
// states.rankings.setPlayerRanking(killed, newRankings[0]);
// states.rankings.setPlayerRanking(killer, newRankings[1]);
//
// MiniMessage mm = MiniMessage.miniMessage();
//
// Component msg = mm.deserialize(String.format("""
// <yellow><white>[</white><dark_red>ELO</dark_red><white>]</white> <u>Updated rankings:</u>
// %s's ranking increased by %s (%s->%s)
// %s's ranking decreased by %s (%s->%s)</yellow>""",
// killer.getName(), newRankings[1] - killerRanking, killerRanking, newRankings[1], killed.getName(), killedRanking - newRankings[0], killedRanking, newRankings[0]));
//
// Utilities.setScore(killed, newRankings[0]);
// Utilities.setScore(killer, newRankings[1]);
//
// Utilities.announce(msg);
//
// List<String> commands = config.getStringList("killCommands");
// Map<String, String> toFill = new HashMap<String, String>();
// toFill.put("<killer>", killer.getName());
// toFill.put("<killed>", killed.getName());
// Utilities.runCommands(commands, toFill);
//
// if (Bukkit.getPluginManager().isPluginEnabled("PlayerPoints")) {
// this.pointsAPI = PlayerPoints.getInstance().getAPI();
//
// if (this.pointsAPI != null) {
// // Take money
// double money = pointsAPI.look(killed.getUniqueId());
// double newMoney = money * ((double) (100 - config.getInt("percentOfMoneyToTakeOnKills")) / 100);
// pointsAPI.set(killed.getUniqueId(), (int) newMoney);
//
// killed.sendRichMessage(String.format("[SMP4] <dark_purple>You lost %s points by getting killed by %s (%s->%s).</dark_purple>", money - newMoney, killed.getName(), money, newMoney));
//
// // Give money
// int moneyGain = newRankings[1] - killerRanking;
// int multiplier = config.getInt("moneyMultiplier");
// pointsAPI.give(killer.getUniqueId(), moneyGain*multiplier);
//
// killer.sendRichMessage(String.format("[SMP4] <dark_purple>You received %s points for killing %s.</dark_purple>", moneyGain*multiplier, killed.getName()));
// }
// }
//
// }
// }
}

View file

@ -27,16 +27,16 @@ public class JoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Integer rank = states.rankings.getPlayerRanking(player);
Integer worth = states.worths.getPlayerWorth(player);
if (rank == null) {
this.states.rankings.setPlayerRanking(player, 1000);
rank = 1000;
if (worth == null) {
this.states.worths.setPlayerWorth(player, 1000);
worth = 1000;
} else {
logger.info(String.format("%s has a ranking of %s", player.getName(), rank));
logger.info(String.format("%s has a ranking of %s", player.getName(), worth));
}
Utilities.setScore(player, rank);
Utilities.setScore(player, worth);
states.usernameCache.cachePlayerUsername(player.getUniqueId(), player.getName());
}
}

View file

@ -4,15 +4,19 @@ import dev.hrz0.smp4.States.States;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
@NullMarked
public class SetRankingCommand implements BasicCommand {
public class SetWorthCommand implements BasicCommand {
States states;
public SetRankingCommand(States states) {
public SetWorthCommand(States states) {
this.states = states;
}
@ -23,12 +27,14 @@ public class SetRankingCommand implements BasicCommand {
return;
}
Player target = Bukkit.getPlayer(args[0]);
int ranking = Integer.parseInt(args[1]);
UUID targetUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + args[0]).getBytes(StandardCharsets.UTF_8));
int worth = Integer.parseInt(args[1]);
if (target != null) {
states.rankings.setPlayerRanking(target, ranking);
source.getSender().sendRichMessage("<green> Ranking changed!");
source.getSender().sendMessage(String.format("%s %s", targetUUID, worth));
if (states.worths.containsKey(targetUUID.toString())) {
states.worths.setPlayerWorth(targetUUID, worth);
source.getSender().sendRichMessage("<green>Worth changed!");
} else {
source.getSender().sendRichMessage("<red>Player not found!");
}
@ -36,6 +42,6 @@ public class SetRankingCommand implements BasicCommand {
@Override
public @Nullable String permission() {
return "smp4.setranking";
return "smp4.setworth";
}
}

View file

@ -56,15 +56,20 @@ public final class Smp4 extends JavaPlugin {
}
registerCommand(
"setranking",
new SetRankingCommand(states)
"setworth",
new SetWorthCommand(states)
);
registerCommand(
"rankings",
"worths",
new LeaderboardCommand(states)
);
registerCommand(
"worth",
new WorthCommand(states)
);
Utilities.initScore();
new BukkitRunnable() {
@ -86,9 +91,9 @@ public final class Smp4 extends JavaPlugin {
toFill.put("<owner>", eggOwner.getName());
Utilities.runCommands(commands, toFill);
int eloGain = config.getInt("eloGainWhenHoldingEgg");
int currentElo = states.rankings.getPlayerRanking(eggOwner);
states.rankings.setPlayerRanking(eggOwner, currentElo + eloGain);
int worthGain = config.getInt("worthGainWhenHoldingEgg");
int currentWorth = states.worths.getPlayerWorth(eggOwner);
states.worths.setPlayerWorth(eggOwner, currentWorth + worthGain);
PlayerPointsAPI pointsAPI = PlayerPoints.getInstance().getAPI();
@ -96,11 +101,33 @@ public final class Smp4 extends JavaPlugin {
int moneyGain = config.getInt("moneyGainWhenHoldingEgg");
pointsAPI.give(eggOwner.getUniqueId(), moneyGain);
eggOwner.sendRichMessage(String.format("[SMP4] <dark_purple>You received %s ELO and %s points for holding the egg.</dark_purple>", eloGain, moneyGain));
eggOwner.sendRichMessage(String.format("[SMP4] <dark_purple>You received %s worth and %s points for holding the egg.</dark_purple>", worthGain, moneyGain));
}
Utilities.setScore(eggOwner, states.worths.getPlayerWorth(eggOwner));
}
}
}.runTaskTimer(this, 0L, config.getInt("eggOwnerCommandInterval") * 20L);
new BukkitRunnable() {
@Override
public void run() {
for (Player p : Bukkit.getOnlinePlayers()) {
float kdr = states.kdr.getPlayerKDR(p);
int maxKDR = config.getInt("maxKDR");
if (kdr > maxKDR) {
kdr = maxKDR;
}
int worthIncrease = (int) (config.getInt("startingWorthIncrease") * kdr);
states.worths.addToPlayerWorth(p, worthIncrease);
p.sendRichMessage(String.format("[SMP4] <gold>Your worth increased by %s.</gold>", worthIncrease));
Utilities.setScore(p, states.worths.getPlayerWorth(p));
}
}
}.runTaskTimer(this, 0L, config.getInt("worthUpdateInterval") * 20L);
}
@Override

View file

@ -0,0 +1,74 @@
package dev.hrz0.smp4.States;
import org.bukkit.entity.Player;
import java.util.HashMap;
// Counters starts at 1 to prevent dividing by 0
public class KDR extends HashMap<String, KDRValues> {
public float getPlayerKDR(Player player) {
if (this.containsKey(player.getUniqueId().toString())) {
KDRValues kdrValues = getPlayerKDRValues(player);
return (float) kdrValues.kills / kdrValues.deaths;
} else {
KDRValues kdrValues = new KDRValues(1, 1);
put(player.getUniqueId().toString(), kdrValues);
return 1;
}
}
public KDRValues getPlayerKDRValues(Player player) {
if (this.containsKey(player.getUniqueId().toString())) {
return get(player.getUniqueId().toString());
} else {
KDRValues kdrValues = new KDRValues(1, 1);
put(player.getUniqueId().toString(), kdrValues);
return kdrValues;
}
}
public int getPlayerKills(Player player) {
if (this.containsKey(player.getUniqueId().toString())) {
return get(player.getUniqueId().toString()).kills;
} else {
KDRValues kdrValues = new KDRValues(1, 1);
put(player.getUniqueId().toString(), kdrValues);
return 0;
}
}
public void increasePlayerKills(Player player) {
if (this.containsKey(player.getUniqueId().toString())) {
KDRValues kdrValue = getPlayerKDRValues(player);
kdrValue.kills += 1;
put(player.getUniqueId().toString(), kdrValue);
} else {
KDRValues kdrValues = new KDRValues(1, 2);
put(player.getUniqueId().toString(), kdrValues);
}
}
public int getPlayerDeaths(Player player) {
if (this.containsKey(player.getUniqueId().toString())) {
return get(player.getUniqueId().toString()).deaths;
} else {
KDRValues kdrValues = new KDRValues(1, 1);
put(player.getUniqueId().toString(), kdrValues);
return 0;
}
}
public void increasePlayerDeaths(Player player) {
if (this.containsKey((player.getUniqueId().toString()))) {
KDRValues kdrValue = getPlayerKDRValues(player);
kdrValue.deaths += 1;
put(player.getUniqueId().toString(), kdrValue);
} else {
KDRValues kdrValues = new KDRValues(2, 1);
put(player.getUniqueId().toString(), kdrValues);
}
}
}

View file

@ -0,0 +1,11 @@
package dev.hrz0.smp4.States;
public class KDRValues {
public int kills = 0;
public int deaths = 0;
public KDRValues(int deaths, int kills) {
this.kills = kills;
this.deaths = deaths;
}
}

View file

@ -1,20 +0,0 @@
package dev.hrz0.smp4.States;
import org.bukkit.entity.Player;
import java.util.HashMap;
public class Rankings extends HashMap<String, Integer> {
public Integer getPlayerRanking(Player player) {
if (this.containsKey(player.getUniqueId().toString())) {
return get(player.getUniqueId().toString());
} else {
put(player.getUniqueId().toString(), 1000);
return 1000;
}
}
public void setPlayerRanking(Player player, int ranking) {
put(player.getUniqueId().toString(), ranking);
}
}

View file

@ -10,14 +10,16 @@ import java.util.logging.Logger;
public class States {
public Rankings rankings;
public UsernameCache usernameCache;
public KDR kdr;
public Worths worths;
public States(JavaPlugin plugin, Logger logger) throws IOException {
Path statesPath = StatesParser.getStatesPath(plugin);
this.rankings = new Rankings();
this.usernameCache = new UsernameCache();
this.kdr = new KDR();
this.worths = new Worths();
logger.log(Level.INFO, "Saving empty states");
StatesParser.save(statesPath, this);

View file

@ -0,0 +1,39 @@
package dev.hrz0.smp4.States;
import org.bukkit.entity.Player;
import java.util.HashMap;
public class Worths extends HashMap<String, Integer> {
public Integer getPlayerWorth(Object player) {
if (player instanceof Player p) {
if (this.containsKey(p.getUniqueId().toString())) {
return get(p.getUniqueId().toString());
} else {
put(p.getUniqueId().toString(), 0);
return 0;
}
} else {
String p = (String) player;
if (this.containsKey(p)) {
return get(p);
} else {
put(p, 0);
return 0;
}
}
}
public void setPlayerWorth(Object player, int worth) {
if (player instanceof Player p) {
put(p.getUniqueId().toString(), worth);
} else if (player instanceof String p) {
put(p, worth);
}
}
public void addToPlayerWorth(Player player, int worthIncrease) {
int currentWorth = getPlayerWorth(player);
setPlayerWorth(player, currentWorth + worthIncrease);
}
}

View file

@ -11,7 +11,7 @@ import java.util.*;
public class Utilities {
static ScoreboardManager sm = Bukkit.getScoreboardManager();
static Scoreboard s = sm.getMainScoreboard();
static Objective elo = s.getObjective("elo");
static Objective elo = s.getObjective("worth");
public static void announce(Component message) {
List<Player> players = new ArrayList<>(Bukkit.getOnlinePlayers());
@ -23,8 +23,8 @@ public class Utilities {
public static void initScore() {
if (elo == null) {
s.registerNewObjective("elo", Criteria.DUMMY, Component.text("ELO"), RenderType.INTEGER);
elo = s.getObjective("elo");
s.registerNewObjective("worth", Criteria.DUMMY, Component.text("worth"), RenderType.INTEGER);
elo = s.getObjective("worth");
}
elo.setDisplaySlot(DisplaySlot.BELOW_NAME);

View file

@ -0,0 +1,32 @@
package dev.hrz0.smp4;
import dev.hrz0.smp4.States.States;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class WorthCommand implements BasicCommand {
States states;
public WorthCommand(States states) {
this.states = states;
}
@Override
public void execute(CommandSourceStack source, String[] args) {
Player runner = (Player) source.getExecutor();
if (runner != null) {
runner.sendRichMessage(String.format("[SMP4] <yellow>You are currently worth %s.</yellow>", states.worths.getPlayerWorth(runner)));
}
}
@Override
public @Nullable String permission() {
return "smp4.worth";
}
}

View file

@ -5,16 +5,21 @@ killCommands:
# Percent of killed's money to take
percentOfMoneyToTakeOnKills: 25
# Constant used to multiply eloGain with, the result being the amount of money given to the killer
moneyMultiplier: 20
# List of commands to run every eggOwnerCommandInterval (in seconds)
# <owner> will be replaced by the current egg owner's name
eggOwnerCommandInterval: 5
eggOwnerCommands:
# How much elo should be given to players holding the dragon egg every eggOwnerCommandInterval
eloGainWhenHoldingEgg: 10
# How much worth should be given to players holding the dragon egg every eggOwnerCommandInterval
worthGainWhenHoldingEgg: 10
# How much money should be given to players holding the dragon egg every eggOwnerCommandInterval
moneyGainWhenHoldingEgg: 100
# When to update players worth depending on their kdr (in s)
worthUpdateInterval: 10
# Base worth added (before multiplying by their kdr)
startingWorthIncrease: 10
# KDR cap
maxKDR: 5
preventMaceLoot: true

View file

@ -4,26 +4,35 @@ main: dev.hrz0.smp4.Smp4
api-version: '1.21'
commands:
setranking:
description: Set the ranking of a player to a another value
setworth:
description: Set the worth of a player to a another value
usage: /<command> <player> <value>
permission: smp4.setranking
permission: smp4.setworth
permission-message: You don't have the permission to do this.
rankings:
description: Show rankings of players
worths:
description: Show worths leaderboard
usage: /<command>
permission: smp4.rankings
permission: smp4.worths
permission-message: You don't have the permission to do this.
worth:
description: Show your worth
usage: /<command>
permission: smp4.worth
permission-message: You don't have the permission to do this.
permissions:
smp4.*:
children:
smp4.setranking: true
smp4.rankings: true
default: true
smp4.setranking:
smp4.setworth: true
smp4.worths: true
smp4.worth: true
default: false
smp4.rankings:
smp4.setworth:
default: false
smp4.worths:
default: true
smp4.worth:
default: true