/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.client.graphics; import illarion.client.IllaClient; import illarion.client.input.*; import illarion.client.resources.ItemFactory; import illarion.client.resources.Resource; import illarion.client.resources.data.ItemTemplate; import illarion.client.util.Lang; import illarion.client.util.LookAtTracker; import illarion.client.world.MapTile; import illarion.client.world.World; import illarion.client.world.interactive.InteractiveMapTile; import illarion.client.world.movement.MouseTargetMovementHandler; import illarion.client.world.movement.TargetMovementHandler; import illarion.common.graphics.MapConstants; import illarion.common.graphics.MapVariance; import illarion.common.gui.AbstractMultiActionHelper; import illarion.common.types.ItemCount; import illarion.common.types.ItemId; import illarion.common.types.ServerCoordinate; import org.illarion.engine.GameContainer; import org.illarion.engine.graphic.Color; import org.illarion.engine.graphic.Graphics; import org.illarion.engine.graphic.SceneEvent; import org.illarion.engine.input.Button; import org.illarion.engine.input.Input; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * A item is a object that is on the game map or in the inventory or in any container showcase of the client. * * @author Martin Karing <nitram@illarion.org> * @author Nop */ @SuppressWarnings("ClassNamingConvention") public final class Item extends AbstractEntity<ItemTemplate> implements Resource { /** * The frame animation object that is used in case the item contains a animation that needs to be played. */ @Nullable private final FrameAnimation animation; /** * The amount of items that are represented by this item instance. So in case the number is larger then 1 this * item represents a stack of items of the same kind. */ @Nullable private ItemCount count; /** * The ID of this item. */ @Nonnull private final ItemId itemId; /** * The text tag is the rendered text that shows the count of the item next to it. */ @Nullable private TextTag number; /** * The tile this item is located on. */ @Nonnull private final MapTile parentTile; /** * This indicates of the number of the item shall be shown. This number shows how many items are on this stack. * Its only useful to show this in case the item actually is a stack, so {@link #count} is greater then 1 and the * item is the only one or the one at the top position on one location. */ private boolean showNumber; /** * True in case the object contains variances instead of a frame animation for the different frames of the image. * So if this is set to {@code true} all the frames of this image are not handles as a animation, * they are used as variances, selected by the location of the item on the map. */ private final boolean variants; /** * The default constructor of a new item. * * @param template the template used to create the new item * @param parentTile the tile this item is located on */ public Item(@Nonnull ItemTemplate template, @Nonnull MapTile parentTile) { super(template); itemId = new ItemId(template.getId()); // an animated item if ((template.getAnimationSpeed() > 0) && (template.getFrames() > 1)) { // start animation right away. All items of this type will share it animation = template.getSharedAnimation(); variants = false; } else if (template.getFrames() > 1) { // a tile with variants variants = true; animation = null; } else { animation = null; variants = false; } this.parentTile = parentTile; setFadingCorridorEffectEnabled(template.isEffectedByFadingCorridor()); } @Override @Nonnull protected Color getParentLight() { Tile parentGraphicTile = parentTile.getTile(); if (parentGraphicTile == null) { return Color.BLACK; } return parentGraphicTile.getLocalLight(); } /** * Create a new item instance for a ID and a specified location. The location is used in case the item has * variances. The item is not set on the map tile of the location by default. * * @param itemID the ID of the item that shall be created * @param locColumn the column on the map where the item shall be created * @param locRow the row on the map where the item shall be created * @param parent the tile this item is located on * @return the new item */ @Nonnull public static Item create( @Nonnull ItemId itemID, int locColumn, int locRow, @Nonnull MapTile parent) { ItemTemplate template = ItemFactory.getInstance().getTemplate(itemID.getValue()); Item item = new Item(template, parent); // Set variant and scaling, this functions check on their own if this is allowed item.setVariant(locColumn, locRow); item.setScale(locColumn, locRow); return item; } /** * Create a new item instance for a ID and a specified location. The location is used in case the item has * variances. The item is not set on the map tile of the location by default. * * @param itemID the ID of the item that shall be created * @param loc the location where the item shall be shown * @param parent the tile this item is located on * @return the new item */ @Nonnull public static Item create( @Nonnull ItemId itemID, @Nonnull ServerCoordinate loc, @Nonnull MapTile parent) { return create(itemID, loc.getX(), loc.getY(), parent); } @Override @Contract(pure = true) public int getHighlight() { return showHighlight; } @Override public void render(@Nonnull Graphics g) { if (performRendering()) { super.render(g); if (showNumber && (number != null)) { number.render(g); } } showHighlight = 0; } /** * Enable the display of numbers for stacked items. * * @param newShowNumber {@code true} to show the number at this item */ public void enableNumbers(boolean newShowNumber) { showNumber = newShowNumber && getTemplate().getItemInfo().isMovable(); } /** * Get the count of the item. A count greater then 1 means that this item * represents a item stack with the returned amount of items of the same * kind on it. * * @return the number of items on the stack or 1 in case there is just one * item */ @Nullable @Contract(pure = true) public ItemCount getCount() { return count; } /** * Get the ID of this item. * * @return the ID of this item */ @Nonnull public ItemId getItemId() { return itemId; } private int showHighlight; /** * The logging instance that takes care for the logging output of this class. */ @Nonnull private static final Logger log = LoggerFactory.getLogger(Item.class); private boolean isEventProcessed(@Nonnull CurrentMouseLocationEvent event) { if (!isMouseInInteractionRect(event.getX(), event.getY())) { return false; } if (!event.isHighlightHandled()) { showHighlight = 1; if (parentTile.getInteractive().isInUseRange()) { showHighlight = 2; } event.setHighlightHandled(true); } if (!parentTile.isBlocked()) { MouseTargetMovementHandler handler = World.getPlayer().getMovementHandler().getTargetMouseMovementHandler(); handler.walkTo(parentTile.getCoordinates(), 0); return true; } return false; } private boolean isEventProcessed(@Nonnull PointOnMapEvent event) { if (!isMouseInInteractionRect(event.getX(), event.getY())) { return false; } if (!LookAtTracker.isLookAtObject(this)) { LookAtTracker.setLookAtObject(this); parentTile.getInteractive().lookAt(this); } return true; } /** * Processes single-clicks on the map * If possible, walks the character to the location at the tile under the mouse * Does not walk the player to the base of objects or avatars. * * @param event the event to be processed * @param container the GameContainer; needed to process input * @return {@code true} if the event was processed */ boolean processMapClick(@Nonnull ClickOnMapEvent event, @Nonnull GameContainer container) { if (event.getKey() != Button.Left) { return false; } Input input = container.getEngine().getInput(); MapTile mouseTile = World.getMap().getInteractive().getTileOnScreenLoc(input.getMouseX(), input.getMouseY()); if ((mouseTile == null) || !mouseTile.isAtPlayerLevel()) { return false; } delayGoToItem.reset(); TargetMovementHandler handler = World.getPlayer().getMovementHandler().getTargetMovementHandler(); boolean mouseTileIsBlocked = mouseTile.isBlocked(); boolean mouseTileInUseRange = mouseTile.getInteractive().isInUseRange(); boolean itemClickedIsBlocked = parentTile.isBlocked(); boolean itemClickedInUseRange = parentTile.getInteractive().isInUseRange(); if (mouseTileInUseRange && mouseTileIsBlocked){ // If it is adjacent and blocked, nowhere to walk return true; } if (itemClickedInUseRange) { if (!itemClickedIsBlocked) { delayGoToItem.setLocation(parentTile.getCoordinates()); delayGoToItem.pulse(); } } else { handler.walkTo(mouseTile.getCoordinates(), mouseTileIsBlocked ? 1 : 0); handler.assumeControl(); } return true; } private boolean isEventProcessed(@Nonnull DoubleClickOnMapEvent event) { if (event.getKey() != Button.Left) { return false; } if (!parentTile.isAtPlayerLevel()) { return false; } delayGoToItem.reset(); if (parentTile.getInteractive().isInUseRange()) { parentTile.getInteractive().use(); } else { InteractiveMapTile interactiveMapTile = parentTile.getInteractive(); TargetMovementHandler handler = World.getPlayer().getMovementHandler().getTargetMovementHandler(); handler.walkTo(parentTile.getCoordinates(), 1); handler.setTargetReachedAction(interactiveMapTile::use); handler.assumeControl(); } return true; } private boolean isEventProcessed(@Nonnull PrimaryKeyMapDrag event) { if (!isMouseInInteractionRect(event.getOldX(), event.getOldY()) || !parentTile.isAtPlayerLevel()) { return false; } return event.startDraggingItemFromTile(parentTile); } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean isEventProcessed(@Nonnull GameContainer container, int delta, @Nonnull SceneEvent event) { if (getAlpha() == 0) { return false; } if (event instanceof AbstractMouseLocationEvent) { AbstractMouseLocationEvent locationEvent = (AbstractMouseLocationEvent) event; if (isMouseInInteractionRect(locationEvent.getX(), locationEvent.getY())) { if (event instanceof CurrentMouseLocationEvent) { return isEventProcessed((CurrentMouseLocationEvent) event); } if (event instanceof PointOnMapEvent) { return isEventProcessed((PointOnMapEvent) event); } // Uses a method from AbstractEntity that walks the player to the point at the mouse if (event instanceof ClickOnMapEvent) { return processMapClick((ClickOnMapEvent) event, container); } if (event instanceof DoubleClickOnMapEvent) { return isEventProcessed((DoubleClickOnMapEvent) event); } } if (event instanceof PrimaryKeyMapDrag) { return isEventProcessed((PrimaryKeyMapDrag) event); } } return false; } private static final class DelayGoToItemHandler extends AbstractMultiActionHelper { @Nullable private ServerCoordinate target; DelayGoToItemHandler() { super(IllaClient.getCfg().getInteger("doubleClickInterval"), 2); } void setLocation(@Nullable ServerCoordinate target) { this.target = target; } @Override public void executeAction(int count) { if ((count == 1) && (target != null)) { TargetMovementHandler handler = World.getPlayer().getMovementHandler().getTargetMovementHandler(); handler.walkTo(target, 0); handler.assumeControl(); } } } @Nonnull private static final DelayGoToItemHandler delayGoToItem = new DelayGoToItemHandler(); @Override protected boolean isMouseInInteractionRect(int mouseX, int mouseY) { if (super.isMouseInInteractionRect(mouseX, mouseY)) { if (isCurrentlyEffectedByFadingCorridor()) { Tile tile = parentTile.getTile(); return (tile != null) && tile.isMouseInInteractionRect(mouseX, mouseY); } return true; } return false; } /** * Set number of stacked items. * * @param newCount the number of items on this stack, in case its more then * one a text is displayed next to the item that shown how many * items are on the stack */ public void setCount(@Nullable ItemCount newCount) { count = newCount; // write number to text for display if (ItemCount.isGreaterOne(count)) { number = new TextTag(count.getShortText(Lang.getInstance().getLocale()), Color.YELLOW); } else { number = null; } } @Nullable private ItemStack parentStack; public void show(@Nonnull ItemStack stack) { //noinspection AssignmentToCollectionOrArrayFieldFromParameter parentStack = stack; if (animation != null) { animation.addTarget(this, true); } } @Override @Contract("->fail") public void show() { throw new IllegalStateException("Calling show directly is not permitted for items."); } @Override public void hide() { parentStack = null; if (animation != null) { animation.removeTarget(this); } } @Override @Contract(pure = true) public boolean isShown() { ItemStack localStack = parentStack; return (localStack != null) && localStack.isShown(); } @Override public void update(@Nonnull GameContainer container, int delta) { super.update(container, delta); if (showNumber && (number != null)) { number.addToCamera(getDisplayCoordinate().getX(), getDisplayCoordinate().getY()); number.updateHeightAndWidth(); number.setOffset((MapConstants.TILE_W / 2) - number.getHeight() - number.getWidth(), -number.getHeight() / 2); number.update(container, delta); } } @Override public int getTargetAlpha() { Tile tileOfItem = parentTile.getTile(); int alphaOfTile = (tileOfItem == null) ? Color.MAX_INT_VALUE : tileOfItem.getTargetAlpha(); return Math.min(super.getTargetAlpha(), alphaOfTile); } /** * Determine the graphical variant from a coordinate and set the needed * frame on this. * * @param locX the first part of the coordinate * @param locY the second part of the coordinate */ private void setVariant(int locX, int locY) { if (variants) { setFrame(MapVariance.getItemFrameVariance(locX, locY, getTemplate().getFrames())); } } /** * Set an individual scale dependent on a location. The new scale value is * directly applied to the item. * * @param locX the first part of the coordinate * @param locY the second part of the coordinate */ private void setScale(int locX, int locY) { if (getTemplate().getItemInfo().hasVariance()) { setScale(MapVariance.getItemScaleVariance(locX, locY, getTemplate().getItemInfo().getVariance())); } } }