components/LogicDirectionalMovement.js

/* global platypus */
import Vector from '../Vector.js';
import createComponentClass from '../factory.js';

const
    processDirection = function (direction) {
        return function (state) {
            this[direction] = !state || (state.pressed !== false);
        };
    },
    doNothing = function () {},
    rotate = {
        x: function (heading, lastHeading) {
            if (heading !== lastHeading) {
                if (((heading > 180) && (lastHeading <= 180)) || ((heading <= 180) && (lastHeading > 180))) {
                    this.owner.triggerEvent('transform', 'vertical');
                }
            }
        },
        y: function (heading, lastHeading) {
            if (heading !== lastHeading) {
                if (((heading > 90 && heading <= 270) && (lastHeading <= 90 || lastHeading > 270)) || ((heading <= 90 || heading > 270) && (lastHeading > 90 && lastHeading <= 270))) {
                    this.owner.triggerEvent('transform', 'horizontal');
                }
            }
        },
        z: function (heading, lastHeading) {
            if (heading !== lastHeading) {
                this.owner.triggerEvent('replace-transform', 'rotate-' + heading);
            }
        }
    };

export default createComponentClass(/** @lends platypus.components.LogicDirectionalMovement.prototype */{
    id: 'LogicDirectionalMovement',
    
    properties: {
        /**
         * Defines the axis around which the entity should be transformed. Defaults to "y" for platforming behavior. Use "z" for top-down behavior.
         *
         * @property axis
         * @type String
         * @default "y"
         */
        axis: 'y',

        /**
         * Defines the distance in world units that the entity should be moved per millisecond.
         *
         * @property speed
         * @type Number
         * @default 0.3
         */
        speed: 0.3
    },
    
    /**
     * This component changes the [Motion](platypus.components.Motion.html) of an entity according to its current speed and heading. It accepts directional messages that can stand alone, or come from a mapped controller, in which case it checks the `pressed` value of the message before changing its course.
     *
     * @memberof platypus.components
     * @uses platypus.Component
     * @constructs
     * @listens platypus.Entity#accelerate
     * @listens platypus.Entity#component-added
     * @listens platypus.Entity#face
     * @listens platypus.Entity#handle-logic
     * @listens platypus.Entity#stop
     * @listens platypus.Entity#go-down
     * @listens platypus.Entity#go-south
     * @listens platypus.Entity#go-down-left
     * @listens platypus.Entity#go-southwest
     * @listens platypus.Entity#go-left
     * @listens platypus.Entity#go-west
     * @listens platypus.Entity#go-up-left
     * @listens platypus.Entity#go-northwest
     * @listens platypus.Entity#go-up
     * @listens platypus.Entity#go-north
     * @listens platypus.Entity#go-up-right
     * @listens platypus.Entity#go-northeast
     * @listens platypus.Entity#go-right
     * @listens platypus.Entity#go-east
     * @listens platypus.Entity#go-down-right
     * @listens platypus.Entity#go-southeast
     * @fires platypus.Entity#replace-transform
     * @fires platypus.Entity#transform
     */
    initialize: function () {
        const
            state = this.state = this.owner.state;
        
        if (typeof this.speed === 'number') {
            this.speed = [this.speed, 0, 0];
        }
        this.initialVector = Vector.setUp(this.speed);
        this.reorient = rotate[this.axis];
        if (!this.reorient) {
            this.reorient = doNothing;
        }
        
        this.moving = state.set('moving', false);
        this.left   = state.set('left', false);
        this.right  = state.set('right', false);
        this.up     = state.set('up', false);
        this.down   = state.set('down', false);

        this.upLeft = false;
        this.upRight = false;
        this.downLeft = false;
        this.downRight = false;
        
        this.heading = 0;
        this.owner.heading = this.owner.heading || 0;
        this.headingsMap = {
            up: 270,
            north: 270,
            down: 90,
            south: 90,
            left: 180,
            west: 180,
            right: 0,
            east: 0,
            "up-left": 225,
            northwest: 225,
            "up-right": 315,
            northeast: 315,
            "down-left": 135,
            southwest: 135,
            "down-right": 45,
            southeast: 45
        };
    },
    events: {
        "component-added": function (component) {
            if (component === this) {
                if (!this.owner.addMover) {
                    platypus.debug.warn('The "LogicDirectionalMovement" component requires a "Mover" component to function correctly.');
                    return;
                }

                this.direction = this.owner.addMover({
                    velocity: this.speed,
                    drag: 0,
                    friction: 0,
                    stopOnCollision: false,
                    orient: false,
                    aliases: {
                        "moving": "control-velocity"
                    }
                }).velocity;
                
                if (this.owner.heading !== this.heading) {
                    this.direction.setVector(this.initialVector).rotate((this.owner.heading / 180) * Math.PI);
                    this.heading = this.owner.heading;
                }
                
                this.owner.triggerEvent('moving', this.moving);
            }
        },
        
        "handle-logic": function () {
            const state = this.state,
                upLeft    = this.upLeft    || (this.up && this.left),
                downLeft  = this.downLeft  || (this.down && this.left),
                downRight = this.downRight || (this.down && this.right),
                upRight   = this.upRight   || (this.up && this.right);
            let up        = this.up        || this.upLeft || this.upRight,
                left      = this.left      || this.upLeft || this.downLeft,
                down      = this.down      || this.downLeft || this.downRight,
                right     = this.right     || this.upRight || this.downRight;
            
            if ((left && right) || (up && down)) {
                this.moving = false;
            } else if (upLeft) {
                this.moving = true;
                this.heading = 225;
            } else if (upRight) {
                this.moving = true;
                this.heading = 315;
            } else if (downLeft) {
                this.moving = true;
                this.heading = 135;
            } else if (downRight) {
                this.moving = true;
                this.heading = 45;
            } else if (left) {
                this.moving = true;
                this.heading = 180;
            } else if (right) {
                this.moving = true;
                this.heading = 0;
            } else if (up) {
                this.moving = true;
                this.heading = 270;
            } else if (down) {
                this.moving = true;
                this.heading = 90;
            } else {
                this.moving = false;
                
                // This is to retain the entity's direction even if there is no movement. There's probably a better way to do this since this is a bit of a retrofit. - DDD
                switch (this.heading) {
                case 270:
                    up = true;
                    break;
                case 90:
                    down = true;
                    break;
                case 180:
                    left = true;
                    break;
                case 225:
                    up = true;
                    left = true;
                    break;
                case 315:
                    up = true;
                    right = true;
                    break;
                case 135:
                    down = true;
                    left = true;
                    break;
                case 45:
                    down = true;
                    right = true;
                    break;
                case 0:
                default:
                    right = true;
                    break;
                }
            }
            
            if (this.owner.heading !== this.heading) {
                this.direction.setVector(this.initialVector).rotate((this.heading / 180) * Math.PI);
                this.reorient(this.heading, this.owner.heading);
                this.owner.heading = this.heading;
            }
            
            //TODO: possibly remove the separation of this.state.direction and this.direction to just use state?
            if (state.get('moving') !== this.moving) {
                this.owner.triggerEvent('moving', this.moving);
                state.set('moving', this.moving);
            }

            state.set('up', up);
            state.set('right', right);
            state.set('down', down);
            state.set('left', left);
        },

        "go-down": processDirection('down'),

        "go-south": processDirection('down'),

        "go-down-left": processDirection('downLeft'),

        "go-southwest": processDirection('downLeft'),

        "go-left": processDirection('left'),

        "go-west": processDirection('left'),

        "go-up-left": processDirection('upLeft'),

        "go-northwest": processDirection('upLeft'),

        "go-up": processDirection('up'),

        "go-north": processDirection('up'),

        "go-up-right": processDirection('upRight'),

        "go-northeast": processDirection('upRight'),

        "go-right": processDirection('right'),

        "go-east": processDirection('right'),

        "go-down-right": processDirection('downRight'),

        "go-southeast": processDirection('downRight'),
        
        "stop": function (state) {
            if (!state || (state.pressed !== false)) {
                this.left = false;
                this.right = false;
                this.up = false;
                this.down = false;
                this.upLeft = false;
                this.upRight = false;
                this.downLeft = false;
                this.downRight = false;
            }
        },
        
        /**
         * Set the direction the entity should face while stopped.
         *
         * @event platypus.Entity#face
         * @param direction {String} A value such as "north" or "left" to point the entity in a particular direction.
         */
        "face": function (direction) {
            this.heading = this.headingsMap[direction] || 0;
        },
        
        /**
         * Changes the velocity of the Entity when in motion.
         *
         * @event platypus.Entity#accelerate
         * @param velocity {Number|platypus.Vector} The magnitude or Vector to multiply the current velocity by.
         */
        "accelerate": function (velocity) {
            this.initialVector.normalize().multiply(velocity);
            this.direction.normalize().multiply(velocity);
        }
    },
    
    methods: {
        destroy: function () {
            this.initialVector.recycle();
            this.state = null;
        }
    }
});