package io.luna.game.model.item; import com.google.common.collect.ImmutableList; import io.luna.game.event.impl.EquipmentChangeEvent; import io.luna.game.model.def.EquipmentDefinition; import io.luna.game.model.item.RefreshListener.PlayerRefreshListener; import io.luna.game.model.mob.Player; import io.luna.game.model.mob.block.UpdateFlagSet.UpdateFlag; import io.luna.game.plugin.PluginManager; import java.util.BitSet; import java.util.Optional; import java.util.OptionalInt; import java.util.function.IntUnaryOperator; import static io.luna.util.OptionalUtils.ifPresent; import static io.luna.util.OptionalUtils.mapToInt; import static io.luna.util.OptionalUtils.matches; /** * An item container model representing a player's equipment. * * @author lare96 <http://github.com/lare96> */ public final class Equipment extends ItemContainer { /** * A listener that updates equipment bonuses and posts equipment change events. */ private final class EquipmentListener implements ItemContainerListener { /** * The player. */ private final Player player; /** * A bit set that keeps track of which bonuses need to be updated. */ private final BitSet writeBonuses = new BitSet(12); /** * Creates a new {@link EquipmentListener}. * * @param player The player. */ public EquipmentListener(Player player) { this.player = player; } @Override public void onSingleUpdate(int index, ItemContainer items, Optional<Item> oldItem, Optional<Item> newItem) { if (isIdUnequal(oldItem, newItem)) { updateBonus(oldItem, newItem); writeBonuses(); flagAppearance(index); } sendEvent(index, oldItem, newItem); } @Override public void onBulkUpdate(int index, ItemContainer items, Optional<Item> oldItem, Optional<Item> newItem) { if (isIdUnequal(oldItem, newItem)) { updateBonus(oldItem, newItem); flagAppearance(index); } sendEvent(index, oldItem, newItem); } @Override public void onBulkUpdateCompleted(ItemContainer items) { writeBonuses(); } /** * Posts an equipment change event. * * @param index The index of the change. * @param oldItem The old item. * @param newItem The new item. */ private void sendEvent(int index, Optional<Item> oldItem, Optional<Item> newItem) { PluginManager plugins = player.getPlugins(); plugins.post(new EquipmentChangeEvent(player, index, oldItem, newItem)); } /** * Determines if the identifiers are unequal. * * @param oldItem The old item. * @param newItem The new item. * @return {@code true} if the identifiers are unequal. */ private boolean isIdUnequal(Optional<Item> oldItem, Optional<Item> newItem) { OptionalInt oldId = mapToInt(oldItem, Item::getId); OptionalInt newId = mapToInt(newItem, Item::getId); return !oldId.equals(newId); } /** * Updates bonuses for two potential items. * * @param oldItem The old item. * @param newItem The new item. */ private void updateBonus(Optional<Item> oldItem, Optional<Item> newItem) { IntUnaryOperator oldBonusFunction, newBonusFunction; if (oldItem.isPresent()) { var equipmentDefinition = oldItem.get().getEquipDef(); oldBonusFunction = equipmentDefinition::getBonus; } else { oldBonusFunction = id -> 0; } if (newItem.isPresent()) { var equipmentDefinition = newItem.get().getEquipDef(); newBonusFunction = equipmentDefinition::getBonus; } else { newBonusFunction = id -> 0; } for (int index = 0; index < bonuses.length; index++) { int old = oldBonusFunction.applyAsInt(index); int replace = newBonusFunction.applyAsInt(index); // Bonus(es) nonzero, this index needs updating. if (old != 0 || replace != 0) { writeBonuses.set(index); } // Apply old (-) and new (+) bonuses. bonuses[index] = bonuses[index] - old + replace; } } /** * Does a smart write of the bonuses to the equipment interface. */ private void writeBonuses() { StringBuilder sb = new StringBuilder(); for (int index = 0; index < bonuses.length; index++) { // Smart write, only write bonuses if they've changed. if (writeBonuses.get(index)) { String name = BONUS_NAMES.get(index); int value = bonuses[index]; boolean positive = value >= 0; int widget = 1675 + index + (index == 10 || index == 11 ? 1 : 0); // Append the bonus string. sb.append(name).append(": "). append(positive ? "+" : ""). append(value); // Queue the packet to display it. player.sendText(sb.toString(), widget); sb.setLength(0); } } writeBonuses.clear(); } } /** * The head index. */ public static final int HEAD = 0; /** * The cape index. */ public static final int CAPE = 1; /** * The amulet index. */ public static final int AMULET = 2; /** * The weapon index. */ public static final int WEAPON = 3; /** * The chest index. */ public static final int CHEST = 4; /** * The shield index. */ public static final int SHIELD = 5; /** * The legs index. */ public static final int LEGS = 7; /** * The hands index. */ public static final int HANDS = 9; /** * The feet index. */ public static final int FEET = 10; /** * The ring index. */ public static final int RING = 12; /** * The ammunition index. */ public static final int AMMUNITION = 13; /** * The stab attack bonus index. */ public static final int STAB_ATTACK = 0; /** * The slash attack bonus index. */ public static final int SLASH_ATTACK = 1; /** * The crush attack bonus index. */ public static final int CRUSH_ATTACK = 2; /** * The magic attack bonus index. */ public static final int MAGIC_ATTACK = 3; /** * The ranged attack bonus index. */ public static final int RANGED_ATTACK = 4; /** * The stab defence bonus index. */ public static final int STAB_DEFENCE = 5; /** * The slash defence bonus index. */ public static final int SLASH_DEFENCE = 6; /** * The crush defence bonus index. */ public static final int CRUSH_DEFENCE = 7; /** * The magic defence bonus index. */ public static final int MAGIC_DEFENCE = 8; /** * The ranged defence bonus index. */ public static final int RANGED_DEFENCE = 9; /** * The strength bonus index. */ public static final int STRENGTH = 10; /** * The prayer bonus index. */ public static final int PRAYER = 11; /** * An immutable list of bonus names. */ public static final ImmutableList<String> BONUS_NAMES = ImmutableList.of( "Stab", "Slash", "Crush", "Magic", "Range", "Stab", "Slash", "Crush", "Magic", "Range", "Strength", "Prayer" ); /** * An error message. */ private static final String ERROR_MSG = "Use set(index, Item) or add(Item) or remove(Item) instead."; /** * The player. */ private final Player player; /** * The inventory. */ private final Inventory inventory; /** * The equipment listener. */ private final EquipmentListener equipmentListener; /** * An array of equipment bonuses. */ private final int[] bonuses = new int[12]; /** * Creates a new {@link Equipment}. * * @param player The player. */ public Equipment(Player player) { super(14, StackPolicy.STANDARD, 1688); this.player = player; inventory = player.getInventory(); EquipmentListener equipmentListener = new EquipmentListener(player); this.equipmentListener = equipmentListener; setListeners(new PlayerRefreshListener(player, ERROR_MSG), equipmentListener, new WeightListener(player)); } @Override public boolean add(Item item) { int index = item.getEquipDef().getIndex(); int amount = item.getAmount(); // Increase amount for stackable items. if (item.getItemDef().isStackable() && matches(computeIdForIndex(index), item::getId)) { amount += computeAmountForIndex(index); } // Equip the item. Item newItem = item.withAmount(amount); set(index, newItem); return true; } @Override public boolean remove(Item item) { int index = item.getEquipDef().getIndex(); // Item does not exist. if (!matches(computeIdForIndex(index), item::getId)) { return false; } // Calculate new item amount after removal. int newAmount = computeAmountForIndex(index) - item.getAmount(); if (newAmount <= 0) { // If it's below or equal to 0, remove the item. set(index, null); } else { // Otherwise set the new amount. set(index, item.withAmount(newAmount)); } return true; } /** * @deprecated This always throws an exception. Use {@code set(int, Item)} or {@code add(Item)} * instead. */ @Deprecated @Override public boolean add(int preferredIndex, Item item) { throw new UnsupportedOperationException(ERROR_MSG); } /** * @deprecated This always throws an exception. Use {@code set(int, Item)} or {@code remove(Item)} * instead. */ @Deprecated @Override public boolean remove(int preferredIndex, Item item) { throw new UnsupportedOperationException(ERROR_MSG); } /** * Equips an item from the inventory. * * @param inventoryIndex The inventory index of the item. * Returns {@code true} if successful. */ public boolean equip(int inventoryIndex) { // Validate index. Item inventoryItem = inventory.get(inventoryIndex); if (inventoryItem == null) { return false; } EquipmentDefinition equipDef = inventoryItem.getEquipDef(); int equipIndex = equipDef.getIndex(); // Check equipment requirements. boolean failedToMeet = ifPresent(equipDef.getFailedRequirement(player), req -> req.sendFailureMessage(player)); if (failedToMeet) { return false; } // Unequip something if we have to. OptionalInt unequipIndex = OptionalInt.empty(); if (equipIndex == WEAPON && equipDef.isTwoHanded()) { // Equipping 2h weapon, so unequip shield. unequipIndex = OptionalInt.of(SHIELD); } else if (equipIndex == Equipment.SHIELD && occupied(WEAPON) && get(WEAPON).getEquipDef().isTwoHanded()) { // Equipping shield, so unequip 2h weapon. unequipIndex = OptionalInt.of(WEAPON); } // Check if inventory has enough space. if (unequipIndex.isPresent()) { int remaining = inventory.computeRemainingSize(); if (remaining == 0 && occupied(unequipIndex.getAsInt()) && occupied(equipIndex)) { inventory.fireCapacityExceededEvent(); return false; } } // Equip item. inventory.set(inventoryIndex, null); unequipIndex.ifPresent(this::unequip); Item equipItem = get(equipIndex); if (equipItem == null || inventory.add(equipItem)) { set(equipIndex, inventoryItem); } return true; } /** * Unequips an item from the player's equipment. * * @param equipmentIndex The equipment index of the item. * @return {@code true} if successful. */ public boolean unequip(int equipmentIndex) { // Validate index. Item equipmentItem = get(equipmentIndex); if (equipmentItem == null) { return false; } // Unequip item. if (player.getInventory().add(equipmentItem)) { set(equipmentIndex, null); return true; } return false; } /** * Flags the appearance block if required. * * @param index The index to flag. */ private void flagAppearance(int index) { if (index != RING && index != AMMUNITION) { player.getFlags().flag(UpdateFlag.APPEARANCE); } } /** * Loads equipment bonuses for the Player by updating and displaying them. */ public void loadBonuses() { for (Item item : this) { if (item == null) { continue; } // Update bonuses. equipmentListener.updateBonus(Optional.empty(), Optional.of(item)); } // Write them all. equipmentListener.writeBonuses.set(0, 12); equipmentListener.writeBonuses(); } }