/* * 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.resources.data.AbstractEntityTemplate; import illarion.client.world.World; import illarion.common.types.DisplayCoordinate; import illarion.common.types.Rectangle; import illarion.common.util.FastMath; import org.illarion.engine.EngineException; import org.illarion.engine.GameContainer; import org.illarion.engine.graphic.*; import org.illarion.engine.graphic.effects.HighlightEffect; import org.illarion.engine.graphic.effects.TextureEffect; 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; import javax.annotation.concurrent.NotThreadSafe; /** * The entity is a object that is shown in the game. It contains a sprite and possibly a frame animation. Also it * performs fade in and out effects. * <p> * It handles static objects on the screen as well as animated ones or objects with variations. * </p> * * @author Nop * @author Martin Karing <nitram@illarion.org> */ @NotThreadSafe public abstract class AbstractEntity<T extends AbstractEntityTemplate> implements DisplayItem, AlphaHandler, AnimatedFrame { public boolean isCurrentlyEffectedByFadingCorridor() { return currentlyEffectedByFadingCorridor; } /** * This class is used in case more then one alpha change listener is added. It forwards a alpha change message to * two other handlers. This way its possible to create a infinite amount of listeners on one entity. */ private static final class AlphaChangeListenerMulticast implements AlphaChangeListener { /** * The first listener to get the event. */ private final AlphaChangeListener listener1; /** * The second listener to get the event. */ private final AlphaChangeListener listener2; /** * Constructor for a multicast object. * * @param l1 the first listener to get the message * @param l2 the second listener to get the message */ private AlphaChangeListenerMulticast(AlphaChangeListener l1, AlphaChangeListener l2) { listener1 = l1; listener2 = l2; } /** * This method receives the alpha changed event and forwards its data to both added listeners. * * @param from the old alpha value * @param to the new alpha value */ @Override public void alphaChanged(int from, int to) { listener1.alphaChanged(from, to); listener2.alphaChanged(from, to); } } /** * The default light that is used in the client. */ @Nonnull protected static final Color DEFAULT_LIGHT = Color.WHITE; /** * The speed value for fading the alpha values by default. */ private static final int FADING_SPEED = 25; /** * The color value of the alpha when the object is faded out fully. */ private static final int FADE_OUT_ALPHA = (int) (0.4f * 255); /** * The alpha listener that is supposed to receive a message in case the alpha value of this entity changed. */ @Nullable private AlphaChangeListener alphaListener; /** * The target of the alpha approaching. The current alpha value will move by default closer to the alpha target * value at every render run. */ private int alphaTarget; /** * The base color is the color the image of the sprite is always colored with, * this color is applied no matter what the localLight is set to. */ @Nullable private Color baseColor; /** * The frame that is currently shown by this entity. */ private int currentFrame; /** * The location of the entity on the display. */ @Nullable private DisplayCoordinate displayCoordinate; /** * The light that effects this entity directly. That could be the {@link #DEFAULT_LIGHT} that ensures that the * object is displayed with its real colors or the ambient light of the weather that ensures that the object is * colored for the display on the map. */ @Nonnull private Color localLight; /** * The light value that is used to render this entity during the next render loop. */ @Nonnull private final Color renderLight = new Color(Color.WHITE); /** * This color is the color that was used last time to render the entity. Its used to check if the color changed * and the entity needs to be rendered again. */ @Nonnull private final Color lastRenderLight = new Color(Color.WHITE); /** * The color that is used to overwrite the real color of this entity. */ @Nullable private Color overWriteBaseColor; /** * The scaling value that is applied to this entity. */ private float scale = 1.f; /** * A flag if this object is currently shown on the screen. */ private boolean shown; /** * The template of this instance. */ @Nonnull private final T template; protected AbstractEntity(@Nonnull T template) { this.template = template; baseColor = template.getDefaultColor(); if (baseColor == null) { alphaTarget = 255; localLight = new Color(Color.WHITE); } else { localLight = new Color(baseColor); alphaTarget = baseColor.getAlpha(); } } @Nonnull public T getTemplate() { return template; } @Override public void addAlphaChangeListener(@Nonnull AlphaChangeListener listener) { if (removedEntity) { LOGGER.warn("Adding a alpha listener to a removed entity is not allowed."); return; } if (alphaListener == null) { alphaListener = listener; return; } alphaListener = new AlphaChangeListenerMulticast(alphaListener, listener); } /** * This function is triggered when a frame animation is done. Overwrite this function in order to archive some * special event handling after the animation. By default it does nothing. * * @param finished true in case the animation is really done */ @Override public void animationFinished(boolean finished) { // nothing needs to be done by default } @Override public void animationStarted() { // nothing to do } /** * Set a new base color of the entity. * * @param newBaseColor the new base color of the entity, {@code null} to get the default color */ public void changeBaseColor(@Nullable Color newBaseColor) { if (removedEntity) { LOGGER.warn("Changing the baseColor of a entity is not allowed after the entity was removed."); return; } if (newBaseColor == null) { overWriteBaseColor = null; return; } if (overWriteBaseColor == null) { overWriteBaseColor = new Color(newBaseColor); } else { overWriteBaseColor.setColor(newBaseColor); } } /** * Get the frame that is currently displayed. * * @return the currently displayed frame */ public int getCurrentFrame() { return currentFrame; } /** * Get the highlighting level of the item * * @return the highlight level of the object */ public int getHighlight() { return 0; } /** * Draw this entity to the screen. This also performs a few basic animations such as fading in and out, * based on the delta time that is supplied to this function. */ @Override public void render(@Nonnull Graphics g) { if (performRendering()) { DisplayCoordinate dc = getDisplayCoordinate(); int renderLocX = dc.getX(); int renderLocY = dc.getY(); int highlight = getHighlight(); if ((highlight > 0) && (highlightEffect != null)) { if (highlight == 1) { highlightEffect.setHighlightColor(COLOR_HIGHLIGHT_WEAK); } else { highlightEffect.setHighlightColor(COLOR_HIGHLIGHT_STRONG); } renderSprite(g, renderLocX, renderLocY, renderLight, highlightEffect); } else { renderSprite(g, renderLocX, renderLocY, renderLight); } } } protected boolean performRendering() { return (getAlpha() > 0) && Camera.getInstance().requiresUpdate(displayRect); } protected void renderSprite( @Nonnull Graphics g, int x, int y, @Nonnull Color light, @Nonnull TextureEffect... effects) { g.drawSprite(template.getSprite(), x, y, light, getCurrentFrame(), getScale(), 0.f, effects); } @Nonnull private static final Color COLOR_HIGHLIGHT_STRONG = new ImmutableColor(1.f, 1.f, 1.f, 0.25f); @Nonnull private static final Color COLOR_HIGHLIGHT_WEAK = new ImmutableColor(1.f, 1.f, 1.f, 0.05f); /** * Get the current alpha value. * * @return the alpha value */ @Override public int getAlpha() { return getLight().getAlpha(); } /** * The current location on the screen this object is displayed yet. */ @Nonnull @Contract(pure = true) public final DisplayCoordinate getDisplayCoordinate() { if (displayCoordinate == null) { throw new IllegalStateException("Display coordinate was not yet set."); } return displayCoordinate; } /** * Get the current light instance that is used by this entity. * * @return the light this entity uses at the rendering functions */ @Nonnull public final Color getLight() { return localLight; } /** * Get the scaling value that is applied to the entity. * * @return the scaling value applied to the entity */ public float getScale() { return scale; } /** * Get the current target of the alpha approaching. * * @return the current alpha target value */ public int getTargetAlpha() { return alphaTarget; } /** * Get the Z Order of this entity that marks the position in the display * list and selects this way, how other images overlay this entity. * * @return the layer of this entity */ @Override public final int getOrder() { return getDisplayCoordinate().getLayer(); } /** * Hide the entity from the screen by removing it from the display list. */ @Override public void hide() { if (shown) { World.getMapDisplay().getGameScene().removeElement(this); shown = false; } } /** * Once this value is set {@code true} the entity can be assumed to be removed. It must not be added to the * display again once this was done. */ private boolean removedEntity; /** * Calling this function marks the entity to be removed from the client for good. Once this function was called * its not allowed to do anything anymore with this entity. */ public void markAsRemoved() { hide(); removedEntity = true; } public boolean isMarkedAsRemoved() { return removedEntity; } /** * Check if the entity is visible. * * @return true in case the entity is visible */ public final boolean isVisible() { return (getTargetAlpha() > 0) || (getLight().getAlpha() > 0); } /** * Set the current alpha value of the entity. This causes that the alpha value is changed right away without any * fading effect. To get a fading effect use {@link #setAlphaTarget(int)}. * * @param newAlpha the new alpha value of this entity */ @Override public void setAlpha(int newAlpha) { if (removedEntity) { LOGGER.warn("Changing the alpha value of a removed entity is not allowed."); return; } int usedAlpha = FastMath.clamp(newAlpha, 0, Color.MAX_INT_VALUE); if (getLight().getAlpha() != usedAlpha) { int oldAlpha = getLight().getAlpha(); getLight().setAlpha(usedAlpha); if (alphaListener != null) { alphaListener.alphaChanged(oldAlpha, getLight().getAlpha()); } } } /** * Set the target of a alpha fading effect. At every rendering run of this entity the real alpha value of this * entity will move closer to the alpha target. To set the alpha value without a fading animation use * {@link #setAlpha(int)}. * * @param newAlphaTarget the target of the alpha fading */ @Override public final void setAlphaTarget(int newAlphaTarget) { if (removedEntity) { LOGGER.warn("Changing the alpha animation target of a entity is not allowed."); return; } alphaTarget = FastMath.clamp(newAlphaTarget, 0, Color.MAX_INT_VALUE); } /** * Set the base color of this entity. This operation does not create a copy of this reference. * * @param newBaseColor the new base color of the entity */ public void setBaseColor(@Nullable Color newBaseColor) { if (removedEntity) { LOGGER.warn("Changing the base color of a entity is not allowed once the entity was removed."); return; } baseColor = newBaseColor; } /** * Set the frame that is currently displayed at the render functions of this entity. * * @param frame the index of the frame that is displayed */ @Override public void setFrame(int frame) { if (removedEntity) { LOGGER.warn("Changing the frame of a removed entity is not allowed."); return; } if (currentFrame != frame) { currentFrame = frame; } } /** * Set the current light of this entity. This sets the instance that is set as parameter directly as local light * color. So any changes applied to the instance that was transferred will effect the light of this entity. * * @param light the new light that shall be used by this entity */ public void setLight(@Nonnull Color light) { if (removedEntity) { LOGGER.warn("Changing the light of a removed entity is not allowed."); return; } float oldAlpha = localLight.getAlphaf(); localLight = new Color(light); localLight.setAlphaf(oldAlpha); } /** * Set the scaling that shall be applied to this entity. * * @param newScale the new scaling value applied to this entity */ public void setScale(float newScale) { if (removedEntity) { LOGGER.warn("Changing the scale of a removed entity is not allowed."); return; } if (scale != newScale) { scale = newScale; } } /** * Set the position of the entity on the display. The display origin is at the origin of the game map. * * @param coordinate the new location on the screen. */ public void setScreenPos(@Nonnull DisplayCoordinate coordinate) { if (removedEntity) { LOGGER.warn("Changing the screen position of a removed entity is not allowed."); return; } DisplayCoordinate oldCoordinate = displayCoordinate; displayCoordinate = coordinate; if (shown && (oldCoordinate != null) && (oldCoordinate.getLayer() != coordinate.getLayer())) { updateDisplayPosition(); } } @Override public boolean isEventProcessed( @Nonnull GameContainer container, int delta, @Nonnull SceneEvent event) { return false; } protected boolean isMouseInInteractionRect(int mouseX, int mouseY) { int mouseXonDisplay = mouseX + Camera.getInstance().getViewportOffsetX(); int mouseYonDisplay = mouseY + Camera.getInstance().getViewportOffsetY(); return getInteractionRect().isInside(mouseXonDisplay, mouseYonDisplay); } protected final boolean isMouseInInteractionRect(@Nonnull Input input) { return isMouseInInteractionRect(input.getMouseX(), input.getMouseY()); } /** * The logging instance of this class. */ @Nonnull private static final Logger LOGGER = LoggerFactory.getLogger(AbstractEntity.class); @Override public void show() { if (removedEntity) { LOGGER.warn("Adding a entity to the display list is not allowed after the entity was removed."); return; } if (displayCoordinate == null) { throw new IllegalStateException("Adding a entity that has no display coordinate is illegal."); } if (shown) { LOGGER.error("Added entity {} twice.", this); } else { World.getMapDisplay().getGameScene().addElement(this); shown = true; } } /** * Update the position of this entity in the display list. */ public void updateDisplayPosition() { if (removedEntity) { LOGGER.warn("Updating the display position is not allowed once the entity was removed."); return; } if (shown) { World.getMapDisplay().getGameScene().updateElementLocation(this); } else { LOGGER.error("Updated display location for hidden item."); } } /** * Check if this entity is currently displayed on the screen. * * @return {@code true} in case this entity is currently displayed on the screen */ protected boolean isShown() { return shown; } @Override public void update(@Nonnull GameContainer container, int delta) { if (removedEntity) { shown = true; hide(); return; } if (!isShown()) { LOGGER.warn("{} Entity that is not shown received update.", this); shown = true; hide(); return; } Sprite sprite = template.getSprite(); int offS = template.getShadowOffset(); DisplayCoordinate dC = getDisplayCoordinate(); sprite.getDisplayArea(dC.getX(), dC.getY(), scale, 0.f, displayRect); int widthNoShadow = displayRect.getWidth() - (int) (offS * scale); if (fadingCorridorEffect) { currentlyEffectedByFadingCorridor = FadingCorridor.getInstance() .isInCorridor(displayRect.getX(), displayRect.getY(), dC.getLayer(), widthNoShadow, displayRect.getHeight()); if (currentlyEffectedByFadingCorridor) { setAlphaTarget(FADE_OUT_ALPHA); } else { setAlphaTarget(255); } } updateAlpha(delta); renderLight.setColor(getLocalLight()); if (renderLight.getAlpha() == 0) { return; } if ((baseColor != null) || (overWriteBaseColor != null)) { if (overWriteBaseColor != null) { renderLight.multiply(overWriteBaseColor); } else { renderLight.multiply(baseColor); } } if (!renderLight.equals(lastRenderLight)) { lastRenderLight.setColor(renderLight); } if (getHighlight() > 0) { try { highlightEffect = container.getEngine().getAssets().getEffectManager().getHighlightEffect(true); } catch (EngineException e) { LOGGER.warn("Failed to fetch highlight effect.", e); } } else { highlightEffect = null; } } private boolean currentlyEffectedByFadingCorridor; @Nullable private HighlightEffect highlightEffect; @Nonnull private final Color tempLight = new Color(Color.WHITE); /** * Get the light local to this tile. * * @return the local light of this entity */ @Nonnull public Color getLocalLight() { Color parentLight = getParentLight(); if (parentLight == null) { return getLight(); } if (parentLight.getAlpha() == 0) { return parentLight; } tempLight.setColor(parentLight); tempLight.multiply(getLight()); return tempLight; } @Nonnull private final Rectangle displayRect = new Rectangle(); @Nonnull private final Rectangle interactionRect = new Rectangle(); /** * Get the current interactive area of the object. * * @return the interactive area of the object */ @Nonnull public final Rectangle getInteractionRect() { if (displayRect.isEmpty()) { return displayRect; } int offS = template.getShadowOffset(); if (offS == 0) { return displayRect; } interactionRect.set(displayRect); interactionRect.expand(0, 0, -offS, 0); return interactionRect; } /** * Get the current display rectangle. * * @return the current display rectangle */ @Nonnull public final Rectangle getDisplayRect() { return displayRect; } private boolean fadingCorridorEffect; public void setFadingCorridorEffectEnabled(boolean value) { fadingCorridorEffect = value; } /** * This function checks if the entity is made transparent due the color its * drawn with. * * @return {@code true} in case the graphic is turned transparent due * its color */ public boolean isTransparent() { return getAlpha() < 255; } /** * Update the alpha value. This function causes the alpha value to approach * the target alpha value and notifies in case its needed all listeners. * * @param delta the time in milliseconds since the last update */ protected final void updateAlpha(int delta) { int currentAlpha = getAlpha(); int targetAlpha = getTargetAlpha(); if (currentAlpha != targetAlpha) { setAlpha(AnimationUtility.translate(currentAlpha, targetAlpha, FADING_SPEED, 0, 255, delta)); } } /** * Get the parent light of this entity. This is the light value that is supplied by some other object. The value of * this color is never altered by this class. The value can be {@code null} to assume the default light. * * @return the parent light */ @Nullable protected Color getParentLight() { return null; } }