package com.asteria.game.character;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import com.asteria.game.Node;
import com.asteria.game.NodeType;
import com.asteria.game.World;
import com.asteria.game.character.combat.Combat;
import com.asteria.game.character.combat.CombatBuilder;
import com.asteria.game.character.combat.CombatStrategy;
import com.asteria.game.character.combat.CombatType;
import com.asteria.game.character.combat.effect.CombatEffectType;
import com.asteria.game.character.combat.magic.CombatSpell;
import com.asteria.game.character.combat.magic.CombatWeaken;
import com.asteria.game.character.npc.Npc;
import com.asteria.game.character.player.Player;
import com.asteria.game.location.Position;
import com.asteria.task.Task;
import com.asteria.utility.MutableNumber;
import com.asteria.utility.Stopwatch;
import com.google.common.base.Preconditions;
/**
* The {@link Node} implementation representing a node that is mobile. This
* includes {@link Player}s and {@link Npc}s.
*
* @author lare96 <http://github.com/lare96>
*/
public abstract class CharacterNode extends Node {
/**
* The combat builder that will handle all combat operations for this
* character.
*/
private final CombatBuilder combatBuilder = new CombatBuilder(this);
/**
* The movement queue that will handle all movement processing for this
* character.
*/
private final MovementQueue movementQueue = new MovementQueue(this);
/**
* The movement queue listener that will allow for actions to be appended to
* the end of the movement queue.
*/
private final MovementQueueListener movementListener = new MovementQueueListener(this);
/**
* The update flags used for signifying that this character needs something
* updated.
*/
private final UpdateFlags flags = new UpdateFlags();
/**
* The collection of stopwatches used for various timing operations.
*/
private final Stopwatch lastCombat = new Stopwatch(), freezeTimer = new Stopwatch();
/**
* The slot this character has been assigned to.
*/
private int slot = -1;
/**
* The flag that determines if this character is visible or not.
*/
private boolean visible = true;
/**
* The amount of poison damage this character has.
*/
private final MutableNumber poisonDamage = new MutableNumber();
/**
* The type of poison that was previously applied.
*/
private PoisonType poisonType;
/**
* The primary movement direction of this character.
*/
private int primaryDirection = -1;
/**
* The secondary movement direction of this character.
*/
private int secondaryDirection = -1;
/**
* The last movement direction made by this character.
*/
private int lastDirection = 0;
/**
* The flag that determines if this character needs placement.
*/
private boolean needsPlacement;
/**
* The flag that determines if this character's movement queue needs to be
* reset.
*/
private boolean resetMovementQueue;
/**
* The combat spell currently being casted by this character.
*/
private CombatSpell currentlyCasting;
/**
* The current animation being performed by this character.
*/
private Animation animation;
/**
* The current graphic being performed by this character.
*/
private Graphic graphic;
/**
* The current text being forced by this character.
*/
private String forcedText;
/**
* The current index being faced by this character.
*/
private int faceIndex;
/**
* The current coordinates being face by this character.
*/
private Position facePosition;
/**
* The current primary hit being dealt on this character.
*/
private Hit primaryHit;
/**
* The current secondary hit being dealt on this character.
*/
private Hit secondaryHit;
/**
* The current region this character is in.
*/
private Position currentRegion;
/**
* The delay representing how long this character is frozen for.
*/
private long freezeDelay;
/**
* The flag determining if this character should fight back when attacked.
*/
private boolean autoRetaliate;
/**
* The flag determining if this character is following someone.
*/
private boolean following;
/**
* The character this character is currently following.
*/
private CharacterNode followCharacter;
/**
* The flag determining if this character is dead.
*/
private boolean dead;
/**
* The last position this character was in.
*/
private Position lastPosition;
/**
* Creates a new {@link CharacterNode}.
*
* @param position
* the position of this character in the world.
* @param type
* the type of node that this character is.
*/
public CharacterNode(Position position, NodeType type) {
super(position, type);
this.autoRetaliate = (type == NodeType.NPC);
Preconditions.checkArgument(type == NodeType.PLAYER || type == NodeType.NPC);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + super.getType().hashCode();
result = prime * result + slot;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (!(obj instanceof CharacterNode))
return false;
CharacterNode other = (CharacterNode) obj;
if (super.getType() != other.getType())
return false;
if (slot != other.slot)
return false;
return true;
}
@Override
public String toString() {
if (super.getType() == NodeType.PLAYER)
return World.getPlayers().get(slot).toString();
else if (super.getType() == NodeType.NPC)
return World.getNpcs().get(slot).toString();
throw new IllegalStateException("Invalid character node type!");
}
/**
* The code executed by the {@link CharacterLogicSequencer} during game
* logic processing.
*
* @throws Exception
* if any errors occur while executing code.
*/
public abstract void sequence() throws Exception;
/**
* Weakens this character using {@code effect}.
*
* @param effect
* the effect to use to weaken this character.
* @return {@code true} if the character was weakened, {@code false}
* otherwise.
*/
public abstract boolean weaken(CombatWeaken effect);
/**
* Gets the attack speed for this character.
*
* @return the attack speed.
*/
public abstract int getAttackSpeed();
/**
* Gets this character's current health.
*
* @return this charater's health.
*/
public abstract int getCurrentHealth();
/**
* Decrements this character's health based on {@code hit}.
*
* @param hit
* the hit to decrement this character's health by.
* @return the modified hit after the health was decremented.
*/
public abstract Hit decrementHealth(Hit hit);
/**
* Calculates and retrieves the combat strategy for this character.
*
* @return the combat strategy.
*/
public abstract CombatStrategy determineStrategy();
/**
* Gets the base attack level for this character based on {@code type}, used
* for combat calculations.
*
* @param type
* the combat type.
* @return the base attack level.
*/
public abstract int getBaseAttack(CombatType type);
/**
* Gets the base defence level for this character based on {@code type},
* used for combat calculations.
*
* @param type
* the combat type.
* @return the base defence level.
*/
public abstract int getBaseDefence(CombatType type);
/**
* Executed on a successful hit, used primarily for poison effects.
*
* @param character
* the victim in this successful hit.
* @param type
* the combat type currently being used.
*/
public abstract void onSuccessfulHit(CharacterNode character, CombatType type);
/**
* Restores this character's health level by {@code amount}.
*
* @param amount
* the amount to restore this health level by.
*/
public abstract void healCharacter(int amount);
/**
* Applies poison with an intensity of {@code type} to the character.
*
* @param type
* the poison type to apply.
*/
public void poison(PoisonType type) {
poisonType = type;
Combat.effect(this, CombatEffectType.POISON);
}
/**
* Calculates and returns the size of this character.
*
* @return the size of this character.
*/
public final int size() {
if (super.getType() == NodeType.PLAYER)
return 1;
return ((Npc) this).getDefinition().getSize();
}
/**
* Resets the prepares this character for the next update sequence.
*/
public final void reset() {
primaryDirection = -1;
secondaryDirection = -1;
flags.reset();
resetMovementQueue = false;
needsPlacement = false;
animation = null;
}
/**
* Executes {@code animation} for this character.
*
* @param animation
* the animation to execute, or {@code null} to reset the current
* animation.
*/
public final void animation(Animation animation) {
if (animation == null)
animation = new Animation(65535, AnimationPriority.HIGH);
if (this.animation == null || this.animation.getPriority().getValue() <= animation.getPriority().getValue()) {
this.animation = animation.copy();
flags.set(Flag.ANIMATION);
}
}
/**
* Executes {@code graphic} for this character.
*
* @param graphic
* the graphic to execute.
*/
public final void graphic(Graphic graphic) {
this.graphic = graphic.copy();
flags.set(Flag.GRAPHICS);
}
/**
* Executes {@code graphic} for this character at a higher height level.
*
* @param graphic
* the graphic to execute.
*/
public final void highGraphic(Graphic graphic) {
this.graphic = new Graphic(graphic.getId(), 6553600);
flags.set(Flag.GRAPHICS);
}
/**
* Executes {@code forcedText} for this character.
*
* @param forcedText
* the forced text to execute.
*/
public final void forceChat(String forcedText) {
this.forcedText = forcedText;
flags.set(Flag.FORCED_CHAT);
}
/**
* Prompts this character to face {@code character}.
*
* @param character
* the character to face, or {@code null} to reset the face.
*/
public final void faceCharacter(CharacterNode character) {
this.faceIndex = character == null ? 65535 : character.getType() == NodeType.PLAYER ? character.slot + 32768 : character.slot;
flags.set(Flag.FACE_CHARACTER);
}
/**
* Prompts this character to face {@code position}.
*
* @param position
* the position to face.
*/
public final void facePosition(Position position) {
facePosition = new Position(2 * position.getX() + 1, 2 * position.getY() + 1);
flags.set(Flag.FACE_COORDINATE);
}
/**
* Deals {@code hit} on this character as a primary hitmark.
*
* @param hit
* the hit to deal on this character.
*/
private final void primaryDamage(Hit hit) {
primaryHit = decrementHealth(Objects.requireNonNull(hit));
flags.set(Flag.HIT);
}
/**
* Deals {@code hit} on this character as a secondary hitmark.
*
* @param hit
* the hit to deal on this character.
*/
private final void secondaryDamage(Hit hit) {
secondaryHit = decrementHealth(Objects.requireNonNull(hit));
flags.set(Flag.HIT_2);
}
/**
* Deals a series of hits to this character.
*
* @param hits
* the hits to deal to this character.
*/
public final void damage(Hit... hits) {
Preconditions.checkArgument(hits.length >= 1 && hits.length <= 4);
switch (hits.length) {
case 1:
sendDamage(hits[0]);
break;
case 2:
sendDamage(hits[0], hits[1]);
break;
case 3:
sendDamage(hits[0], hits[1], hits[2]);
break;
case 4:
sendDamage(hits[0], hits[1], hits[2], hits[3]);
break;
}
}
/**
* Deals {@code hit} to this character.
*
* @param hit
* the hit to deal to this character.
*/
private final void sendDamage(Hit hit) {
if (flags.get(Flag.HIT)) {
secondaryDamage(hit);
return;
}
primaryDamage(hit);
}
/**
* Deals {@code hit} and {@code hit2} to this character.
*
* @param hit
* the first hit to deal to this character.
* @param hit2
* the second hit to deal to this character.
*/
private final void sendDamage(Hit hit, Hit hit2) {
sendDamage(hit);
secondaryDamage(hit2);
}
/**
* Deals {@code hit}, {@code hit2}, and {@code hit3} to this character.
*
* @param hit
* the first hit to deal to this character.
* @param hit2
* the second hit to deal to this character.
* @param hit3
* the third hit to deal to this character.
*/
private final void sendDamage(Hit hit, Hit hit2, Hit hit3) {
sendDamage(hit, hit2);
World.submit(new Task(1, false) {
@Override
public void execute() {
this.cancel();
if (!CharacterNode.super.isRegistered()) {
return;
}
sendDamage(hit3);
}
});
}
/**
* Deals {@code hit}, {@code hit2}, {@code hit3}, and {@code hit4} to this
* character.
*
* @param hit
* the first hit to deal to this character.
* @param hit2
* the second hit to deal to this character.
* @param hit3
* the third hit to deal to this character.
* @param hit4
* the fourth hit to deal to this character.
*/
private final void sendDamage(Hit hit, Hit hit2, Hit hit3, Hit hit4) {
sendDamage(hit, hit2);
World.submit(new Task(1, false) {
@Override
public void execute() {
this.cancel();
if (!CharacterNode.super.isRegistered()) {
return;
}
sendDamage(hit3, hit4);
}
});
}
/**
* Prepares to cast the {@code spell} on {@code victim}.
*
* @param spell
* the spell to cast on the victim.
* @param victim
* the victim that the spell will be cast on.
*/
public final void prepareSpell(CombatSpell spell, CharacterNode victim) {
currentlyCasting = spell;
currentlyCasting.startCast(this, victim);
}
/**
* Freezes this character for the desired time in {@code SECONDS}.
*
* @param time
* the time to freeze this character for.
*/
public final void freeze(long time) {
if (isFrozen())
return;
freezeDelay = time;
freezeTimer.reset();
movementQueue.reset();
}
/**
* Unfreezes this character completely allowing them to reestablish
* movement.
*/
public final void unfreeze() {
freezeDelay = 0;
freezeTimer.stop();
}
/**
* Determines if this character is poisoned.
*
* @return {@code true} if this character is poisoned, {@code false}
* otherwise.
*/
public final boolean isPoisoned() {
return poisonDamage.get() > 0;
}
/**
* Determines if this character is frozen.
*
* @return {@code true} if this character is frozen, {@code false}
* otherwise.
*/
public final boolean isFrozen() {
return !freezeTimer.elapsed(freezeDelay, TimeUnit.SECONDS);
}
/**
* Gets the slot this character has been assigned to.
*
* @return the slot of this character.
*/
public final int getSlot() {
return slot;
}
/**
* Sets the value for {@link CharacterNode#slot}.
*
* @param slot
* the new value to set.
*/
public final void setSlot(int slot) {
this.slot = slot;
}
/**
* Gets the amount of poison damage this character has.
*
* @return the amount of poison damage.
*/
public final MutableNumber getPoisonDamage() {
return poisonDamage;
}
/**
* Gets the primary direction this character is facing.
*
* @return the primary direction.
*/
public final int getPrimaryDirection() {
return primaryDirection;
}
/**
* Sets the value for {@link CharacterNode#primaryDirection}.
*
* @param primaryDirection
* the new value to set.
*/
public final void setPrimaryDirection(int primaryDirection) {
this.primaryDirection = primaryDirection;
}
/**
* Gets the secondary direction this character is facing.
*
* @return the secondary direction.
*/
public final int getSecondaryDirection() {
return secondaryDirection;
}
/**
* Sets the value for {@link CharacterNode#secondaryDirection}.
*
* @param secondaryDirection
* the new value to set.
*/
public final void setSecondaryDirection(int secondaryDirection) {
this.secondaryDirection = secondaryDirection;
}
/**
* Gets the last direction this character was facing.
*
* @return the last direction.
*/
public final int getLastDirection() {
return lastDirection;
}
/**
* Sets the value for {@link CharacterNode#lastDirection}.
*
* @param lastDirection
* the new value to set.
*/
public final void setLastDirection(int lastDirection) {
this.lastDirection = lastDirection;
}
/**
* Determines if this character needs placement.
*
* @return {@code true} if this character needs placement, {@code false}
* otherwise.
*/
public final boolean isNeedsPlacement() {
return needsPlacement;
}
/**
* Sets the value for {@link CharacterNode#needsPlacement}.
*
* @param needsPlacement
* the new value to set.
*/
public final void setNeedsPlacement(boolean needsPlacement) {
this.needsPlacement = needsPlacement;
}
/**
* Determines if this character needs to reset their movement queue.
*
* @return {@code true} if this character needs to reset their movement
* queue, {@code false} otherwise.
*/
public final boolean isResetMovementQueue() {
return resetMovementQueue;
}
/**
* Sets the value for {@link CharacterNode#resetMovementQueue}.
*
* @param resetMovementQueue
* the new value to set.
*/
public final void setResetMovementQueue(boolean resetMovementQueue) {
this.resetMovementQueue = resetMovementQueue;
}
/**
* Gets the spell that this character is currently casting.
*
* @return the spell currently being casted.
*/
public final CombatSpell getCurrentlyCasting() {
return currentlyCasting;
}
/**
* Sets the value for {@link CharacterNode#currentlyCasting}.
*
* @param currentlyCasting
* the new value to set.
*/
public final void setCurrentlyCasting(CombatSpell currentlyCasting) {
this.currentlyCasting = currentlyCasting;
}
/**
* Gets the current region this character is in.
*
* @return the current region.
*/
public final Position getCurrentRegion() {
return currentRegion;
}
/**
* Sets the value for {@link CharacterNode#currentRegion}.
*
* @param currentRegion
* the new value to set.
*/
public final void setCurrentRegion(Position currentRegion) {
this.currentRegion = currentRegion;
}
/**
* Determines if this character is auto-retaliating.
*
* @return {@code true} if this character is auto-retaliating, {@code false}
* otherwise.
*/
public final boolean isAutoRetaliate() {
return autoRetaliate;
}
/**
* Sets the value for {@link CharacterNode#autoRetaliate}.
*
* @param autoRetaliate
* the new value to set.
*/
public final void setAutoRetaliate(boolean autoRetaliate) {
this.autoRetaliate = autoRetaliate;
}
/**
* Determines if this character is following someone.
*
* @return {@code true} if this character is following someone,
* {@code false} otherwise.
*/
public final boolean isFollowing() {
return following;
}
/**
* Sets the value for {@link CharacterNode#following}.
*
* @param following
* the new value to set.
*/
public final void setFollowing(boolean following) {
this.following = following;
}
/**
* Gets the character that is currently being followed.
*
* @return the character being followed.
*/
public final CharacterNode getFollowCharacter() {
return followCharacter;
}
/**
* Sets the value for {@link CharacterNode#followCharacter}.
*
* @param followCharacter
* the new value to set.
*/
public final void setFollowCharacter(CharacterNode followCharacter) {
this.followCharacter = followCharacter;
}
/**
* Determines if this character is dead or not.
*
* @return {@code true} if this character is dead, {@code false} otherwise.
*/
public final boolean isDead() {
return dead;
}
/**
* Sets the value for {@link CharacterNode#dead}.
*
* @param dead
* the new value to set.
*/
public final void setDead(boolean dead) {
this.dead = dead;
}
/**
* Gets the last position this character was on.
*
* @return the last position.
*/
public final Position getLastPosition() {
return lastPosition;
}
/**
* Sets the value for {@link CharacterNode#lastPosition}.
*
* @param lastPosition
* the new value to set.
*/
public final void setLastPosition(Position lastPosition) {
this.lastPosition = lastPosition;
}
/**
* Determines if this character is visible or not.
*
* @return {@code true} if this character is visible, {@code false}
* otherwise.
*/
public boolean isVisible() {
return visible;
}
/**
* Sets the value for {@link CharacterNode#visible}.
*
* @param visible
* the new value to set.
*/
public void setVisible(boolean visible) {
this.visible = visible;
}
/**
* Gets the combat builder that will handle all combat operations for this
* character.
*
* @return the combat builder.
*/
public final CombatBuilder getCombatBuilder() {
return combatBuilder;
}
/**
* Gets the movement queue that will handle all movement processing for this
* character.
*
* @return the movement queue.
*/
public final MovementQueue getMovementQueue() {
return movementQueue;
}
/**
* Gets the movement queue listener that will allow for actions to be
* appended to the end of the movement queue.
*
* @return the movement queue listener.
*/
public final MovementQueueListener getMovementListener() {
return movementListener;
}
/**
* Gets the update flags used for signifying that this character needs
* something updated.
*
* @return the update flags.
*/
public final UpdateFlags getFlags() {
return flags;
}
/**
* Gets the timer that records the difference in time between now and the
* last time the player was in combat.
*
* @return the timer that determines when the player was last in combat.
*/
public final Stopwatch getLastCombat() {
return lastCombat;
}
/**
* Gets the animation update block value.
*
* @return the animation update block value.
*/
public final Animation getAnimation() {
return animation;
}
/**
* Gets the graphic update block value.
*
* @return the graphic update block value.
*/
public final Graphic getGraphic() {
return graphic;
}
/**
* Gets the forced text update block value.
*
* @return the forced text update block value.
*/
public final String getForcedText() {
return forcedText;
}
/**
* Gets the face index update block value.
*
* @return the face index update block value.
*/
public final int getFaceIndex() {
return faceIndex;
}
/**
* Gets the face position update block value.
*
* @return the face position update block value.
*/
public final Position getFacePosition() {
return facePosition;
}
/**
* Gets the primary hit update block value.
*
* @return the primary hit update block value.
*/
public final Hit getPrimaryHit() {
return primaryHit;
}
/**
* Gets the secondary hit update block value.
*
* @return the secondary hit update block value.
*/
public final Hit getSecondaryHit() {
return secondaryHit;
}
/**
* Gets the type of poison that was previously applied.
*
* @return the type of poison.
*/
public PoisonType getPoisonType() {
return poisonType;
}
/**
* Sets the value for {@link CharacterNode#poisonType}.
*
* @param poisonType
* the new value to set.
*/
public void setPoisonType(PoisonType poisonType) {
this.poisonType = poisonType;
}
}