package tc.oc.pgm.picker; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import me.anxuiz.settings.bukkit.PlayerSettings; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.TranslatableComponent; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.player.PlayerLocaleChangeEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.LeatherArmorMeta; import org.bukkit.material.MaterialData; import tc.oc.commons.bukkit.chat.ComponentRenderContext; import tc.oc.commons.bukkit.event.ObserverKitApplyEvent; import tc.oc.commons.bukkit.item.ItemUtils; import tc.oc.commons.bukkit.item.StringItemTag; import tc.oc.commons.core.chat.ChatUtils; import tc.oc.commons.core.chat.Component; import tc.oc.commons.core.formatting.StringUtils; import tc.oc.pgm.PGMTranslations; import tc.oc.pgm.blitz.BlitzEvent; import tc.oc.pgm.classes.ClassMatchModule; import tc.oc.pgm.classes.ClassModule; import tc.oc.pgm.classes.PlayerClass; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.MatchBeginEvent; import tc.oc.pgm.events.MatchEndEvent; import tc.oc.pgm.events.ObserverInteractEvent; import tc.oc.pgm.events.PlayerJoinMatchEvent; import tc.oc.pgm.events.PlayerPartyChangeEvent; import tc.oc.pgm.join.JoinMatchModule; import tc.oc.pgm.join.JoinRequest; import tc.oc.pgm.join.JoinResult; import tc.oc.pgm.blitz.BlitzMatchModule; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.spawns.events.DeathKitApplyEvent; import tc.oc.pgm.teams.Team; import tc.oc.pgm.teams.TeamMatchModule; import tc.oc.pgm.teams.TeamModule; import static com.google.common.base.Preconditions.checkState; @ListenerScope(MatchScope.LOADED) public class PickerMatchModule extends MatchModule implements Listener { private static final StringItemTag ITEM_TAG = new StringItemTag(PickerMatchModule.class.getSimpleName(), null); private static final String OPEN_BUTTON_PREFIX = ChatColor.GREEN + ChatColor.BOLD.toString(); private static final int OPEN_BUTTON_SLOT = 2; private static final int LORE_WIDTH_PIXELS = 120; private static final int WIDTH = 9; // Inventory width in slots (for readability) enum Button { AUTO_JOIN(Material.CHAINMAIL_HELMET), TEAM_JOIN(Material.LEATHER_HELMET), JOIN(Material.LEATHER_HELMET), LEAVE(Material.LEATHER_BOOTS), ; public final Material material; Button(Material material) { this.material = material; } public boolean matches(MaterialData material) { return this.material.equals(material.getItemType()); } } private final ComponentRenderContext renderer; private final JoinMatchModule jmm; private final BlitzMatchModule bmm; private final boolean hasTeams; private final boolean hasClasses; private final Set<MatchPlayer> picking = new HashSet<>(); @Inject PickerMatchModule(ComponentRenderContext renderer, JoinMatchModule jmm, BlitzMatchModule bmm, Optional<TeamModule> teamModule, Optional<ClassModule> classModule) { this.renderer = renderer; this.jmm = jmm; this.bmm = bmm; this.hasTeams = teamModule.isPresent(); this.hasClasses = classModule.isPresent(); } protected boolean settingEnabled(MatchPlayer player) { return PlayerSettings.getManager(player.getBukkit()).getValue(PickerSettings.PICKER, Boolean.class); } private boolean hasJoined(MatchPlayer joining) { return joining.isParticipatingType() || jmm.isQueuedToJoin(joining); } private boolean canChooseMultipleTeams(MatchPlayer joining) { return getChoosableTeams(joining).size() > 1; } private Set<Team> getChoosableTeams(MatchPlayer joining) { TeamMatchModule tmm = getMatch().getMatchModule(TeamMatchModule.class); if(tmm == null) return Collections.emptySet(); Set<Team> teams = new HashSet<>(); for(Team team : tmm.getTeams()) { JoinResult result = tmm.queryJoin(joining, JoinRequest.user(team)); if(result != null && result.isVisible()) { teams.add(team); } } return teams; } /** * Does the player have any use for the picker? */ private boolean canUse(MatchPlayer player) { if(player == null) return false; // Player is eliminated from Blitz if(bmm.activated() && getMatch().isRunning()) return false; // Player is not observing or dead if(!(player.isObserving() || player.isDead())) return false; return jmm.queryJoin(player, JoinRequest.user()).isVisible(); } /** * Does the player have any use for the picker dialog? If the player can join, * but there is nothing to pick (i.e. FFA without classes) then this returns * false, while {@link #canUse} returns true. */ private boolean canOpenWindow(MatchPlayer player) { return canUse(player) && (hasClasses || canChooseMultipleTeams(player)); } private boolean isPicking(MatchPlayer player) { return picking.contains(player); } private void refreshCountsAll() { for(MatchPlayer player : ImmutableSet.copyOf(picking)) { refreshWindow(player); } } private String getWindowTitle(MatchPlayer player) { checkState(hasTeams || hasClasses); // Window should not open if there is nothing to pick String key; if(hasTeams && hasClasses) { key = "teamClass.picker.title"; } else if(hasTeams) { key = "teamSelection.picker.title"; } else { key = "class.picker.title"; } return ChatColor.DARK_RED + PGMTranslations.t(key, player); } private ItemStack createJoinButton(final MatchPlayer player) { ItemStack stack = new ItemStack(Button.JOIN.material); ItemMeta meta = stack.getItemMeta(); meta.addItemFlags(ItemFlag.values()); String key; if(!canOpenWindow(player)) { key = "ffa.picker.displayName"; } else if(hasTeams && hasClasses) { key = "teamClass.picker.displayName"; } else if(hasTeams) { key = "teamSelection.picker.displayName"; } else if(hasClasses) { key = "class.picker.displayName"; } else { key = "ffa.picker.displayName"; } meta.setDisplayName(OPEN_BUTTON_PREFIX + PGMTranslations.t(key, player)); meta.setLore(Lists.newArrayList(ChatColor.DARK_PURPLE + PGMTranslations.t("teamSelection.picker.tooltip", player))); stack.setItemMeta(meta); ITEM_TAG.set(stack, "join"); return stack; } private ItemStack createLeaveButton(final MatchPlayer player) { ItemStack stack = new ItemStack(Button.LEAVE.material); ItemMeta meta = stack.getItemMeta(); meta.addItemFlags(ItemFlag.values()); meta.setDisplayName(OPEN_BUTTON_PREFIX + PGMTranslations.t("leave.picker.displayName", player)); meta.setLore(Lists.newArrayList(ChatColor.DARK_PURPLE + PGMTranslations.t("leave.picker.tooltip", player))); stack.setItemMeta(meta); ITEM_TAG.set(stack, "leave"); return stack; } public void refreshKit(final MatchPlayer player) { if(canUse(player)) { logger.fine("Giving kit to " + player); ItemStack stack; if(hasJoined(player) && !canOpenWindow(player)) { stack = createLeaveButton(player); } else { stack = createJoinButton(player); } player.getInventory().setItem(OPEN_BUTTON_SLOT, stack); } else if(ITEM_TAG.has(player.getInventory().getItem(OPEN_BUTTON_SLOT))) { player.getInventory().setItem(OPEN_BUTTON_SLOT, null); } } private void refreshKitAll() { for(MatchPlayer player : getMatch().getObservingPlayers()) { refreshKit(player); } } @EventHandler(priority = EventPriority.MONITOR) public void join(PlayerJoinMatchEvent event) { final MatchPlayer player = event.getPlayer(); player.nextTick(() -> { if(settingEnabled(player) && canOpenWindow(player)) { showWindow(player); } }); } @EventHandler(priority = EventPriority.LOWEST) public void checkInventoryClick(InventoryClickEvent event) { if(event.getCurrentItem() == null || event.getCurrentItem().getItemMeta() == null || event.getCurrentItem().getItemMeta().getDisplayName() == null) return; match.player(event.getActor()).ifPresent(player -> { if(!this.picking.contains(player)) return; this.handleInventoryClick( player, ChatColor.stripColor(event.getCurrentItem().getItemMeta().getDisplayName()), event.getCurrentItem().getData() ); event.setCancelled(true); }); } @EventHandler public void handleLocaleChange(final PlayerLocaleChangeEvent event) { final MatchPlayer player = getMatch().getPlayer(event.getPlayer()); if(player != null) refreshKit(player); } @EventHandler public void closeMonitoredInventory(final InventoryCloseEvent event) { this.picking.remove(getMatch().getPlayer((Player) event.getPlayer())); } @EventHandler public void rightClickIcon(final ObserverInteractEvent event) { if(event.getClickType() != ClickType.RIGHT) return; MatchPlayer player = event.getPlayer(); if(!canUse(player)) return; ItemStack hand = event.getClickedItem(); if(ItemUtils.isNothing(hand)) return; String displayName = hand.getItemMeta().getDisplayName(); if(displayName == null) return; if(hand.getType() == Button.JOIN.material) { event.setCancelled(true); if(canOpenWindow(player)) { showWindow(player); } else { // If there is nothing to pick, just join immediately jmm.requestJoin(player, JoinRequest.user()); } } else if(hand.getType() == Button.LEAVE.material) { event.setCancelled(true); jmm.requestObserve(player); } } @EventHandler public void giveKitToObservers(ObserverKitApplyEvent event) { match.player(event.getPlayer()) .ifPresent(this::refreshKit); } @EventHandler public void giveKitToDead(final DeathKitApplyEvent event) { refreshKit(event.getPlayer()); } @EventHandler public void teamSwitch(final PlayerPartyChangeEvent event) { refreshCountsAll(); refreshKit(event.getPlayer()); if(event.getNewParty() == null) { picking.remove(event.getPlayer()); } } @EventHandler public void matchBegin(final MatchBeginEvent event) { refreshCountsAll(); refreshKitAll(); } @EventHandler public void matchEnd(final MatchEndEvent event) { refreshCountsAll(); refreshKitAll(); } @EventHandler public void blitzEnable(final BlitzEvent event) { refreshCountsAll(); refreshKitAll(); } /** * Open the window for the given player, or refresh its contents * if they already have it open, and return the current contents. * * If the window is currently open but too small to hold the current * contents, it will be closed and reopened. * * If the player is not currently allowed to have the window open, * close any window they have open and return null. */ private @Nullable Inventory showWindow(MatchPlayer player) { if(!checkWindow(player)) return null; ItemStack[] contents = createWindowContents(player); Inventory inv = getOpenWindow(player); if(inv != null && inv.getSize() < contents.length) { inv = null; closeWindow(player); } if(inv == null) { inv = openWindow(player, contents); } else { inv.setContents(contents); } return inv; } /** * If the given player currently has the window open, refresh its contents * and return the updated inventory. The window will be closed and reopened * if it is too small to hold the current contents. * * If the window is open but should be closed, close it and return null. * * If the player does not have the window open, return null. */ private @Nullable Inventory refreshWindow(MatchPlayer player) { if(!checkWindow(player)) return null; Inventory inv = getOpenWindow(player); if(inv != null) { ItemStack[] contents = createWindowContents(player); if(inv.getSize() < contents.length) { closeWindow(player); inv = openWindow(player, contents); } else { inv.setContents(contents); } } return inv; } /** * Return true if the given player is currently allowed to have an open window. * If they are not allowed, close any window they have open, and return false. */ private boolean checkWindow(MatchPlayer player) { if(!player.isOnline()) return false; if(!canOpenWindow(player)) { closeWindow(player); return false; } return true; } /** * Return the inventory of the given player's currently open window, * or null if the player does not have the window open. */ private @Nullable Inventory getOpenWindow(MatchPlayer player) { if(picking.contains(player)) { return player.getBukkit().getOpenInventory().getTopInventory(); } return null; } /** * Close any window that is currently open for the given player */ private void closeWindow(MatchPlayer player) { if(picking.contains(player)) { player.getBukkit().closeInventory(); } } /** * Open a new window for the given player displaying the given contents */ private Inventory openWindow(MatchPlayer player, ItemStack[] contents) { closeWindow(player); Inventory inv = getMatch().getServer().createInventory(player.getBukkit(), contents.length, StringUtils.truncate(getWindowTitle(player), 32)); inv.setContents(contents); player.getBukkit().openInventory(inv); picking.add(player); return inv; } /** * Generate current picker contents for the given player */ private ItemStack[] createWindowContents(final MatchPlayer player) { final List<ItemStack> slots = new ArrayList<>(); final Set<Team> teams = getChoosableTeams(player); if(!teams.isEmpty()) { // Auto-join button at start of row if(teams.size() > 1) { final JoinResult autoResult = jmm.queryJoin(player, JoinRequest.user()); if(autoResult.isVisible()) { slots.add(createAutoJoinButton(player)); } } // Team buttons if(teams.size() > 1 || !hasJoined(player)) { for(Team team : teams) { slots.add(createTeamJoinButton(player, team)); } } } // Skip to next empty row while(slots.size() % WIDTH != 0) slots.add(null); // Class buttons if(hasClasses) { for(PlayerClass cls : getMatch().getMatchModule(ClassMatchModule.class).getClasses()) { slots.add(createClassButton(player, cls)); } } // Pad last row to width while(slots.size() % WIDTH != 0) slots.add(null); if(hasJoined(player)) { // Put leave button in first empty slot of the last column for(int slot = WIDTH - 1;; slot += WIDTH) { while(slots.size() <= slot) { slots.add(null); } if(slots.get(slot) == null) { slots.set(slot, createLeaveButton(player)); break; } } } return slots.toArray(new ItemStack[slots.size()]); } private ItemStack createClassButton(MatchPlayer viewer, PlayerClass cls) { ItemStack item = cls.getIcon().toItemStack(1); ItemMeta meta = item.getItemMeta(); meta.addItemFlags(ItemFlag.values()); meta.setDisplayName((cls.canUse(viewer.getBukkit()) ? ChatColor.GREEN : ChatColor.RED) + cls.getName()); if(getMatch().getMatchModule(ClassMatchModule.class).selectedClass(viewer.getDocument()).equals(cls)) { meta.addEnchant(Enchantment.ARROW_INFINITE, 1, true); } List<String> lore = Lists.newArrayList(); if(cls.getLongDescription() != null) { ChatUtils.wordWrap(ChatColor.GOLD + cls.getLongDescription(), LORE_WIDTH_PIXELS, lore); } else if(cls.getDescription() != null) { lore.add(ChatColor.GOLD + cls.getDescription()); } if(!cls.canUse(viewer.getBukkit())) { lore.add(ChatColor.RED + PGMTranslations.t("class.picker.restricted", viewer)); } meta.setLore(lore); item.setItemMeta(meta); return item; } private ItemStack createAutoJoinButton(MatchPlayer viewer) { ItemStack autojoin = new ItemStack(Button.AUTO_JOIN.material); ItemMeta autojoinMeta = autojoin.getItemMeta(); autojoinMeta.addItemFlags(ItemFlag.values()); autojoinMeta.setDisplayName(ChatColor.GRAY.toString() + ChatColor.BOLD + PGMTranslations.t("teamSelection.picker.autoJoin.displayName", viewer)); autojoinMeta.setLore(Lists.newArrayList(this.getTeamSizeDescription(viewer.getMatch().getParticipatingPlayers().size(), viewer.getMatch().getMaxPlayers()), ChatColor.AQUA + PGMTranslations.t("teamSelection.picker.autoJoin.tooltip", viewer))); autojoin.setItemMeta(autojoinMeta); return autojoin; } private ItemStack createTeamJoinButton(final MatchPlayer player, final Team team) { ItemStack item = new ItemStack(Button.TEAM_JOIN.material); String capacityMessage = this.getTeamSizeDescription(team.getPlayers().size(), team.getMaxPlayers()); List<String> lore = Lists.newArrayList(capacityMessage); final JoinResult result = jmm.queryJoin(player, JoinRequest.user(team)); if(result.isAllowed()) { final String label = result.isRejoin() ? "teamSelection.picker.clickToRejoin" : "teamSelection.picker.clickToJoin"; lore.add(renderer.renderLegacy(new Component(new TranslatableComponent(label), ChatColor.GREEN), player.getBukkit())); } else if(result.message().isPresent()) { lore.add(renderer.renderLegacy(new Component(result.message().get(), ChatColor.RED), player.getBukkit())); result.extra().forEach(line -> { lore.add(renderer.renderLegacy(line, player.getBukkit())); }); } LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta(); meta.addItemFlags(ItemFlag.values()); meta.setColor(team.getFullColor()); meta.setDisplayName(team.getColor().toString() + ChatColor.BOLD + team.getName()); meta.setLore(lore); meta.addItemFlags(ItemFlag.values()); // Hides a lot more than potion effects item.setItemMeta(meta); return item; } private String getTeamSizeDescription(final int num, final int max) { return (num >= max ? ChatColor.RED : ChatColor.GREEN).toString() + num + ChatColor.GOLD + " / " + ChatColor.RED + max; } private void handleInventoryClick(final MatchPlayer player, final String name, final MaterialData material) { player.playSound(Sound.UI_BUTTON_CLICK, 1, 2); if(hasClasses) { ClassMatchModule cmm = player.getMatch().needMatchModule(ClassMatchModule.class); PlayerClass cls = cmm.getPlayerClass(name); if(cls != null && cls.getIcon().equals(material)) { if(!cls.canUse(player.getBukkit())) return; if(!Objects.equals(cls, cmm.selectedClass(player.getDocument()))) { if(cmm.getCanChangeClass(player.getPlayerId())) { cmm.setPlayerClass(player.getPlayerId(), cls); player.sendMessage(ChatColor.GOLD + PGMTranslations.t("command.class.select.confirm", player, ChatColor.GREEN + name)); scheduleRefresh(player); } else { player.sendMessage(ChatColor.RED + PGMTranslations.t("command.class.stickyClass", player)); } } if(!canChooseMultipleTeams(player) && !hasJoined(player)) { this.scheduleClose(player); this.scheduleJoin(player, null); } return; } } if(hasTeams && Button.TEAM_JOIN.matches(material)) { Team team = player.getMatch().needMatchModule(TeamMatchModule.class).bestFuzzyMatch(name, 1); if(team != null) { this.scheduleClose(player); this.scheduleJoin(player, team); } } else if(Button.AUTO_JOIN.matches(material)) { this.scheduleClose(player); this.scheduleJoin(player, null); } else if(Button.LEAVE.matches(material)) { this.scheduleClose(player); this.scheduleLeave(player); } } private void scheduleClose(final MatchPlayer player) { player.nextTick(() -> { player.getBukkit().getOpenInventory().getTopInventory().clear(); player.getBukkit().closeInventory(); }); } private void scheduleJoin(final MatchPlayer player, @Nullable final Team team) { player.nextTick(() -> { if(team == null) { if(hasJoined(player)) return; } else { if(player.getParty().equals(team)) return; } jmm.requestJoin(player, JoinRequest.user(team)); }); } private void scheduleLeave(final MatchPlayer player) { player.nextTick(() -> { if(!hasJoined(player)) return; jmm.requestObserve(player); }); } private void scheduleRefresh(final MatchPlayer viewer) { viewer.nextTick(() -> refreshWindow(viewer)); } }