/*
* 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.graphics.FrameAnimation.Mode;
import illarion.client.input.AbstractMouseLocationEvent;
import illarion.client.input.ClickOnMapEvent;
import illarion.client.input.CurrentMouseLocationEvent;
import illarion.client.input.DoubleClickOnMapEvent;
import illarion.client.resources.CharacterFactory;
import illarion.client.resources.MiscImageFactory;
import illarion.client.resources.Resource;
import illarion.client.resources.data.AvatarTemplate;
import illarion.client.util.Lang;
import illarion.client.world.Char;
import illarion.client.world.MapTile;
import illarion.client.world.World;
import illarion.client.world.interactive.InteractiveChar;
import illarion.client.world.movement.TargetMovementHandler;
import illarion.common.gui.AbstractMultiActionHelper;
import illarion.common.types.DisplayCoordinate;
import org.illarion.engine.GameContainer;
import org.illarion.engine.graphic.Color;
import org.illarion.engine.graphic.Graphics;
import org.illarion.engine.graphic.ImmutableColor;
import org.illarion.engine.graphic.SceneEvent;
import org.illarion.engine.input.Button;
import org.illarion.engine.input.Input;
import org.illarion.engine.input.Key;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Class for the avatar of a characters. The avatar is the visual representation of a character on a map. All
* characters, including monsters and NPCs have a avatar.
*
* @author Nop
* @author Martin Karing <nitram@illarion.org>
*/
public final class Avatar extends AbstractEntity<AvatarTemplate> implements Resource {
@Nonnull
private static final Logger log = LoggerFactory.getLogger(Avatar.class);
/**
* The minimal alpha value of a avatar that is needed to show the name tag above the avatar graphic.
*/
private static final int HIDE_NAME_ALPHA = 127;
/**
* The frame animation that handles the animation of this avatar.
*/
@Nullable
private final FrameAnimation animation;
/**
* In case the light shall be animated this value is set to true. In special
* cases its not good if the light is animated, such as the switch of levels
* and the sudden appearance of characters on the map. In such cases
*/
private boolean animateLight;
/**
* The render system for the clothes of this avatar.
*/
@Nonnull
private final transient AvatarClothRenderer clothRender;
/**
* The mark that is displayed in case the character is the target of a attack.
*/
@Nonnull
private final AvatarMarker attackMark;
@Nonnull
private final AvatarMarker attackAvailableMark;
/**
* The text tag is the small text box shown above the avatar that contains
* the name of the avatar.
*/
@Nonnull
private final AvatarTextTag avatarTextTag;
/**
* The character that created this avatar.
*/
@Nonnull
private final Char parentChar;
/**
* This variable changes to true in case the attack marker is supposed to be displayed.
*/
private boolean attackMarkerVisible;
private boolean showAttackAvailable;
/**
* Stores if the name shall be rendered or not. It is checked at every
* update if this flag is valid or not.
*/
private boolean renderName;
/**
* The target light of this avatar. In case the light is set to be animated
* the color this avatar is rendered with will approach this target light.
*/
@Nonnull
private Color targetLight;
private int showHighlight;
private Avatar(@Nonnull AvatarTemplate template, @Nonnull Char parentChar) {
super(template);
attackMark = new AvatarMarker(MiscImageFactory.ATTACK_MARKER, this);
attackAvailableMark = new AvatarMarker(MiscImageFactory.ATTACK_MARKER, this);
clothRender = new AvatarClothRenderer(template.getDirection(), template.getFrames());
clothRender.setLight(getLight());
clothRender.setFrame(0);
targetLight = DEFAULT_LIGHT;
animateLight = false;
avatarTextTag = new AvatarTextTag();
avatarTextTag.setAvatarHeight(template.getSprite().getHeight());
if (template.getFrames() > 1) {
animation = new FrameAnimation(this);
animation.setup(template.getFrames(), template.getStillFrame(), 150);
} else {
animation = null;
}
this.parentChar = parentChar;
if (parentChar.isHuman()) {
int interval = IllaClient.getCfg().getInteger("doubleClickInterval");
delayedWalkingHandler = new AbstractMultiActionHelper(interval, 2) {
@Override
public void executeAction(int count) {
if (count == 1) {
walkToCharacter(1);
}
}
};
} else {
delayedWalkingHandler = null;
}
}
/**
* Create a avatar from the avatar factory. This either creates a new instance of the avatar class or it takes a
* existing instance from the list of currently unused instances.
*
* @param avatarID the ID of the character that identifies the name and the sex and the direction of the avatar
* that is needed
* @return a instance of the needed avatar type
*/
@Nullable
public static Avatar create(int avatarID, @Nonnull Char parent) {
try {
AvatarTemplate template = CharacterFactory.getInstance().getTemplate(avatarID);
return new Avatar(template, parent);
} catch (@Nonnull Exception ex) {
LOGGER.error("Requesting new Avatar with ID {} for {} failed.", avatarID, parent);
}
return null;
}
@Override
public int getTargetAlpha() {
MapTile mapTileOfChar = World.getMap().getMapAt(parentChar.getVisibleLocation());
if (mapTileOfChar == null) {
return Color.MAX_INT_VALUE;
} else {
Tile tileOfChar = mapTileOfChar.getTile();
return (tileOfChar == null) ? Color.MAX_INT_VALUE : tileOfChar.getTargetAlpha();
}
}
public void changeAnimationDuration(int newDuration) {
if ((animation != null) && animation.isRunning()) {
animation.setDuration(newDuration);
}
}
/**
* Start a animation for this avatar.
*
* @param duration the duration of the animation in milliseconds
* @param loop true in case the animation shall never stop and rather run
* forever
*/
public void animate(int duration, boolean loop) {
animate(duration, loop, false, 1.f);
}
/**
* Start a animation for this avatar.
*
* @param duration the duration of the animation in milliseconds
* @param loop true in case the animation shall never stop and rather run
* forever
*/
public void animate(int duration, boolean loop, boolean expandStorybook, float length) {
if (isMarkedAsRemoved()) {
LOGGER.warn("Animating a removed avatar is illegal.");
return;
}
if (animation == null) {
return;
}
if (expandStorybook) {
animation.continueStoryboard(length);
} else {
animation.resetStoryboard();
}
animation.setDuration(duration);
if (loop) {
animation.updateMode(Mode.Looped);
} else {
animation.updateMode();
}
animation.restart();
}
/**
* Stop the execution of the current animation.
*/
public void stopAnimation() {
if (animation != null) {
animation.stop();
}
}
/**
* This function is triggered in case a animation that is not looped finished.
*
* @param finished set true in case the animation is really done
*/
@Override
public void animationFinished(boolean finished) {
// Do not reset the character if the parent character is not shown anymore.
// This may happen in case a currently animated character changes it's avatar.
if (isShown()) {
parentChar.resetAnimation(finished);
}
}
/**
* Change the color of one paperdolling object.
*
* @param slot the slot of the object that shall get a different color
* @param color the new color that shall be used to color the graphic itself
*/
public void changeClothColor(int slot, Color color) {
clothRender.changeBaseColor(slot, color);
}
@Nullable
private final AbstractMultiActionHelper delayedWalkingHandler;
@Override
public boolean isEventProcessed(@Nonnull GameContainer container, int delta, @Nonnull SceneEvent event) {
if (event instanceof ClickOnMapEvent) {
return isEventProcessed(container, delta, (ClickOnMapEvent) event);
}
if (parentChar.isNPC()) {
if (event instanceof CurrentMouseLocationEvent) {
CurrentMouseLocationEvent moveEvent = (CurrentMouseLocationEvent) event;
if (!isMouseInInteractionRect(moveEvent.getX(), moveEvent.getY())) {
return false;
}
showHighlight = 1;
InteractiveChar interactiveChar = parentChar.getInteractive();
if (interactiveChar.isInUseRange()) {
showHighlight = 2;
}
return true;
}
}
if (event instanceof DoubleClickOnMapEvent) {
if (delayedWalkingHandler != null) {
delayedWalkingHandler.reset();
}
return isEventProcessed(container, delta, (DoubleClickOnMapEvent) event);
}
return super.isEventProcessed(container, delta, event);
}
/**
* This function handles click events on the avatars.
*
* @param container the game container
* @param delta the time since the last update
* @param event the event that actually happened
* @return {@code true} in case the event was handled
*/
@SuppressWarnings("UnusedParameters")
private boolean isEventProcessed(
GameContainer container, int delta, @Nonnull ClickOnMapEvent event) {
if (World.getPlayer().isPlayer(parentChar.getCharId())) {
return false;
}
if (!isMouseInInteractiveOrOnTag(event)) {
return false;
}
if (event.getKey() == Button.Right) {
World.getPlayer().getCombatHandler().toggleAttackOnCharacter(parentChar);
return true;
}
if (!isMouseInInteractionRect(event.getX(), event.getY())) {
return false;
}
if (event.getKey() == Button.Left) {
if (delayedWalkingHandler != null) {
delayedWalkingHandler.pulse();
} else {
walkToCharacter(1);
}
return true;
}
return false;
}
private void walkToCharacter(int distance) {
log.debug("Walking to the character {}", parentChar);
TargetMovementHandler handler = World.getPlayer().getMovementHandler().getTargetMovementHandler();
handler.walkTo(parentChar.getLocation(), distance);
handler.assumeControl();
}
/**
* This function handles double click events on the avatars.
*
* @param container the game container
* @param delta the time since the last update
* @param event the event that actually happened
* @return {@code true} in case the event was handled
*/
@SuppressWarnings("UnusedParameters")
private boolean isEventProcessed(
GameContainer container, int delta, @Nonnull DoubleClickOnMapEvent event) {
if (event.getKey() != Button.Left) {
return false;
}
if (World.getPlayer().isPlayer(parentChar.getCharId())) {
return false;
}
if (!isMouseInInteractionRect(event.getX(), event.getY())) {
return false;
}
if (parentChar.isHuman()) {
Char charToName = parentChar;
World.getUpdateTaskManager().addTaskForLater((container1, delta1) -> World.getGameGui().getDialogInputGui().showNamingDialog(charToName));
} else {
InteractiveChar interactiveChar = parentChar.getInteractive();
if (interactiveChar.isInUseRange()) {
log.debug("Using the character {}", interactiveChar);
interactiveChar.use();
} else {
log.debug("Walking to and using the character {}", interactiveChar);
TargetMovementHandler handler = World.getPlayer().getMovementHandler().getTargetMovementHandler();
handler.walkTo(parentChar.getLocation(), 1);
handler.setTargetReachedAction(interactiveChar::use);
handler.assumeControl();
}
}
return true;
}
/**
* Check if a mouse event points at the interactive area of a avatar or on its tag.
*
* @param event the mouse event
* @return {@code true} in case the mouse is on the interactive area of the avatar or on its tag
*/
private boolean isMouseInInteractiveOrOnTag(@Nonnull AbstractMouseLocationEvent event) {
int mouseXonDisplay = event.getX() + Camera.getInstance().getViewportOffsetX();
int mouseYonDisplay = event.getY() + Camera.getInstance().getViewportOffsetY();
if (renderName && avatarTextTag.getDisplayRect().isInside(mouseXonDisplay, mouseYonDisplay)) {
return true;
}
return isMouseInInteractionRect(event.getX(), event.getY());
}
private static final Logger LOGGER = LoggerFactory.getLogger(Avatar.class);
@Override
public int getHighlight() {
return showHighlight;
}
/**
* Draw the avatar to the game screen. Calling this function causes the light value to approach the target light
* in case the light values are different. It also draws the name above the avatar in case it needs to be shown.
*/
@Override
public void render(@Nonnull Graphics g) {
if (performRendering()) {
if (attackMarkerVisible) {
attackMark.render(g);
} else if (showAttackAvailable) {
attackAvailableMark.render(g);
}
// draw the avatar, naked!! :O
super.render(g);
// draw the clothes
clothRender.render(g);
if (renderName) {
avatarTextTag.render(g);
}
showHighlight = 0;
}
}
/**
* Check if the light is currently animated. Means the light is currently
* changing towards a target light color.
*
* @return true in case the light is currently animated
*/
public boolean hasAnimatedLight() {
return animateLight;
}
@Override
public void hide() {
super.hide();
stopAnimation();
}
/**
* Remove a item from the list of items that are shown as clothes.
*
* @param group the group that shall be cleaned
*/
public void removeClothItem(int group) {
clothRender.setCloth(group, null);
}
/**
* Set a item as a clothing item to a specified body location. In case its defined the cloth renderer will try to
* show the cloth on the avatar.
*
* @param group the group of the item, so the location of the item, where it shall be displayed
* @param itemID the ID of the item that shall be displayed
*/
public void setClothItem(int group, int itemID) {
clothRender.setCloth(group, getTemplate().getClothes().getCloth(group, itemID, this));
}
/**
* Set the current frame of the avatar. This forwards the frame to the Entity super function but sends it also to
* the cloth render.
*
* @param frame the index of the frame that shall be rendered next
*/
@Override
public void setFrame(int frame) {
super.setFrame(frame);
clothRender.setFrame(frame);
log.debug("{}: Now showing animation frame {}", this, frame);
}
/**
* Set the light this avatar is colored with. Setting the light with this
* function will disable the smooth change of the light and sets the light
* color right away.
*
* @param light the light the avatar is enlighten with
*/
@Override
public void setLight(@Nonnull Color light) {
super.setLight(light);
clothRender.setLight(light);
attackMark.setLight(light);
attackAvailableMark.setLight(light);
attackAvailableMark.setBaseColor(Color.BLACK);
animateLight = false;
}
/**
* Set the light this avatar is colored with. Setting the light with this function will enable the smooth change
* of the light and so the light color of the avatar will slowly approach the color of the light set with
* this function.
*
* @param light the target light color for this avatar
*/
public void setLightTarget(@Nonnull Color light) {
targetLight = light;
clothRender.setLight(light);
attackMark.setLight(light);
attackAvailableMark.setLight(light);
attackAvailableMark.setBaseColor(Color.BLACK);
animateLight = true;
}
/**
* Set the name that is displayed in the tag above the avatar graphic.
*
* @param charName the name that is displayed above the character graphic
*/
public void setName(@Nonnull String charName) {
if (charName.isEmpty()) {
avatarTextTag.setCharacterName("unknown");
} else {
avatarTextTag.setCharacterName(charName);
}
avatarTextTag.setCharNameColor(Color.YELLOW);
}
/**
* Set the color of the text that is shown above the avatar that is shown.
*
* @param color the color that is used for the font of the the text that is
* shown above the character and shows the name of the character
*/
public void setNameColor(Color color) {
avatarTextTag.setCharNameColor(color);
}
private static final Color COLOR_UNHARMED = new ImmutableColor(0, 255, 0);
private static final Color COLOR_SLIGHTLY_HARMED = new ImmutableColor(127, 255, 0);
private static final Color COLOR_HARMED = new ImmutableColor(255, 255, 0);
private static final Color COLOR_BADLY_HARMED = new ImmutableColor(255, 127, 0);
private static final Color COLOR_NEAR_DEATH = new ImmutableColor(255, 0, 0);
private static final Color COLOR_DEAD = new ImmutableColor(173, 173, 173);
public void setHealthPoints(int value) {
//noinspection IfStatementWithTooManyBranches
if (value == 10000) {
avatarTextTag.setHealthState(Lang.getMsg("char.health.unharmed"));
avatarTextTag.setHealthStateColor(COLOR_UNHARMED);
} else if (value > 8000) {
avatarTextTag.setHealthState(Lang.getMsg("char.health.slightlyHarmed"));
avatarTextTag.setHealthStateColor(COLOR_SLIGHTLY_HARMED);
} else if (value > 5000) {
avatarTextTag.setHealthState(Lang.getMsg("char.health.harmed"));
avatarTextTag.setHealthStateColor(COLOR_HARMED);
} else if (value > 2000) {
avatarTextTag.setHealthState(Lang.getMsg("char.health.badlyHarmed"));
avatarTextTag.setHealthStateColor(COLOR_BADLY_HARMED);
} else if (value > 0) {
avatarTextTag.setHealthState(Lang.getMsg("char.health.nearDead"));
avatarTextTag.setHealthStateColor(COLOR_NEAR_DEATH);
} else {
avatarTextTag.setHealthState(Lang.getMsg("char.health.dead"));
avatarTextTag.setHealthStateColor(COLOR_DEAD);
}
}
@Override
public void setScale(float newScale) {
super.setScale(newScale);
clothRender.setScale(newScale);
}
@Override
public void setScreenPos(@Nonnull DisplayCoordinate coordinate) {
super.setScreenPos(coordinate);
clothRender.setScreenPos(coordinate);
attackMark.setScreenPos(coordinate);
attackAvailableMark.setScreenPos(coordinate);
}
@Override
public void update(@Nonnull GameContainer container, int delta) {
super.update(container, delta);
if (!isShown()) {
return;
}
int usedAlpha = getAlpha();
clothRender.setAlpha(usedAlpha);
clothRender.update(container, delta);
Color locLight = getLight();
if (animateLight && !AnimationUtility.approach(locLight, targetLight, delta)) {
targetLight = locLight;
animateLight = false;
}
locLight.setAlpha(usedAlpha);
Input input = container.getEngine().getInput();
if (World.getPlayer().isPlayer(parentChar.getCharId())) {
renderName = false;
} else if (getAlpha() > HIDE_NAME_ALPHA) {
renderName = World.getPeople().isAvatarTagShown(parentChar.getCharId()) || input.isKeyDown(Key.RightAlt) ||
isMouseInInteractionRect(input);
}
if (isMouseInInteractionRect(input) && World.getPlayer().getCombatHandler().canBeAttacked(parentChar)) {
showAttackAvailable = true;
attackAvailableMark.setAlpha(usedAlpha);
attackAvailableMark.update(container, delta);
} else {
showAttackAvailable = false;
}
if (renderName) {
avatarTextTag.setDisplayLocation(getDisplayCoordinate());
avatarTextTag.update(container, delta);
}
if (World.getPlayer().getCombatHandler().isAttacking(parentChar)) {
attackMarkerVisible = true;
attackMark.setAlpha(usedAlpha);
attackMark.update(container, delta);
} else if (World.getPlayer().getCombatHandler().isGoingToAttack(parentChar)) {
attackMarkerVisible = true;
attackMark.setAlpha(usedAlpha / 2);
attackMark.update(container, delta);
} else {
attackMarkerVisible = false;
}
}
@Override
@Nonnull
public String toString() {
return "Avatar of " + parentChar;
}
}