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


package engine.objects;

import engine.InterestManagement.InterestManager;
import engine.InterestManagement.Terrain;
import engine.InterestManagement.WorldGrid;
import engine.gameManager.*;
import engine.job.AbstractJob;
import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.ChantJob;
import engine.jobs.PersistentAoeJob;
import engine.jobs.TrackJob;
import engine.math.AtomicFloat;
import engine.math.Bounds;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
import engine.net.ByteBufferWriter;
import engine.net.Dispatch;
import engine.net.client.ClientConnection;
import engine.net.client.msg.ApplyRuneMsg;
import engine.net.client.msg.UpdateStateMsg;
import engine.powers.EffectsBase;
import engine.powers.PowersBase;
import engine.powers.effectmodifiers.AbstractEffectModifier;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public abstract class AbstractCharacter extends AbstractWorldObject {

    public CharacterItemManager charItemManager = new CharacterItemManager(this);
    private final ReentrantReadWriteLock healthLock = new ReentrantReadWriteLock();
    public short level;
    public AbstractWorldObject combatTarget;
    public int contractUUID;
    public Contract contract;

    public String firstName;
    public String lastName;
    protected short statStrCurrent;
    protected short statDexCurrent;
    protected short statConCurrent;
    protected short statIntCurrent;
    protected short statSpiCurrent;
    protected short unusedStatPoints;
    protected int exp;
    public int buildingUUID;
    public Building building;

    public Vector3fImmutable bindLoc;
    protected Vector3fImmutable faceDir;

    public int guildUUID;
    public Guild guild;
    protected byte runningTrains;
    protected ConcurrentHashMap<Integer, CharacterPower> powers;
    public ConcurrentHashMap<String, CharacterSkill> skills;
    // Variables NOT to be stored in db
    protected boolean sit = false;
    protected boolean walkMode;
    protected boolean combat = false;
    public Vector3fImmutable endLoc = Vector3fImmutable.ZERO;
    protected boolean itemCasting = false;
    // nextEndLoc is used to store the next end location when someone is clicking
    // around the ground while other timers like changeAltitude are still
    // ticking down so that mobs/players following dont just move away to your projected location
    protected Vector3fImmutable nextEndLoc = Vector3fImmutable.ZERO;
    protected float speed;
    protected AtomicFloat stamina = new AtomicFloat();
    protected float staminaMax;
    protected AtomicFloat mana = new AtomicFloat();
    protected float manaMax;                                            // Health/Mana/Stamina
    protected AtomicBoolean isAlive = new AtomicBoolean(true);
    public Resists resists = new Resists("Default");
    protected ConcurrentHashMap<String, JobContainer> timers;
    protected ConcurrentHashMap<String, Long> timestamps;
    public int atrHandOne;
    public int atrHandTwo;
    public int minDamageHandOne;
    public int maxDamageHandOne;
    public int minDamageHandTwo;
    public int maxDamageHandTwo;
    public float rangeHandOne;
    public float rangeHandTwo;
    public float speedHandOne;
    public float speedHandTwo;
    public int defenseRating;
    protected boolean isActive; // <-Do not use this for deleting character!
    protected float altitude = 0; // 0=on terrain, 1=tier 1, 2=tier 2, etc.
    protected ConcurrentHashMap<Integer, JobContainer> recycleTimers;
    protected PlayerBonuses bonuses;
    protected JobContainer lastChant;
    protected boolean isCasting = false;
    protected long lastSetLocUpdate = 0L;
    protected int inBuilding = -1; // -1 not in building 0 on ground floor, 1 on first floor etc
    protected int inBuildingID = 0;
    protected int inFloorID = -1;
    protected int liveCounter = 0;
    protected int debug = 0;
    protected boolean movingUp = false;
    private float desiredAltitude = 0;
    private long takeOffTime = 0;
    private byte aoecntr = 0;

    public int hidden = 0; // current rank of hide/sneak/invis
    public CopyOnWriteArrayList<Integer> minions = new CopyOnWriteArrayList();

    public ArrayList<CharacterRune> runes;


    public mbEnums.MonsterType absRace;
    public ClassType absBaseClass = null;
    public ClassType absPromotionClass = null;
    public mbEnums.SexType absGender = null;
    public EnumSet<DisciplineType> absDisciplines;

    // Modifiers
    public short statStrBase;
    public short statDexBase;
    public short statConBase;
    public short statIntBase;
    public short statSpiBase;
    public Race race;
    public BaseClass baseClass;
    public PromotionClass promotionClass;

    public AbstractCharacter() {
        super();
        this.firstName = "";
        this.lastName = "";

        this.statStrCurrent = (short) 0;
        this.statDexCurrent = (short) 0;
        this.statConCurrent = (short) 0;
        this.statIntCurrent = (short) 0;
        this.statSpiCurrent = (short) 0;

        this.unusedStatPoints = (short) 0;

        this.level = (short) 0; // TODO get this from MobsBase later
        this.exp = 1;
        this.walkMode = true;
        this.bindLoc = Vector3fImmutable.ZERO;
        this.faceDir = Vector3fImmutable.ZERO;

        this.runningTrains = (byte) 0;

        this.skills = new ConcurrentHashMap<>();
        this.powers = new ConcurrentHashMap<>();
        this.initializeCharacter();

    }

    /**
     * No Id Constructor
     */
    public AbstractCharacter(
            final String firstName,
            final String lastName,
            final short statStrCurrent,
            final short statDexCurrent,
            final short statConCurrent,
            final short statIntCurrent,
            final short statSpiCurrent,
            final short level,
            final int exp,
            final Vector3fImmutable bindLoc,
            final Vector3fImmutable faceDir,
            final Guild guild,
            final byte runningTrains
    ) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;

        this.statStrCurrent = statStrCurrent;
        this.statDexCurrent = statDexCurrent;
        this.statConCurrent = statConCurrent;
        this.statIntCurrent = statIntCurrent;
        this.statSpiCurrent = statSpiCurrent;
        this.level = level;
        this.exp = exp;
        this.walkMode = true;
        this.bindLoc = bindLoc;
        this.faceDir = faceDir;
        this.guild = guild;
        this.runningTrains = runningTrains;
        this.powers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        this.skills = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        this.initializeCharacter();

    }

    /**
     * Normal Constructor
     */
    public AbstractCharacter(
            final String firstName,
            final String lastName,
            final short statStrCurrent,
            final short statDexCurrent,
            final short statConCurrent,
            final short statIntCurrent,
            final short statSpiCurrent,
            final short level,
            final int exp,
            final Vector3fImmutable bindLoc,
            final Vector3fImmutable currentLoc,
            final Vector3fImmutable faceDir,
            final Guild guild,
            final byte runningTrains,
            final int newUUID
    ) {

        super(newUUID);
        this.firstName = firstName;
        this.lastName = lastName;

        this.statStrCurrent = statStrCurrent;
        this.statDexCurrent = statDexCurrent;
        this.statConCurrent = statConCurrent;
        this.statIntCurrent = statIntCurrent;
        this.statSpiCurrent = statSpiCurrent;
        this.level = level;
        this.exp = exp;
        this.walkMode = true;

        this.bindLoc = bindLoc;
        this.faceDir = faceDir;
        this.guild = guild;

        this.runningTrains = runningTrains;
        this.powers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        this.skills = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        this.initializeCharacter();

    }

    /**
     * ResultSet Constructor for players
     */
    public AbstractCharacter(
            final ResultSet rs,
            final boolean isPlayer
    ) throws SQLException {
        super(rs);

        this.firstName = rs.getString("char_firstname");
        this.lastName = rs.getString("char_lastname");

        this.level = 1;
        this.exp = rs.getInt("char_experience");
        this.walkMode = false;

        this.bindLoc = new Vector3fImmutable(0f, 0f, 0f);
        this.endLoc = Vector3fImmutable.ZERO;

        this.faceDir = Vector3fImmutable.ZERO;

        final int guildID = rs.getInt("GuildUID");
        final Guild errantGuild = Guild.getErrantGuild();

        if (guildID == errantGuild.getObjectUUID()) {
            this.guild = errantGuild;
        } else {
            this.guild = Guild.getGuild(guildID);
            if (this.guild == null) {
                this.guild = Guild.getErrantGuild();
            }
        }

        if (this.guild == null)
            this.guild = errantGuild;

        this.skills = new ConcurrentHashMap<>();
        this.powers = new ConcurrentHashMap<>();
        this.initializeCharacter();

    }

    /**
     * ResultSet Constructor for NPC/Mobs
     */
    public AbstractCharacter(final ResultSet rs) throws SQLException {
        super(rs);

        this.firstName = "";
        this.lastName = "";

        this.statStrCurrent = (short) 0;
        this.statDexCurrent = (short) 0;
        this.statConCurrent = (short) 0;
        this.statIntCurrent = (short) 0;
        this.statSpiCurrent = (short) 0;

        this.unusedStatPoints = (short) 0;

        this.level = (short) 0; // TODO get this from MobsBase later
        this.exp = 1;
        this.walkMode = true;
        this.bindLoc = Vector3fImmutable.ZERO;
        this.faceDir = Vector3fImmutable.ZERO;

        this.runningTrains = (byte) 0;

        this.skills = new ConcurrentHashMap<>();
        this.powers = new ConcurrentHashMap<>();
        initializeCharacter();

    }

    /**
     * ResultSet Constructor for static Mobs
     */
    public AbstractCharacter(final ResultSet rs, final int objectUUID) throws SQLException {

        super(objectUUID);

        this.firstName = "";
        this.lastName = "";

        this.statStrCurrent = (short) 0;
        this.statDexCurrent = (short) 0;
        this.statConCurrent = (short) 0;
        this.statIntCurrent = (short) 0;
        this.statSpiCurrent = (short) 0;

        this.unusedStatPoints = (short) 0;

        this.level = (short) 0; // TODO get this from MobsBase later
        this.exp = 1;
        this.walkMode = true;

        this.bindLoc = new Vector3fImmutable(rs.getFloat("spawnX"), rs.getFloat("spawnY"), rs.getFloat("spawnZ"));

        if (ConfigManager.serverType.equals(mbEnums.ServerType.WORLDSERVER))
            this.setLoc(this.bindLoc);
        this.endLoc = Vector3fImmutable.ZERO;


        this.faceDir = Vector3fImmutable.ZERO;

        final int guildID = rs.getInt("GuildID");

        if (guildID == Guild.getErrantGuild().getObjectUUID()) {
            this.guild = Guild.getErrantGuild();
        } else {
            this.guild = Guild.getGuild(guildID);
        }

        if (this.guild == null)
            this.guild = Guild.getErrantGuild();

        this.runningTrains = (byte) 0;
        this.skills = new ConcurrentHashMap<>();
        this.powers = new ConcurrentHashMap<>();

        this.initializeCharacter();
    }

    public static int getBankCapacity() {
        return 500;
    }

    public static int getVaultCapacity() {
        return 5000;
    }

    public static void _serializeForClientMsg(AbstractCharacter abstractCharacter, final ByteBufferWriter writer) {
        AbstractCharacter.__serializeForClientMsg(abstractCharacter, writer);
    }

    public static void __serializeForClientMsg(AbstractCharacter abstractCharacter, final ByteBufferWriter writer) {
    }

    public static void serializeForClientMsgOtherPlayer(AbstractCharacter abstractCharacter, final ByteBufferWriter writer, final boolean asciiLastName) {

        switch (abstractCharacter.getObjectType()) {
            case PlayerCharacter:
                PlayerCharacter.serializePlayerForClientMsgOtherPlayer((PlayerCharacter) abstractCharacter, writer, asciiLastName);
                break;
            case Mob:
                Mob.serializeMobForClientMsgOtherPlayer((Mob) abstractCharacter, writer);
                break;
            case NPC:
                NPC.serializeNpcForClientMsgOtherPlayer((NPC) abstractCharacter, writer, asciiLastName);
                break;
        }


        //TODO INPUT SWITCH CASE ON GAME OBJECTS TO CALL SPECIFIC METHODS.
    }

    public static final void serializeForTrack(AbstractCharacter abstractCharacter, final ByteBufferWriter writer, boolean isGroup) {
        writer.putInt(abstractCharacter.getObjectType().ordinal());
        writer.putInt(abstractCharacter.getObjectUUID());

        if (abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter)) {
            writer.putString(abstractCharacter.getName());
        } else {
            writer.putString(abstractCharacter.getFirstName());
        }
        writer.put(isGroup ? (byte) 1 : (byte) 0);
        if (abstractCharacter.guild != null) {
            Guild.serializeForTrack(abstractCharacter.guild, writer);
        } else {
            Guild.serializeErrantForTrack(writer);
        }
    }

    public static void runBonusesOnLoad(PlayerCharacter playerCharacter) {

        // synchronized with getBonuses()

        synchronized (playerCharacter.bonuses) {
            try {
                //run until no new bonuses are applied

                // clear bonuses and reapply rune bonuses
                if (playerCharacter.getObjectType() == GameObjectType.PlayerCharacter) {
                    playerCharacter.bonuses.calculateRuneBaseEffects(playerCharacter);
                } else {
                    playerCharacter.bonuses.clearRuneBaseEffects();
                }

                // apply effect bonuses

                for (Effect eff : playerCharacter.effects.values()) {
                    eff.applyBonus(playerCharacter);
                }

                //apply item bonuses for equipped items
                ConcurrentHashMap<EquipSlotType, Item> equip = null;

                if (playerCharacter.charItemManager != null)
                    equip = playerCharacter.charItemManager.getEquipped();

                if (equip != null) {
                    for (Item item : equip.values()) {
                        item.clearBonuses();
                        if (item != null) {
                            ConcurrentHashMap<String, Effect> effects = item.getEffects();
                            if (effects != null) {
                                for (Effect eff : effects.values()) {
                                    eff.applyBonus(item, playerCharacter);
                                }
                            }
                        }
                    }
                }

                //recalculate passive defenses
                playerCharacter.setPassives();

                //flip the active bonus set for synchronization purposes
                //do this after all bonus updates, but before recalculations.
                // recalculate everything
                //calculate item bonuses
                calculateItemBonuses(playerCharacter);

                //recalculate formulas
                PlayerCharacter.recalculatePlayerStatsOnLoad(playerCharacter);

            } catch (Exception e) {
                Logger.error(e);
            }

        }

    }

    public static Regions InsideBuildingRegionGoingDown(AbstractCharacter abstractCharacter) {

        HashSet<AbstractWorldObject> buildings = WorldGrid.getObjectsInRangePartial(abstractCharacter, 1000, MBServerStatics.MASK_BUILDING);

        Regions tempRegion = null;
        for (AbstractWorldObject awo : buildings) {

            Building building = (Building) awo;
            if (building.getBounds() == null)
                continue;

            if (!Bounds.collide(abstractCharacter.getLoc(), building.getBounds()))
                continue;

            for (Regions region : building.getBounds().getRegions()) {

                if (!region.isPointInPolygon(abstractCharacter.getLoc()))
                    continue;

                if (!region.isOutside())
                    continue;
                if (tempRegion == null)
                    tempRegion = region;

                if (tempRegion.highLerp.y < region.highLerp.y)
                    tempRegion = region;
            }

            if (tempRegion != null)
                break;
        }
        return tempRegion;
    }

    public static boolean CanFly(AbstractCharacter flyer) {
        boolean canFly = false;
        PlayerBonuses bonus = flyer.getBonuses();

        if (bonus != null && !bonus.getBool(ModType.NoMod, SourceType.Fly) && bonus.getBool(ModType.Fly, SourceType.None) && flyer.isAlive())
            canFly = true;

        return canFly;

    }

    public static void teleport(AbstractCharacter abstractCharacter, final Vector3fImmutable targetLoc) {
        abstractCharacter.locationLock.writeLock().lock();
        try {
            MovementManager.translocate(abstractCharacter, targetLoc);
            if (abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter))
                InterestManager.INTERESTMANAGER.HandleLoadForTeleport((PlayerCharacter) abstractCharacter);
        } catch (Exception e) {
            Logger.error(e);
        } finally {
            abstractCharacter.locationLock.writeLock().unlock();
        }
    }

    public static void removeRune(PlayerCharacter playerCharacter, ClientConnection origin, int runeID) {

        if (playerCharacter == null || origin == null) {
            return;
        }

        //remove only if rune is discipline
        if (runeID < 3001 || runeID > 3048) {
            return;
        }

        //see if pc has rune
        ArrayList<CharacterRune> runes = playerCharacter.getRunes();

        if (runes == null)
            return;

        CharacterRune found = playerCharacter.getRune(runeID);

        if (found == null)
            return;

        //TODO see if player needs to refine skills or powers first
        //attempt remove rune from player

        if (!CharacterRune.removeRune(playerCharacter, runeID))
            return;

        //update client with removed rune.
        ApplyRuneMsg arm = new ApplyRuneMsg(playerCharacter.getObjectType().ordinal(), playerCharacter.getObjectUUID(), runeID);
        Dispatch dispatch = Dispatch.borrow(playerCharacter, arm);
        DispatchManager.dispatchMsgDispatch(dispatch, DispatchChannel.SECONDARY);
    }

    public static void removeAllBlessings(PlayerCharacter playerCharacter) {

        PowersBase[] powers = new PowersBase[3];

        powers[0] = PowersManager.getPowerByIDString("BLS-POWER");
        powers[1] = PowersManager.getPowerByIDString("BLS-FORTUNE");
        powers[2] = PowersManager.getPowerByIDString("BLS-WISDOM");

        for (PowersBase power : powers) {
            PowersManager.removeEffect(playerCharacter, power.getActions().get(0), true, false);
        }

    }

    public static void AssignDefenseValue(AbstractCharacter abstractCharacter) {
        ConcurrentHashMap<EquipSlotType, Item> equipped = abstractCharacter.charItemManager.getEquipped();
        if (abstractCharacter.effects != null && abstractCharacter.effects.containsKey("DeathShroud"))
            abstractCharacter.defenseRating = (short) 0;
        else {
            // calculate defense for equipment
            float defense = abstractCharacter.statDexCurrent * 2;
            defense += getShieldDefense(abstractCharacter, equipped.get(EquipSlotType.LHELD));
            defense += getArmorDefense(abstractCharacter, equipped.get(EquipSlotType.HELM));
            defense += getArmorDefense(abstractCharacter, equipped.get(EquipSlotType.CHEST));
            defense += getArmorDefense(abstractCharacter, equipped.get(EquipSlotType.UPARM));
            defense += getArmorDefense(abstractCharacter, equipped.get(EquipSlotType.HANDS));
            defense += getArmorDefense(abstractCharacter, equipped.get(EquipSlotType.LEGS));
            defense += getArmorDefense(abstractCharacter, equipped.get(EquipSlotType.FEET));
            defense += getWeaponDefense(abstractCharacter, equipped);

            if (abstractCharacter.bonuses != null) {
                // add any bonuses
                defense += (short) abstractCharacter.bonuses.getFloat(ModType.DCV, SourceType.None);

                // Finally multiply any percent modifiers. DO THIS LAST!
                float pos_Bonus = abstractCharacter.bonuses.getFloatPercentPositive(ModType.DCV, SourceType.None);
                defense = (short) (defense * (1 + pos_Bonus));

                //Lucky rune applies next
                //applied runes will be calculated and added to the normal bonuses. no need for this garbage anymore
                //defense = (short) (defense * (1 + ((float) this.bonuses.getShort("rune.Defense") / 100)));

                //and negative percent modifiers
                //already done...
                float neg_Bonus = abstractCharacter.bonuses.getFloatPercentNegative(ModType.DCV, SourceType.None);
                defense = (short) (defense * (1 + neg_Bonus));

            } else
                // TODO add error log here
                Logger.error("Error: missing bonuses");

            defense = (defense < 1) ? 1 : defense;
            abstractCharacter.defenseRating = (short) (defense + 0.5f);
        }
    }

    /**
     * @param abstractCharacter
     * @ Calculates Atr (both hands) Defense, and Damage for pc
     */
    public static void calculateAtrDefenseDamage(AbstractCharacter abstractCharacter) {
        if (abstractCharacter.charItemManager == null || abstractCharacter.charItemManager.getEquipped() == null || abstractCharacter.skills == null) {
            Logger.error("Player " + abstractCharacter.getObjectUUID() + " missing skills or equipment");
            defaultAtrAndDamage(abstractCharacter, true);
            defaultAtrAndDamage(abstractCharacter, false);
            abstractCharacter.defenseRating = 0;
            return;
        }
        AssignDefenseValue(abstractCharacter);
        // calculate atr and damage for each hand
        ConcurrentHashMap<EquipSlotType, Item> equipped = abstractCharacter.charItemManager.getEquipped();
        AssignDamageAtrForPlayers(abstractCharacter, equipped.get(EquipSlotType.RHELD), true, equipped.get(EquipSlotType.RHELD));
        AssignDamageAtrForPlayers(abstractCharacter, equipped.get(EquipSlotType.LHELD), false, equipped.get(EquipSlotType.LHELD));
        if (abstractCharacter.getObjectType().equals(GameObjectType.Mob)) {
            Mob mob = (Mob) abstractCharacter;
            abstractCharacter.minDamageHandOne = (int) mob.mobBase.getDamageMin();
            abstractCharacter.minDamageHandTwo = (int) mob.mobBase.getDamageMin();
            abstractCharacter.maxDamageHandOne = (int) mob.mobBase.getDamageMax();
            abstractCharacter.maxDamageHandTwo = (int) mob.mobBase.getDamageMax();
            abstractCharacter.atrHandOne = mob.mobBase.getAttackRating();
            abstractCharacter.atrHandTwo = mob.mobBase.getAttackRating();
            abstractCharacter.defenseRating = mob.mobBase.getDefenseRating();
        }
    }

    /**
     * @ Calculates Atr, and Damage for each weapon
     */
    public static void AssignDamageAtrForPlayers(AbstractCharacter abstractCharacter, Item weapon, boolean mainHand, Item otherHand) {

        // make sure weapon exists
        boolean noWeapon = false;
        ItemTemplate weaponTemplate = null;

        if (weapon == null)
            noWeapon = true;
        else {
            ItemTemplate itemTemplate = weapon.template;

            if (itemTemplate == null)
                noWeapon = true;
            else if (!weapon.template.item_type.equals(ItemType.WEAPON)) {
                defaultAtrAndDamage(abstractCharacter, mainHand);
                return;
            } else
                weaponTemplate = itemTemplate;
        }

        float skillPercentage, masteryPercentage;
        float mastDam;
        int min, max;
        float speed = 20f;
        boolean strBased = false;

        // get skill percentages and min and max damage for weapons
        if (noWeapon) {
            if (mainHand) {
                Item off = abstractCharacter.charItemManager.getEquipped().get(EquipSlotType.LHELD);
                if (off != null && off.template != null && off.template.item_type.equals(ItemType.WEAPON))
                    abstractCharacter.rangeHandOne = 10 * (1 + (abstractCharacter.statStrBase / 600)); // Set
                    // to
                    // no
                    // weapon
                    // range
                else
                    abstractCharacter.rangeHandOne = -1; // set to do not attack
            } else
                abstractCharacter.rangeHandTwo = -1; // set to do not attack

            skillPercentage = getModifiedAmount(abstractCharacter.skills.get("Unarmed Combat"));
            masteryPercentage = getModifiedAmount(abstractCharacter.skills.get("Unarmed Combat Mastery"));

            if (masteryPercentage == 0f)
                mastDam = CharacterSkill.getQuickMastery(abstractCharacter, "Unarmed Combat Mastery");
            else
                mastDam = masteryPercentage;
            // TODO Correct these
            min = 1;
            max = 3;

        } else {
            if (mainHand)
                abstractCharacter.rangeHandOne = weapon.template.item_weapon_max_range * (1 + (abstractCharacter.statStrBase / 600));
            else
                abstractCharacter.rangeHandTwo = weapon.template.item_weapon_max_range * (1 + (abstractCharacter.statStrBase / 600));

            if (abstractCharacter.bonuses != null) {
                float range_bonus = 1 + abstractCharacter.bonuses.getFloatPercentAll(ModType.WeaponRange, SourceType.None);

                if (mainHand)
                    abstractCharacter.rangeHandOne *= range_bonus;
                else
                    abstractCharacter.rangeHandTwo *= range_bonus;

            }

            skillPercentage = getModifiedAmount(abstractCharacter.skills.get(weapon.template.item_skill_used));
            masteryPercentage = getModifiedAmount(abstractCharacter.skills.get(weaponTemplate.item_skill_mastery_used));

            if (masteryPercentage == 0f)
                mastDam = 0f;
                //				mastDam = CharacterSkill.getQuickMastery(this, wb.getMastery());
            else
                mastDam = masteryPercentage;
            int[] damages = (int[]) weaponTemplate.item_weapon_damage.values().toArray()[0];

            min = damages[0];
            max = damages[1];

            strBased = weaponTemplate.item_primary_attr.equals(AttributeType.Strength);
        }

        if (abstractCharacter.effects != null && abstractCharacter.effects.containsKey("DeathShroud"))
            // No Atr in deathshroud.
            if (mainHand)
                abstractCharacter.atrHandOne = (short) 0;
            else
                abstractCharacter.atrHandTwo = (short) 0;
        else {
            // calculate atr
            float atr = 0;
            atr += (int) skillPercentage * 4f; //<-round down skill% -
            atr += (int) masteryPercentage * 3f;
            if (abstractCharacter.statStrCurrent > abstractCharacter.statDexCurrent)
                atr += abstractCharacter.statStrCurrent / 2;
            else
                atr += abstractCharacter.statDexCurrent / 2;

            // add in any bonuses to atr
            if (abstractCharacter.bonuses != null) {
                // Add any base bonuses
                atr += abstractCharacter.bonuses.getFloat(ModType.OCV, SourceType.None);

                // Finally use any multipliers. DO THIS LAST!
                float pos_Bonus = (1 + abstractCharacter.bonuses.getFloatPercentPositive(ModType.OCV, SourceType.None));
                atr *= pos_Bonus;

                // next precise
                //runes will have their own bonuses.
                //	atr *= (1 + ((float) this.bonuses.getShort("rune.Attack") / 100));

                //and negative percent modifiers
                float neg_Bonus = abstractCharacter.bonuses.getFloatPercentNegative(ModType.OCV, SourceType.None);

                atr *= (1 + neg_Bonus);
            }

            atr = (atr < 1) ? 1 : atr;

            // set atr
            if (mainHand)
                abstractCharacter.atrHandOne = (short) (atr + 0.5f);
            else
                abstractCharacter.atrHandTwo = (short) (atr + 0.5f);
        }

        //calculate speed
        if (weaponTemplate != null)
            speed = weaponTemplate.item_weapon_wepspeed;
        else
            speed = 20f; //unarmed attack speed
        if (weapon != null)
            speed *= (1 + abstractCharacter.bonuses.getFloatPercentAll(ModType.WeaponSpeed, SourceType.None));

        PlayerBonuses bonuses = abstractCharacter.bonuses;
        if (bonuses != null) {
            ModType modType = ModType.AttackDelay;
            for (AbstractEffectModifier mod : bonuses.bonusFloats.keySet()) {

                if (mod.getPercentMod() == 0)
                    continue;


                if (!mod.modType.equals(modType))
                    continue;

                if (bonuses.bonusFloats.get(mod) == null)
                    continue;

                speed *= (1 + bonuses.bonusFloats.get(mod));
            }
        }
        //speed *= (1 + abstractCharacter.bonuses.getFloatPercentAll(ModType.AttackDelay, SourceType.None));

        if (speed < 10)
            speed = 10;

        //add min/max damage bonuses for weapon
        if (weapon != null) {
            // Add any base bonuses

            min += weapon.getBonus(ModType.MinDamage, SourceType.None);
            max += weapon.getBonus(ModType.MaxDamage, SourceType.None);

            min += weapon.getBonus(ModType.MeleeDamageModifier, SourceType.None);
            max += weapon.getBonus(ModType.MeleeDamageModifier, SourceType.None);

            // Finally use any multipliers. DO THIS LAST!

            float percentMinDamage = 1;
            float percentMaxDamage = 1;

            percentMinDamage += weapon.getBonusPercent(ModType.MinDamage, SourceType.None);
            percentMinDamage += weapon.getBonusPercent(ModType.MeleeDamageModifier, SourceType.None);

            percentMaxDamage += weapon.getBonusPercent(ModType.MaxDamage, SourceType.None);
            percentMaxDamage += weapon.getBonusPercent(ModType.MeleeDamageModifier, SourceType.None);


            min *= percentMinDamage;
            max *= percentMaxDamage;
        }

        //if duel wielding, cut damage by 30%

        if (otherHand != null) {
            ItemTemplate ibo = otherHand.template;

            if (ibo != null && otherHand.template.equals(ItemType.WEAPON)) {
                min *= 0.7f;
                max *= 0.7f;
            }
        }

        // calculate damage
        float minDamage;
        float maxDamage;
        float pri = (strBased) ? (float) abstractCharacter.statStrCurrent : (float) abstractCharacter.statDexCurrent;
        float sec = (strBased) ? (float) abstractCharacter.statDexCurrent : (float) abstractCharacter.statStrCurrent;
        minDamage = (float) (min * ((0.0315f * Math.pow(pri, 0.75f)) + (0.042f * Math.pow(sec, 0.75f)) + (0.01f * ((int) skillPercentage + (int) mastDam))));
        maxDamage = (float) (max * ((0.0785f * Math.pow(pri, 0.75f)) + (0.016f * Math.pow(sec, 0.75f)) + (0.0075f * ((int) skillPercentage + (int) mastDam))));
        minDamage = (float) ((int) (minDamage + 0.5f)); //round to nearest decimal
        maxDamage = (float) ((int) (maxDamage + 0.5f)); //round to nearest decimal

        // Half damage if in death shroud
        if (abstractCharacter.effects != null && abstractCharacter.effects.containsKey("DeathShroud")) {
            minDamage *= 0.5f;
            maxDamage *= 0.5f;
        }

        // add in any bonuses to damage
        if (abstractCharacter.bonuses != null) {
            // Add any base bonuses
            minDamage += abstractCharacter.bonuses.getFloat(ModType.MinDamage, SourceType.None);
            maxDamage += abstractCharacter.bonuses.getFloat(ModType.MaxDamage, SourceType.None);

            minDamage += abstractCharacter.bonuses.getFloat(ModType.MeleeDamageModifier, SourceType.None);
            maxDamage += abstractCharacter.bonuses.getFloat(ModType.MeleeDamageModifier, SourceType.None);
            // Finally use any multipliers. DO THIS LAST!

            float percentMinDamage = 1;
            float percentMaxDamage = 1;

            percentMinDamage += abstractCharacter.bonuses.getFloatPercentAll(ModType.MinDamage, SourceType.None);
            percentMinDamage += abstractCharacter.bonuses.getFloatPercentAll(ModType.MeleeDamageModifier, SourceType.None);

            percentMaxDamage += abstractCharacter.bonuses.getFloatPercentAll(ModType.MaxDamage, SourceType.None);
            percentMaxDamage += abstractCharacter.bonuses.getFloatPercentAll(ModType.MeleeDamageModifier, SourceType.None);

            minDamage *= percentMinDamage;
            maxDamage *= percentMaxDamage;

        }

        // set damages
        if (mainHand) {
            abstractCharacter.minDamageHandOne = (int) minDamage;
            abstractCharacter.maxDamageHandOne = (int) maxDamage;
            abstractCharacter.speedHandOne = speed;
        } else {
            abstractCharacter.minDamageHandTwo = (int) minDamage;
            abstractCharacter.maxDamageHandTwo = (int) maxDamage;
            abstractCharacter.speedHandTwo = speed;
        }
    }

    /**
     * @ Calculates Defense for shield
     */
    private static float getShieldDefense(AbstractCharacter abstractCharacter, Item shield) {

        if (shield == null)
            return 0;

        if (ItemManager.isShield(shield) == false)
            return 0;

        ItemTemplate template = shield.template;

        if (template == null)
            return 0;

        CharacterSkill blockSkill = abstractCharacter.skills.get("Block");

        float skillMod;

        if (blockSkill == null) {
            skillMod = 0;
        } else
            skillMod = blockSkill.getModifiedAmount();

        float def = template.item_defense_rating;

        //apply item defense bonuses

        if (shield != null) {
            def += shield.getBonus(ModType.DR, SourceType.None);
            def *= (1 + shield.getBonusPercent(ModType.DR, SourceType.None));

        }

        // float val = ((float)ab.getDefense()) * (1 + (skillMod / 100));
        return (def * (1 + ((int) skillMod / 100f)));
    }

    /**
     * @ Calculates Defense for armor
     */
    private static float getArmorDefense(AbstractCharacter abstractCharacter, Item armor) {

        if (armor == null)
            return 0;

        ItemTemplate template = armor.template;

        if (template == null)
            return 0;

        if (!armor.template.item_type.equals(ItemType.ARMOR))
            return 0;

        if (armor.template.item_skill_used.isEmpty())
            return template.item_defense_rating;

        CharacterSkill armorSkill = abstractCharacter.skills.get(armor.template.item_skill_used);

        if (abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter) && armorSkill == null) {
            Logger.error("Player " + abstractCharacter.getObjectUUID()
                    + " has armor equipped without the nescessary skill to equip it");
            return template.item_defense_rating;
        }

        if (armorSkill == null)
            return template.item_defense_rating;  // Mobiles do not have armor skills @TODO

        float def = template.item_defense_rating;

        //apply item defense bonuses

        if (armor != null) {
            def += armor.getBonus(ModType.DR, SourceType.None);
            def *= (1 + armor.getBonusPercent(ModType.DR, SourceType.None));
        }

        return (def * (1 + ((int) armorSkill.getModifiedAmount() / 50f)));
    }

    /**
     * @ Calculates Defense for weapon
     */
    private static float getWeaponDefense(AbstractCharacter abstractCharacter, ConcurrentHashMap<EquipSlotType, Item> equipped) {

        Item weapon = equipped.get(EquipSlotType.RHELD);
        ItemTemplate weaponTemplate = null;
        CharacterSkill skill, mastery;
        float val = 0;
        boolean unarmed = false;

        if (weapon == null) {
            weapon = equipped.get(EquipSlotType.LHELD);
            if (weapon == null || ItemManager.isShield(weapon))
                unarmed = true;
            else
                weaponTemplate = weapon.template;
        } else
            weaponTemplate = weapon.template;

        if (weaponTemplate == null)
            unarmed = true;

        if (unarmed) {
            skill = abstractCharacter.skills.get("Unarmed Combat");
            mastery = abstractCharacter.skills.get("Unarmed Combat Mastery");
        } else {
            skill = abstractCharacter.skills.get(weapon.template.item_skill_used);
            mastery = abstractCharacter.skills.get(weaponTemplate.item_skill_mastery_used);
        }

        if (skill != null)
            val += (int) skill.getModifiedAmount() / 2f;

        if (mastery != null)
            val += (int) mastery.getModifiedAmount() / 2f;

        return val;
    }

    //Don't call this function directly. linked from pc.calculateSkills()
    //through SkillCalcJob. Designed to only run from one worker thread
    public static void runSkillCalc(AbstractCharacter abstractCharacter) {

        //see if any new skills or powers granted
        CharacterSkill.calculateSkills(abstractCharacter);
        // calculate granted Trains in powers.
        CharacterPower.grantTrains(abstractCharacter);
        //see if any new powers unlocked from previous check
        CharacterPower.calculatePowers(abstractCharacter);

    }

    //calculate item bonuses here
    public static void calculateItemBonuses(AbstractCharacter abstractCharacter) {

        if (abstractCharacter.charItemManager == null || abstractCharacter.bonuses == null)
            return;

        ConcurrentHashMap<EquipSlotType, Item> equipped = abstractCharacter.charItemManager.getEquipped();

        for (Item item : equipped.values()) {

            ItemTemplate ib = item.template;

            if (ib == null)
                continue;
            //TODO add effect bonuses in here for equipped items
        }
    }

    /**
     * @ Defaults ATR, Defense and Damage for player
     */
    private static void defaultAtrAndDamage(AbstractCharacter abstractCharacter, boolean mainHand) {

        if (mainHand) {
            abstractCharacter.atrHandOne = 0;
            abstractCharacter.minDamageHandOne = 0;
            abstractCharacter.maxDamageHandOne = 0;
            abstractCharacter.rangeHandOne = -1;
            abstractCharacter.speedHandOne = 20;
        } else {
            abstractCharacter.atrHandTwo = 0;
            abstractCharacter.minDamageHandTwo = 0;
            abstractCharacter.maxDamageHandTwo = 0;
            abstractCharacter.rangeHandTwo = -1;
            abstractCharacter.speedHandTwo = 20;
        }
    }

    private static float getModifiedAmount(CharacterSkill skill) {
        if (skill == null)
            return 0f;
        return skill.getModifiedAmount();
    }

    private void initializeCharacter() {
        this.timers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        this.timestamps = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        final long l = System.currentTimeMillis();
        this.timestamps.put("Health Recovery", l);
        this.timestamps.put("Stamina Recovery", l);
        this.timestamps.put("Mana Recovery", l);
        this.recycleTimers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
    }

    protected abstract ConcurrentHashMap<Integer, CharacterPower> initializePowers();

    public final void addPersistantAoe(
            final String name,
            final int duration,
            final PersistentAoeJob asj,
            final EffectsBase eb,
            final int trains
    ) {
        if (!isAlive()) {
            return;
        }
        final JobContainer jc = JobScheduler.getInstance().scheduleJob(asj, duration);
        final Effect eff = new Effect(jc, eb, trains);
        aoecntr++;
        this.effects.put(name + aoecntr, eff);
        eff.setPAOE();
    }

    public final void setLastChant(
            final int duration,
            final ChantJob cj
    ) {
        if (!isAlive()) {
            return;
        }
        if (this.lastChant != null) {
            this.lastChant.cancelJob();
        }
        this.lastChant = JobScheduler.getInstance().scheduleJob(cj, duration);
    }

    public final void cancelLastChant() {
        if (this.lastChant != null) {
            this.lastChant.cancelJob();
            this.lastChant = null;
        }
    }

    public final void cancelLastChantIfSame(final Effect eff) {
        if (eff == null || this.lastChant == null) {
            return;
        }
        final AbstractJob aj = this.lastChant.getJob();
        if (aj == null || (!(aj instanceof ChantJob))) {
            return;
        }
        final int token = ((ChantJob) aj).getPowerToken();
        if (eff.getPowerToken() == token && token != 0) {
            this.cancelLastChant();
        }
    }

    /*
     * Getters
     */
    public final short getUnusedStatPoints() {
        return this.unusedStatPoints;
    }

    @Override
    public String getName() {
        if (this.firstName.length() == 0 && this.lastName.length() == 0) {
            return "Unnamed " + '(' + this.getObjectUUID() + ')';
        } else if (this.lastName.length() == 0) {
            return this.getFirstName();
        } else {
            return this.getFirstName() + ' ' + this.getLastName();
        }
    }

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(final String name) {
        this.firstName = name;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(final String name) {
        this.lastName = name;
    }

    public final short getStatStrCurrent() {
        return this.statStrCurrent;
    }

    public final short getStatDexCurrent() {
        return this.statDexCurrent;
    }

    public final short getStatConCurrent() {
        return this.statConCurrent;
    }

    public final short getStatIntCurrent() {
        return this.statIntCurrent;
    }

    public final short getStatSpiCurrent() {
        return this.statSpiCurrent;
    }

    public short getLevel() {
        return this.level;
    }

    public void setLevel(final short value) {
        this.level = value;
    }

    public final boolean isActive() {
        return this.isActive;
    }

    public final void setActive(final boolean value) {
        this.isActive = value;
    }

    public final Resists getResists() {
        if (this.resists == null)
            return Resists.getResists(0);
        return this.resists;
    }

    public final void setResists(final Resists value) {
        this.resists = value;
    }

    public final int getExp() {
        return this.exp;
    }

    public final JobContainer getLastPower() {
        if (this.timers == null) {
            return null;
        }
        return this.timers.get("LastPower");
    }

    public final void setLastPower(final JobContainer jc) {
        if (this.timers != null) {
            this.timers.put("LastPower", jc);
        }
    }

    public final void clearLastPower() {
        if (this.timers != null) {
            this.timers.remove("LastPower");
        }
    }

    public final JobContainer getLastItem() {
        if (this.timers == null) {
            return null;
        }
        return this.timers.get("LastItem");
    }

    public final int getIsSittingAsInt() {
        if (!this.isAlive()) {
            return 1;
        }

        if (this.sit) {
            return 4;
        } else {
            if (this.isMoving())
                return 7;
            else
                return 5;
        }
    }

    public final int getIsWalkingAsInt() {
        if (this.walkMode) {
            return 1;
        }
        return 2;
    }

    public final int getIsCombatAsInt() {
        if (this.combat) {
            return 2;
        }
        return 1;
    }

    public final int getIsFlightAsInt() {
        if (this.altitude > 0) {
            return 3;
        }

        if (this.getObjectType().equals(GameObjectType.PlayerCharacter))
            if (((PlayerCharacter) this).isLastSwimming())
                return 1; //swimming

        return 2; //ground
    }

    public final void clearTimer(final String name) {
        if (this.timers != null) {
            this.timers.remove(name);
        }
    }

    public abstract Vector3fImmutable getBindLoc();

    public final void setBindLoc(final Vector3fImmutable value) {
        this.bindLoc = value;
    }

    public final Vector3fImmutable getFaceDir() {
        return this.faceDir;
    }

    public final void setFaceDir(final Vector3fImmutable value) {
        this.faceDir = value;
    }

    public final Vector3fImmutable getEndLoc() {
        return this.endLoc;
    }

    public final void setEndLoc(final Vector3fImmutable value) {
        if (value.x > MBServerStatics.MAX_PLAYER_X_LOC)
            return;
        if (value.z < MBServerStatics.MAX_PLAYER_Y_LOC)
            return;

        this.endLoc = value;
        // reset the location timer so our next call to getLoc is correct
        this.resetLastSetLocUpdate();
    }

    public final Vector3fImmutable getNextEndLoc() {
        // this is only used when users are changing their end
        // location while a timer like changeAltitude is ticking down
        return this.nextEndLoc;
    }

    public final void stopMovement(Vector3fImmutable stopLoc) {


        locationLock.writeLock().lock();

        try {
            this.setLoc(stopLoc);
            this.endLoc = Vector3fImmutable.ZERO;
            this.resetLastSetLocUpdate();
        } catch (Exception e) {
            Logger.error(e);
        } finally {
            locationLock.writeLock().unlock();
        }
    }

    public final boolean isMoving() {

        // I might be on my way but my movement is paused
        // due to a flight alt change
        //TODO who the fuck wrote changeHeightJob. FIX THIS.


        if (this.endLoc.equals(Vector3fImmutable.ZERO) || this.endLoc.equals(this.bindLoc))
            return false;

        if (this.takeOffTime != 0)
            return false;

        if (this.isCasting && this.getObjectType().equals(GameObjectType.PlayerCharacter))
            return false;

        return true;
    }

    public final boolean useFlyMoveRegen() {


        if (this.endLoc.x != 0 && this.endLoc.z != 0)
            return true;

        return false;
    }

    public boolean asciiLastName() {
        return true;
    }

    public final ConcurrentHashMap<String, CharacterSkill> getSkills() {
        return this.skills;
    }

    public final ConcurrentHashMap<Integer, CharacterPower> getPowers() {
        return this.powers;
    }

    public final int getInBuilding() {
        return this.inBuilding;
    }

    public final void setInBuilding(final int floor) {
        this.inBuilding = floor;
    }

    public Guild getGuild() {
        return this.guild;
    }

    /*
     * Setters
     */
    public void setGuild(final Guild value) {
        this.guild = value;
    }

    public int getGuildUUID() {
        return this.guild.getObjectUUID();
    }

    public final int getRank() {
        return (this.level / 10);
    }

    public final int getAtrHandOne() {
        return this.atrHandOne;
    }

    public final int getAtrHandTwo() {
        return this.atrHandTwo;
    }

    public final int getMinDamageHandOne() {
        return this.minDamageHandOne;
    }

    public final int getMaxDamageHandOne() {
        return this.maxDamageHandOne;
    }

    public final int getMinDamageHandTwo() {
        return this.minDamageHandTwo;
    }

    public final int getMaxDamageHandTwo() {
        return this.maxDamageHandTwo;
    }

    public final int getDefenseRating() {
        return this.defenseRating;
    }

    public final float getSpeedHandOne() {
        return this.speedHandOne;
    }

    public final float getSpeedHandTwo() {
        return this.speedHandTwo;
    }

    public final float getRange() {

        if (this.getObjectType() == GameObjectType.Mob) {

            Mob mob = (Mob) this;

            if (mob.isSiege()) {
                return 300;
            }
            float range = 8;
            if ((this).charItemManager.equipped.get(EquipSlotType.RHELD) != null) {
                range = ((Mob) this).charItemManager.equipped.get(EquipSlotType.RHELD).template.item_weapon_max_range;
            } else if (((Mob) this).charItemManager.equipped.get(EquipSlotType.LHELD) != null) {
                range = ((Mob) this).charItemManager.equipped.get(EquipSlotType.LHELD).template.item_weapon_max_range;
            }

            // TODO Is this clamp from live?

            if (range > 80)
                range = 80;

            return range;
        }

        if (this.rangeHandOne > this.rangeHandTwo)
            return this.rangeHandOne;

        return this.rangeHandTwo;
    }

    public abstract float getPassiveChance(
            final String type,
            final int attackerLevel,
            final boolean fromCombat);

    public abstract float getSpeed();

    public final int getBankCapacityRemaining() {
        return (AbstractCharacter.getBankCapacity() - this.charItemManager.getBankWeight());
    }

    public final int getVaultCapacityRemaining() {
        return (AbstractCharacter.getVaultCapacity() - this.charItemManager.getVaultWeight());
    }

    public final ArrayList<Item> getInventory() {
        return this.getInventory(false);
    }
    /*
     * Utils
     */

    public final ArrayList<Item> getInventory(final boolean getGold) {
        if (this.charItemManager == null) {
            return new ArrayList<>();
        }
        return this.charItemManager.getInventory(getGold);
    }

    @Override
    public Vector3fImmutable getLoc() {

        return super.getLoc();
    }

    @Override
    public final void setLoc(final Vector3fImmutable value) {

        Building building = BuildingManager.getBuildingAtLocation(this.loc);
        Regions region = null;
        if (building != null) {
            //look for region in the building we are in
            for (Regions regionCycle : building.getBounds().getRegions()) {
                float regionHeight = regionCycle.highLerp.y - regionCycle.lowLerp.y;
                if (regionHeight < 10)
                    regionHeight = 10;
                if (regionCycle.isPointInPolygon(value) && Math.abs(regionCycle.highLerp.y - value.y) < regionHeight)
                    region = regionCycle;
            }
        }
        float regionHeightOffset = 0;
        if (region != null) {
            this.region = region;
            regionHeightOffset = region.lerpY(this);
            this.inBuilding = region.level; // -1 not in building 0 on ground floor, 1 on first floor etc
            this.inBuildingID = region.parentBuildingID;
            this.inFloorID = region.room;
        } else {
            this.region = null;
            this.inBuilding = -1;
            this.inBuildingID = 0;
            this.inFloorID = -1;
        }

        float terrainHeight = Terrain.getWorldHeight(value);
        Vector3fImmutable finalLocation = new Vector3fImmutable(value.x, terrainHeight + regionHeightOffset, value.z);
        super.setLoc(finalLocation); // set the location in the world
        this.resetLastSetLocUpdate();

    }

    public Vector3fImmutable getMovementLoc() {

        if (this.endLoc.equals(Vector3fImmutable.ZERO))
            return super.getLoc();
        if (this.takeOffTime != 0)
            return super.getLoc();

        return super.getLoc().moveTowards(this.endLoc, this.getSpeed() * ((System.currentTimeMillis() - lastSetLocUpdate) * .001f));

    }

    public final void setBindLoc(final float x, final float y, final float z) {
        this.bindLoc = new Vector3fImmutable(x, y, z);
    }

    public final void resetLastSetLocUpdate() {
        this.lastSetLocUpdate = System.currentTimeMillis();
    }

    public void setIsCasting(final boolean isCasting) {
        this.isCasting = isCasting;
    }

    public final boolean isCasting() {
        return this.isCasting;
    }

    @Override
    public final boolean isAlive() {
        return this.isAlive.get();
    }

    public final boolean isSafeMode() {

        if (this.resists == null)
            return false;

        for (Effect eff : this.getEffects().values()) {
            if (eff.getEffectToken() == -1661750486)
                return true;
        }
        return this.resists.immuneToAll();
    }

    public abstract void killCharacter(final AbstractCharacter killer);

    public abstract void killCharacter(final String reason);

    /**
     * Determines if the character is in a lootable state.
     *
     * @return True if lootable.
     */
    public abstract boolean canBeLooted();

    public float calcHitBox() {
        if (this.getObjectType() == GameObjectType.PlayerCharacter) {
            // hit box radius is str/100 (gets diameter of hitbox) /2 (as we want a radius)
            // note this formula is guesswork
            if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG) {
                Logger.info("Hit box radius for " + this.getFirstName() + " is " + (this.statStrCurrent / 200f));
            }
            return ((PlayerCharacter) this).getStrForClient() / 200f;
            //TODO CALCULATE MOB HITBOX BECAUSE FAIL  EMU IS FAIL!!!!!!!
        } else if (this.getObjectType() == GameObjectType.Mob) {
            if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG) {
                Logger.info("Hit box radius for " + this.getFirstName() + " is " + ((Mob) this).getMobBase().getHitBoxRadius());
            }
            return ((Mob) this).getMobBase().getHitBoxRadius();
        }
        return 0f;
    }

    public final boolean isSit() {
        return this.sit;
    }

    public final void setSit(final boolean value) {

        if (this.sit != value) {
            // change sit/stand and sync location
            this.sit = value;
            if (value == true) // we have been told to sit
            {
                this.stopMovement(this.getLoc());
            }
        }

    }

    public final boolean isWalk() {
        return this.walkMode;
    }

    public final boolean isCombat() {
        return this.combat;
    }

    public final void setCombat(final boolean value) {
        this.combat = value;
    }

    public final void setWalkMode(final boolean value) {
        // sync movement location as getLoc gets where we are at the exact moment in time (i.e. not the last updated loc)
        this.setLoc(this.getLoc());
        if (this.walkMode == value) {
            return;
        } else {
            this.walkMode = value;
        }
    }

    public final AbstractWorldObject getCombatTarget() {
        return this.combatTarget;
    }

    public final void setCombatTarget(final AbstractWorldObject value) {

        if (this.getObjectTypeMask() == 2050) {//MOB?
            if (value == null) {
                if (this.isCombat()) {
                    this.setCombat(false);
                    UpdateStateMsg rwss = new UpdateStateMsg();
                    rwss.setPlayer(this);
                    DispatchManager.sendToAllInRange(this, rwss);
                }
            } else {
                if (!this.isCombat()) {
                    this.setCombat(true);
                    UpdateStateMsg rwss = new UpdateStateMsg();
                    rwss.setPlayer(this);
                    DispatchManager.sendToAllInRange(this, rwss);
                }
            }
        }

        this.combatTarget = value;

    }

    public final ConcurrentHashMap<String, JobContainer> getTimers() {
        if (this.timers == null) {
            this.timers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        }
        return this.timers;
    }

    public final int getLiveCounter() {
        return this.liveCounter;
    }

    public final void addTimer(
            final String name,
            final AbstractJob asj,
            final int duration
    ) {
        final JobContainer jc = JobScheduler.getInstance().scheduleJob(asj, duration);
        if (this.timers == null) {
            this.timers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        }
        this.timers.put(name, jc);
    }

    public final void renewTimer(
            final String name,
            final AbstractJob asj,
            final int duration
    ) {
        this.cancelTimer(name);
        this.addTimer(name, asj, duration);
    }

    public final ConcurrentHashMap<Integer, JobContainer> getRecycleTimers() {
        return this.recycleTimers;
    }

    public final ConcurrentHashMap<String, Long> getTimestamps() {
        return this.timestamps;
    }

    public final long getTimeStamp(final String name) {
        if (this.timestamps.containsKey(name)) {
            return this.timestamps.get(name);
        }
        return 0L;
    }

    public final void setTimeStamp(final String name, final long value) {
        this.timestamps.put(name, value);
    }

    public final void setTimeStampNow(final String name) {
        this.timestamps.put(name, System.currentTimeMillis());
    }

    public final void cancelTimer(final String name) {
        cancelTimer(name, true);
    }

    public final void cancelTimer(final String name, final boolean jobRunning) {
        if (this.timers == null) {
            this.timers = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
        }
        if (this.timers.containsKey(name)) {
            if (jobRunning) {
                this.timers.get(name).cancelJob();
            }
            this.timers.remove(name);
        }
    }

    public final float modifyHealth(
            final float value,
            final AbstractCharacter attacker,
            final boolean fromCost) {

        try {

            try {
                boolean ready = this.healthLock.writeLock().tryLock(1, TimeUnit.SECONDS);

                while (!ready)
                    ready = this.healthLock.writeLock().tryLock(1, TimeUnit.SECONDS);

                if (!this.isAlive())
                    return 0;

                Float oldHealth, newHealth;

                if (!this.isAlive())
                    return 0f;

                oldHealth = this.health.get();
                newHealth = oldHealth + value;

                if (newHealth > this.healthMax)
                    newHealth = healthMax;

                this.health.set(newHealth);

                if (newHealth <= 0) {
                    if (this.isAlive.compareAndSet(true, false)) {
                        killCharacter(attacker);
                        return newHealth - oldHealth;
                    } else
                        return 0f; //already dead, don't send damage again
                }                                 // past this lock!

                //TODO why is Handle REtaliate and cancelontakedamage in modifyHealth? shouldnt this be outside this method?
                if (value < 0f && !fromCost) {
                    this.cancelOnTakeDamage();
                    CombatManager.handleRetaliate(this, attacker);
                }

                if (this.getObjectType().equals(GameObjectType.Mob)) {
                    //handle hate value addition
                    Mob target = (Mob) this;
                    if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter)) {

                        if (target.playerAgroMap.containsKey(attacker.getObjectUUID()))
                            target.playerAgroMap.put(attacker.getObjectUUID(), target.playerAgroMap.get(attacker.getObjectUUID()) + value);
                        else
                            target.playerAgroMap.put(attacker.getObjectUUID(), value);


                        if (target.isPlayerGuard()) {
                            if (target.guardedCity != null && target.guardedCity.cityOutlaws.contains(attacker.getObjectUUID()) == false)
                                target.guardedCity.cityOutlaws.add(attacker.getObjectUUID());
                        }
                    }
                } else if (this.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                    City playerCity = ZoneManager.getCityAtLocation(this.loc);
                    if (playerCity != null) {
                        if (!attacker.getGuild().getNation().equals(playerCity.getGuild().getNation()))
                            if (!playerCity.getGuild().getNation().getAllyList().contains(attacker.getGuild().getNation()))
                                if (!playerCity.cityOutlaws.contains(attacker.getObjectUUID()))
                                    playerCity.cityOutlaws.add(attacker.getObjectUUID());
                    }
                }
                return newHealth - oldHealth;
            } finally {
                this.healthLock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return 0;
    }

    public float getCurrentHitpoints() {
        return this.health.get();
    }

    public final float modifyMana(
            final float value,
            final AbstractCharacter attacker
    ) {
        return this.modifyMana(value, attacker, false);
    }

    public final float modifyMana(
            final float value,
            final AbstractCharacter attacker,
            final boolean fromCost
    ) {

        if (!this.isAlive()) {
            return 0f;
        }
        boolean worked = false;
        Float oldMana = 0f, newMana = 0f;
        while (!worked) {
            oldMana = this.mana.get();
            newMana = oldMana + value;
            if (newMana > this.manaMax) {
                newMana = manaMax;
            } else if (newMana < 0) {
                newMana = 0f;
            }
            worked = this.mana.compareAndSet(oldMana, newMana);
        }
        if (value < 0f && !fromCost) {
            this.cancelOnTakeDamage();
            CombatManager.handleRetaliate(this, attacker);
        }
        return newMana - oldMana;
    }

    /*
     * Serializing
     */

    public final float modifyStamina(
            final float value,
            final AbstractCharacter attacker
    ) {
        return this.modifyStamina(value, attacker, false);
    }

    public final float modifyStamina(
            final float value,
            final AbstractCharacter attacker,
            final boolean fromCost
    ) {

        if (!this.isAlive()) {
            return 0f;
        }
        boolean worked = false;
        Float oldStamina = 0f, newStamina = 0f;
        while (!worked) {
            oldStamina = this.stamina.get();
            newStamina = oldStamina + value;
            if (newStamina > this.staminaMax) {
                newStamina = staminaMax;
            } else if (newStamina < 0) {
                newStamina = 0f;
            }
            worked = this.stamina.compareAndSet(oldStamina, newStamina);
        }
        if (value < 0f && !fromCost) {
            this.cancelOnTakeDamage();
            CombatManager.handleRetaliate(this, attacker);
        }
        return newStamina - oldStamina;
    }

    public final float setMana(
            final float value,
            final AbstractCharacter attacker
    ) {
        return setMana(value, attacker, false);
    }

    public final float setMana(
            final float value,
            final AbstractCharacter attacker,
            final boolean fromCost
    ) {

        if (!this.isAlive()) {
            return 0f;
        }
        boolean worked = false;
        Float oldMana = 0f, newMana = 0f;
        while (!worked) {
            oldMana = this.mana.get();
            newMana = value;
            if (newMana > this.manaMax) {
                newMana = manaMax;
            } else if (newMana < 0) {
                newMana = 0f;
            }
            worked = this.mana.compareAndSet(oldMana, newMana);
        }
        if (oldMana > newMana && !fromCost) {
            this.cancelOnTakeDamage();
            CombatManager.handleRetaliate(this, attacker);
        }
        return newMana - oldMana;
    }

    public final float setStamina(
            final float value,
            final AbstractCharacter attacker
    ) {
        return setStamina(value, attacker, false);
    }

    public final float setStamina(
            final float value,
            final AbstractCharacter attacker,
            final boolean fromCost
    ) {

        if (!this.isAlive()) {
            return 0f;
        }
        boolean worked = false;
        Float oldStamina = 0f, newStamina = 0f;
        while (!worked) {
            oldStamina = this.stamina.get();
            newStamina = value;
            if (newStamina > this.staminaMax) {
                newStamina = staminaMax;
            } else if (newStamina < 0) {
                newStamina = 0f;
            }
            worked = this.stamina.compareAndSet(oldStamina, newStamina);
        }
        if (oldStamina > newStamina && !fromCost) {
            this.cancelOnTakeDamage();
            CombatManager.handleRetaliate(this, attacker);
        }
        return newStamina - oldStamina;

    }

    public final float getStamina() {
        if (this.getObjectType() == GameObjectType.Mob)
            return this.getStaminaMax();
        return this.stamina.get();
    }

    public final float getMana() {
        if (this.getObjectType() == GameObjectType.Mob)
            return this.getManaMax();
        return this.mana.get();
    }

    public final float getStaminaMax() {
        if (this.getObjectType() == GameObjectType.Mob)
            return 2000;
        return this.staminaMax;
    }

    public final float getManaMax() {
        if (this.getObjectType() == GameObjectType.Mob)
            return 2000;
        return this.manaMax;
    }

    public final PlayerBonuses getBonuses() {
        return this.bonuses;
    }

    public void teleport(final Vector3fImmutable targetLoc) {
        locationLock.writeLock().lock();
        try {
            MovementManager.translocate(this, targetLoc);
            MovementManager.sendRWSSMsg(this);
        } catch (Exception e) {
            Logger.error(e);
        } finally {
            locationLock.writeLock().unlock();
        }
    }

    /*
     * Cancel effects upon actions
     */
    public final void cancelOnAttack() { // added to one spot

        boolean changed = false;

        for (String s : this.effects.keySet()) {

            Effect eff = this.effects.get(s);

            if (eff == null)
                continue;
            if (eff.cancelOnAttack() && eff.cancel()) {
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }

        if (changed) {
            applyBonuses();
        }

        PowersManager.cancelOnAttack(this);
    }

    public final void cancelOnAttackSwing() { // added
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.cancelOnAttackSwing() && eff.cancel()) {
                //System.out.println("canceling on AttackSwing");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnAttackSwing(this);
    }

    public final void cancelOnCast() {
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);

            if (eff == null)
                continue;
            if (eff.cancelOnCast() && eff.cancel()) {

                // Don't cancel the track effect on the character being tracked
                if (eff.getJob() != null && eff.getJob() instanceof TrackJob) {
                    if (((TrackJob) eff.getJob()).getSource().getObjectUUID()
                            == this.getObjectUUID()) {
                        continue;
                    }
                }

                //System.out.println("canceling on Cast");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnCast(this);
    }

    public final void cancelOnSpell() {
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.cancelOnCastSpell() && eff.cancel()) {
                //System.out.println("canceling on CastSpell");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnSpell(this);
    }

    public final void cancelOnMove() { // added
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.cancelOnMove() && eff.cancel()) {
                //System.out.println("canceling on Move");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnMove(this);
    }

    public final void cancelOnSit() { // added
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.cancelOnSit() && eff.cancel()) {
                //System.out.println("canceling on Sit");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnSit(this);
    }

    public final void cancelOnTakeDamage() {
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.cancelOnTakeDamage() && eff.cancel()) {
                //System.out.println("canceling on Take Damage");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnTakeDamage(this);
    }

    public final void cancelOnTakeDamage(final DamageType type, final float amount) {
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.cancelOnTakeDamage(type, amount) && eff.cancel()) {
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
    }

    public final Effect getDamageAbsorber() {
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.isDamageAbsorber()) {
                return eff;
            }
        }
        return null;
    }

    public final void cancelOnUnEquip() {
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);
            if (eff == null)
                continue;
            if (eff.cancelOnUnEquip() && eff.cancel()) {
                //System.out.println("canceling on UnEquip");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnUnEquip(this);
    }

    public final void cancelOnStun() {
        boolean changed = false;
        for (String s : this.effects.keySet()) {
            Effect eff = this.effects.get(s);

            if (eff == null) {
                Logger.error("null effect for " + this.getObjectUUID() + " : effect " + s);
                continue;
            }
            if (eff.cancelOnStun() && eff.cancel()) {
                //System.out.println("canceling on Stun");
                eff.cancelJob();
                this.effects.remove(s);
                changed = true;
            }
        }
        if (changed) {
            applyBonuses();
        }
        PowersManager.cancelOnStun(this);
    }

    //Call to apply any new effects to player
    public synchronized void applyBonuses() {
        PlayerCharacter player;
        //tell the player to applyBonuses because something has changed

        //start running the bonus calculations

        try {
            runBonuses();

            // Check if calculations affected flight.

            if (this.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                player = (PlayerCharacter) this;

                // Ground players who cannot fly but are currently flying

                if (CanFly(player) == false &&
                        player.getMovementState().equals(MovementState.FLYING))
                    PlayerCharacter.GroundPlayer(player);
            }

        } catch (Exception e) {
            Logger.error("Error in run bonuses for object UUID " + this.getObjectUUID());
            Logger.error(e);
        }
    }

    //Don't call this function directly. linked from ac.applyBonuses()
    //through BonusCalcJob. Designed to only run from one worker thread
    public final void runBonuses() {
        // synchronized with getBonuses()
        synchronized (this.bonuses) {
            try {
                //run until no new bonuses are applied

                // clear bonuses and reapply rune bonuses
                if (this.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                    this.bonuses.calculateRuneBaseEffects((PlayerCharacter) this);
                } else {
                    this.bonuses.clearRuneBaseEffects();
                }

                // apply effect bonuses
                for (Effect eff : this.effects.values()) {
                    eff.applyBonus(this);
                }

                //apply item bonuses for equipped items
                ConcurrentHashMap<EquipSlotType, Item> equip = null;

                if (this.charItemManager != null) {
                    equip = this.charItemManager.getEquipped();
                }
                if (equip != null) {
                    for (Item item : equip.values()) {
                        item.clearBonuses();
                        if (item != null) {
                            ConcurrentHashMap<String, Effect> effects = item.getEffects();
                            if (effects != null) {
                                for (Effect eff : effects.values()) {
                                    eff.applyBonus(item, this);
                                }
                            }
                        }
                    }
                }

                //recalculate passive defenses
                if (this.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                    ((PlayerCharacter) this).setPassives();
                }


                // recalculate everything
                if (this.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                    PlayerCharacter pc = (PlayerCharacter) this;

                    //calculate item bonuses
                    calculateItemBonuses(pc);

                    //recalculate formulas
                    pc.recalculatePlayerStats(true);


                } else if (this.getObjectType().equals(GameObjectType.Mob)) {
                    Mob mob = (Mob) this;

                    //recalculate formulas
                    mob.recalculateStats();
                }
            } catch (Exception e) {
                Logger.error(e);
            }
        }
    }

    public int getInBuildingID() {
        return inBuildingID;
    }

    public void setInBuildingID(int inBuildingID) {
        this.inBuildingID = inBuildingID;
    }

    public int getInFloorID() {
        return inFloorID;
    }

    public void setInFloorID(int inFloorID) {
        this.inFloorID = inFloorID;
    }

    public float getDesiredAltitude() {
        return desiredAltitude;
    }

    public void setDesiredAltitude(float desiredAltitude) {
        this.desiredAltitude = desiredAltitude;
    }

    public long getTakeOffTime() {
        return takeOffTime;
    }

    public void setTakeOffTime(long takeOffTime) {
        this.takeOffTime = takeOffTime;
    }

    public boolean isItemCasting() {
        return itemCasting;
    }

    public void setItemCasting(boolean itemCasting) {
        this.itemCasting = itemCasting;
    }

    //updates
    public void update() {
    }

    public void updateRegen() {
    }

    public void updateMovementState() {
    }

    public void updateLocation() {
    }

    public void updateFlight() {
    }

    public void dynamicUpdate(UpdateType updateType) {
        if (this.updateLock.writeLock().tryLock()) {
            try {
                switch (updateType) {
                    case ALL:
                        update();
                        break;
                    case REGEN:
                        updateRegen();
                        break;
                    case LOCATION:
                        update();
                        break;
                    case MOVEMENTSTATE:
                        update();
                        break;
                    case FLIGHT:
                        updateFlight();
                        break;
                }

            } catch (Exception e) {
                Logger.error(e);
            } finally {
                this.updateLock.writeLock().unlock();
            }
        }

    }

    public boolean isMovingUp() {
        return movingUp;
    }

    public void setMovingUp(boolean movingUp) {
        this.movingUp = movingUp;
    }
}
