Skip to content

Commit

Permalink
Add several enhancments to map voting, including book gui
Browse files Browse the repository at this point in the history
Signed-off-by: Pablete1234 <[email protected]>
  • Loading branch information
Pablete1234 committed Jan 5, 2020
1 parent 8dc5855 commit 45ed042
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 172 deletions.
2 changes: 2 additions & 0 deletions src/main/i18n/templates/strings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@ command.pools.mapPoolList.title = Loaded Map Pools
command.pools.skip.message = Skipped a total of {0} positions.
command.pools.skip.noNegative = You may not skip negative positions!

command.pool.vote.book.title = Map Vote
command.pool.vote.book.header = Click to select all maps you'd like to play:
command.pool.vote.hover = Click to toggle vote
command.pool.vote.noVote = There is no poll to vote in currently.
command.pool.vote.voted = You have voted for {0}.
Expand Down
32 changes: 19 additions & 13 deletions src/main/java/tc/oc/pgm/commands/MapPoolCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import app.ashcon.intake.parametric.annotation.Default;
import app.ashcon.intake.parametric.annotation.Switch;
import app.ashcon.intake.parametric.annotation.Text;
import java.text.DecimalFormat;
import java.util.List;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
Expand All @@ -17,26 +18,26 @@
import tc.oc.pgm.api.match.MatchManager;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.map.PGMMap;
import tc.oc.pgm.rotation.MapPool;
import tc.oc.pgm.rotation.MapPoolManager;
import tc.oc.pgm.rotation.Rotation;
import tc.oc.pgm.rotation.VotingPool;
import tc.oc.pgm.rotation.*;
import tc.oc.pgm.util.PrettyPaginatedResult;
import tc.oc.util.components.ComponentUtils;

public class MapPoolCommands {
private static final DecimalFormat SCORE_FORMAT = new DecimalFormat("00.00%");

@Command(
aliases = {"rotation", "rot", "pool"},
desc = "Shows the maps in the active rotation",
usage = "[page] [-r rotation]",
help = "Shows all the maps that are currently in the active rotation.")
desc = "Shows the maps in the active map pool",
usage = "[page] [-p pool] [-s scores]",
help = "Shows all the maps that are currently in the active map pool.")
public static void rotation(
Audience audience,
CommandSender sender,
MatchManager matchManager,
@Default("1") int page,
@Switch('r') String rotationName,
@Switch('p') String poolName)
@Switch('p') String poolName,
@Switch('s') boolean scores)
throws CommandException {
if (rotationName != null) poolName = rotationName;

Expand All @@ -61,15 +62,20 @@ public static void rotation(
ChatColor.DARK_AQUA + " (" + ChatColor.AQUA + mapPool.getName() + ChatColor.DARK_AQUA + ")";
title = ComponentUtils.paginate(title, page, pages);
title = ComponentUtils.horizontalLineHeading(title, ChatColor.BLUE, 250);

VotingPool votes = scores && mapPool instanceof VotingPool ? (VotingPool) mapPool : null;

int nextPos = mapPool instanceof Rotation ? ((Rotation) mapPool).getNextPosition() : -1;

new PrettyPaginatedResult<PGMMap>(title, resultsPerPage) {
@Override
public String format(PGMMap map, int index) {
index++;
String indexString =
nextPos == index ? ChatColor.DARK_AQUA.toString() + index : String.valueOf(index);
return (indexString) + ". " + ChatColor.RESET + map.getInfo().getShortDescription(sender);
String str = (nextPos == index ? ChatColor.DARK_AQUA + "" : "") + index + ". ";
if (votes != null)
str += ChatColor.YELLOW + SCORE_FORMAT.format(votes.getMapScore(map)) + " ";
str += ChatColor.RESET + map.getInfo().getShortDescription(sender);
return str;
}
}.display(audience, maps, page);
}
Expand Down Expand Up @@ -168,7 +174,7 @@ public static void voteNext(
MatchPlayer player, CommandSender sender, MatchManager matchManager, @Text PGMMap map)
throws CommandException {
MapPool pool = getMapPoolManager(sender, matchManager).getActiveMapPool();
VotingPool.Poll poll = pool instanceof VotingPool ? ((VotingPool) pool).getCurrentPoll() : null;
MapPoll poll = pool instanceof VotingPool ? ((VotingPool) pool).getCurrentPoll() : null;
if (poll == null) {
sender.sendMessage(
ChatColor.RED + AllTranslations.get().translate("command.pool.vote.noVote", sender));
Expand All @@ -181,7 +187,7 @@ public static void voteNext(
voteResult ? "command.pool.vote.voted" : "command.pool.vote.removedVote",
map.getName());
sender.sendMessage(new PersonalizedText(tr, voteResult ? ChatColor.GREEN : ChatColor.RED));
poll.sendMessage(player, false);
poll.sendBook(player);
}

private static MapPoolManager getMapPoolManager(CommandSender sender, MatchManager matchManager)
Expand Down
199 changes: 199 additions & 0 deletions src/main/java/tc/oc/pgm/rotation/MapPoll.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package tc.oc.pgm.rotation;

import app.ashcon.intake.CommandException;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import tc.oc.component.Component;
import tc.oc.component.types.PersonalizedText;
import tc.oc.component.types.PersonalizedTranslatable;
import tc.oc.pgm.AllTranslations;
import tc.oc.pgm.api.Permissions;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.map.PGMMap;
import tc.oc.world.NMSHacks;

import java.lang.ref.WeakReference;
import java.util.*;
import java.util.stream.Collectors;

/** Represents a polling process, with a set of options. */
public class MapPoll {
private static final String SYMBOL_IGNORE = "\u2715"; // ✕
private static final String SYMBOL_VOTED = "\u2714"; // ✔

private final WeakReference<Match> match;
private final Map<PGMMap, Double> mapScores;
private final Map<PGMMap, Set<UUID>> votes = new HashMap<>();

MapPoll(Match match, Map<PGMMap, Double> mapScores, int voteSize) {
this.match = new WeakReference<>(match);
this.mapScores = mapScores;
// Sorting beforehand, saves future key remaps, as bigger values are placed at the end
List<PGMMap> sortedDist =
mapScores.entrySet().stream()
.sorted(Comparator.comparingDouble(Map.Entry::getValue))
.map(Map.Entry::getKey)
.collect(Collectors.toList());

NavigableMap<Double, PGMMap> cumulativeScores = new TreeMap<>();
double maxWeight = cummulativeMap(0, sortedDist, cumulativeScores);

for (int i = 0; i < voteSize; i++) {
NavigableMap<Double, PGMMap> subMap =
cumulativeScores.tailMap(Math.random() * maxWeight, true);
Map.Entry<Double, PGMMap> selected = subMap.pollFirstEntry();
// Add map to votes
votes.put(selected.getValue(), new HashSet<>());
// No need to do replace logic after maps have been selected
if (votes.size() >= voteSize) break;

// Remove map from pool, updating cumulative scores
double selectedWeight = mapScores.get(selected.getValue());
maxWeight -= selectedWeight;

NavigableMap<Double, PGMMap> temp = new TreeMap<>();
cummulativeMap(selected.getKey() - selectedWeight, subMap.values(), temp);

subMap.clear();
cumulativeScores.putAll(temp);
}
}

private double cummulativeMap(
double currWeight, Collection<PGMMap> maps, Map<Double, PGMMap> result) {
for (PGMMap map : maps) {
double score = mapScores.get(map);
if (score > 0) result.put(currWeight += score, map);
}
return currWeight;
}

public void sendMessage(MatchPlayer viewer) {
for (PGMMap pgmMap : votes.keySet()) viewer.sendMessage(getMapChatComponent(viewer, pgmMap));
}

private Component getMapChatComponent(MatchPlayer viewer, PGMMap map) {
boolean voted = votes.get(map).contains(viewer.getId());
return new PersonalizedText(
new PersonalizedText("["),
new PersonalizedText(
voted ? SYMBOL_VOTED : SYMBOL_IGNORE, voted ? ChatColor.GREEN : ChatColor.DARK_RED),
new PersonalizedText(" ").bold(!voted), // Fix 1px symbol diff
new PersonalizedText("" + countVotes(votes.get(map)), ChatColor.YELLOW),
new PersonalizedText("] "),
new PersonalizedText(map.getInfo().getShortDescription(viewer.getBukkit()) + " "));
}

public void sendBook(MatchPlayer viewer) {
String title = ChatColor.GOLD + "" + ChatColor.BOLD;
title += AllTranslations.get().translate("command.pool.vote.book.title", viewer.getBukkit());

ItemStack is = new ItemStack(Material.WRITTEN_BOOK);
BookMeta meta = (BookMeta) is.getItemMeta();
meta.setAuthor("PGM");
meta.setTitle(title);

List<Component> content = new ArrayList<>(votes.size() + 2);
content.add(
new PersonalizedText(
new PersonalizedTranslatable("command.pool.vote.book.header"),
ChatColor.DARK_PURPLE));
content.add(new PersonalizedText("\n\n"));

for (PGMMap pgmMap : votes.keySet()) content.add(getMapBookComponent(viewer, pgmMap));

NMSHacks.setBookPages(meta, new PersonalizedText(content).render(viewer.getBukkit()));
is.setItemMeta(meta);

ItemStack held = viewer.getInventory().getItemInHand();
if (held.getType() != Material.WRITTEN_BOOK
|| !title.equals(((BookMeta) is.getItemMeta()).getTitle())) {
viewer.getInventory().setHeldItemSlot(2);
}
viewer.getInventory().setItemInHand(is);
NMSHacks.openBook(is, viewer.getBukkit());
}

private Component getMapBookComponent(MatchPlayer viewer, PGMMap map) {
boolean voted = votes.get(map).contains(viewer.getId());
return new PersonalizedText(
new PersonalizedText(
voted ? SYMBOL_VOTED : SYMBOL_IGNORE,
voted ? ChatColor.DARK_GREEN : ChatColor.DARK_RED),
new PersonalizedText(" ").bold(!voted), // Fix 1px symbol diff
new PersonalizedText(map.getName() + "\n", ChatColor.BOLD, ChatColor.GOLD))
.hoverEvent(
HoverEvent.Action.SHOW_TEXT,
new PersonalizedTranslatable("command.pool.vote.hover").render(viewer.getBukkit()))
.clickEvent(ClickEvent.Action.RUN_COMMAND, "/votenext " + map.getName());
}

/**
* Toggle the vote of a user for a certain map. Player is allowed to vote for several maps.
*
* @param vote The map to vote for/against
* @param player The player voting
* @return true if the player is now voting for the map, false otherwise
* @throws CommandException If the map is not an option in the poll
*/
public boolean toggleVote(PGMMap vote, UUID player) throws CommandException {
Set<UUID> votes = this.votes.get(vote);
if (votes == null)
throw new CommandException(vote.getName() + " is not an option in the poll");

if (votes.add(player)) return true;
votes.remove(player);
return false;
}

/** @return The map currently winning the vote, null if no vote is running. */
private PGMMap getMostVotedMap() {
return votes.entrySet().stream()
.max(Comparator.comparingInt(e -> countVotes(e.getValue())))
.map(Map.Entry::getKey)
.orElse(null);
}

/**
* Count the amount of votes for a set of uuids. Players with the pgm.premium permission get
* double votes.
*
* @param uuids The players who voted
* @return The number of votes counted
*/
private int countVotes(Set<UUID> uuids) {
return uuids.stream()
.map(Bukkit::getPlayer)
// Count disconnected players as 1, can't test for their perms
.mapToInt(p -> p == null || !p.hasPermission(Permissions.PREMIUM) ? 1 : 2)
.sum();
}

/**
* Picks a winner and ends the vote, updating map scores based on votes
*
* @return The picked map to play after the vote
*/
PGMMap finishVote() {
PGMMap picked = getMostVotedMap();
Match match = this.match.get();
if (match != null) {
match.getPlayers().forEach(this::sendMessage);
}

updateScores();
return picked;
}

private void updateScores() {
double voters = votes.values().stream().flatMap(Collection::stream).distinct().count();
if (voters == 0) return;
votes.forEach((m, v) -> mapScores.put(m, Math.max(v.size() / voters, Double.MIN_VALUE)));
}
}
Loading

0 comments on commit 45ed042

Please sign in to comment.