package com.asteria.game.shop; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import com.asteria.game.GameConstants; import com.asteria.game.World; import com.asteria.game.character.player.Player; import com.asteria.game.item.Item; import com.asteria.game.item.container.ItemContainer; import com.asteria.game.item.container.ItemContainerPolicy; import com.asteria.utility.TextUtils; /** * The container that represents a shop players can buy and sell items from. * * @author lare96 <http://github.com/lare96> */ public final class Shop { /** * The map that holds all of the shop names mapped to their shop instances. */ public static final Map<String, Shop> SHOPS = new HashMap<>(); /** * The name of this current shop. */ private final String name; /** * The item container that contains the items within this shop. */ private final ItemContainer container = new ItemContainer(40, ItemContainerPolicy.STACK_ALWAYS); /** * The flag that determines if this shop will restock its items. */ private final boolean restock; /** * The flag that determines if items can be sold to this shop. */ private final boolean canSell; /** * The currency that items within this shop will be bought with. */ private final Currency currency; /** * The set of players that are currently viewing this shop. */ private final Set<Player> players = new HashSet<>(); /** * The map of cached shop item identifications and their amounts. */ private final Map<Integer, Integer> itemCache; /** * The shop restock task that will restock the shops. */ private ShopRestockTask restockTask; /** * Creates a new {@link Shop}. * * @param name * the name of this current shop. * @param items * the items within this shop. * @param restock * the flag that determines if this shop will restock its items. * @param canSell * the flag that determines if items can be sold to this shop. * @param currency * the currency that items within this shop will be bought with. */ public Shop(String name, Item[] items, boolean restock, boolean canSell, Currency currency) { this.name = name; this.restock = restock; this.canSell = canSell; this.currency = currency; this.container.setItems(items); this.itemCache = new HashMap<>(container.capacity()); Arrays.stream(items).filter(Objects::nonNull).forEach(item -> itemCache.put(item.getId(), item.getAmount())); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof Shop)) return false; Shop other = (Shop) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } /** * Opens this shop by displaying the interface for {@code player}. * * @param player * the player to open the shop for. */ public void openShop(Player player) { player.getMessages().sendItemsOnInterface(3823, player.getInventory().container()); player.getMessages().sendItemsOnInterface(3900, container.container(), container.size()); player.setOpenShop(name); player.getMessages().sendInventoryInterface(3824, 3822); player.getMessages().sendString(name, 3901); players.add(player); } /** * Updates the items and the containers that display items for * {@code player}. * * @param player * the player this shop will be updated for. * @param checkStock * if the stock should be checked. */ public void updateShop(Player player, boolean checkStock) { player.getMessages().sendItemsOnInterface(3823, player.getInventory().container()); int size = container.size(); players.stream().filter(Objects::nonNull).forEach(p -> p.getMessages().sendItemsOnInterface(3900, container.container(), size)); if (checkStock && restock) { if (restockTask != null && restockTask.isRunning()) return; if (!needsRestock()) return; restockTask = new ShopRestockTask(this); World.submit(restockTask); } } /** * Sends the determined selling value of {@code item} to {@code player}. * * @param player * the player to send the value to. * @param item * the item to send the value of. */ public void sendSellingPrice(Player player, Item item) { String itemName = item.getDefinition().getName(); if (!canSell) { player.getMessages().sendMessage("You cannot sell any items to this store."); return; } if (Arrays.stream(GameConstants.INVALID_SHOP_ITEMS).anyMatch(i -> i == item.getId())) { player.getMessages().sendMessage("You can't sell " + itemName + " " + "here."); return; } if (!container.contains(item.getId()) && !name.equalsIgnoreCase("General Store")) { player.getMessages().sendMessage("You can't sell " + itemName + " " + "to this store."); return; } String formatPrice = TextUtils.formatPrice((int) Math.floor(determinePrice(item) / 2)); player.getMessages().sendMessage(itemName + ": shop will buy for " + formatPrice + " " + currency + "."); } /** * Sends the determined purchase value of {@code item} to {@code player}. * * @param player * the player to send the value to. * @param item * the item to send the value of. */ public void sendPurchasePrice(Player player, Item item) { Item shopItem = container.searchItem(item.getId()).orElse(null); if (shopItem == null) return; if (shopItem.getAmount() <= 0) { player.getMessages().sendMessage("There is none of this item left in stock!"); return; } player .getMessages() .sendMessage( item.getDefinition().getName() + ": " + "shop will sell for " + TextUtils.formatPrice(determinePrice(item)) + " " + currency + "."); } /** * The method that allows {@code player} to purchase {@code item}. * * @param player * the player who will purchase this item. * @param item * the item that will be purchased. * @return {@code true} if the player purchased the item, {@code false} * otherwise. */ public boolean purchase(Player player, Item item) { Item shopItem = container.searchItem(item.getId()).orElse(null); if (shopItem == null) return false; if (shopItem.getAmount() <= 0) { player.getMessages().sendMessage("There is none of this item left in stock!"); return false; } if (item.getAmount() > shopItem.getAmount()) item.setAmount(shopItem.getAmount()); if (!player.getInventory().spaceFor(item)) { item.setAmount(player.getInventory().remaining()); if (item.getAmount() == 0) { player.getMessages().sendMessage("You do not have enough space in your inventory to buy this item!"); return false; } } int value = currency == Currency.COINS ? item.getDefinition().getGeneralPrice() : item.getDefinition().getSpecialPrice(); if (!(currency.getCurrency().currencyAmount(player) >= (value * item.getAmount()))) { player.getMessages().sendMessage("You do not have enough " + currency + " to buy this item."); return false; } if (player.getInventory().remaining() >= item.getAmount() && !item.getDefinition().isStackable() || player.getInventory() .remaining() >= 1 && item.getDefinition().isStackable() || player.getInventory().contains(item.getId()) && item.getDefinition() .isStackable()) { if (itemCache.containsKey(item.getId())) { container.searchItem(item.getId()).ifPresent(i -> i.decrementAmountBy(item.getAmount())); } else if (!itemCache.containsKey(item.getId())) { container.remove(item); } currency.getCurrency().takeCurrency(player, item.getAmount() * value); player.getInventory().add(item); } else { player.getMessages().sendMessage("You don't have enough space in " + "your inventory."); return false; } updateShop(player, true); return true; } /** * The method that allows {@code player} to sell {@code item}. * * @param player * the player who will sell this item. * @param item * the item that will be sold. * @return {@code true} if the player sold the item, {@code false} * otherwise. */ public boolean sell(Player player, Item item, int fromSlot) { if (!Item.valid(item)) return false; if (!canSell) { player.getMessages().sendMessage("You cannot sell items here."); return false; } if (Arrays.stream(GameConstants.INVALID_SHOP_ITEMS).anyMatch(i -> i == item.getId())) { player.getMessages().sendMessage("You can't sell " + item.getDefinition().getName() + " here."); return false; } if (!player.getInventory().contains(item.getId())) return false; if (!container.contains(item.getId()) && !name.equalsIgnoreCase("General Store")) { player.getMessages().sendMessage("You can't sell " + item.getDefinition().getName() + " to this store."); return false; } if (!container.spaceFor(item)) { player.getMessages().sendMessage("There is no room in this store " + "for the item you are trying to sell!"); return false; } if (player.getInventory().remaining() == 0 && !currency.getCurrency().canRecieveCurrency(player)) { player.getMessages().sendMessage("You do not have enough space in " + "your inventory to sell this item!"); return false; } int amount = player.getInventory().amount(item.getId()); if (item.getAmount() > amount && !item.getDefinition().isStackable()) { item.setAmount(amount); } else if (item.getAmount() > player.getInventory().get(fromSlot).getAmount() && item.getDefinition().isStackable()) { item.setAmount(player.getInventory().get(fromSlot).getAmount()); } player.getInventory().remove(item, fromSlot); currency.getCurrency().recieveCurrency(player, item.getAmount() * (int) Math.floor(determinePrice(item) / 2)); if (container.contains(item.getId())) { container.searchItem(item.getId()).ifPresent(i -> i.incrementAmountBy(item.getAmount())); } else { container.add(item); } updateShop(player, false); return true; } /** * Determines if the items in the container need to be restocked. * * @return {@code true} if the items need to be restocked, {@code false} * otherwise. */ protected boolean needsRestock() { return container.stream().filter(Objects::nonNull).anyMatch(i -> i.getAmount() <= 0 && itemCache.containsKey(i.getId())); } /** * Determines if the items in the container no longer need to be restocked. * * @return {@code true} if the items don't to be restocked, {@code false} * otherwise. */ protected boolean restockCompleted() { return container.stream().filter(Objects::nonNull).allMatch( i -> itemCache.containsKey(i.getId()) && i.getAmount() >= itemCache.get(i.getId())); } /** * Determines the price of {@code item} based on the currency. * * @param item * the item to determine the price of. * @return the price of the item based on the currency. */ private int determinePrice(Item item) { return currency == Currency.COINS ? item.getDefinition().getGeneralPrice() : item.getDefinition().getSpecialPrice(); } /** * Gets the name of this current shop. * * @return the name of this shop. */ public String getName() { return name; } /** * Gets the item container that contains the items within this shop. * * @return the container that contains the items. */ public ItemContainer getContainer() { return container; } /** * Determines if this shop will restock its items. * * @return {@code true} if this shop will restock, {@code false} otherwise. */ public boolean isRestock() { return restock; } /** * Determines if items can be sold to this shop. * * @return {@code true} if items can be sold, {@code false} otherwise. */ public boolean isCanSell() { return canSell; } /** * Gets the currency that items within this shop will be bought with. * * @return the currency that items will be bought with. */ public Currency getCurrency() { return currency; } /** * Gets the the set of players that are currently viewing this shop. * * @return the set of players viewing the shop. */ public Set<Player> getPlayers() { return players; } /** * Gets an unmodifiable version of the map of cached shop item * identifications and their amounts. * * @return the map of cached shop items. */ public Map<Integer, Integer> getItemCache() { return Collections.unmodifiableMap(itemCache); } }