package com.pretzel.dev.villagertradelimiter.listeners; import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter; import com.pretzel.dev.villagertradelimiter.data.Cooldown; import com.pretzel.dev.villagertradelimiter.data.PlayerData; import com.pretzel.dev.villagertradelimiter.settings.Settings; import com.pretzel.dev.villagertradelimiter.wrappers.*; import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.entity.Villager; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import java.time.Instant; import java.util.Date; import java.util.List; public class PlayerListener implements Listener { private final VillagerTradeLimiter instance; private final Settings settings; /** * @param instance The instance of VillagerTradeLimiter.java * @param settings The settings instance */ public PlayerListener(final VillagerTradeLimiter instance, final Settings settings) { this.instance = instance; this.settings = settings; } /** Handles when a player begins trading with a villager */ @EventHandler public void onPlayerBeginTrading(final PlayerInteractEntityEvent event) { if(event.isCancelled()) return; //Skips when event is already cancelled if(!(event.getRightClicked() instanceof Villager)) return; //Skips non-villager entities final Player player = event.getPlayer(); final Villager villager = (Villager)event.getRightClicked(); //Skips when player is holding an ignored item Material heldItemType = player.getInventory().getItem(event.getHand()).getType(); for(String ignoredType : instance.getCfg().getStringList("IgnoredHeldItems")) { if(heldItemType.equals(Material.matchMaterial(ignoredType))) { event.setCancelled(true); return; } } if(settings.shouldSkipNPC(event.getPlayer()) || settings.shouldSkipNPC(villager)) return; //Skips NPCs if(villager.getProfession() == Villager.Profession.NONE || villager.getProfession() == Villager.Profession.NITWIT || villager.getRecipeCount() == 0) return; //Skips non-trading villagers //DisableTrading feature if(instance.getCfg().isBoolean("DisableTrading")) { //If all trading is disabled if(instance.getCfg().getBoolean("DisableTrading", false)) { event.setCancelled(true); return; } } else { //If trading in the world the player is in is disabled final List disabledWorlds = instance.getCfg().getStringList("DisableTrading"); final String world = event.getPlayer().getWorld().getName(); for(String disabledWorld : disabledWorlds) { if(world.equals(disabledWorld)) { event.setCancelled(true); return; } } } //Cancel the original event, and open the adjusted trade view event.setCancelled(true); if(!instance.getPlayerData().containsKey(player.getUniqueId())) { instance.getPlayerData().put(player.getUniqueId(), new PlayerData()); } if(!instance.getPlayerData().containsKey(villager.getUniqueId())) { instance.getPlayerData().put(villager.getUniqueId(), new PlayerData()); } this.see(villager, player, player); } /** * Opens the villager's trading menu, with the adjusted trades of another player (or the same player) * @param villager The villager whose trades you want to see * @param player The player who calls the command, or the player that has begun trading * @param other The other player to view trades for, or the player that has just begun trading */ public void see(final Villager villager, final Player player, final OfflinePlayer other) { //Wraps the villager and player into wrapper classes final VillagerWrapper villagerWrapper = new VillagerWrapper(villager); final PlayerWrapper otherWrapper = new PlayerWrapper(other); final Player otherPlayer = otherWrapper.getPlayer(); if(settings.shouldSkipNPC(player) || settings.shouldSkipNPC(villager) || otherPlayer == null || settings.shouldSkipNPC(otherPlayer)) return; //Skips NPCs final PlayerData playerData = instance.getPlayerData().get(other.getUniqueId()); if(playerData != null) playerData.setTradingVillager(villagerWrapper); //Checks if the version is old, before the 1.16 UUID changes String version = instance.getServer().getClass().getPackage().getName(); boolean isOld = version.contains("1_13_") || version.contains("1_14_") || version.contains("1_15_"); //Calculates the player's total reputation and Hero of the Village discount int totalReputation = villagerWrapper.getTotalReputation(villagerWrapper, otherWrapper, isOld); double hotvDiscount = getHotvDiscount(otherWrapper); //Adjusts the recipe prices, MaxUses, and ingredients final List recipes = villagerWrapper.getRecipes(); for(RecipeWrapper recipe : recipes) { //Set the special price (discount) recipe.setSpecialPrice(getDiscount(recipe, totalReputation, hotvDiscount)); //Set ingredient materials and amounts final ConfigurationSection override = settings.getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell")); if(override != null) { setIngredient(override.getConfigurationSection("Item1"), recipe.getIngredient1()); setIngredient(override.getConfigurationSection("Item2"), recipe.getIngredient2()); setIngredient(override.getConfigurationSection("Result"), recipe.getResult()); } //Set the maximum number of uses (trades/day) recipe.setMaxUses(getMaxUses(recipe, other)); } //Open the villager's trading menu player.openMerchant(villager, false); } /** * @param recipe The recipe to get the base price for * @return The initial price of a recipe/trade, before any discounts are applied */ private int getBasePrice(final RecipeWrapper recipe) { int basePrice = recipe.getIngredient1().getAmount(); basePrice = settings.fetchInt(recipe, "Item1.Amount", basePrice); return Math.min(Math.max(basePrice, 1), 64); } /** * @param recipe The recipe to get the demand for * @return The current value of the demand for the given recipe */ private int getDemand(final RecipeWrapper recipe) { int demand = recipe.getDemand(); int maxDemand = settings.fetchInt(recipe, "MaxDemand", -1); if(maxDemand >= 0 && demand > maxDemand) return maxDemand; return demand; } /** * @param recipe The recipe to get the discount for * @param totalReputation The player's total reputation from a villager's gossips * @param hotvDiscount The total discount from the Hero of the Village effect * @return The total discount for the recipe, which is added to the base price to get the final price */ private int getDiscount(final RecipeWrapper recipe, int totalReputation, double hotvDiscount) { //Calculates the total discount int basePrice = getBasePrice(recipe); int demand = getDemand(recipe); float priceMultiplier = recipe.getPriceMultiplier(); int discount = -(int)(totalReputation * priceMultiplier) - (int)(hotvDiscount * basePrice) + Math.max(0, (int)(demand * priceMultiplier * basePrice)); double maxDiscount = settings.fetchDouble(recipe, "MaxDiscount", 0.3); if(maxDiscount >= 0.0 && maxDiscount <= 1.0) { //Change the discount to the smaller MaxDiscount if(basePrice + discount < basePrice * (1.0 - maxDiscount)) { discount = -(int)(basePrice * maxDiscount); } } else if(maxDiscount > 1.0) { //Change the discount to the larger MaxDiscount //TODO: Allow for better fine-tuning discount = (int)(discount * maxDiscount); } return discount; } /** * @param recipe The recipe to get the MaxUses for * @return The current maximum number of times a player can make a trade before the villager restocks */ private int getMaxUses(final RecipeWrapper recipe, final OfflinePlayer player) { int uses = recipe.getMaxUses(); int maxUses = settings.fetchInt(recipe, "MaxUses", -1); boolean disabled = settings.fetchBoolean(recipe, "Disabled", false); //Disables the trade if the player has an active cooldown for the trade final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId()); if(playerData != null && playerData.getTradingVillager() != null) { final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); if(overrides != null) { final String type = settings.getType(recipe.getItemStack("sell"), recipe.getItemStack("buy"), recipe.getItemStack("buyB")); final String global = instance.getCfg().getString("Cooldown", "0"); final String local = overrides.getString(type+".Cooldown", global); if(type != null && !local.equals("0")) { if(playerData.getTradingCooldowns().containsKey(type)) { final Date now = Date.from(Instant.now()); final Date lastTrade = Cooldown.parseTime(playerData.getTradingCooldowns().get(type)); long cooldown = Cooldown.parseCooldown(local); if(lastTrade != null && (now.getTime()/1000L >= lastTrade.getTime()/1000L + cooldown)) { playerData.getTradingCooldowns().remove(type); } else { maxUses = 0; } } } } } if(maxUses < 0) maxUses = uses; if(disabled) maxUses = 0; return maxUses; } /** * @param playerWrapper The wrapped player to check the hotv effect for * @return The Hero of the Village discount factor, adjusted by config */ private double getHotvDiscount(final PlayerWrapper playerWrapper) { final Player player = playerWrapper.getPlayer(); if(player == null) return 0.0; final PotionEffectType effectType = PotionEffectType.HERO_OF_THE_VILLAGE; if(!player.hasPotionEffect(effectType)) return 0.0; final PotionEffect effect = player.getPotionEffect(effectType); if(effect == null) return 0.0; //Calculates the discount factor from the player's current effect level or the defined maximum int heroLevel = effect.getAmplifier()+1; final int maxHeroLevel = instance.getCfg().getInt("MaxHeroLevel", -1); if(maxHeroLevel == 0 || heroLevel == 0) return 0.0; if(maxHeroLevel > 0 && heroLevel > maxHeroLevel) { heroLevel = maxHeroLevel; } return 0.0625*(heroLevel-1) + 0.3; } /** * @param item The config section that contains the settings for Item1, Item2, or Result items in the trade * @param ingredient The respective ingredient to change, based on config.yml */ private void setIngredient(final ConfigurationSection item, final IngredientWrapper ingredient) { if(item == null) return; ingredient.setMaterialId("minecraft:"+item.getString("Material", ingredient.getMaterialId()).toLowerCase().replace("minecraft:","")); ingredient.setAmount(item.getInt("Amount", ingredient.getAmount())); } }