/*
* 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.world;
import illarion.client.net.client.AttackCmd;
import illarion.client.net.client.StandDownCmd;
import illarion.common.types.CharacterId;
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.ThreadSafe;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class is used to store and set the current combat mode. It will forward all changes to the combat mode to the
* other parts of the client, that need to be notified about it.
*
* @author Martin Karing <nitram@illarion.org>
*/
@ThreadSafe
public final class CombatHandler {
/**
* The logging instance of this class.
*/
@Nonnull
private static final Logger log = LoggerFactory.getLogger(CombatHandler.class);
/**
* The character that is currently under attack.
*/
@Nullable
private Char attackedChar;
/**
* The characters that were not yet confirmed by the server for the attack.
*/
@Nonnull
private final Queue<Char> unconfirmedChars = new LinkedList<>();
/**
* This flag is used to track if the next target lost command from the server is supposed to be ignored.
*/
@Nonnull
private final AtomicBoolean ignoreNextTargetLost = new AtomicBoolean(false);
/**
* Test if the player is currently attacking anyone.
*
* @return {@code true} in case anyone is attacked
*/
@Contract(pure = true)
public boolean isAttacking() {
return (attackedChar != null) || !unconfirmedChars.isEmpty();
}
/**
* In case this character is attacked, stand down. In case its not attacked, attack it.
*
* @param character the character to start or stop attacking
*/
public void toggleAttackOnCharacter(@Nonnull Char character) {
if (isAttacking(character)) {
standDown();
} else {
setAttackTarget(character);
}
}
/**
* Test if a character is currently attacked.
*
* @param testChar the char to check if he is the current target
* @return {@code true} in case the character is the current target
*/
@Contract(pure = true)
public boolean isAttacking(@Nonnull Char testChar) {
return testChar.equals(attackedChar);
}
/**
* Test if a character is about to be attacked, but the attack is not yet confirmed by the server.
*
* @param testChar the character to check
* @return {@code true} if that character is scheduled to be attacked.
*/
@Contract(pure = true)
public boolean isGoingToAttack(@Nonnull Char testChar) {
return unconfirmedChars.contains(testChar);
}
/**
* Stop the current attack any report this to the server in case its needed. This does not directly stop the
* attack. It just requests to stop the attack from the server.
*/
public void standDown() {
Char localAttackedChat = attackedChar;
attackedChar = null;
if (localAttackedChat != null) {
ignoreNextTargetLost.set(true);
World.getNet().sendCommand(new StandDownCmd());
}
}
/**
* This function does nearly the same as {@link #standDown()}. How ever it does not result in sending a stand down
* command to the server.
*/
public void targetLost() {
if (ignoreNextTargetLost.getAndSet(false)) {
log.debug("Expected target lost received from server. No action taken.");
} else {
log.debug("Target lost received from server. Stopping attack.");
attackedChar = null;
unconfirmedChars.poll();
}
}
/**
* This function is called once the server is confirming that the attack starts.
*/
public void confirmAttack() {
attackedChar = unconfirmedChars.poll();
log.debug("Attack confirmed received from server. Now attacking: {}", attackedChar);
}
/**
* Set the character that is attacked from now in.
*
* @param character the character that is now attacked
*/
public void setAttackTarget(@Nonnull Char character) {
// Disable chat box to allow proper movement. Does not clear input.
World.getGameGui().getChatGui().deactivateChatBox(false);
if (isAttacking(character) || isGoingToAttack(character)) {
return;
}
CharacterId characterId = character.getCharId();
if (characterId == null) {
log.error("Trying to attack a character without character ID.");
standDown();
return;
}
if (canBeAttacked(character)) {
unconfirmedChars.offer(character);
sendAttackToServer(characterId);
} else {
standDown();
}
}
/**
* Check if the character can be attacked.
*
* @param character the character to check
* @return {@code true} in case the character is not the player and not a NPC.
*/
@Contract(pure = true)
public boolean canBeAttacked(@Nonnull Char character) {
return !World.getPlayer().isPlayer(character.getCharId()) && !character.isNPC();
}
/**
* Send a attack command to the server that initiates a fight with the character that ID is set in the parameter.
*
* @param id the ID of the character to fight
*/
private static void sendAttackToServer(@Nonnull CharacterId id) {
World.getNet().sendCommand(new AttackCmd(id));
}
}