// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      Magicbane Emulator Project © 2013 - 2022
//                www.magicbane.com

package engine.gameManager;

import engine.Enum.*;
import engine.exception.MsgSendException;
import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.AttackJob;
import engine.jobs.DeferredPowerJob;
import engine.math.Vector3fImmutable;
import engine.net.DispatchMessage;
import engine.net.client.ClientConnection;
import engine.net.client.msg.*;
import engine.objects.*;
import engine.powers.DamageShield;
import engine.powers.PowersBase;
import engine.powers.effectmodifiers.AbstractEffectModifier;
import engine.powers.effectmodifiers.WeaponProcEffectModifier;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

import static engine.math.FastMath.sqr;

public enum CombatManager {

	COMBATMANAGER;

	public static int animation = 0;

	/**
	 * Message sent by player to attack something.
	 */
	public static void setAttackTarget(AttackCmdMsg msg, ClientConnection origin) throws MsgSendException {

		PlayerCharacter player;
		int targetType;
		AbstractWorldObject target;

		if (TargetedActionMsg.un2cnt == 60 || TargetedActionMsg.un2cnt == 70) {
			return;
		}

		player = SessionManager.getPlayerCharacter(origin);

		if (player == null) {

			return;
		}

		//source must match player this account belongs to
		if (player.getObjectUUID() != msg.getSourceID() || player.getObjectType().ordinal() != msg.getSourceType()) {
			Logger.error("Msg Source ID " + msg.getSourceID() + " Does not Match Player ID " + player.getObjectUUID());

			return;
		}

		targetType = msg.getTargetType();

		if (targetType == GameObjectType.PlayerCharacter.ordinal()) {
			target = PlayerCharacter.getFromCache(msg.getTargetID());
		} else if (targetType == GameObjectType.Building.ordinal()) {
			target = BuildingManager.getBuildingFromCache(msg.getTargetID());
		} else if (targetType == GameObjectType.Mob.ordinal()) {
			target = Mob.getFromCache(msg.getTargetID());
		} else {
			player.setCombatTarget(null);
			return; //not valid type to attack
		}
		// quit of the combat target is already the current combat target
		// or there is no combat target
		if (target == null) {
			return;
		}

		//set sources target
		player.setCombatTarget(target);

		//put in combat if not already
		if (!player.isCombat()) {
			toggleCombat(true, origin);
		}

		//make character stand if sitting
		if (player.isSit()) {
			toggleSit(false, origin);
		}

		AttackTarget(player, target);

	}

	public static void AttackTarget(PlayerCharacter playerCharacter, AbstractWorldObject target) {

		boolean swingOffhand = false;

		//check my weapon can I do an offhand attack
		Item weaponOff = playerCharacter.getCharItemManager().getEquipped().get(MBServerStatics.SLOT_OFFHAND);
		Item weaponMain = playerCharacter.getCharItemManager().getEquipped().get(MBServerStatics.SLOT_MAINHAND);

		// if you carry something in the offhand thats a weapon you get to swing it
		if (weaponOff != null) {
			if (weaponOff.getItemBase().getType().equals(ItemType.WEAPON)) {
				swingOffhand = true;
			}
		}
		// if you carry  nothing in either hand you get to swing your offhand
		if (weaponOff == null && weaponMain == null) {
			swingOffhand = true;
		}

		//we always swing our mainhand if we are not on timer
		JobContainer main = playerCharacter.getTimers().get("Attack" + MBServerStatics.SLOT_MAINHAND);
		if (main == null) {
			// no timers on the mainhand, lets submit a job to swing
			CombatManager.createTimer(playerCharacter, MBServerStatics.SLOT_MAINHAND, 1, true); // attack in 0.1 of a second
		}

		if (swingOffhand) {
			/*
            only swing offhand if we have a weapon in it or are unarmed in both hands
            and no timers running
			 */
			JobContainer off = playerCharacter.getTimers().get("Attack" + MBServerStatics.SLOT_OFFHAND);
			if (off == null) {
				CombatManager.createTimer(playerCharacter, MBServerStatics.SLOT_OFFHAND, 1, true); // attack in 0.1 of a second
			}
		}
		City playerCity = ZoneManager.getCityAtLocation(playerCharacter.getLoc());
		if (playerCity != null && playerCity.getGuild().getNation().equals(playerCharacter.getGuild().getNation()) == false && playerCity.cityOutlaws.contains(playerCharacter.getObjectUUID()) == false)
			playerCity.cityOutlaws.add(playerCharacter.getObjectUUID());
	}

	public static void setAttackTarget(PetAttackMsg msg, ClientConnection origin) throws MsgSendException {

		PlayerCharacter player;
		Mob pet;
		int targetType;
		AbstractWorldObject target;

		if (TargetedActionMsg.un2cnt == 60 || TargetedActionMsg.un2cnt == 70)
			return;

		player = SessionManager.getPlayerCharacter(origin);

		if (player == null)
			return;

		pet = player.getPet();

		if (pet == null)
			return;

		targetType = msg.getTargetType();

		if (targetType == GameObjectType.PlayerCharacter.ordinal())
			target = PlayerCharacter.getFromCache(msg.getTargetID());
		else if (targetType == GameObjectType.Building.ordinal())
			target = BuildingManager.getBuildingFromCache(msg.getTargetID());
		else if (targetType == GameObjectType.Mob.ordinal())
			target = Mob.getFromCache(msg.getTargetID());
		else {
			pet.setCombatTarget(null);
			return; //not valid type to attack
		}

		if (pet.equals(target))
			return;

		// quit of the combat target is already the current combat target
		// or there is no combat target

		if (target == null || target == pet.getCombatTarget())
			return;


		//set sources target
		pet.setCombatTarget(target);
		//		setFirstHitCombatTarget(player,target);

		//put in combat if not already
		if (!pet.isCombat())
			pet.setCombat(true);

		//make character stand if sitting
		if (pet.isSit())
			toggleSit(false, origin);

	}

	private static void removeAttackTimers(AbstractCharacter ac) {

		JobContainer main;
		JobContainer off;

		if (ac == null)
			return;

		main = ac.getTimers().get("Attack" + MBServerStatics.SLOT_MAINHAND);
		off = ac.getTimers().get("Attack" + MBServerStatics.SLOT_OFFHAND);

		if (main != null)
			JobScheduler.getInstance().cancelScheduledJob(main);

		ac.getTimers().remove("Attack" + MBServerStatics.SLOT_MAINHAND);

		if (off != null)
			JobScheduler.getInstance().cancelScheduledJob(off);

		ac.getTimers().remove("Attack" + MBServerStatics.SLOT_OFFHAND);

		ac.setCombatTarget(null);

	}

	/**
	 * Begin Attacking
	 */
	public static void doCombat(AbstractCharacter ac, int slot) {

		int ret = 0;

		if (ac == null)
			return;

		// Attempt to eat null targets until we can clean
		// up this unholy mess and refactor it into a thread.


		ret = attemptCombat(ac, slot);

		//handle pets
		if (ret < 2 && ac.getObjectType().equals(GameObjectType.Mob)) {
			Mob mob = (Mob) ac;
			if (mob.isPet()) {
				return;
			}
		}

		//ret values
		//0: not valid attack, fail attack
		//1: cannot attack, wrong hand
		//2: valid attack
		//3: cannot attack currently, continue checking

		if (ret == 0 || ret == 1) {

			//Could not attack, clear timer

			ConcurrentHashMap<String, JobContainer> timers = ac.getTimers();

			if (timers != null && timers.containsKey("Attack" + slot))
				timers.remove("Attack" + slot);

			//clear combat target if not valid attack
			if (ret == 0)
				ac.setCombatTarget(null);

		} else if (ret == 3) {
			//Failed but continue checking. reset timer
			createTimer(ac, slot, 5, false);
		}
	}

	/**
	 * Verify can attack target
	 */
	private static int attemptCombat(AbstractCharacter abstractCharacter, int slot) {

		if (abstractCharacter == null) {
			// debugCombat(ac, "Source is null");
			return 0;
		}

		try {
			//Make sure player can attack
			PlayerBonuses bonus = abstractCharacter.getBonuses();

			if (bonus != null && bonus.getBool(ModType.ImmuneToAttack, SourceType.None))
				return 0;

			AbstractWorldObject target = abstractCharacter.getCombatTarget();

			if (target == null) {
				return 0;
			}


			//target must be valid type
			if (AbstractWorldObject.IsAbstractCharacter(target)) {
				AbstractCharacter tar = (AbstractCharacter) target;
				//must be alive, attackable and in World
				if (!tar.isAlive()) {
					return 0;
				} else if (tar.isSafeMode()) {
					return 0;
				} else if (!tar.isActive()) {
					return 0;
				}

				if (target.getObjectType().equals(GameObjectType.PlayerCharacter) && abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter) && abstractCharacter.getTimers().get("Attack" + slot) == null) {
					if (!((PlayerCharacter) abstractCharacter).canSee((PlayerCharacter) target)) {
						return 0;
					}
				}

				//must not be immune to all or immune to attack
				Resists res = tar.getResists();
				bonus = tar.getBonuses();
				if (bonus != null && !bonus.getBool(ModType.NoMod, SourceType.ImmuneToAttack)) {
					if (res != null) {
						if (res.immuneToAll() || res.immuneToAttacks()) {
							return 0;
						}
					}
				}
			} else if (target.getObjectType().equals(GameObjectType.Building)) {
				Building tar = (Building) target;

				// Cannot attack an invuln building

				if (tar.isVulnerable() == false) {
					return 0;
				}

			} else {
				return 0; //only characters and buildings may be attacked
			}

			//source must be in world and alive
			if (!abstractCharacter.isActive()) {
				return 0;
			} else if (!abstractCharacter.isAlive()) {
				return 0;
			}

			//make sure source is in combat mode
			if (!abstractCharacter.isCombat()) {
				return 0;
			}

			//See if either target is in safe zone
			if (abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter) && target.getObjectType().equals(GameObjectType.PlayerCharacter)) {
				if (((PlayerCharacter) abstractCharacter).inSafeZone() || ((PlayerCharacter) target).inSafeZone()) {
					return 0;
				}
			}

			if (!(slot == MBServerStatics.SLOT_MAINHAND || slot == MBServerStatics.SLOT_OFFHAND)) {
				return 0;
			}

			if (abstractCharacter.getCharItemManager() == null) {
				return 0;
			}

			//get equippment
			ConcurrentHashMap<Integer, Item> equipped = abstractCharacter.getCharItemManager().getEquipped();
			boolean hasNoWeapon = false;

			if (equipped == null) {
				return 0;
			}

			//get Weapon
			boolean isWeapon = true;
			Item weapon = equipped.get(slot);
			ItemBase wb = null;
			if (weapon == null) {
				isWeapon = false;
			} else {
				ItemBase ib = weapon.getItemBase();
				if (ib == null || !ib.getType().equals(ItemType.WEAPON)) {
					isWeapon = false;
				} else {
					wb = ib;
				}
			}

			//if no weapon, see if other hand has a weapon
			if (!isWeapon) {
				//no weapon, see if other hand has a weapon
				if (slot == MBServerStatics.SLOT_MAINHAND) {
					//make sure offhand has weapon, not shield
					Item weaponOff = equipped.get(MBServerStatics.SLOT_OFFHAND);
					if (weaponOff != null) {
						ItemBase ib = weaponOff.getItemBase();
						if (ib == null || !ib.getType().equals(ItemType.WEAPON)) {
							hasNoWeapon = true;
						} else {
							// debugCombat(ac, "mainhand, weapon in other hand");
							return 1; //no need to attack with this hand
						}
					} else {
						hasNoWeapon = true;
					}
				} else {
					if (equipped.get(MBServerStatics.SLOT_MAINHAND) == null) {
						// debgCombat(ac, "offhand, weapon in other hand");
						return 1; //no need to attack with this hand
					}
				}
			}

			//Source can attack.
			//NOTE Don't 'return;' beyond this point until timer created
			boolean attackFailure = false;

			//Target can't attack on move with ranged weapons.
			if ((wb != null) && (wb.getRange() > 35f) && abstractCharacter.isMoving()) {
				// debugCombat(ac, "Cannot attack with throwing weapon while moving");
				attackFailure = true;
			}

			//if not enough stamina, then skip attack
			if (wb == null) {
				if (abstractCharacter.getStamina() < 1) {
					// debugCombat(ac, "Not enough stamina to attack");
					attackFailure = true;
				}
			} else if (abstractCharacter.getStamina() < wb.getWeight()) {
				// debugCombat(ac, "Not enough stamina to attack");
				attackFailure = true;
			}

			//skipping for now to test out mask casting.
			//		//if attacker is casting, then skip this attack
			//		if (ac.getLastPower() != null) {
			//			debugCombat(ac, "Cannot attack, curently casting");
			//			attackFailure = true;
			//		}
			//see if attacker is stunned. If so, stop here
			bonus = abstractCharacter.getBonuses();
			if (bonus != null && bonus.getBool(ModType.Stunned, SourceType.None)) {
				// debugCombat(ac, "Cannot attack while stunned");
				attackFailure = true;
			}

			//Get Range of weapon
			float range;
			if (hasNoWeapon) {
				range = MBServerStatics.NO_WEAPON_RANGE;
			} else {
				range = getWeaponRange(wb, bonus);
			}

			if (abstractCharacter.getObjectType() == GameObjectType.Mob) {
				Mob minion = (Mob) abstractCharacter;
				if (minion.isSiege()) {
					range = 300f;
				}
			}

			//Range check.
			if (NotInRange(abstractCharacter, target, range)) {
				//target is in stealth and can't be seen by source
				if (target.getObjectType().equals(GameObjectType.PlayerCharacter) && abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter)) {
					if (!((PlayerCharacter) abstractCharacter).canSee((PlayerCharacter) target)) {
						// debugCombat(ac, "cannot see target.");
						return 0;
					}
				}
				attackFailure = true;
			}

			//handle pet, skip timers (handled by AI)
			if (abstractCharacter.getObjectType().equals(GameObjectType.Mob)) {
				Mob mob = (Mob) abstractCharacter;
				if (mob.isPet()) {
					attack(abstractCharacter, target, weapon, wb, (slot == MBServerStatics.SLOT_MAINHAND) ? true : false);
					return 2;
				}
			}

			//TODO Verify attacker has los (if not ranged weapon).
			if (!attackFailure) {
				if (hasNoWeapon || abstractCharacter.getObjectType().equals(GameObjectType.Mob)) {
					createTimer(abstractCharacter, slot, 20, true); //2 second for no weapon
				} else {
					int wepSpeed = (int) (wb.getSpeed());
					if (weapon != null && weapon.getBonusPercent(ModType.WeaponSpeed, SourceType.None) != 0f) //add weapon speed bonus
					{
						wepSpeed *= (1 + weapon.getBonus(ModType.WeaponSpeed, SourceType.None));
					}
					if (abstractCharacter.getBonuses() != null && abstractCharacter.getBonuses().getFloatPercentAll(ModType.AttackDelay, SourceType.None) != 0f) //add effects speed bonus
					{
						wepSpeed *= (1 + abstractCharacter.getBonuses().getFloatPercentAll(ModType.AttackDelay, SourceType.None));
					}
					if (wepSpeed < 10) {
						wepSpeed = 10; //Old was 10, but it can be reached lower with legit buffs,effects.
					}
					createTimer(abstractCharacter, slot, wepSpeed, true);
				}

				if (target == null)
					return 0;

				attack(abstractCharacter, target, weapon, wb, (slot == MBServerStatics.SLOT_MAINHAND) ? true : false);
			} else {
				// changed this to half a second to make combat attempts more aggressive than movement sync
				createTimer(abstractCharacter, slot, 5, false); //0.5 second timer if attack fails
				//System.out.println("Attack attempt failed");
			}

		} catch (Exception e) {
			return 0;
		}
		return 2;
	}

	private static void debugCombat(AbstractCharacter ac, String reason) {
		if (ac == null) {
			return;
		}

		//if DebugMeleeSync is on, then debug reason for melee failure
		if (ac.getDebug(64)) {
			if (ac.getObjectType().equals(GameObjectType.PlayerCharacter)) {
				String out = "Attack Failure: " + reason;
				ChatManager.chatSystemInfo((PlayerCharacter) ac, out);
			}
		}
	}

	private static void debugCombatRange(AbstractCharacter ac, Vector3fImmutable sl, Vector3fImmutable tl, float range, float distance) {
		if (ac == null || sl == null || tl == null) {
			return;
		}

		//if DebugMeleeSync is on, then debug reason for melee failure
		if (ac.getDebug(64)) {
			if (ac.getObjectType().equals(GameObjectType.PlayerCharacter)) {
				String out = "Attack Failure: Out of Range: Range: " + distance + ", weaponRange: " + range;
				out += ", sourceLoc: " + sl.x + ", " + sl.y + ", " + sl.z;
				out += ", targetLoc: " + tl.x + ", " + tl.y + ", " + tl.z;
				ChatManager.chatSystemInfo((PlayerCharacter) ac, out);
			}
		}
	}

	private static void createTimer(AbstractCharacter ac, int slot, int time, boolean success) {
		ConcurrentHashMap<String, JobContainer> timers = ac.getTimers();
		if (timers != null) {
			AttackJob aj = new AttackJob(ac, slot, success);
			JobContainer job;
			job = JobScheduler.getInstance().scheduleJob(aj, (time * 100));
			timers.put("Attack" + slot, job);
		} else {
			Logger.error("Unable to find Timers for Character " + ac.getObjectUUID());
		}
	}

	/**
	 * Attempt to attack target
	 */
	private static void attack(AbstractCharacter ac, AbstractWorldObject target, Item weapon, ItemBase wb, boolean mainHand) {

		float atr;
		int minDamage, maxDamage;
		int errorTrack = 0;

		try {

			if (ac == null)
				return;

			if (target == null)
				return;

			if (mainHand) {
				atr = ac.getAtrHandOne();
				minDamage = ac.getMinDamageHandOne();
				maxDamage = ac.getMaxDamageHandOne();
			} else {
				atr = ac.getAtrHandTwo();
				minDamage = ac.getMinDamageHandTwo();
				maxDamage = ac.getMaxDamageHandTwo();
			}

			boolean tarIsRat = false;

			if (target.getObjectTypeMask() == MBServerStatics.MASK_RAT)
				tarIsRat = true;
			else if (target.getObjectType() == GameObjectType.PlayerCharacter) {
				PlayerCharacter pTar = (PlayerCharacter) target;
				for (Effect eff : pTar.getEffects().values()) {
					if (eff.getPowerToken() == 429513599 || eff.getPowerToken() == 429415295) {
						tarIsRat = true;
					}
				}
			}

			//Dont think we need to do this anymore.
			if (tarIsRat) {
				//strip away current % dmg buffs then add with rat %
				if (ac.getBonuses().getFloatPercentAll(ModType.Slay, SourceType.Rat) != 0) {


					float percent = 1 + ac.getBonuses().getFloatPercentAll(ModType.Slay, SourceType.Rat);

					minDamage *= percent;
					maxDamage *= percent;
				}

			}

			errorTrack = 1;

			//subtract stamina
			if (wb == null) {
				ac.modifyStamina(-0.5f, ac, true);
			} else {
				float stam = wb.getWeight() / 3;
				stam = (stam < 1) ? 1 : stam;
				ac.modifyStamina(-(stam), ac, true);
			}

			ac.cancelOnAttackSwing();

			errorTrack = 2;

			//set last time this player has attacked something.
			if (target.getObjectType().equals(GameObjectType.PlayerCharacter) && target.getObjectUUID() != ac.getObjectUUID() && ac.getObjectType() == GameObjectType.PlayerCharacter) {
				ac.setTimeStamp("LastCombatPlayer", System.currentTimeMillis());
				((PlayerCharacter) target).setTimeStamp("LastCombatPlayer", System.currentTimeMillis());
			} else {
				ac.setTimeStamp("LastCombatMob", System.currentTimeMillis());
			}

			errorTrack = 3;

			//Get defense for target
			float defense;
			if (target.getObjectType().equals(GameObjectType.Building)) {

				if (BuildingManager.getBuildingFromCache(target.getObjectUUID()) == null) {
					ac.setCombatTarget(null);
					return;
				}
				defense = 0;

				Building building = (Building) target;
				if (building.getParentZone() != null && building.getParentZone().isPlayerCity()) {

					if (System.currentTimeMillis() > building.getTimeStamp("CallForHelp")) {
						building.getTimestamps().put("CallForHelp", System.currentTimeMillis() + 15000);
						int count = 0;
						for (Mob mob : building.getParentZone().zoneMobSet) {
							if (!mob.isPlayerGuard())
								continue;
							if (mob.getCombatTarget() != null)
								continue;
							if (mob.getGuild() != null && building.getGuild() != null)
								if (!Guild.sameGuild(mob.getGuild().getNation(), building.getGuild().getNation()))
									continue;

							if (mob.getLoc().distanceSquared2D(building.getLoc()) > sqr(300))
								continue;

							if (count == 5)
								count++;

							mob.setCombatTarget(ac);
						}
					}
				}
			} else {
				AbstractCharacter tar = (AbstractCharacter) target;
				defense = tar.getDefenseRating();
				//Handle target attacking back if in combat and has no other target
				handleRetaliate(tar, ac);
			}

			errorTrack = 4;

			//Get hit chance
			int chance;
			float dif = atr - defense;
			if (dif > 100) {
				chance = 94;
			} else if (dif < -100) {
				chance = 4;
			} else {
				chance = (int) ((0.45 * dif) + 49);
			}

			errorTrack = 5;

			//calculate hit/miss
			int roll = ThreadLocalRandom.current().nextInt(100);
			DeferredPowerJob dpj = null;
			if (roll < chance) {
				if (ac.getObjectType().equals(GameObjectType.PlayerCharacter)) {
					updateAttackTimers((PlayerCharacter) ac, target, true);
				}

				boolean skipPassives = false;
				PlayerBonuses bonuses = ac.getBonuses();
				if (bonuses != null && bonuses.getBool(ModType.IgnorePassiveDefense, SourceType.None)) {
					skipPassives = true;
				}

				AbstractCharacter tarAc = null;
				if (AbstractWorldObject.IsAbstractCharacter(target)) {
					tarAc = (AbstractCharacter) target;
				}

				errorTrack = 6;

				// Apply Weapon power effect if any. don't try to apply twice if
				// dual wielding. Perform after passive test for sync purposes.


				if (ac.getObjectType().equals(GameObjectType.PlayerCharacter) && (mainHand || wb.isTwoHanded())) {
					dpj = ((PlayerCharacter) ac).getWeaponPower();
					if (dpj != null) {
						PlayerBonuses bonus = ac.getBonuses();
						float attackRange = getWeaponRange(wb, bonus);
						dpj.attack(target, attackRange);

						if (dpj.getPower() != null && (dpj.getPowerToken() == -1851459567 || dpj.getPowerToken() == -1851489518))
							((PlayerCharacter) ac).setWeaponPower(dpj);
					}
				}
				//check to apply second backstab.
				if (ac.getObjectType().equals(GameObjectType.PlayerCharacter) && !mainHand) {
					dpj = ((PlayerCharacter) ac).getWeaponPower();
					if (dpj != null && dpj.getPower() != null && (dpj.getPowerToken() == -1851459567 || dpj.getPowerToken() == -1851489518)) {
						float attackRange = getWeaponRange(wb,bonuses);
						dpj.attack(target, attackRange);
					}
				}

				errorTrack = 7;

				//Hit, check if passive kicked in
				boolean passiveFired = false;
				if (!skipPassives && tarAc != null) {
					if (target.getObjectType().equals(GameObjectType.PlayerCharacter)) {

						//Handle Block passive
						if (testPassive(ac, tarAc, "Block") && canTestBlock(ac, target)) {

							if (!target.isAlive())
								return;

							sendPassiveDefenseMessage(ac, wb, target, MBServerStatics.COMBAT_SEND_BLOCK, dpj, mainHand);
							passiveFired = true;
						}

						//Handle Parry passive
						if (!passiveFired) {
							if (canTestParry(ac, target) && testPassive(ac, tarAc, "Parry")) {
								if (!target.isAlive())
									return;
								sendPassiveDefenseMessage(ac, wb, target, MBServerStatics.COMBAT_SEND_PARRY, dpj, mainHand);
								passiveFired = true;
							}
						}
					}

					errorTrack = 8;

					//Handle Dodge passive
					if (!passiveFired) {
						if (testPassive(ac, tarAc, "Dodge")) {

							if (!target.isAlive())
								return;

							sendPassiveDefenseMessage(ac, wb, target, MBServerStatics.COMBAT_SEND_DODGE, dpj, mainHand);
							passiveFired = true;
						}
					}
				}

				//return if passive (Block, Parry, Dodge) fired

				if (passiveFired)
					return;

				errorTrack = 9;

				//Hit and no passives
				//if target is player, set last attack timestamp
				if (target.getObjectType().equals(GameObjectType.PlayerCharacter)) {
					updateAttackTimers((PlayerCharacter) target, ac, false);
				}

				//Get damage Type
				DamageType damageType;
				if (wb != null) {
					damageType = wb.getDamageType();
				} else if (ac.getObjectType().equals(GameObjectType.Mob) && ((Mob) ac).isSiege()) {
					damageType = DamageType.Siege;
				} else {
					damageType = DamageType.Crush;
				}

				errorTrack = 10;

				//Get target resists
				Resists resists = null;

				if (tarAc != null) {
					resists = tarAc.getResists();
				} else if (target.getObjectType().equals(GameObjectType.Building)) {
					resists = ((Building) target).getResists();
				}

				//make sure target is not immune to damage type;
				if (resists != null && resists.immuneTo(damageType)) {
					sendCombatMessage(ac, target, 0f, wb, dpj, mainHand);
					return;
				}

				//				PowerProjectileMsg ppm = new PowerProjectileMsg(ac,tarAc);
				//				DispatchMessage.dispatchMsgToInterestArea(ac, ppm, DispatchChannel.SECONDARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);
				//

				errorTrack = 11;

				//Calculate Damage done

				float damage;

				if (wb != null) {
					damage = calculateDamage(ac, tarAc, minDamage, maxDamage, damageType, resists);
				} else {
					damage = calculateDamage(ac, tarAc, minDamage, maxDamage, damageType, resists);
				}

				float d = 0f;

				errorTrack = 12;

				//Subtract Damage from target's health
				if (tarAc != null) {
					if (tarAc.isSit()) {
						damage *= 2.5f; //increase damage if sitting
					}
					if (tarAc.getObjectType() == GameObjectType.Mob) {
						ac.setHateValue(damage * MBServerStatics.PLAYER_COMBAT_HATE_MODIFIER);
						((Mob) tarAc).handleDirectAggro(ac);
					}

					if (tarAc.getHealth() > 0)
						d = tarAc.modifyHealth(-damage, ac, false);

				} else if (target.getObjectType().equals(GameObjectType.Building)) {

					if (BuildingManager.getBuildingFromCache(target.getObjectUUID()) == null) {
						ac.setCombatTarget(null);
						return;
					}
					if (target.getHealth() > 0)
						d = ((Building) target).modifyHealth(-damage, ac);
				}

				errorTrack = 13;

				//Test to see if any damage needs done to weapon or armor
				testItemDamage(ac, target, weapon, wb);

				// if target is dead, we got the killing blow, remove attack timers on our weapons
				if (tarAc != null && !tarAc.isAlive()) {
					removeAttackTimers(ac);
				}

				//test double death fix
				if (d != 0) {
					sendCombatMessage(ac, target, damage, wb, dpj, mainHand); //send damage message
				}

				errorTrack = 14;

				//handle procs
				if (weapon != null && tarAc != null && tarAc.isAlive()) {
					ConcurrentHashMap<String, Effect> effects = weapon.getEffects();
					for (Effect eff : effects.values()) {
						if (eff == null) {
							continue;
						}
						HashSet<AbstractEffectModifier> aems = eff.getEffectModifiers();
						if (aems != null) {
							for (AbstractEffectModifier aem : aems) {
								if (!tarAc.isAlive()) {
									break;
								}
								if (aem instanceof WeaponProcEffectModifier) {
									int procChance = ThreadLocalRandom.current().nextInt(100);
									if (procChance < MBServerStatics.PROC_CHANCE) {
										((WeaponProcEffectModifier) aem).applyProc(ac, target);
									}
								}
							}
						}
					}
				}

				errorTrack = 15;

				//handle damage shields
				if (ac.isAlive() && tarAc != null && tarAc.isAlive()) {
					handleDamageShields(ac, tarAc, damage);
				}
			} else {
				int animationOverride = 0;
				// Apply Weapon power effect if any.
				// don't try to apply twice if dual wielding.
				if (ac.getObjectType().equals(GameObjectType.PlayerCharacter) && (mainHand || wb.isTwoHanded())) {
					dpj = null;
					dpj = ((PlayerCharacter) ac).getWeaponPower();

					if (dpj != null) {
						PowersBase wp = dpj.getPower();
						if (wp.requiresHitRoll() == false) {
							PlayerBonuses bonus = ac.getBonuses();
							float attackRange = getWeaponRange(wb,bonus);
							dpj.attack(target, attackRange);
						} else {
							((PlayerCharacter) ac).setWeaponPower(null);
						}

					}
				}
				if (target.getObjectType() == GameObjectType.Mob) {
					((Mob) target).handleDirectAggro(ac);
				}

				errorTrack = 17;

				//miss, Send miss message
				sendCombatMessage(ac, target, 0f, wb, dpj, mainHand);

				//if attacker is player, set last attack timestamp
				if (ac.getObjectType().equals(GameObjectType.PlayerCharacter)) {
					updateAttackTimers((PlayerCharacter) ac, target, true);
				}
			}

			errorTrack = 18;

			//cancel effects that break on attack or attackSwing
			ac.cancelOnAttack();

		} catch (Exception e) {
			Logger.error(ac.getName() + ' ' + errorTrack + ' ' + e.toString());
		}
	}

	public static boolean canTestParry(AbstractCharacter ac, AbstractWorldObject target) {

		if (ac == null || target == null || !AbstractWorldObject.IsAbstractCharacter(target))
			return false;

		AbstractCharacter tar = (AbstractCharacter) target;

		CharacterItemManager acItem = ac.getCharItemManager();
		CharacterItemManager tarItem = tar.getCharItemManager();

		if (acItem == null || tarItem == null)
			return false;

		Item acMain = acItem.getItemFromEquipped(1);
		Item acOff = acItem.getItemFromEquipped(2);
		Item tarMain = tarItem.getItemFromEquipped(1);
		Item tarOff = tarItem.getItemFromEquipped(2);

		return !isRanged(acMain) && !isRanged(acOff) && !isRanged(tarMain) && !isRanged(tarOff);
	}

	public static boolean canTestBlock(AbstractCharacter ac, AbstractWorldObject target) {

		if (ac == null || target == null || !AbstractWorldObject.IsAbstractCharacter(target))
			return false;

		AbstractCharacter tar = (AbstractCharacter) target;

		CharacterItemManager acItem = ac.getCharItemManager();
		CharacterItemManager tarItem = tar.getCharItemManager();

		if (acItem == null || tarItem == null)
			return false;


		Item tarOff = tarItem.getItemFromEquipped(2);


		if (tarOff == null)
			return false;

		return tarOff.getItemBase().isShield() != false;
	}

	private static boolean isRanged(Item item) {

		if (item == null)
			return false;

		ItemBase ib = item.getItemBase();

		if (ib == null)
			return false;

		if (ib.getType().equals(ItemType.WEAPON) == false)
			return false;

		return ib.getRange() > MBServerStatics.RANGED_WEAPON_RANGE;


	}

	private static float calculateDamage(AbstractCharacter source, AbstractCharacter target, float minDamage, float maxDamage, DamageType damageType, Resists resists) {
		//get range between min and max
		float range = maxDamage - minDamage;

		//Damage is calculated twice to average a more central point
		float damage = ThreadLocalRandom.current().nextFloat() * range;
		damage = (damage + (ThreadLocalRandom.current().nextFloat() * range)) * .5f;

		//put it back between min and max
		damage += minDamage;

		//calculate resists in if any
		if (resists != null) {
			return resists.getResistedDamage(source, target, damageType, damage, 0);
		} else {
			return damage;
		}
	}

	private static void sendPassiveDefenseMessage(AbstractCharacter source, ItemBase wb, AbstractWorldObject target, int passiveType, DeferredPowerJob dpj, boolean mainHand) {

		int swingAnimation = getSwingAnimation(wb, dpj, mainHand);

		if (dpj != null) {
			if (PowersManager.AnimationOverrides.containsKey(dpj.getAction().getEffectID()))
				swingAnimation = PowersManager.AnimationOverrides.get(dpj.getAction().getEffectID());
		}
		TargetedActionMsg cmm = new TargetedActionMsg(source, swingAnimation, target, passiveType);
		DispatchMessage.sendToAllInRange(target, cmm);

	}

	private static void sendCombatMessage(AbstractCharacter source, AbstractWorldObject target, float damage, ItemBase wb, DeferredPowerJob dpj, boolean mainHand) {

		int swingAnimation = getSwingAnimation(wb, dpj, mainHand);

		if (dpj != null) {
			if (PowersManager.AnimationOverrides.containsKey(dpj.getAction().getEffectID()))
				swingAnimation = PowersManager.AnimationOverrides.get(dpj.getAction().getEffectID());
		}

		if (source.getObjectType() == GameObjectType.PlayerCharacter) {
			for (Effect eff : source.getEffects().values()) {
				if (eff.getPower() != null && (eff.getPower().getToken() == 429506943 || eff.getPower().getToken() == 429408639 || eff.getPower().getToken() == 429513599 || eff.getPower().getToken() == 429415295))
					swingAnimation = 0;
			}
		}
		TargetedActionMsg cmm = new TargetedActionMsg(source, target, damage, swingAnimation);
		DispatchMessage.sendToAllInRange(target, cmm);
	}

	public static int getSwingAnimation(ItemBase wb, DeferredPowerJob dpj, boolean mainHand) {
		int token = 0;
		if (dpj != null) {
			token = (dpj.getPower() != null) ? dpj.getPower().getToken() : 0;
		}

		if (token == 563721004) //kick animation
		{
			return 79;
		}

		if (CombatManager.animation != 0) {
			return CombatManager.animation;
		}

		if (wb == null) {
			return 75;
		}
		if (mainHand) {
			if (wb.getAnimations().size() > 0) {
				int animation = wb.getAnimations().get(0);
				int random = ThreadLocalRandom.current().nextInt(wb.getAnimations().size());
				try {
					animation = wb.getAnimations().get(random);
					return animation;
				} catch (Exception e) {
					Logger.error(e.getMessage());
					return wb.getAnimations().get(0);

				}

			} else if (wb.getOffHandAnimations().size() > 0) {
				int animation = wb.getOffHandAnimations().get(0);
				int random = ThreadLocalRandom.current().nextInt(wb.getOffHandAnimations().size());
				try {
					animation = wb.getOffHandAnimations().get(random);
					return animation;
				} catch (Exception e) {
					Logger.error(e.getMessage());
					return wb.getOffHandAnimations().get(0);

				}
			}
		} else {
			if (wb.getOffHandAnimations().size() > 0) {
				int animation = wb.getOffHandAnimations().get(0);
				int random = ThreadLocalRandom.current().nextInt(wb.getOffHandAnimations().size());
				try {
					animation = wb.getOffHandAnimations().get(random);
					return animation;
				} catch (Exception e) {
					Logger.error(e.getMessage());
					return wb.getOffHandAnimations().get(0);

				}
			} else if (wb.getAnimations().size() > 0) {
				int animation = wb.getAnimations().get(0);
				int random = ThreadLocalRandom.current().nextInt(wb.getAnimations().size());
				try {
					animation = wb.getAnimations().get(random);
					return animation;
				} catch (Exception e) {
					Logger.error(e.getMessage());
					return wb.getAnimations().get(0);

				}

			}
		}


		String required = wb.getSkillRequired();
		String mastery = wb.getMastery();
		if (required.equals("Unarmed Combat")) {
			return 75;
		} else if (required.equals("Sword")) {
			if (wb.isTwoHanded()) {
				return 105;
			} else {
				return 98;
			}
		} else if (required.equals("Staff") || required.equals("Pole Arm")) {
			return 85;
		} else if (required.equals("Spear")) {
			return 92;
		} else if (required.equals("Hammer") || required.equals("Axe")) {
			if (wb.isTwoHanded()) {
				return 105;
			} else if (mastery.equals("Throwing")) {
				return 115;
			} else {
				return 100;
			}
		} else if (required.equals("Dagger")) {
			if (mastery.equals("Throwing")) {
				return 117;
			} else {
				return 81;
			}
		} else if (required.equals("Crossbow")) {
			return 110;
		} else if (required.equals("Bow")) {
			return 109;
		} else if (wb.isTwoHanded()) {
			return 105;
		} else {
			return 100;
		}
	}

	private static boolean testPassive(AbstractCharacter source, AbstractCharacter target, String type) {

		float chance = target.getPassiveChance(type, source.getLevel(), true);

		if (chance == 0f)
			return false;


		//max 75% chance of passive to fire
		if (chance > 75f)
			chance = 75f;

		int roll = ThreadLocalRandom.current().nextInt(100);

		//Passive fired
		//Passive did not fire
		return roll < chance;

	}

	private static void updateAttackTimers(PlayerCharacter pc, AbstractWorldObject target, boolean attack) {

		//Set Attack Timers
		if (target.getObjectType().equals(GameObjectType.PlayerCharacter))
			pc.setLastPlayerAttackTime();
		else
			pc.setLastMobAttackTime();
	}

	public static float getWeaponRange(ItemBase weapon, PlayerBonuses bonus) {
		if (weapon == null)
			return 0f;
		float rangeMod = 1.0f;
		if (bonus != null) {
			//rangeMod += bonus.getFloat(ModType.WeaponRange, SourceType.None);
			rangeMod += bonus.getFloatPercentAll(ModType.WeaponRange, SourceType.None);
		}
		return weapon.getRange() * rangeMod;
	}

	public static void toggleCombat(ToggleCombatMsg msg, ClientConnection origin) {
		toggleCombat(msg.toggleCombat(), origin);
	}

	public static void toggleCombat(SetCombatModeMsg msg, ClientConnection origin) {
		toggleCombat(msg.getToggle(), origin);
	}

	private static void toggleCombat(boolean toggle, ClientConnection origin) {

		PlayerCharacter pc = SessionManager.getPlayerCharacter(origin);

		if (pc == null)
			return;

		pc.setCombat(toggle);

		if (!toggle) // toggle is move it to false so clear combat target
			pc.setCombatTarget(null); //clear last combat target

		UpdateStateMsg rwss = new UpdateStateMsg();
		rwss.setPlayer(pc);
		DispatchMessage.dispatchMsgToInterestArea(pc, rwss, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);
	}

	private static void toggleSit(boolean toggle, ClientConnection origin) {

		PlayerCharacter pc = SessionManager.getPlayerCharacter(origin);

		if (pc == null)
			return;

		pc.setSit(toggle);

		UpdateStateMsg rwss = new UpdateStateMsg();
		rwss.setPlayer(pc);
		DispatchMessage.dispatchMsgToInterestArea(pc, rwss, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
	}

	public static boolean NotInRange(AbstractCharacter ac, AbstractWorldObject target, float range) {
		Vector3fImmutable sl = ac.getLoc();
		Vector3fImmutable tl = target.getLoc();
		//add Hitbox's to range.
		range += (calcHitBox(ac) + calcHitBox(target));

		float magnitudeSquared = tl.distanceSquared(sl);

		return magnitudeSquared > range * range;

	}

	//Called when character takes damage.
	public static void handleRetaliate(AbstractCharacter tarAc, AbstractCharacter ac) {
		if (ac == null || tarAc == null) {
			return;
		}
		if (ac.equals(tarAc)) {
			return;
		}

		if (tarAc.isMoving() && tarAc.getObjectType().equals(GameObjectType.PlayerCharacter))
			return;

		if (!tarAc.isAlive() || !ac.isAlive())
			return;
		boolean isCombat = tarAc.isCombat();
		//If target in combat and has no target, then attack back
		AbstractWorldObject awoCombTar = tarAc.getCombatTarget();
		if ((tarAc.isCombat() && awoCombTar == null) || (isCombat && awoCombTar != null && (!awoCombTar.isAlive() || tarAc.isCombat() && NotInRange(tarAc, awoCombTar, tarAc.getRange()))) || (tarAc != null && tarAc.getObjectType() == GameObjectType.Mob && ((Mob) tarAc).isSiege())) {
			// we are in combat with no valid target
			if (tarAc.getObjectType().equals(GameObjectType.PlayerCharacter)) {
				PlayerCharacter pc = (PlayerCharacter) tarAc;
				tarAc.setCombatTarget(ac);
				pc.setLastTarget(ac.getObjectType(), ac.getObjectUUID());
				if (tarAc.getTimers() != null) {
					if (!tarAc.getTimers().containsKey("Attack" + MBServerStatics.SLOT_MAINHAND)) {
						CombatManager.AttackTarget((PlayerCharacter) tarAc, tarAc.getCombatTarget());
					}
				}
			}
		}

		//Handle pet retaliate if assist is on and pet doesn't have a target.
		if (tarAc.getObjectType().equals(GameObjectType.PlayerCharacter)) {
			Mob pet = ((PlayerCharacter) tarAc).getPet();
			if (pet != null && pet.assist() && pet.getCombatTarget() == null) {
				pet.setCombatTarget(ac);
			}
		}

		//Handle Mob Retaliate.
		if (tarAc.getObjectType() == GameObjectType.Mob) {
			Mob retaliater = (Mob) tarAc;
			if (retaliater.getCombatTarget() != null && !retaliater.isSiege())
				return;
			if (ac.getObjectType() == GameObjectType.Mob && retaliater.isSiege())
				return;
			retaliater.setCombatTarget(ac);

		}
	}

	public static void handleDamageShields(AbstractCharacter ac, AbstractCharacter target, float damage) {
		if (ac == null || target == null) {
			return;
		}
		PlayerBonuses bonuses = target.getBonuses();
		if (bonuses != null) {
			ConcurrentHashMap<AbstractEffectModifier, DamageShield> damageShields = bonuses.getDamageShields();
			float total = 0;
			for (DamageShield ds : damageShields.values()) {
				//get amount to damage back
				float amount;
				if (ds.usePercent()) {
					amount = damage * ds.getAmount() / 100;
				} else {
					amount = ds.getAmount();
				}

				//get resisted damage for damagetype
				Resists resists = ac.getResists();
				if (resists != null) {
					amount = resists.getResistedDamage(target, ac, ds.getDamageType(), amount, 0);
				}

				total += amount;
			}
			if (total > 0) {
				//apply Damage back
				ac.modifyHealth(-total, target, true);

				TargetedActionMsg cmm = new TargetedActionMsg(ac, ac, total, 0);
				DispatchMessage.sendToAllInRange(target, cmm);

			}
		}
	}

	public static float calcHitBox(AbstractWorldObject ac) {
		//TODO Figure out how Str Affects HitBox
		float hitBox = 1;
		switch (ac.getObjectType()) {
			case PlayerCharacter:
				PlayerCharacter pc = (PlayerCharacter) ac;
				if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG) {
					Logger.info("Hit box radius for " + pc.getFirstName() + " is " + ((int) pc.statStrBase / 20f));
				}
				hitBox = 1.5f + (int) ((PlayerCharacter) ac).statStrBase / 20f;
				break;

			case Mob:
				Mob mob = (Mob) ac;
				if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG)
					Logger.info("Hit box radius for " + mob.getFirstName()
							+ " is " + ((Mob) ac).getMobBase().getHitBoxRadius());

				hitBox = ((Mob) ac).getMobBase().getHitBoxRadius();
				break;
			case Building:
				Building building = (Building) ac;
				if (building.getBlueprint() == null)
					return 32;
				hitBox = Math.max(building.getBlueprint().getBuildingGroup().getExtents().x,
						building.getBlueprint().getBuildingGroup().getExtents().y);
				if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG)
					Logger.info("Hit box radius for " + building.getName() + " is " + hitBox);
				break;

		}
		return hitBox;
	}

	private static void testItemDamage(AbstractCharacter ac, AbstractWorldObject awo, Item weapon, ItemBase wb) {
		if (ac == null) {
			return;
		}

		//get chance to damage
		int chance = 4500;
		if (wb != null) {
			if (wb.isGlass()) //glass used weighted so fast weapons don't break faster
			{
				chance = 9000 / wb.getWeight();
			}
		}
		//test damaging attackers weapon
		int takeDamage = ThreadLocalRandom.current().nextInt(chance);
		if (takeDamage == 0 && wb != null && (ac.getObjectType().equals(GameObjectType.PlayerCharacter))) {
			ac.getCharItemManager().damageItem(weapon, 1);
		}

		//test damaging targets gear
		takeDamage = ThreadLocalRandom.current().nextInt(chance);
		if (takeDamage == 0 && awo != null && (awo.getObjectType().equals(GameObjectType.PlayerCharacter))) {
			((AbstractCharacter) awo).getCharItemManager().damageRandomArmor(1);
		}
	}

}