Package org.terasology.logic.characters

Source Code of org.terasology.logic.characters.KinematicCharacterMover$MoveResult

/*
* Copyright 2013 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.logic.characters;

import com.bulletphysics.linearmath.QuaternionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.logic.characters.events.FootstepEvent;
import org.terasology.logic.characters.events.HorizontalCollisionEvent;
import org.terasology.logic.characters.events.JumpEvent;
import org.terasology.logic.characters.events.OnEnterBlockEvent;
import org.terasology.logic.characters.events.SwimStrokeEvent;
import org.terasology.logic.characters.events.VerticalCollisionEvent;
import org.terasology.logic.location.LocationComponent;
import org.terasology.math.TeraMath;
import org.terasology.math.Vector3fUtil;
import org.terasology.math.Vector3i;
import org.terasology.physics.engine.CharacterCollider;
import org.terasology.physics.engine.PhysicsEngine;
import org.terasology.physics.engine.SweepCallback;
import org.terasology.physics.events.MovedEvent;
import org.terasology.world.WorldProvider;
import org.terasology.world.block.Block;

import javax.vecmath.AxisAngle4f;
import javax.vecmath.Quat4f;
import javax.vecmath.Vector3f;

/**
* Calculates character movement using a physics-engine provided CharacterCollider.
* This collider is swept through the world to detect collisions.
* The general process for this is:
* <ol>
* <li>If moving upwards, sweep up</li>
* <li>Sweep sideways</li>
* <li>If an obstacle is hit, step up and resume sideways motion</li>
* <li>If an slope is hit, slide up it</li>
* <li>Finally sweep downwards to undo any stepping, and for falling</li>
* </ol>
* <p/>
* TODO: Refactor to allow additional movement modes.
* TODO: Detect entry and exit from water while ghosting.
*
* @author Immortius
*/
public class KinematicCharacterMover implements CharacterMover {

    public static final float GRAVITY = 28.0f;
    public static final float TERMINAL_VELOCITY = 64.0f;

    /**
     * The amount of horizontal penetration to allow.
     */
    private static final float HORIZONTAL_PENETRATION = 0.03f;

    /**
     * The amount of extra distance added to horizontal movement to allow for penetration.
     */
    private static final float HORIZONTAL_PENETRATION_LEEWAY = 0.04f;

    /**
     * The amount of vertical penetration to allow.
     */
    private static final float VERTICAL_PENETRATION = 0.04f;

    /**
     * The amount of extra distance added to vertical movement to allow for penetration.
     */
    private static final float VERTICAL_PENETRATION_LEEWAY = 0.05f;
    private static final float CHECK_FORWARD_DIST = 0.05f;

    private final Logger logger = LoggerFactory.getLogger(KinematicCharacterMover.class);
    private boolean stepped;

    // Processing state variables
    private float steppedUpDist;
    private WorldProvider worldProvider;
    private PhysicsEngine physics;

    public KinematicCharacterMover(WorldProvider wp, PhysicsEngine physicsEngine) {
        this.worldProvider = wp;
        physics = physicsEngine;
    }

    @Override
    public CharacterStateEvent step(CharacterStateEvent initial, CharacterMoveInputEvent input, EntityRef entity) {
        CharacterMovementComponent characterMovementComponent = entity.getComponent(CharacterMovementComponent.class);
        CharacterStateEvent result = new CharacterStateEvent(initial);
        result.setSequenceNumber(input.getSequenceNumber());
        if (worldProvider.isBlockRelevant(initial.getPosition())) {
            updatePosition(characterMovementComponent, result, input, entity);

            if (input.isFirstRun()) {
                checkBlockEntry(entity, new Vector3i(initial.getPosition(), 0.5f), new Vector3i(result.getPosition(), 0.5f), characterMovementComponent.height);
            }

            if (result.getMode() != MovementMode.GHOSTING && result.getMode() != MovementMode.NONE) {
                checkMode(characterMovementComponent, result, initial, entity, input.isFirstRun());
            }
        }
        result.setTime(initial.getTime() + input.getDeltaMs());
        updateRotation(characterMovementComponent, result, input);
        result.setPitch(input.getPitch());
        result.setYaw(input.getYaw());
        input.runComplete();
        return result;
    }

    private float getMaxSpeed(EntityRef character, CharacterMovementComponent characterMovement) {
        GetMaxSpeedEvent speedEvent = new GetMaxSpeedEvent(characterMovement.mode.maxSpeed, characterMovement.mode);
        character.send(speedEvent);
        return Math.max(0, speedEvent.getResultValue());
    }

    /*
    * Figure out if our position has put us into a new set of blocks and fire the appropriate events.
    */
    private void checkBlockEntry(EntityRef entity, Vector3i oldPosition, Vector3i newPosition, float characterHeight) {
        // TODO: This will only work for tall mobs/players and single block mobs
        // is this a different position than previously
        if (!oldPosition.equals(newPosition)) {
            // get the old position's blocks
            Block[] oldBlocks = new Block[(int) Math.ceil(characterHeight)];
            Vector3i currentPosition = oldPosition.clone();
            for (int currentHeight = 0; currentHeight < oldBlocks.length; currentHeight++) {
                oldBlocks[currentHeight] = worldProvider.getBlock(currentPosition);
                currentPosition.add(0, 1, 0);
            }

            // get the new position's blocks
            Block[] newBlocks = new Block[(int) Math.ceil(characterHeight)];
            currentPosition = newPosition.clone();
            for (int currentHeight = 0; currentHeight < characterHeight; currentHeight++) {
                newBlocks[currentHeight] = worldProvider.getBlock(currentPosition);
                currentPosition.add(0, 1, 0);
            }

            for (int i = 0; i < characterHeight; i++) {
                // send a block enter/leave event for this character
                entity.send(new OnEnterBlockEvent(oldBlocks[i], newBlocks[i], new Vector3i(0, i, 0)));
            }
        }
    }

    /**
     * Checks whether a character should change movement mode (from being underwater or in a ladder). A higher and lower point of the
     * character is tested for being in water, only if both points are in water does the character count as swimming.
     * <p/>
     * Sends the OnEnterLiquidEvent and OnLeaveLiquidEvent events.
     *
     * @param movementComp The movement component of the character.
     * @param state        The current state of the character.
     */
    private void checkMode(final CharacterMovementComponent movementComp, final CharacterStateEvent state,
                           final CharacterStateEvent oldState, EntityRef entity, boolean firstRun) {
        //If we are ghosting or we can't move, the mode cannot be changed.
        if (!state.getMode().respondToEnvironment) {
            return;
        }

        Vector3f worldPos = state.getPosition();
        Vector3f top = new Vector3f(worldPos);
        Vector3f bottom = new Vector3f(worldPos);
        top.y += 0.25f * movementComp.height;
        bottom.y -= 0.25f * movementComp.height;

        final boolean topUnderwater = worldProvider.getBlock(top).isLiquid();
        final boolean bottomUnderwater = worldProvider.getBlock(bottom).isLiquid();

        final boolean newSwimming = topUnderwater && bottomUnderwater;
        boolean newClimbing = false;

        //TODO: refactor this knot of if-else statements into something easy to read. Some sub-methods and switch statements would be nice.
        if (!newSwimming) {
            Vector3f[] sides = {new Vector3f(worldPos), new Vector3f(worldPos), new Vector3f(worldPos), new Vector3f(
                    worldPos), new Vector3f(worldPos)};
            float factor = 1.0f;
            sides[0].x += factor * movementComp.radius;
            sides[1].x -= factor * movementComp.radius;
            sides[2].z += factor * movementComp.radius;
            sides[3].z -= factor * movementComp.radius;
            sides[4].y -= movementComp.height;

            float distance = 100f;

            for (Vector3f side : sides) {
                Block block = worldProvider.getBlock(side);
                if (block.isClimbable()) {
                    //If any of our sides are near a climbable block, check if we are near to the side
                    Vector3i myPos = new Vector3i(worldPos, 0.5f);
                    Vector3i climbBlockPos = new Vector3i(side, 0.5f);
                    Vector3i dir = block.getDirection().getVector3i().clone();
                    float currentDistance = 10f;

                    if (dir.x != 0 && Math.abs(worldPos.x - (float) climbBlockPos.x + (float) dir.x * .5f) < movementComp.radius + 0.1f) {
                        newClimbing = true;
                        if (myPos.x < climbBlockPos.x) {
                            dir.x = -dir.x;
                        }
                        currentDistance = Math.abs(climbBlockPos.z - worldPos.z);

                    } else if (dir.z != 0 && Math.abs(worldPos.z - (float) climbBlockPos.z + (float) dir.z * .5f) < movementComp.radius + 0.1f) {
                        newClimbing = true;
                        if (myPos.z < climbBlockPos.z) {
                            dir.z = -dir.z;
                        }
                        currentDistance = Math.abs(climbBlockPos.z - worldPos.z);
                    }

                    // if there are multiple climb blocks, choose the nearest one. This can happen when there are two
                    // adjacent ledges around a corner.
                    if (currentDistance < distance) {
                        distance = currentDistance;
                        state.setClimbDirection(dir);
                    }
                }
            }
        }

        if (newSwimming) {
            //Note that you cannot climb under water!
            if (state.getMode() != MovementMode.SWIMMING) {
                state.setMode(MovementMode.SWIMMING);
            }
        } else if (state.getMode() == MovementMode.SWIMMING) {
            if (newClimbing) {
                state.setMode(MovementMode.CLIMBING);
                state.getVelocity().y = 0;
            } else {
                if (state.getVelocity().y > 0) {
                    state.getVelocity().y += 8;
                }
                state.setMode(MovementMode.WALKING);
            }
        } else if (newClimbing != (state.getMode() == MovementMode.CLIMBING)) {
            //We need to toggle the climbing mode
            state.getVelocity().y = 0;
            state.setMode((newClimbing) ? MovementMode.CLIMBING : MovementMode.WALKING);
        }
    }

    /**
     * Checks of the player will step up to an object. In a single movement step the player can only step up a single item.
     *
     * @param collider
     * @param position
     * @param direction
     * @param callback
     * @param slopeFactor
     * @param stepHeight
     * @return
     */
    private boolean checkStep(CharacterCollider collider, Vector3f position, Vector3f direction, SweepCallback callback,
                              float slopeFactor, float stepHeight) {
        if (!stepped) {
            stepped = true;

            boolean moveUpStep = callback.checkForStep(direction, stepHeight, slopeFactor, CHECK_FORWARD_DIST);

            if (moveUpStep) {
                steppedUpDist = moveUp(stepHeight, collider, position);
                return true;
            }
        }
        return false;
    }

    private Vector3f extractResidualMovement(Vector3f hitNormal, Vector3f direction) {
        return extractResidualMovement(hitNormal, direction, 1f);
    }

    private Vector3f extractResidualMovement(Vector3f hitNormal, Vector3f direction, float normalMag) {
        float movementLength = direction.length();
        if (movementLength > physics.getEpsilon()) {
            direction.normalize();
            Vector3f reflectDir = Vector3fUtil.reflect(direction, hitNormal, new Vector3f());
            reflectDir.normalize();
            Vector3f perpendicularDir = Vector3fUtil.getPerpendicularComponent(reflectDir, hitNormal, new Vector3f());
            if (normalMag != 0.0f) {
                Vector3f perpComponent = new Vector3f();
                perpComponent.scale(normalMag * movementLength, perpendicularDir);
                direction.set(perpComponent);
            }
        }
        return direction;
    }

    private void followToParent(final CharacterStateEvent state, EntityRef entity) {
        LocationComponent locationComponent = entity.getComponent(LocationComponent.class);
        if (!locationComponent.getParent().equals(EntityRef.NULL)) {
            Vector3f velocity = new Vector3f(locationComponent.getWorldPosition());
            velocity.sub(state.getPosition());
            state.getVelocity().set(velocity);
            state.getPosition().set(locationComponent.getWorldPosition());
        }
    }

    private MoveResult move(final Vector3f startPosition, final Vector3f moveDelta, final float stepHeight,
                            final float slopeFactor, final CharacterCollider collider) {
        steppedUpDist = 0;
        stepped = false;
        Vector3f position = new Vector3f(startPosition);
        boolean hitTop = false;
        boolean hitBottom = false;
        boolean hitSide;

        // Actual upwards movement
        if (moveDelta.y > 0) {
            hitTop = moveDelta.y - moveUp(moveDelta.y, collider, position) > physics.getEpsilon();
        }
        hitSide = moveHorizontal(new Vector3f(moveDelta.x, 0, moveDelta.z), collider, position, slopeFactor, stepHeight);
        if (moveDelta.y < 0 || steppedUpDist > 0) {
            float dist = (moveDelta.y < 0) ? moveDelta.y : 0;
            dist -= steppedUpDist;
            hitBottom = moveDown(dist, slopeFactor, collider, position);
        }
        if (!hitBottom && stepHeight > 0) {
            Vector3f tempPos = new Vector3f(position);
            hitBottom = moveDown(-stepHeight, slopeFactor, collider, tempPos);
            // Don't apply step down if nothing to step onto
            if (hitBottom) {
                position.set(tempPos);
            }
        }
        return new MoveResult(position, hitSide, hitBottom, hitTop);
    }

    private boolean moveDown(float dist, float slopeFactor, CharacterCollider collider, Vector3f position) {
        if (collider == null) {
            position.y += dist;
            return false;
        }

        float remainingDist = -dist;
        Vector3f targetPos = new Vector3f(position);
        targetPos.y -= remainingDist + VERTICAL_PENETRATION_LEEWAY;
        Vector3f normalizedDir = new Vector3f(0, -1, 0);
        boolean hit = false;
        int iteration = 0;
        while (remainingDist > physics.getEpsilon() && iteration++ < 10) {
            SweepCallback callback = collider.sweep(position, targetPos, VERTICAL_PENETRATION, -1.0f);
            float actualDist = Math.max(0,
                    (remainingDist + VERTICAL_PENETRATION_LEEWAY) * callback.getClosestHitFraction() - VERTICAL_PENETRATION_LEEWAY);
            Vector3f expectedMove = new Vector3f(targetPos);
            expectedMove.sub(position);
            if (expectedMove.lengthSquared() > physics.getEpsilon()) {
                expectedMove.normalize();
                expectedMove.scale(actualDist);
                position.add(expectedMove);
            }
            remainingDist -= actualDist;
            if (remainingDist < physics.getEpsilon()) {
                break;
            }
            if (callback.hasHit()) {
                float originalSlope = callback.getHitNormalWorld().dot(new Vector3f(0, 1, 0));
                if (originalSlope < slopeFactor) {
                    float slope = callback.calculateAverageSlope(originalSlope, CHECK_FORWARD_DIST);
                    if (slope < slopeFactor) {
                        remainingDist -= actualDist;
                        expectedMove.set(targetPos);
                        expectedMove.sub(position);
                        extractResidualMovement(callback.getHitNormalWorld(), expectedMove);
                        float sqrDist = expectedMove.lengthSquared();
                        if (sqrDist > physics.getEpsilon()) {
                            expectedMove.normalize();
                            if (expectedMove.dot(normalizedDir) <= 0.0f) {
                                hit = true;
                                break;
                            }
                        } else {
                            hit = true;
                            break;
                        }
                        if (expectedMove.y > -physics.getEpsilon()) {
                            hit = true;
                            break;
                        }
                        normalizedDir.set(expectedMove);
                        expectedMove.scale(-remainingDist / expectedMove.y + HORIZONTAL_PENETRATION_LEEWAY);
                        targetPos.add(position, expectedMove);
                    } else {
                        hit = true;
                        break;
                    }
                } else {
                    hit = true;
                    break;
                }
            } else {
                break;
            }
        }
        if (iteration >= 10) {
            hit = true;
        }
        return hit;
    }

    private boolean moveHorizontal(Vector3f horizMove, CharacterCollider collider, Vector3f position, float slopeFactor,
                                   float stepHeight) {
        float remainingFraction = 1.0f;
        float dist = horizMove.length();
        if (dist < physics.getEpsilon()) {
            return false;
        }
        boolean horizontalHit = false;
        Vector3f normalizedDir = Vector3fUtil.safeNormalize(horizMove, new Vector3f());

        if (collider == null) {
            // ignore collision
            normalizedDir.scale(dist);
            position.add(normalizedDir);
            return false;
        }

        Vector3f targetPos = new Vector3f(normalizedDir);
        targetPos.scale(dist + HORIZONTAL_PENETRATION_LEEWAY);
        targetPos.add(position);
        int iteration = 0;
        Vector3f lastHitNormal = new Vector3f(0, 1, 0);
        while (remainingFraction >= 0.01f && iteration++ < 10) {
            SweepCallback callback = collider.sweep(position, targetPos, HORIZONTAL_PENETRATION, slopeFactor);

            /* Note: this isn't quite correct (after the first iteration the closestHitFraction is only for part of the moment)
             but probably close enough */
            float actualDist = Math.max(0,
                    (dist + HORIZONTAL_PENETRATION_LEEWAY) * callback.getClosestHitFraction() - HORIZONTAL_PENETRATION_LEEWAY);
            if (actualDist != 0) {
                remainingFraction -= actualDist / dist;
            }
            if (callback.hasHit()) {
                if (actualDist > physics.getEpsilon()) {
                    Vector3f actualMove = new Vector3f(normalizedDir);
                    actualMove.scale(actualDist);
                    position.add(actualMove);
                }
                dist -= actualDist;
                Vector3f newDir = new Vector3f(normalizedDir);
                newDir.scale(dist);
                float slope = callback.getHitNormalWorld().dot(new Vector3f(0, 1, 0));

                // We step up if we're hitting a big slope, or if we're grazing
                // the ground, otherwise we move up a shallow slope.
                if (slope < slopeFactor || 1 - slope < physics.getEpsilon()) {
                    boolean stepping = checkStep(collider, position, newDir, callback, slopeFactor, stepHeight);
                    if (!stepping) {
                        horizontalHit = true;
                        Vector3f newHorizDir = new Vector3f(newDir.x, 0, newDir.z);
                        Vector3f horizNormal = new Vector3f(callback.getHitNormalWorld().x, 0,
                                callback.getHitNormalWorld().z);
                        if (horizNormal.lengthSquared() > physics.getEpsilon()) {
                            horizNormal.normalize();
                            if (lastHitNormal.dot(horizNormal) > physics.getEpsilon()) {
                                break;
                            }
                            lastHitNormal.set(horizNormal);
                            extractResidualMovement(horizNormal, newHorizDir);
                        }
                        newDir.set(newHorizDir);
                    }
                } else {
                    // Hitting a shallow slope, move up it
                    Vector3f newHorizDir = new Vector3f(newDir.x, 0, newDir.z);
                    extractResidualMovement(callback.getHitNormalWorld(), newDir);
                    Vector3f modHorizDir = new Vector3f(newDir);
                    modHorizDir.y = 0;
                    newDir.scale(newHorizDir.length() / modHorizDir.length());
                }
                float sqrDist = newDir.lengthSquared();
                if (sqrDist > physics.getEpsilon()) {
                    newDir.normalize();
                    if (newDir.dot(normalizedDir) <= 0.0f) {
                        break;
                    }
                } else {
                    break;
                }
                dist = (float) Math.sqrt(sqrDist);
                normalizedDir.set(newDir);
                targetPos.set(normalizedDir);
                targetPos.scale(dist + HORIZONTAL_PENETRATION_LEEWAY);
                targetPos.add(position);
            } else {
                normalizedDir.scale(dist);
                position.add(normalizedDir);
                break;
            }
        }
        return horizontalHit;
    }

    private float moveUp(float riseAmount, CharacterCollider collider, Vector3f position) {
        Vector3f to = new Vector3f(position.x, position.y + riseAmount + VERTICAL_PENETRATION_LEEWAY, position.z);
        if (collider != null) {
            SweepCallback callback = collider.sweep(position, to, VERTICAL_PENETRATION_LEEWAY, -1f);
            if (callback.hasHit()) {
                float actualDist = Math.max(0,
                        ((riseAmount + VERTICAL_PENETRATION_LEEWAY) * callback.getClosestHitFraction()) - VERTICAL_PENETRATION_LEEWAY);
                position.y += actualDist;
                return actualDist;
            }
        }
        position.y += riseAmount;
        return riseAmount;
    }

    private void updatePosition(final CharacterMovementComponent movementComp, final CharacterStateEvent state,
                                CharacterMoveInputEvent input, EntityRef entity) {
        switch (state.getMode()) {
            case NONE:
                followToParent(state, entity);
                break;
            default:
                walk(movementComp, state, input, entity);
                break;
        }
    }

    @SuppressWarnings(value = "SuspiciousNameCombination")
    private void updateRotation(CharacterMovementComponent movementComp, CharacterStateEvent result,
                                CharacterMoveInputEvent input) {
        if (movementComp.faceMovementDirection && result.getVelocity().lengthSquared() > 0.01f) {
            float yaw = (float) Math.atan2(result.getVelocity().x, result.getVelocity().z);
            AxisAngle4f axisAngle = new AxisAngle4f(0, 1, 0, yaw);
            result.getRotation().set(axisAngle);
        } else {
            QuaternionUtil.setEuler(result.getRotation(), TeraMath.DEG_TO_RAD * input.getYaw(), 0, 0);
        }
    }

    private void walk(final CharacterMovementComponent movementComp, final CharacterStateEvent state,
                      CharacterMoveInputEvent input, EntityRef entity) {
        Vector3f desiredVelocity = new Vector3f(input.getMovementDirection());

        float lengthSquared = desiredVelocity.lengthSquared();

        // If the length of desired movement is > 1, normalise it to prevent movement being faster than allowed.
        // (Desired velocity < 1 is allowed, as the character may wish to walk/crawl/otherwise move slowly)
        if (lengthSquared > 1) {
            desiredVelocity.normalize();
        }
        desiredVelocity.scale(movementComp.speedMultiplier);

        float maxSpeed = getMaxSpeed(entity, movementComp);
        if (input.isRunning()) {
            maxSpeed *= movementComp.runFactor;
        }

        // As we can't use it, remove the y component of desired movement while maintaining speed.
        if (movementComp.grounded && desiredVelocity.y != 0) {
            float speed = desiredVelocity.length();
            desiredVelocity.y = 0;
            if (desiredVelocity.x != 0 || desiredVelocity.z != 0) {
                desiredVelocity.normalize();
                desiredVelocity.scale(speed);
            }
        }
        desiredVelocity.scale(maxSpeed);

        if (movementComp.mode == MovementMode.CLIMBING) {
            climb(state, input, desiredVelocity);
        }

        // Modify velocity towards desired, up to the maximum rate determined by friction
        Vector3f velocityDiff = new Vector3f(desiredVelocity);
        velocityDiff.sub(state.getVelocity());
        velocityDiff.scale(Math.min(movementComp.mode.scaleInertia * input.getDelta(), 1.0f));
        Vector3f endVelocity = new Vector3f(state.getVelocity());
        endVelocity.x += velocityDiff.x;
        endVelocity.z += velocityDiff.z;
        if (movementComp.mode.scaleGravity == 0) {
            // apply the velocity without gravity
            endVelocity.y += velocityDiff.y;
        } else if (movementComp.mode.applyInertiaToVertical) {
            endVelocity.y += Math.max(-TERMINAL_VELOCITY, velocityDiff.y - (GRAVITY * movementComp.mode.scaleGravity) * input.getDelta());
        } else {
            endVelocity.y = Math.max(-TERMINAL_VELOCITY, state.getVelocity().y - (GRAVITY * movementComp.mode.scaleGravity) * input.getDelta());
        }
        Vector3f moveDelta = new Vector3f(endVelocity);
        moveDelta.scale(input.getDelta());
        CharacterCollider collider = movementComp.mode.useCollision ? physics.getCharacterCollider(entity) : null;
        MoveResult moveResult = move(state.getPosition(), moveDelta,
                (state.getMode() != MovementMode.CLIMBING && state.isGrounded() && movementComp.mode.canBeGrounded) ? movementComp.stepHeight : 0,
                movementComp.slopeFactor, collider);
        Vector3f distanceMoved = new Vector3f(moveResult.getFinalPosition());
        distanceMoved.sub(state.getPosition());
        state.getPosition().set(moveResult.getFinalPosition());
        if (input.isFirstRun() && distanceMoved.length() > 0) {
            entity.send(new MovedEvent(distanceMoved, state.getPosition()));
        }

        if (moveResult.isBottomHit()) {
            if (!state.isGrounded() && movementComp.mode.canBeGrounded) {
                if (input.isFirstRun()) {
                    Vector3f landVelocity = new Vector3f(state.getVelocity());
                    landVelocity.y += (distanceMoved.y / moveDelta.y) * (endVelocity.y - state.getVelocity().y);
                    logger.debug("Landed at " + landVelocity);
                    entity.send(new VerticalCollisionEvent(state.getPosition(), landVelocity));
                }
                state.setGrounded(true);
            }
            endVelocity.y = 0;

            // Jumping is only possible, if the entity is standing on ground
            if (input.isJumpRequested()) {
                state.setGrounded(false);
                endVelocity.y += movementComp.jumpSpeed;
                if (input.isFirstRun()) {
                    entity.send(new JumpEvent());
                }
            }
        } else {
            if (moveResult.isTopHit() && endVelocity.y > 0) {
                endVelocity.y = -0.5f * endVelocity.y;
            }
            state.setGrounded(false);
        }
        state.getVelocity().set(endVelocity);
        if (input.isFirstRun() && moveResult.isHorizontalHit()) {
            entity.send(new HorizontalCollisionEvent(state.getPosition(), state.getVelocity()));
        }
        if (state.isGrounded() || movementComp.mode == MovementMode.SWIMMING) {
            state.setFootstepDelta(
                    state.getFootstepDelta() + distanceMoved.length() / movementComp.distanceBetweenFootsteps);
            if (state.getFootstepDelta() > 1) {
                state.setFootstepDelta(state.getFootstepDelta() - 1);
                if (input.isFirstRun()) {
                    switch (movementComp.mode) {
                        case WALKING:
                            entity.send(new FootstepEvent());
                            break;
                        case SWIMMING:
                            entity.send(new SwimStrokeEvent(worldProvider.getBlock(state.getPosition())));
                            break;
                    }
                }
            }
        }
    }

    private void climb(final CharacterStateEvent state, CharacterMoveInputEvent input, Vector3f desiredVelocity) {
        if (state.getClimbDirection() == null) {
            return;
        }
        Quat4f rotation = new Quat4f();
        Vector3f tmp;

        Vector3i climbDir3i = state.getClimbDirection();
        Vector3f climbDir3f = climbDir3i.toVector3f();

        QuaternionUtil.setEuler(rotation, TeraMath.DEG_TO_RAD * state.getYaw(), 0, 0);
        tmp = new Vector3f(0.0f, 0.0f, -1.0f);
        QuaternionUtil.quatRotate(rotation, tmp, tmp);
        float angleToClimbDirection = tmp.angle(climbDir3f);

        boolean clearMovementToDirection = !state.isGrounded();

        // facing the ladder or looking down or up
        if (angleToClimbDirection < Math.PI / 4.0 || Math.abs(input.getPitch()) > 60f) {
            float pitchAmount = state.isGrounded() ? 45f : 90f;
            float pitch = input.getPitch() > 30f ? pitchAmount : -pitchAmount;
            QuaternionUtil.setEuler(rotation, TeraMath.DEG_TO_RAD * state.getYaw(), TeraMath.DEG_TO_RAD * pitch, 0);
            QuaternionUtil.quatRotate(rotation, desiredVelocity, desiredVelocity);

        // looking sidewards from ladder
        } else if (angleToClimbDirection < Math.PI * 3.0 / 4.0) {
            float rollAmount = state.isGrounded() ? 45f : 90f;
            tmp = new Vector3f();
            QuaternionUtil.quatRotate(rotation, climbDir3f, tmp);
            float leftOrRight = tmp.x;
            float plusOrMinus = (leftOrRight < 0f ? -1.0f : 1.0f) * (climbDir3i.x != 0 ? -1.0f : 1.0f);
            QuaternionUtil.setEuler(rotation, TeraMath.DEG_TO_RAD * input.getYaw(), 0f,
                TeraMath.DEG_TO_RAD * rollAmount * plusOrMinus
            );
            QuaternionUtil.quatRotate(rotation, desiredVelocity, desiredVelocity);

        // facing away from ladder
        } else {
            QuaternionUtil.setEuler(rotation, TeraMath.DEG_TO_RAD * state.getYaw(), 0, 0);
            QuaternionUtil.quatRotate(rotation, desiredVelocity, desiredVelocity);
            clearMovementToDirection = false;
        }

        // clear out movement towards or away from the ladder
        if (clearMovementToDirection) {
            if (climbDir3i.x != 0) {
                desiredVelocity.x = 0f;
            }
            if (climbDir3i.z != 0) {
                desiredVelocity.z = 0f;
            }
        }
    }

    /**
     * Holds the result of movement.
     */
    public static class MoveResult {

        private Vector3f finalPosition;
        private boolean horizontalHit;
        private boolean bottomHit;
        private boolean topHit;

        public MoveResult(Vector3f finalPosition, boolean hitHorizontal, boolean hitBottom, boolean hitTop) {
            this.finalPosition = finalPosition;
            this.horizontalHit = hitHorizontal;
            this.bottomHit = hitBottom;
            this.topHit = hitTop;
        }

        public Vector3f getFinalPosition() {
            return finalPosition;
        }

        public boolean isHorizontalHit() {
            return horizontalHit;
        }

        public boolean isBottomHit() {
            return bottomHit;
        }

        public boolean isTopHit() {
            return topHit;
        }
    }
}
TOP

Related Classes of org.terasology.logic.characters.KinematicCharacterMover$MoveResult

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.