// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.npc.movement.controllers;

import com.hypixel.hytale.server.npc.NPCPlugin;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
import com.hypixel.hytale.server.core.entity.InteractionChain;
import com.hypixel.hytale.server.core.modules.collision.CollisionConfig;
import com.hypixel.hytale.server.core.modules.collision.BoxBlockIntersectionEvaluator;
import com.hypixel.hytale.protocol.GameMode;
import com.hypixel.hytale.server.core.entity.entities.player.movement.MovementManager;
import com.hypixel.hytale.server.core.modules.physics.component.PhysicsValues;
import com.hypixel.hytale.server.core.modules.collision.CollisionModuleConfig;
import com.hypixel.hytale.server.core.modules.collision.CollisionModule;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent;
import com.hypixel.hytale.server.npc.role.RoleDebugFlags;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause;
import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
import com.hypixel.hytale.server.core.entity.Entity;
import com.hypixel.hytale.server.core.modules.collision.BlockCollisionData;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple;
import com.hypixel.hytale.server.core.modules.splitvelocity.SplitVelocity;
import com.hypixel.hytale.server.core.modules.splitvelocity.VelocityConfig;
import java.util.function.BiPredicate;
import com.hypixel.hytale.server.core.asset.modifiers.MovementEffects;
import com.hypixel.hytale.server.core.modules.interaction.InteractionModule;
import com.hypixel.hytale.server.core.entity.InteractionManager;
import com.hypixel.hytale.protocol.Rangef;
import com.hypixel.hytale.server.core.asset.type.model.config.camera.CameraSettings;
import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent;
import com.hypixel.hytale.server.core.modules.physics.util.PhysicsMath;
import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation;
import com.hypixel.hytale.server.npc.util.NPCPhysicsMath;
import java.util.logging.Level;
import com.hypixel.hytale.server.npc.movement.Steering;
import com.hypixel.hytale.protocol.MovementStates;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.math.vector.Vector3f;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import java.util.Objects;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.server.core.asset.type.model.config.Model;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Ref;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.server.npc.movement.controllers.builders.BuilderMotionControllerBase;
import com.hypixel.hytale.server.npc.asset.builder.BuilderSupport;
import com.hypixel.hytale.protocol.MovementSettings;
import com.hypixel.hytale.server.npc.movement.MotionKind;
import com.hypixel.hytale.server.npc.movement.NavState;
import java.util.List;
import com.hypixel.hytale.server.core.modules.collision.CollisionResult;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.vector.Vector3d;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.npc.role.Role;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.npc.entities.NPCEntity;
import com.hypixel.hytale.logger.HytaleLogger;

public abstract class MotionControllerBase implements MotionController
{
    public static final double FORCE_SCALE = 5.0;
    public static final double BISECT_DIST = 0.05;
    public static final double FILTER_COEFFICIENT = 0.7;
    public static final double DOT_PRODUCT_EPSILON = 0.001;
    public static final double DEFAULT_BLOCK_DRAG = 0.82;
    protected static final HytaleLogger LOGGER;
    public static final boolean DEBUG_APPLIED_FORCES = false;
    @Nonnull
    protected final NPCEntity entity;
    protected final String type;
    protected final double epsilonSpeed;
    protected final float epsilonAngle;
    protected final double forceVelocityDamping;
    protected final double maxHorizontalSpeed;
    protected final double fastMotionThreshold;
    protected final double fastMotionThresholdRange;
    protected final float maxHeadRotationSpeed;
    protected Role role;
    protected double inertia;
    protected double knockbackScale;
    protected double gravity;
    @Nullable
    protected float[] headPitchAngleRange;
    protected boolean debugModeSteer;
    protected boolean debugModeMove;
    protected boolean debugModeCollisions;
    protected boolean debugModeBlockCollisions;
    protected boolean debugModeProbeBlockCollisions;
    protected boolean debugModeValidatePositions;
    protected boolean debugModeOverlaps;
    protected boolean debugModeValidateMath;
    protected final Vector3d position;
    protected final Box collisionBoundingBox;
    protected final CollisionResult collisionResult;
    protected final Vector3d translation;
    protected final Vector3d bisectValidPosition;
    protected final Vector3d bisectInvalidPosition;
    protected final Vector3d lastValidPosition;
    protected final Vector3d forceVelocity;
    protected final Vector3d appliedForce;
    protected boolean ignoreDamping;
    protected final List<AppliedVelocity> appliedVelocities;
    protected boolean isObstructed;
    protected NavState navState;
    protected double throttleDuration;
    protected double targetDeltaSquared;
    protected boolean recomputePath;
    protected final Vector3d worldNormal;
    protected final Vector3d worldAntiNormal;
    protected final Vector3d componentSelector;
    protected final Vector3d planarComponentSelector;
    protected boolean enableTriggers;
    protected boolean enableBlockDamage;
    protected boolean isReceivingBlockDamage;
    protected boolean isAvoidingBlockDamage;
    protected boolean requiresPreciseMovement;
    protected boolean requiresDepthProbing;
    protected boolean havePreciseMovementTarget;
    @Nonnull
    protected Vector3d preciseMovementTarget;
    protected boolean isRelaxedMoveConstraints;
    protected boolean isBlendingHeading;
    protected double blendHeading;
    protected boolean haveBlendHeadingPosition;
    @Nonnull
    protected Vector3d blendHeadingPosition;
    protected double blendLevelAtTargetPosition;
    protected boolean fastMotionKind;
    protected boolean idleMotionKind;
    protected boolean horizontalIdleKind;
    protected double moveSpeed;
    protected double previousSpeed;
    protected MotionKind motionKind;
    protected MotionKind lastMovementStateUpdatedMotionKind;
    protected MotionKind previousMotionKind;
    protected double effectHorizontalSpeedMultiplier;
    protected boolean cachedMovementBlocked;
    private float yaw;
    private float pitch;
    private float roll;
    private final Vector3d beforeTriggerForce;
    private final Vector3d beforeTriggerPosition;
    private boolean processTriggersHasMoved;
    protected MovementSettings movementSettings;
    
    public MotionControllerBase(@Nonnull final BuilderSupport builderSupport, @Nonnull final BuilderMotionControllerBase builder) {
        this.position = new Vector3d();
        this.collisionBoundingBox = new Box();
        this.collisionResult = new CollisionResult();
        this.translation = new Vector3d();
        this.bisectValidPosition = new Vector3d();
        this.bisectInvalidPosition = new Vector3d();
        this.lastValidPosition = new Vector3d();
        this.forceVelocity = new Vector3d();
        this.appliedForce = new Vector3d();
        this.appliedVelocities = new ObjectArrayList<AppliedVelocity>();
        this.worldNormal = Vector3d.UP;
        this.worldAntiNormal = Vector3d.DOWN;
        this.componentSelector = new Vector3d(1.0, 0.0, 1.0);
        this.planarComponentSelector = new Vector3d(1.0, 0.0, 1.0);
        this.enableTriggers = true;
        this.enableBlockDamage = true;
        this.isAvoidingBlockDamage = true;
        this.preciseMovementTarget = new Vector3d();
        this.blendHeadingPosition = new Vector3d();
        this.blendLevelAtTargetPosition = 0.5;
        this.beforeTriggerForce = new Vector3d();
        this.beforeTriggerPosition = new Vector3d();
        this.entity = builderSupport.getEntity();
        this.type = builder.getType();
        this.epsilonSpeed = builder.getEpsilonSpeed();
        this.epsilonAngle = builder.getEpsilonAngle();
        this.forceVelocityDamping = builder.getForceVelocityDamping();
        this.maxHorizontalSpeed = builder.getMaxHorizontalSpeed(builderSupport);
        this.fastMotionThreshold = builder.getFastHorizontalThreshold(builderSupport);
        this.fastMotionThresholdRange = builder.getFastHorizontalThresholdRange();
        this.maxHeadRotationSpeed = builder.getMaxHeadRotationSpeed(builderSupport);
        this.setInertia(1.0);
        this.setKnockbackScale(1.0);
        this.setGravity(10.0);
    }
    
    @Override
    public Role getRole() {
        return this.role;
    }
    
    @Override
    public void setRole(final Role role) {
        this.role = role;
    }
    
    @Override
    public void setInertia(final double inertia) {
        this.inertia = Math.max(inertia, 1.0E-4);
    }
    
    @Override
    public void setKnockbackScale(final double knockbackScale) {
        this.knockbackScale = Math.max(0.0, knockbackScale);
    }
    
    @Override
    public void updateModelParameters(final Ref<EntityStore> ref, final Model model, @Nonnull final Box boundingBox, final ComponentAccessor<EntityStore> componentAccessor) {
        Objects.requireNonNull(boundingBox, "updateModelParameters: MotionController needs a bounding box");
        this.collisionBoundingBox.assign(boundingBox);
    }
    
    @Override
    public void setHeadPitchAngleRange(final float[] headPitchAngleRange) {
        if (headPitchAngleRange == null) {
            this.headPitchAngleRange = null;
            return;
        }
        assert headPitchAngleRange.length == 2;
        this.headPitchAngleRange = headPitchAngleRange.clone();
    }
    
    protected void readEntityPosition(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType());
        assert transformComponent != null;
        final Vector3f bodyRotation = transformComponent.getRotation();
        this.position.assign(transformComponent.getPosition());
        this.yaw = bodyRotation.getY();
        this.pitch = bodyRotation.getPitch();
        this.roll = bodyRotation.getRoll();
        this.adjustReadPosition(ref, componentAccessor);
        this.postReadPosition(ref, componentAccessor);
    }
    
    public void postReadPosition(final Ref<EntityStore> ref, final ComponentAccessor<EntityStore> componentAccessor) {
    }
    
    public void moveEntity(@Nonnull final Ref<EntityStore> ref, final double dt, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType());
        assert transformComponent != null;
        this.adjustWritePosition(ref, dt, componentAccessor);
        final Vector3f bodyRotation = transformComponent.getRotation();
        bodyRotation.setYaw(this.yaw);
        bodyRotation.setPitch(this.pitch);
        bodyRotation.setRoll(this.roll);
        this.entity.moveTo(ref, this.position.x, this.position.y, this.position.z, componentAccessor);
    }
    
    public float getYaw() {
        return this.yaw;
    }
    
    public float getPitch() {
        return this.pitch;
    }
    
    public float getRoll() {
        return this.roll;
    }
    
    public boolean touchesWater(final boolean defaultValue, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final World world = componentAccessor.getExternalData().getWorld();
        final ChunkStore chunkStore = world.getChunkStore();
        final long chunkIndex = ChunkUtil.indexChunkFromBlock(this.position.getX(), this.position.getZ());
        final Ref<ChunkStore> chunkRef = chunkStore.getChunkReference(chunkIndex);
        if (chunkRef == null || !chunkRef.isValid()) {
            return defaultValue;
        }
        final WorldChunk worldChunkComponent = chunkStore.getStore().getComponent(chunkRef, WorldChunk.getComponentType());
        assert worldChunkComponent != null;
        final int blockX = MathUtil.floor(this.position.getX());
        final int blockY = MathUtil.floor(this.position.getY() + this.collisionBoundingBox.min.y);
        final int blockZ = MathUtil.floor(this.position.getZ());
        final int fluidId = worldChunkComponent.getFluidId(blockX, blockY, blockZ);
        return fluidId != 0;
    }
    
    @Override
    public void updateMovementState(@Nonnull final Ref<EntityStore> ref, @Nonnull final MovementStates movementStates, @Nonnull final Steering steering, @Nonnull final Vector3d velocity, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final boolean lastFastMotion = movementStates.running;
        movementStates.climbing = false;
        movementStates.swimJumping = false;
        movementStates.inFluid = this.touchesWater(movementStates.inFluid, componentAccessor);
        movementStates.onGround = this.role.isOnGround();
        double speed = this.waypointDistance(Vector3d.ZERO, velocity);
        speed = 0.7 * this.previousSpeed + 0.30000000000000004 * speed;
        this.previousSpeed = speed;
        this.fastMotionKind = this.isFastMotionKind(speed);
        this.idleMotionKind = steering.getTranslation().equals(Vector3d.ZERO);
        this.horizontalIdleKind = this.isHorizontalIdle(speed);
        if (this.motionKind != this.lastMovementStateUpdatedMotionKind || lastFastMotion != this.fastMotionKind || movementStates.idle != this.idleMotionKind || movementStates.horizontalIdle != this.horizontalIdleKind) {
            switch (this.motionKind) {
                case FLYING: {
                    this.updateFlyingStates(movementStates, this.idleMotionKind, this.fastMotionKind);
                    break;
                }
                case SWIMMING: {
                    this.updateSwimmingStates(movementStates, this.idleMotionKind, this.fastMotionKind, this.horizontalIdleKind);
                    break;
                }
                case SWIMMING_TURNING: {
                    this.updateSwimmingStates(movementStates, false, true, false);
                    break;
                }
                case ASCENDING: {
                    this.updateAscendingStates(ref, movementStates, this.fastMotionKind, this.horizontalIdleKind, componentAccessor);
                    break;
                }
                case MOVING: {
                    updateMovingStates(ref, movementStates, this.fastMotionKind, componentAccessor);
                    break;
                }
                case DESCENDING: {
                    final NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType());
                    assert npcComponent != null;
                    this.updateDescendingStates(ref, movementStates, this.fastMotionKind, npcComponent.getHoverHeight() > 0.0, componentAccessor);
                    break;
                }
                case DROPPING: {
                    this.updateDroppingStates(movementStates);
                    break;
                }
                default: {
                    final NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType());
                    assert npcComponent != null;
                    this.updateStandingStates(movementStates, this.motionKind, npcComponent.getHoverHeight() > 0.0);
                    break;
                }
            }
        }
        this.lastMovementStateUpdatedMotionKind = this.motionKind;
    }
    
    protected abstract boolean isFastMotionKind(final double p0);
    
    protected void updateFlyingStates(@Nonnull final MovementStates movementStates, final boolean idle, final boolean fastMotionKind) {
        movementStates.flying = true;
        movementStates.idle = idle;
        movementStates.horizontalIdle = false;
        movementStates.walking = !fastMotionKind;
        movementStates.running = fastMotionKind;
        movementStates.falling = false;
        movementStates.swimming = false;
        movementStates.jumping = false;
    }
    
    protected void updateSwimmingStates(@Nonnull final MovementStates movementStates, final boolean idle, final boolean fastMotionKind, final boolean horizontalIdleKind) {
        movementStates.flying = false;
        movementStates.idle = idle;
        movementStates.horizontalIdle = horizontalIdleKind;
        movementStates.walking = !fastMotionKind;
        movementStates.running = fastMotionKind;
        movementStates.falling = false;
        movementStates.swimming = true;
        movementStates.jumping = false;
    }
    
    protected static void updateMovingStates(@Nonnull final Ref<EntityStore> ref, @Nonnull final MovementStates movementStates, final boolean fastMotionKind, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType());
        assert npcComponent != null;
        movementStates.flying = (npcComponent.getHoverHeight() > 0.0);
        movementStates.idle = false;
        movementStates.horizontalIdle = false;
        movementStates.falling = false;
        movementStates.walking = !fastMotionKind;
        movementStates.running = fastMotionKind;
        movementStates.swimming = false;
        movementStates.jumping = false;
    }
    
    protected void updateAscendingStates(@Nonnull final Ref<EntityStore> ref, @Nonnull final MovementStates movementStates, final boolean fastMotionKind, final boolean horizontalIdleKind, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        updateMovingStates(ref, movementStates, fastMotionKind, componentAccessor);
    }
    
    protected void updateDescendingStates(@Nonnull final Ref<EntityStore> ref, @Nonnull final MovementStates movementStates, final boolean fastMotionKind, final boolean hovering, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        updateMovingStates(ref, movementStates, fastMotionKind, componentAccessor);
    }
    
    protected void updateDroppingStates(@Nonnull final MovementStates movementStates) {
        movementStates.falling = true;
    }
    
    protected void updateStandingStates(@Nonnull final MovementStates movementStates, @Nonnull final MotionKind motionKind, final boolean hovering) {
        movementStates.flying = hovering;
        movementStates.idle = true;
        movementStates.horizontalIdle = true;
        movementStates.walking = false;
        movementStates.running = false;
        movementStates.falling = false;
        movementStates.swimming = false;
        movementStates.jumping = false;
    }
    
    @Override
    public double steer(@Nonnull final Ref<EntityStore> ref, @Nonnull final Role role, @Nonnull final Steering bodySteering, @Nonnull final Steering headSteering, final double interval, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        this.readEntityPosition(ref, componentAccessor);
        if (this.debugModeSteer) {
            double dx = this.position.x;
            double dz = this.position.z;
            final double st = this.steer0(ref, role, bodySteering, headSteering, interval, componentAccessor);
            final double t = interval - st;
            dx = this.position.x - dx;
            dz = this.position.z - dz;
            final double l = Math.sqrt(dx * dx + dz * dz);
            final double v = (t > 0.0) ? (l / t) : 0.0;
            MotionControllerBase.LOGGER.at(Level.INFO).log("==  Steer %s  = t =%.4f dt=%.4f h =%.4f l =%.4f v =%.4f motion=%s", this.getType(), interval, t, 57.295776f * this.yaw, l, v, role.getSteeringMotionName());
            return st;
        }
        return this.steer0(ref, role, bodySteering, headSteering, interval, componentAccessor);
    }
    
    public double steer0(@Nonnull final Ref<EntityStore> ref, @Nonnull final Role role, @Nonnull final Steering bodySteering, @Nonnull final Steering headSteering, final double interval, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final World world = componentAccessor.getExternalData().getWorld();
        final NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType());
        assert npcComponent != null;
        this.effectHorizontalSpeedMultiplier = npcComponent.getCurrentHorizontalSpeedMultiplier(ref, componentAccessor);
        this.setAvoidingBlockDamage(this.isAvoidingBlockDamage && !this.isReceivingBlockDamage);
        this.translation.assign(0.0);
        this.cachedMovementBlocked = this.isMovementBlocked(ref, componentAccessor);
        this.computeMove(ref, role, bodySteering, interval, this.translation, componentAccessor);
        if (this.debugModeValidateMath && !NPCPhysicsMath.isValid(this.translation)) {
            throw new IllegalArgumentException(String.valueOf(this.translation));
        }
        if (this.translation.squaredLength() > 1000000.0) {
            if (this.debugModeValidateMath) {
                MotionControllerBase.LOGGER.at(Level.WARNING).log("NPC with role %s has abnormal high speed! (Distance=%s, MotionController=%s)", role.getRoleName(), this.translation.length(), this.type);
            }
            this.translation.assign(Vector3d.ZERO);
        }
        this.executeMove(ref, role, interval, this.translation, componentAccessor);
        this.postExecuteMove();
        this.clearRequirePreciseMovement();
        this.clearRequireDepthProbing();
        this.clearBlendHeading();
        this.setAvoidingBlockDamage(!this.isReceivingBlockDamage);
        this.setRelaxedMoveConstraints(false);
        final float maxBodyRotation = (float)(interval * this.getCurrentMaxBodyRotationSpeed() * bodySteering.getRelativeTurnSpeed());
        final float maxHeadRotation = (float)(interval * this.maxHeadRotationSpeed * headSteering.getRelativeTurnSpeed() * this.effectHorizontalSpeedMultiplier);
        this.calculateYaw(ref, bodySteering, headSteering, maxHeadRotation, maxBodyRotation, componentAccessor);
        this.calculatePitch(ref, bodySteering, headSteering, maxHeadRotation, componentAccessor);
        this.calculateRoll(bodySteering, headSteering);
        this.moveEntity(ref, interval, componentAccessor);
        final HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType());
        assert headRotationComponent != null;
        final Vector3f headRotation = headRotationComponent.getRotation();
        headRotation.setYaw(headSteering.getYaw());
        headRotation.setPitch(headSteering.getPitch());
        headRotation.setRoll(headSteering.getRoll());
        if (!this.forceVelocity.equals(Vector3d.ZERO) && !this.ignoreDamping) {
            final double movementThresholdSquared = 1.0000000000000002E-10;
            if (this.forceVelocity.squaredLength() >= movementThresholdSquared) {
                this.dampForceVelocity(this.forceVelocity, this.forceVelocityDamping, interval, componentAccessor);
            }
            else {
                this.forceVelocity.assign(Vector3d.ZERO);
            }
        }
        final double clientTps = 60.0;
        final int serverTps = world.getTps();
        final double rate = clientTps / serverTps;
        final boolean dampenY = this.shouldDampenAppliedVelocitiesY();
        final boolean useGroundResistance = this.shouldAlwaysUseGroundResistance() || this.onGround();
        for (int i = 0; i < this.appliedVelocities.size(); ++i) {
            final AppliedVelocity entry = this.appliedVelocities.get(i);
            float min;
            float max;
            if (useGroundResistance) {
                min = entry.config.getGroundResistance();
                max = entry.config.getGroundResistanceMax();
            }
            else {
                min = entry.config.getAirResistance();
                max = entry.config.getAirResistanceMax();
            }
            float resistance = min;
            if (max >= 0.0f) {
                resistance = switch (entry.config.getStyle()) {
                    default -> throw new MatchException(null, null);
                    case Linear -> {
                        final float len = (float)entry.velocity.length();
                        if (len < entry.config.getThreshold()) {
                            final float mul = len / entry.config.getThreshold();
                            yield min * mul + max * (1.0f - mul);
                        }
                        yield min;
                    }
                    case Exp -> {
                        final float len = (float)entry.velocity.squaredLength();
                        if (len < entry.config.getThreshold() * entry.config.getThreshold()) {
                            final float mul = len / (entry.config.getThreshold() * entry.config.getThreshold());
                            yield min * mul + max * (1.0f - mul);
                        }
                        yield min;
                    }
                };
            }
            final double resistanceScale = Math.pow(resistance, rate);
            final Vector3d velocity = entry.velocity;
            velocity.x *= resistanceScale;
            final Vector3d velocity2 = entry.velocity;
            velocity2.z *= resistanceScale;
            if (dampenY) {
                final Vector3d velocity3 = entry.velocity;
                velocity3.y *= resistanceScale;
            }
        }
        this.appliedVelocities.removeIf(v -> v.velocity.squaredLength() < 0.001);
        return interval;
    }
    
    protected boolean shouldDampenAppliedVelocitiesY() {
        return false;
    }
    
    protected boolean shouldAlwaysUseGroundResistance() {
        return false;
    }
    
    protected void calculateYaw(@Nonnull final Ref<EntityStore> ref, @Nonnull final Steering bodySteering, @Nonnull final Steering headSteering, final float maxHeadRotation, final float maxBodyRotation, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (bodySteering.hasYaw()) {
            this.yaw = bodySteering.getYaw();
        }
        else if (NPCPhysicsMath.dotProduct(this.translation.x, 0.0, this.translation.z) > 0.001) {
            this.yaw = PhysicsMath.headingFromDirection(this.translation.x, this.translation.z);
        }
        final boolean hasHeadSteering = headSteering.hasYaw();
        if (!hasHeadSteering) {
            headSteering.setYaw(this.yaw);
        }
        final HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType());
        assert headRotationComponent != null;
        final ModelComponent modelComponent = componentAccessor.getComponent(ref, ModelComponent.getComponentType());
        assert modelComponent != null;
        final Vector3f headRotation = headRotationComponent.getRotation();
        final float currentYaw = headRotation.getYaw();
        final float targetYaw = headSteering.getYaw();
        final float turnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentYaw, targetYaw), -maxHeadRotation, maxHeadRotation);
        headSteering.setYaw(PhysicsMath.normalizeTurnAngle(currentYaw + turnAngle));
        if (hasHeadSteering) {
            final float yawOffset = MathUtil.wrapAngle(headSteering.getYaw() - this.yaw);
            final CameraSettings headRotationRestrictions = modelComponent.getModel().getCamera();
            float yawMin;
            float yawMax;
            if (headRotationRestrictions != null && headRotationRestrictions.getYaw() != null && headRotationRestrictions.getYaw().getAngleRange() != null) {
                final Rangef yawRange = headRotationRestrictions.getYaw().getAngleRange();
                yawMin = yawRange.min * 0.017453292f;
                yawMax = yawRange.max * 0.017453292f;
            }
            else {
                yawMin = -0.7853982f;
                yawMax = 0.7853982f;
            }
            if (yawOffset > yawMax) {
                final float initialBodyYaw = this.yaw;
                if (!bodySteering.hasYaw()) {
                    this.yaw = this.blendBodyYaw(ref, yawOffset, maxBodyRotation, componentAccessor);
                }
                headSteering.setYaw(MathUtil.wrapAngle(initialBodyYaw + yawMax));
            }
            else if (yawOffset < yawMin) {
                final float initialBodyYaw = this.yaw;
                if (!bodySteering.hasYaw()) {
                    this.yaw = this.blendBodyYaw(ref, yawOffset, maxBodyRotation, componentAccessor);
                }
                headSteering.setYaw(MathUtil.wrapAngle(initialBodyYaw + yawMin));
            }
        }
    }
    
    protected float blendBodyYaw(@Nonnull final Ref<EntityStore> ref, final float yawOffset, final float maxBodyRotation, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType());
        assert transformComponent != null;
        final Vector3f bodyRotation = transformComponent.getRotation();
        final float currentBodyYaw = bodyRotation.getYaw();
        final float targetBodyYaw = MathUtil.wrapAngle(this.yaw + yawOffset);
        final float bodyTurnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentBodyYaw, targetBodyYaw), -maxBodyRotation, maxBodyRotation);
        return MathUtil.wrapAngle(this.yaw + bodyTurnAngle);
    }
    
    protected void calculatePitch(@Nonnull final Ref<EntityStore> ref, @Nonnull final Steering bodySteering, @Nonnull final Steering headSteering, final float maxHeadRotation, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (bodySteering.hasPitch()) {
            this.pitch = bodySteering.getPitch();
        }
        else if (NPCPhysicsMath.dotProduct(this.translation.x, this.translation.y, this.translation.z) > 0.001) {
            this.pitch = PhysicsMath.pitchFromDirection(this.translation.x, this.translation.y, this.translation.z);
        }
        final boolean hasHeadSteering = headSteering.hasPitch();
        if (!hasHeadSteering) {
            headSteering.setPitch(this.pitch);
        }
        final HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType());
        assert headRotationComponent != null;
        final Vector3f headRotation = headRotationComponent.getRotation();
        final float currentPitch = headRotation.getPitch();
        final float targetPitch = headSteering.getPitch();
        final float turnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentPitch, targetPitch), -maxHeadRotation, maxHeadRotation);
        headSteering.setPitch(PhysicsMath.normalizeTurnAngle(currentPitch + turnAngle));
        if (hasHeadSteering) {
            final ModelComponent modelComponent = componentAccessor.getComponent(ref, ModelComponent.getComponentType());
            assert modelComponent != null;
            final float bodyPitch = this.pitch;
            final float pitchOffset = MathUtil.wrapAngle(headSteering.getPitch() - bodyPitch);
            final CameraSettings headRotationRestrictions = modelComponent.getModel().getCamera();
            float pitchMin;
            float pitchMax;
            if (this.headPitchAngleRange != null) {
                pitchMin = this.headPitchAngleRange[0];
                pitchMax = this.headPitchAngleRange[1];
            }
            else if (headRotationRestrictions != null && headRotationRestrictions.getPitch() != null && headRotationRestrictions.getPitch().getAngleRange() != null) {
                final Rangef pitchRange = headRotationRestrictions.getPitch().getAngleRange();
                pitchMin = pitchRange.min * 0.017453292f;
                pitchMax = pitchRange.max * 0.017453292f;
            }
            else {
                pitchMin = -0.7853982f;
                pitchMax = 0.7853982f;
            }
            if (pitchOffset > pitchMax) {
                headSteering.setPitch(MathUtil.wrapAngle(bodyPitch + pitchMax));
            }
            else if (pitchOffset < pitchMin) {
                headSteering.setPitch(MathUtil.wrapAngle(bodyPitch + pitchMin));
            }
        }
    }
    
    protected void calculateRoll(@Nonnull final Steering bodySteering, @Nonnull final Steering headSteering) {
        if (bodySteering.hasRoll()) {
            this.roll = bodySteering.getRoll();
        }
        if (!headSteering.hasRoll()) {
            headSteering.setRoll(this.roll);
        }
    }
    
    protected void dampForceVelocity(@Nonnull final Vector3d forceVelocity, final double forceVelocityDamping, final double interval, final ComponentAccessor<EntityStore> componentAccessor) {
        final World world = componentAccessor.getExternalData().getWorld();
        double drag = 0.0;
        if (this.motionKind != MotionKind.FLYING) {
            if (!this.onGround() && this.motionKind != MotionKind.SWIMMING && this.motionKind != MotionKind.SWIMMING_TURNING) {
                final double horizontalSpeed = Math.sqrt(forceVelocity.x * forceVelocity.x + forceVelocity.z * forceVelocity.z);
                drag = convertToNewRange(horizontalSpeed, this.movementSettings.airDragMinSpeed, this.movementSettings.airDragMaxSpeed, this.movementSettings.airDragMin, this.movementSettings.airDragMax);
            }
            else {
                drag = 0.82;
            }
        }
        final double clientTps = 60.0;
        final int serverTps = world.getTps();
        final double rate = 60.0 / serverTps;
        drag = Math.pow(drag, rate);
        forceVelocity.x *= drag;
        forceVelocity.z *= drag;
        final float velocityEpsilon = 0.1f;
        if (Math.abs(forceVelocity.x) <= velocityEpsilon) {
            forceVelocity.x = 0.0;
        }
        if (Math.abs(forceVelocity.y) <= velocityEpsilon) {
            forceVelocity.y = 0.0;
        }
        if (Math.abs(forceVelocity.z) <= velocityEpsilon) {
            forceVelocity.z = 0.0;
        }
    }
    
    private static double convertToNewRange(final double value, final double oldMinRange, final double oldMaxRange, final double newMinRange, final double newMaxRange) {
        if (newMinRange == newMaxRange || oldMinRange == oldMaxRange) {
            return newMinRange;
        }
        final double newValue = (value - oldMinRange) * (newMaxRange - newMinRange) / (oldMaxRange - oldMinRange) + newMinRange;
        return MathUtil.clamp(newValue, Math.min(newMinRange, newMaxRange), Math.max(newMinRange, newMaxRange));
    }
    
    @Override
    public double probeMove(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3d position, @Nonnull final Vector3d direction, @Nonnull final ProbeMoveData probeMoveData, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        probeMoveData.setPosition(position).setDirection(direction);
        return this.probeMove(ref, probeMoveData, componentAccessor);
    }
    
    protected void postExecuteMove() {
    }
    
    protected void adjustReadPosition(final Ref<EntityStore> ref, final ComponentAccessor<EntityStore> componentAccessor) {
    }
    
    protected void adjustWritePosition(final Ref<EntityStore> ref, final double dt, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
    }
    
    @Override
    public boolean isInProgress() {
        return false;
    }
    
    @Override
    public boolean isObstructed() {
        return this.isObstructed;
    }
    
    @Override
    public NavState getNavState() {
        return this.navState;
    }
    
    @Override
    public double getThrottleDuration() {
        return this.throttleDuration;
    }
    
    @Override
    public double getTargetDeltaSquared() {
        return this.targetDeltaSquared;
    }
    
    @Override
    public void setNavState(final NavState navState, final double throttleDuration, final double targetDeltaSquared) {
        this.navState = navState;
        this.throttleDuration = throttleDuration;
        this.targetDeltaSquared = targetDeltaSquared;
    }
    
    @Override
    public boolean isForceRecomputePath() {
        return this.recomputePath;
    }
    
    @Override
    public void setForceRecomputePath(final boolean recomputePath) {
        this.recomputePath = recomputePath;
    }
    
    @Override
    public void beforeInstructionSensorsAndActions(final double physicsTickDuration) {
        this.recomputePath = false;
    }
    
    @Override
    public void beforeInstructionMotion(final double physicsTickDuration) {
        this.resetNavState();
    }
    
    public boolean isHorizontalIdle(final double speed) {
        return speed == 0.0;
    }
    
    @Override
    public boolean canAct(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return this.isAlive(ref, componentAccessor) && this.role.couldBreatheCached() && this.forceVelocity.equals(Vector3d.ZERO) && this.appliedVelocities.isEmpty();
    }
    
    public boolean isMovementBlocked(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final InteractionManager interactionManager = componentAccessor.getComponent(ref, InteractionModule.get().getInteractionManagerComponent());
        if (interactionManager != null) {
            final Boolean movementBlocked = interactionManager.forEachInteraction((chain, interaction, val) -> {
                if (val) {
                    return Boolean.TRUE;
                }
                else {
                    final MovementEffects movementEffects = interaction.getEffects().getMovementEffects();
                    if (movementEffects != null) {
                        return movementEffects.isDisableAll();
                    }
                    else {
                        return Boolean.FALSE;
                    }
                }
            }, Boolean.FALSE);
            return movementBlocked;
        }
        return false;
    }
    
    protected abstract double computeMove(@Nonnull final Ref<EntityStore> p0, @Nonnull final Role p1, final Steering p2, final double p3, final Vector3d p4, @Nonnull final ComponentAccessor<EntityStore> p5);
    
    protected abstract double executeMove(@Nonnull final Ref<EntityStore> p0, @Nonnull final Role p1, final double p2, final Vector3d p3, @Nonnull final ComponentAccessor<EntityStore> p4);
    
    public <T> double bisect(@Nonnull final Vector3d validPosition, @Nonnull final Vector3d invalidPosition, @Nonnull final T t, @Nonnull final BiPredicate<T, Vector3d> validate, @Nonnull final Vector3d result) {
        return this.bisect(validPosition, invalidPosition, t, validate, 0.05, result);
    }
    
    public <T> double bisect(@Nonnull final Vector3d validPosition, @Nonnull final Vector3d invalidPosition, @Nonnull final T t, @Nonnull final BiPredicate<T, Vector3d> validate, double maxDistance, @Nonnull final Vector3d result) {
        double validDistance = 0.0;
        double invalidDistance = 1.0;
        this.bisectValidPosition.assign(validPosition);
        this.bisectInvalidPosition.assign(invalidPosition);
        maxDistance *= maxDistance;
        double validWeight = 0.1;
        double invalidWeight = 0.9;
        while (this.bisectValidPosition.distanceSquaredTo(this.bisectInvalidPosition) > maxDistance) {
            final double distance = validWeight * validDistance + invalidWeight * invalidDistance;
            result.x = validWeight * this.bisectValidPosition.x + invalidWeight * this.bisectInvalidPosition.x;
            result.y = validWeight * this.bisectValidPosition.y + invalidWeight * this.bisectInvalidPosition.y;
            result.z = validWeight * this.bisectValidPosition.z + invalidWeight * this.bisectInvalidPosition.z;
            if (validate.test(t, result)) {
                validDistance = distance;
                this.bisectValidPosition.assign(result);
            }
            else {
                invalidDistance = distance;
                this.bisectInvalidPosition.assign(result);
                validWeight = 0.5;
                invalidWeight = 0.5;
            }
        }
        result.assign(this.bisectValidPosition);
        return validDistance;
    }
    
    @Nonnull
    @Override
    public Vector3d getForce() {
        return this.forceVelocity;
    }
    
    @Override
    public void addForce(@Nonnull final Vector3d force, final VelocityConfig velocityConfig) {
        final double scale = this.knockbackScale;
        if (!SplitVelocity.SHOULD_MODIFY_VELOCITY && velocityConfig != null) {
            this.appliedVelocities.add(new AppliedVelocity(new Vector3d(force.x * scale, force.y * scale, force.z * scale), velocityConfig));
            return;
        }
        final double horzMul = 0.18000000000000005 * this.movementSettings.velocityResistance;
        this.forceVelocity.add(force.x * scale * horzMul, force.y * scale, force.z * scale * horzMul);
        this.appliedForce.assign(this.forceVelocity);
        this.ignoreDamping = false;
    }
    
    @Override
    public void forceVelocity(@Nonnull final Vector3d velocity, @Nullable final VelocityConfig velocityConfig, final boolean ignoreDamping) {
        if (!SplitVelocity.SHOULD_MODIFY_VELOCITY && velocityConfig != null) {
            this.appliedVelocities.clear();
            this.appliedVelocities.add(new AppliedVelocity(velocity.clone(), velocityConfig));
            return;
        }
        this.forceVelocity.assign(velocity);
        this.ignoreDamping = ignoreDamping;
    }
    
    public void clearForce() {
        this.forceVelocity.assign(Vector3d.ZERO);
    }
    
    protected void dumpCollisionResults() {
        String slideString = "";
        if (this.collisionResult.isSliding) {
            slideString = String.format("SLIDE: start/end=%f/%f", this.collisionResult.slideStart, this.collisionResult.slideEnd);
        }
        MotionControllerBase.LOGGER.at(Level.INFO).log("CollRes: pos=%s yaw=%f count=%d %s", Vector3d.formatShortString(this.position), 57.295776f * this.yaw, this.collisionResult.getBlockCollisionCount(), slideString);
        if (this.collisionResult.getBlockCollisionCount() > 0) {
            for (int i = 0; i < this.collisionResult.getBlockCollisionCount(); ++i) {
                final BlockCollisionData cd = this.collisionResult.getBlockCollision(i);
                final String materialName = (cd.blockMaterial != null) ? cd.blockMaterial.name() : "none";
                final String typeName = (cd.blockType != null) ? cd.blockType.getId() : "none";
                final String hitboxName = (cd.blockType != null) ? cd.blockType.getHitboxType() : "none";
                String rotation;
                if (cd.blockType != null) {
                    final RotationTuple blockRotation = RotationTuple.get(cd.rotation);
                    rotation = String.valueOf(blockRotation.yaw()) + " " + String.valueOf(blockRotation.pitch());
                }
                else {
                    rotation = "none";
                }
                MotionControllerBase.LOGGER.at(Level.INFO).log("   COLL: blk=%s/%s/%s start=%f norm=%s pos=%s mat=%s block=%s hitbox=%s rot=%s", cd.x, cd.y, cd.z, cd.collisionStart, Vector3d.formatShortString(cd.collisionNormal), Vector3d.formatShortString(cd.collisionPoint), materialName, typeName, hitboxName, rotation);
            }
        }
    }
    
    public void setEnableTriggers(final boolean enableTriggers) {
        this.enableTriggers = enableTriggers;
    }
    
    public void setEnableBlockDamage(final boolean enableBlockDamage) {
        this.enableBlockDamage = enableBlockDamage;
    }
    
    @Override
    public boolean willReceiveBlockDamage() {
        return this.isReceivingBlockDamage;
    }
    
    @Override
    public void setAvoidingBlockDamage(final boolean avoid) {
        this.isAvoidingBlockDamage = avoid;
    }
    
    @Override
    public boolean isAvoidingBlockDamage() {
        return this.isAvoidingBlockDamage;
    }
    
    public void processTriggers(@Nonnull final Ref<EntityStore> ref, @Nonnull final CollisionResult collisionResult, final double t, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        this.processTriggersHasMoved = false;
        this.isReceivingBlockDamage = false;
        if (!this.enableTriggers && !this.enableBlockDamage) {
            return;
        }
        collisionResult.pruneTriggerBlocks(t);
        final int count = collisionResult.getTriggerBlocks().size();
        if (count == 0) {
            return;
        }
        if (this.enableTriggers) {
            this.beforeTriggerForce.assign(this.getForce());
            this.beforeTriggerPosition.assign(this.position);
        }
        this.moveEntity(ref, 0.0, componentAccessor);
        final InteractionManager interactionManagerComponent = componentAccessor.getComponent(ref, InteractionModule.get().getInteractionManagerComponent());
        assert interactionManagerComponent != null;
        final int damageToEntity = collisionResult.defaultTriggerBlocksProcessing(interactionManagerComponent, this.entity, ref, this.enableTriggers, componentAccessor);
        if (this.enableBlockDamage && damageToEntity > 0) {
            final Damage damage = new Damage(Damage.NULL_SOURCE, DamageCause.ENVIRONMENT, (float)damageToEntity);
            DamageSystems.executeDamage(ref, componentAccessor, damage);
            this.isReceivingBlockDamage = true;
        }
        this.readEntityPosition(ref, componentAccessor);
        if (this.enableTriggers) {
            this.processTriggersHasMoved = (!this.beforeTriggerForce.equals(this.getForce()) || !this.beforeTriggerPosition.equals(this.position));
        }
    }
    
    protected boolean isDebugMode(final RoleDebugFlags mode) {
        return this.getRole() != null && this.getRole().getDebugSupport().getDebugFlags().contains(mode);
    }
    
    public boolean isProcessTriggersHasMoved() {
        return this.processTriggersHasMoved;
    }
    
    protected boolean isAlive(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return !componentAccessor.getArchetype(ref).contains(DeathComponent.getComponentType());
    }
    
    @Override
    public void activate() {
        this.debugModeSteer = this.isDebugMode(RoleDebugFlags.MotionControllerSteer);
        this.debugModeMove = this.isDebugMode(RoleDebugFlags.MotionControllerMove);
        this.debugModeCollisions = this.isDebugMode(RoleDebugFlags.Collisions);
        this.debugModeBlockCollisions = this.isDebugMode(RoleDebugFlags.BlockCollisions);
        this.debugModeProbeBlockCollisions = this.isDebugMode(RoleDebugFlags.ProbeBlockCollisions);
        this.debugModeValidatePositions = this.isDebugMode(RoleDebugFlags.ValidatePositions);
        this.debugModeOverlaps = this.isDebugMode(RoleDebugFlags.Overlaps);
        this.debugModeValidateMath = this.isDebugMode(RoleDebugFlags.ValidateMath);
        this.resetObstructedFlags();
        this.resetNavState();
    }
    
    public void resetNavState() {
        this.navState = NavState.AT_GOAL;
        this.throttleDuration = 0.0;
        this.targetDeltaSquared = 0.0;
    }
    
    public void resetObstructedFlags() {
        this.isObstructed = false;
    }
    
    @Override
    public void deactivate() {
    }
    
    public double getEpsilonSpeed() {
        return this.epsilonSpeed;
    }
    
    public float getEpsilonAngle() {
        return this.epsilonAngle;
    }
    
    @Nonnull
    @Override
    public Vector3d getComponentSelector() {
        return this.componentSelector;
    }
    
    @Nonnull
    @Override
    public Vector3d getPlanarComponentSelector() {
        return this.planarComponentSelector;
    }
    
    @Override
    public void setComponentSelector(@Nonnull final Vector3d componentSelector) {
        this.componentSelector.assign(componentSelector);
    }
    
    @Override
    public Vector3d getWorldNormal() {
        return this.worldNormal;
    }
    
    @Override
    public Vector3d getWorldAntiNormal() {
        return this.worldAntiNormal;
    }
    
    @Override
    public double waypointDistance(@Nonnull final Vector3d p, @Nonnull final Vector3d q) {
        return Math.sqrt(this.waypointDistanceSquared(p, q));
    }
    
    @Override
    public double waypointDistanceSquared(@Nonnull final Vector3d p, @Nonnull final Vector3d q) {
        final double dx = (p.x - q.x) * this.getComponentSelector().x;
        final double dy = (p.y - q.y) * this.getComponentSelector().y;
        final double dz = (p.z - q.z) * this.getComponentSelector().z;
        return dx * dx + dy * dy + dz * dz;
    }
    
    @Override
    public double waypointDistance(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3d p, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return Math.sqrt(this.waypointDistanceSquared(ref, p, componentAccessor));
    }
    
    @Override
    public double waypointDistanceSquared(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3d p, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType());
        assert transformComponent != null;
        final Vector3d position = transformComponent.getPosition();
        final double dx = (p.x - position.getX()) * this.getComponentSelector().x;
        final double dy = (p.y - position.getY()) * this.getComponentSelector().y;
        final double dz = (p.z - position.getZ()) * this.getComponentSelector().z;
        return dx * dx + dy * dy + dz * dz;
    }
    
    @Override
    public boolean isValidPosition(@Nonnull final Vector3d position, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return this.isValidPosition(position, this.collisionResult, componentAccessor);
    }
    
    public boolean isValidPosition(@Nonnull final Vector3d position, @Nonnull final CollisionResult collisionResult, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final World world = componentAccessor.getExternalData().getWorld();
        final CollisionModule module = CollisionModule.get();
        final CollisionModuleConfig config = module.getConfig();
        final boolean saveDebugModeOverlaps = config.isDumpInvalidBlocks();
        config.setDumpInvalidBlocks(this.debugModeOverlaps);
        final boolean isValid = module.validatePosition(world, this.collisionBoundingBox, position, this.getInvalidOverlapMaterials(), null, (_this, collisionCode, collision, collisionConfig) -> collisionConfig.blockId != Integer.MIN_VALUE, collisionResult) != -1;
        config.setDumpInvalidBlocks(saveDebugModeOverlaps);
        return isValid;
    }
    
    public int getInvalidOverlapMaterials() {
        return 4;
    }
    
    protected void saveMotionKind() {
        this.previousMotionKind = this.getMotionKind();
    }
    
    protected boolean switchedToMotionKind(final MotionKind motionKind) {
        return this.getMotionKind() == motionKind && this.previousMotionKind != motionKind;
    }
    
    public MotionKind getMotionKind() {
        return this.motionKind;
    }
    
    public void setMotionKind(final MotionKind motionKind) {
        this.motionKind = motionKind;
    }
    
    @Override
    public double getGravity() {
        return this.gravity;
    }
    
    public void setGravity(final double gravity) {
        this.gravity = gravity;
    }
    
    @Override
    public boolean translateToAccessiblePosition(final Vector3d position, final Box boundingBox, final double minYValue, final double maxYValue, final ComponentAccessor<EntityStore> componentAccessor) {
        return true;
    }
    
    @Override
    public boolean standingOnBlockOfType(final int blockSet) {
        return false;
    }
    
    @Override
    public void requirePreciseMovement(@Nullable final Vector3d positionHint) {
        this.requiresPreciseMovement = true;
        this.havePreciseMovementTarget = (positionHint != null);
        if (this.havePreciseMovementTarget) {
            this.preciseMovementTarget.assign(positionHint);
        }
    }
    
    public void clearRequirePreciseMovement() {
        this.requiresPreciseMovement = false;
        this.havePreciseMovementTarget = false;
    }
    
    public boolean isRequiresPreciseMovement() {
        return this.requiresPreciseMovement;
    }
    
    @Override
    public void requireDepthProbing() {
        this.requiresDepthProbing = true;
    }
    
    public void clearRequireDepthProbing() {
        this.requiresDepthProbing = false;
    }
    
    public boolean isRequiresDepthProbing() {
        return this.requiresDepthProbing;
    }
    
    @Override
    public void enableHeadingBlending(final double heading, @Nullable final Vector3d targetPosition, final double blendLevel) {
        this.isBlendingHeading = true;
        this.blendHeading = heading;
        this.haveBlendHeadingPosition = (targetPosition != null);
        if (this.haveBlendHeadingPosition) {
            this.blendHeadingPosition.assign(targetPosition);
        }
        this.blendLevelAtTargetPosition = blendLevel;
    }
    
    @Override
    public void enableHeadingBlending() {
        this.enableHeadingBlending(Double.NaN, null, 0.0);
    }
    
    public void clearBlendHeading() {
        this.isBlendingHeading = false;
        this.haveBlendHeadingPosition = false;
    }
    
    @Override
    public void setRelaxedMoveConstraints(final boolean relax) {
        this.isRelaxedMoveConstraints = relax;
    }
    
    @Override
    public boolean isRelaxedMoveConstraints() {
        return this.isRelaxedMoveConstraints;
    }
    
    @Override
    public void updatePhysicsValues(final PhysicsValues values) {
        this.movementSettings = MovementManager.MASTER_DEFAULT.apply(values, GameMode.Adventure);
    }
    
    static {
        LOGGER = NPCPlugin.get().getLogger();
    }
    
    protected static class AppliedVelocity
    {
        protected final Vector3d velocity;
        protected final VelocityConfig config;
        protected boolean canClear;
        
        public AppliedVelocity(final Vector3d velocity, final VelocityConfig config) {
            this.velocity = velocity;
            this.config = config;
        }
    }
}
