434 lines
15 KiB
Java
434 lines
15 KiB
Java
package tc.oc.pgm.mapratings;
|
|
|
|
import java.time.Duration;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import javax.annotation.Nullable;
|
|
import javax.inject.Inject;
|
|
|
|
import me.anxuiz.settings.bukkit.PlayerSettings;
|
|
import org.bukkit.ChatColor;
|
|
import org.bukkit.DyeColor;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.Sound;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.Listener;
|
|
import org.bukkit.event.block.Action;
|
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
|
import org.bukkit.event.inventory.InventoryType;
|
|
import org.bukkit.event.player.PlayerInteractEvent;
|
|
import org.bukkit.inventory.Inventory;
|
|
import org.bukkit.inventory.InventoryView;
|
|
import org.bukkit.inventory.ItemFlag;
|
|
import org.bukkit.inventory.ItemStack;
|
|
import org.bukkit.inventory.meta.ItemMeta;
|
|
import org.bukkit.material.MaterialData;
|
|
import org.bukkit.material.Wool;
|
|
import org.bukkit.permissions.Permission;
|
|
import tc.oc.api.docs.MapRating;
|
|
import tc.oc.api.docs.UserId;
|
|
import tc.oc.api.maps.MapRatingsRequest;
|
|
import tc.oc.api.maps.MapService;
|
|
import tc.oc.commons.bukkit.event.ObserverKitApplyEvent;
|
|
import tc.oc.commons.core.commands.CommandFutureCallback;
|
|
import tc.oc.commons.core.formatting.StringUtils;
|
|
import tc.oc.commons.core.util.Comparables;
|
|
import tc.oc.pgm.PGMTranslations;
|
|
import tc.oc.pgm.events.ListenerScope;
|
|
import tc.oc.pgm.events.PlayerLeaveMatchEvent;
|
|
import tc.oc.pgm.blitz.BlitzMatchModule;
|
|
import tc.oc.pgm.match.Match;
|
|
import tc.oc.pgm.match.MatchExecutor;
|
|
import tc.oc.pgm.match.MatchModule;
|
|
import tc.oc.pgm.match.MatchPlayer;
|
|
import tc.oc.pgm.match.MatchPlayerExecutor;
|
|
import tc.oc.pgm.match.MatchScope;
|
|
import tc.oc.pgm.match.MultiPlayerParty;
|
|
import tc.oc.pgm.settings.Settings;
|
|
|
|
@ListenerScope(MatchScope.LOADED)
|
|
public class MapRatingsMatchModule extends MatchModule implements Listener {
|
|
public static final String RATE_PERM_NAME = "map.rating.rate";
|
|
public static final Permission RATE_PERM = new Permission(RATE_PERM_NAME);
|
|
public static final Permission VIEW_LIVE_PERM = new Permission("map.rating.view.live");
|
|
|
|
// Player is allowed to rate if the match has ended and they participated at least this much
|
|
private static final double MIN_PARTICIPATION_PERCENT = 0.75f;
|
|
|
|
// Player is allowed to rate if they participated for this long, regardless of match state
|
|
private static final Duration MIN_PARTICIPATION_TIME = Duration.ofMinutes(10);
|
|
|
|
// Time between the end of the match and automatically showing the rating dialog
|
|
// This is synced with the match end title
|
|
private static final Duration DIALOG_DELAY = Duration.ofSeconds(4);
|
|
|
|
// Inventory slot for the button that opens the rating dialog
|
|
private static final int OPEN_BUTTON_SLOT = 5;
|
|
|
|
// Magic invisible string used to identify items serving as buttons
|
|
private static final String BUTTON_PREFIX = ChatColor.COLOR_CHAR + "z";
|
|
|
|
private static final DyeColor[] BUTTON_COLORS = new DyeColor[] {
|
|
DyeColor.RED,
|
|
DyeColor.PURPLE,
|
|
DyeColor.BLUE,
|
|
DyeColor.CYAN,
|
|
DyeColor.LIME
|
|
};
|
|
|
|
private static final String[] BUTTON_LABELS = new String[] {
|
|
"rating.choice.terrible",
|
|
"rating.choice.bad",
|
|
"rating.choice.ok",
|
|
"rating.choice.good",
|
|
"rating.choice.amazing"
|
|
};
|
|
|
|
private static final ChatColor[] BUTTON_LABEL_COLORS = new ChatColor[] {
|
|
ChatColor.RED,
|
|
ChatColor.LIGHT_PURPLE,
|
|
ChatColor.BLUE,
|
|
ChatColor.AQUA,
|
|
ChatColor.GREEN
|
|
};
|
|
|
|
@Inject private MapRatingsConfiguration config;
|
|
@Inject private MapService mapService;
|
|
@Inject private MatchExecutor matchExecutor;
|
|
@Inject private BlitzMatchModule blitz;
|
|
|
|
private final Map<MatchPlayer, Integer> playerRatings = new HashMap<>();
|
|
|
|
private final int minimumScore, maximumScore;
|
|
|
|
@Inject MapRatingsMatchModule(Match match) {
|
|
super(match);
|
|
|
|
this.minimumScore = 1;
|
|
this.maximumScore = 5;
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldLoad() {
|
|
return config.enabled();
|
|
}
|
|
|
|
private static String formatScore(@Nullable Integer score) {
|
|
return score == null ? "" : BUTTON_LABEL_COLORS[score - 1].toString() + ChatColor.BOLD + score;
|
|
}
|
|
|
|
public int getMinimumScore() {
|
|
return minimumScore;
|
|
}
|
|
|
|
public int getMaximumScore() {
|
|
return maximumScore;
|
|
}
|
|
|
|
public boolean isScoreValid(int score) {
|
|
return score >= minimumScore && score <= maximumScore;
|
|
}
|
|
|
|
/**
|
|
* Return a friendly description of why the given player is not allowed to rate maps,
|
|
* or null if they are allowed.
|
|
*/
|
|
public @Nullable String cantRateReason(MatchPlayer player) {
|
|
if(!player.getBukkit().hasPermission(RATE_PERM)) {
|
|
return PGMTranslations.t("noPermission", player);
|
|
}
|
|
|
|
if(Comparables.lessThan(player.getCumulativeParticipationTime(), MIN_PARTICIPATION_TIME) &&
|
|
!(blitz.eliminated(player)) &&
|
|
!(this.getMatch().isFinished() && player.getCumulativeParticipationPercent() > MIN_PARTICIPATION_PERCENT)) {
|
|
return PGMTranslations.t("rating.lowParticipation", player);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public @Nullable String cantShowDialogReason(MatchPlayer player) {
|
|
if(player.isParticipating()) {
|
|
return PGMTranslations.get().t(
|
|
ChatColor.RED.toString(),
|
|
"rating.whilePlaying",
|
|
player.getBukkit(),
|
|
ChatColor.GOLD + "/rate " + ChatColor.ITALIC + minimumScore + "..." + maximumScore
|
|
);
|
|
}
|
|
return this.cantRateReason(player);
|
|
}
|
|
|
|
public boolean canRate(MatchPlayer player) {
|
|
return this.cantRateReason(player) == null;
|
|
}
|
|
|
|
public boolean canShowDialog(MatchPlayer player) {
|
|
return this.cantShowDialogReason(player) == null;
|
|
}
|
|
|
|
public boolean checkCanRate(MatchPlayer player) {
|
|
String reason = this.cantRateReason(player);
|
|
if(reason == null) return true;
|
|
|
|
player.sendWarning(reason, false);
|
|
return false;
|
|
}
|
|
|
|
public boolean checkCanShowDialog(MatchPlayer player) {
|
|
String reason = this.cantShowDialogReason(player);
|
|
if(reason == null) return true;
|
|
|
|
player.sendWarning(reason, false);
|
|
return false;
|
|
}
|
|
|
|
private ItemStack getOpenButton(MatchPlayer player) {
|
|
ItemStack stack = new ItemStack(Material.HOPPER);
|
|
ItemMeta meta = stack.getItemMeta();
|
|
meta.addItemFlags(ItemFlag.values());
|
|
meta.setDisplayName(BUTTON_PREFIX + ChatColor.BLUE.toString() + ChatColor.BOLD + PGMTranslations.t("rating.rateThisMap", player));
|
|
stack.setItemMeta(meta);
|
|
return stack;
|
|
}
|
|
|
|
private ItemStack getScoreButton(MatchPlayer player, int i) {
|
|
Integer score = this.playerRatings.get(player);
|
|
|
|
MaterialData material;
|
|
if(score != null && score == i + 1) {
|
|
material = new MaterialData(Material.CARPET, BUTTON_COLORS[i].getWoolData());
|
|
} else {
|
|
material = new Wool(BUTTON_COLORS[i]);
|
|
}
|
|
ItemStack stack = material.toItemStack(i + 1);
|
|
|
|
ItemMeta meta = stack.getItemMeta();
|
|
meta.addItemFlags(ItemFlag.values());
|
|
meta.setDisplayName(BUTTON_PREFIX + BUTTON_LABEL_COLORS[i] + ChatColor.BOLD + PGMTranslations.t(BUTTON_LABELS[i], player));
|
|
stack.setItemMeta(meta);
|
|
|
|
return stack;
|
|
}
|
|
|
|
private void updateScoreButtons(MatchPlayer player, Inventory inv) {
|
|
for(int i = minimumScore - 1; i < maximumScore; i++) {
|
|
inv.setItem(2 + i, this.getScoreButton(player, i));
|
|
}
|
|
}
|
|
|
|
public void showDialog(final MatchPlayer player) {
|
|
if(!checkCanShowDialog(player)) return;
|
|
|
|
this.loadPlayerRating(player, () -> {
|
|
String title = ChatColor.DARK_BLUE.toString() + ChatColor.BOLD + PGMTranslations.t("rating.rateThisMap", player);
|
|
Inventory inv = getMatch().getServer().createInventory(
|
|
player.getBukkit(),
|
|
9,
|
|
StringUtils.truncate(title, 32)
|
|
);
|
|
updateScoreButtons(player, inv);
|
|
player.getBukkit().openInventory(inv);
|
|
});
|
|
}
|
|
|
|
public void rate(final MatchPlayer player, final int score) {
|
|
if(!this.checkCanRate(player)) return;
|
|
|
|
final Integer oldScore = this.playerRatings.put(player, score);
|
|
|
|
InventoryView inv = player.getBukkit().getOpenInventory();
|
|
if(inv.getTopInventory().getType() == InventoryType.HOPPER) {
|
|
this.updateScoreButtons(player, inv.getTopInventory());
|
|
}
|
|
|
|
if(oldScore != null && score == oldScore) {
|
|
player.sendWarning(PGMTranslations.t("rating.sameRating", player, score));
|
|
return;
|
|
}
|
|
|
|
player.facet(MatchPlayerExecutor.class).callback(mapService.rate(new MapRating(
|
|
player.getPlayerId(),
|
|
getMatch().getMap().getDocument(),
|
|
score,
|
|
null
|
|
)), CommandFutureCallback.onSuccess(player.getBukkit(), result -> {
|
|
if(result.first.isOnline()) {
|
|
result.first.getBukkit().closeInventory();
|
|
}
|
|
notifyRating(result.first, score, oldScore);
|
|
}));
|
|
}
|
|
|
|
private void notifyRating(MatchPlayer rater, int score, @Nullable Integer oldScore) {
|
|
rater.sendMessage(PGMTranslations.get().t(
|
|
ChatColor.WHITE.toString(),
|
|
oldScore == null ? "command.rate.successful" : "command.rate.update",
|
|
rater.getBukkit(),
|
|
formatScore(score),
|
|
this.getMatch().getMapInfo().getColoredName(),
|
|
this.getMatch().getMapInfo().getColoredVersion(),
|
|
formatScore(oldScore)
|
|
));
|
|
|
|
if(oldScore == null) {
|
|
rater.sendMessage(PGMTranslations.get().t(
|
|
ChatColor.BLUE.toString(),
|
|
"rating.changeLater",
|
|
rater.getBukkit(),
|
|
ChatColor.GOLD + "/rate"
|
|
));
|
|
}
|
|
|
|
for(MatchPlayer viewer : this.getMatch().getPlayers()) {
|
|
if(viewer != rater && viewer.getBukkit().hasPermission(VIEW_LIVE_PERM)) {
|
|
String message;
|
|
if(rater.getParty() instanceof MultiPlayerParty) {
|
|
viewer.sendMessage(PGMTranslations.get().t(
|
|
ChatColor.WHITE.toString(),
|
|
oldScore == null ? "rating.create.notify" : "rating.update.notify",
|
|
viewer.getBukkit(),
|
|
rater.getParty().getColoredName(),
|
|
formatScore(score),
|
|
formatScore(oldScore)
|
|
));
|
|
} else {
|
|
viewer.sendMessage(PGMTranslations.get().t(
|
|
ChatColor.WHITE.toString(),
|
|
oldScore == null ? "rating.create.notify.ffa" : "rating.update.notify.ffa",
|
|
viewer.getBukkit(),
|
|
formatScore(score),
|
|
formatScore(oldScore)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@EventHandler
|
|
public void onOpenButtonClick(PlayerInteractEvent event) {
|
|
if(event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
|
|
|
|
MatchPlayer player = this.getMatch().getPlayer(event.getPlayer());
|
|
if(player == null) return;
|
|
|
|
ItemStack stack = event.getPlayer().getItemInHand();
|
|
if(stack == null) return;
|
|
if(stack.getType() != Material.HOPPER) return;
|
|
|
|
String name = stack.getItemMeta().getDisplayName();
|
|
if(name == null || !name.startsWith(BUTTON_PREFIX)) return;
|
|
this.showDialog(player);
|
|
}
|
|
|
|
@EventHandler
|
|
public void onButtonClick(final InventoryClickEvent event) {
|
|
ItemStack stack = event.getCurrentItem();
|
|
final MatchPlayer player = this.getMatch().getPlayer(event.getWhoClicked());
|
|
|
|
if(stack == null || player == null) return;
|
|
if(stack.getType() != Material.WOOL && stack.getType() != Material.CARPET) return;
|
|
ItemMeta meta = stack.getItemMeta();
|
|
if(!meta.hasDisplayName()) return;
|
|
String name = meta.getDisplayName();
|
|
if(!name.startsWith(BUTTON_PREFIX)) return;
|
|
|
|
event.setCancelled(true);
|
|
|
|
final int score = stack.getAmount();
|
|
if(!isScoreValid(score)) return;
|
|
|
|
this.getMatch().getScheduler(MatchScope.LOADED).createTask(() -> {
|
|
Integer oldScore = playerRatings.get(player);
|
|
if(oldScore == null || oldScore != score) {
|
|
player.playSound(Sound.UI_BUTTON_CLICK, 1, 2);
|
|
rate(player, score);
|
|
}
|
|
else {
|
|
player.getBukkit().closeInventory();
|
|
}
|
|
});
|
|
}
|
|
|
|
protected void loadPlayerRating(final MatchPlayer player, final @Nullable Runnable callback) {
|
|
if(this.playerRatings.containsKey(player)) {
|
|
if(callback != null) callback.run();
|
|
return;
|
|
}
|
|
|
|
matchExecutor.callback(
|
|
mapService.getRatings(new MapRatingsRequest(
|
|
getMatch().getMap().getDocument(),
|
|
Collections.singletonList(player.getPlayerId())
|
|
)),
|
|
(match, result) -> {
|
|
if(player.isOnline()) {
|
|
Integer score = result.player_ratings().get(player.getPlayerId());
|
|
playerRatings.put(player, score);
|
|
if(callback != null)
|
|
callback.run();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
@Override
|
|
public void disable() {
|
|
super.disable();
|
|
|
|
this.getMatch().getScheduler(MatchScope.LOADED).createDelayedTask(DIALOG_DELAY, () -> {
|
|
loadAllPlayerRatings(this::showDialogToAll);
|
|
});
|
|
}
|
|
|
|
private void showDialogToAll() {
|
|
for(MatchPlayer player : this.getMatch().getPlayers()) {
|
|
if(this.canRate(player) &&
|
|
this.playerRatings.get(player) == null &&
|
|
PlayerSettings.getManager(player.getBukkit()).getValue(Settings.RATINGS, Boolean.class)) {
|
|
|
|
this.showDialog(player);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void loadAllPlayerRatings(final Runnable callback) {
|
|
List<UserId> userIds = new ArrayList<>();
|
|
|
|
for(MatchPlayer player : this.getMatch().getPlayers()) {
|
|
if(this.canRate(player)) {
|
|
userIds.add(player.getPlayerId());
|
|
}
|
|
}
|
|
|
|
matchExecutor.callback(
|
|
mapService.getRatings(new MapRatingsRequest(
|
|
getMatch().getMap().getDocument(),
|
|
userIds
|
|
)),
|
|
(match, ratings) -> {
|
|
for(MatchPlayer player : getMatch().getPlayers()) {
|
|
playerRatings.put(player, ratings.player_ratings().get(player.getPlayerId()));
|
|
}
|
|
|
|
if(callback != null) callback.run();
|
|
}
|
|
);
|
|
}
|
|
|
|
@EventHandler
|
|
public void onPlayerLeave(PlayerLeaveMatchEvent event) {
|
|
this.playerRatings.remove(event.getPlayer());
|
|
}
|
|
|
|
@EventHandler
|
|
public void giveKit(ObserverKitApplyEvent event) {
|
|
match.player(event.getPlayer())
|
|
.filter(player -> player.isObserving() && canShowDialog(player))
|
|
.ifPresent(player -> player.getInventory().setItem(OPEN_BUTTON_SLOT, getOpenButton(player)));
|
|
}
|
|
}
|