/* global platypus */
import {arrayCache, greenSplice} from '../utils/array.js';
import Vector from '../Vector.js';
import createComponentClass from '../factory.js';
var tempVector = Vector.setUp(),
updateMax = function (delta, interim, goal, time) {
if (delta && (interim !== goal)) {
if (interim < goal) {
return Math.min(interim + delta * time, goal);
} else {
return Math.max(interim - delta * time, goal);
}
}
return interim;
},
clampNumber = function (v, d) {
var mIn = this.maxMagnitudeInterim = updateMax(this.maxMagnitudeDelta, this.maxMagnitudeInterim, this.maxMagnitude, d);
if (v.magnitude() > mIn) {
v.normalize().multiply(mIn);
}
},
clampObject = function (v, d) {
var max = this.maxMagnitude,
mD = this.maxMagnitudeDelta,
mIn = this.maxMagnitudeInterim;
mIn.up = updateMax(mD, mIn.up, max.up, d);
mIn.right = updateMax(mD, mIn.right, max.right, d);
mIn.down = updateMax(mD, mIn.down, max.down, d);
mIn.left = updateMax(mD, mIn.left, max.left, d);
if (v.x > 0) {
if (v.x > mIn.right) {
v.x = mIn.right;
}
} else if (v.x < 0) {
if (v.x < -mIn.left) {
v.x = -mIn.left;
}
}
if (v.y > 0) {
if (v.y > mIn.down) {
v.y = mIn.down;
}
} else if (v.y < 0) {
if (v.y < -mIn.up) {
v.y = -mIn.up;
}
}
};
export default createComponentClass(/** @lends platypus.components.Mover.prototype */{
id: 'Mover',
properties: {
/** This is a normalized vector describing the direction the ground should face away from the entity.
*
* @property ground
* @type Array|Vector
* @default Vector(0, 1)
*/
ground: [0, 1]
},
publicProperties: {
/**
* A list of key/value pairs describing vectors or vector-like objects describing acceleration and velocity on the entity. See the ["Motion"]("Motion"%20Component.html) component for properties.
*
* @property movers
* @type Array
* @default []
*/
movers: [],
/**
* If specified, the property adds gravity motion to the entity.
*
* @property gravity
* @type number|Array|Vector
* @default: 0
*/
gravity: 0,
/**
* If specified, the property adds jumping motion to the entity.
*
* @property jump
* @type number|Array|Vector
* @default: 0
*/
jump: 0,
/**
* If specified, the property adds velocity to the entity.
*
* @property speed
* @type number|Array|Vector
* @default: 0
*/
speed: 0,
/**
* This property determines how quickly velocity is dampened when the entity is not in a "grounded" state. This should be a value between 1 (no motion) and 0 (no drag).
*
* @property drag
* @type number
* @default 0.01
*/
drag: 0.01,
/**
* This property determines how quickly velocity is dampened when the entity is in a "grounded" state. This should be a value between 1 (no motion) and 0 (no friction).
*
* @property friction
* @type number
* @default 0.06
*/
friction: 0.06,
/**
* This property determines the maximum amount of velocity this entity can maintain. This can be a number or an object describing maximum velocity in a particular direction. For example:
*
* {
* "up": 8,
* "right": 12,
* "down": 0.4,
* "left": 12
* }
*
* @property maxMagnitude
* @type number|Object
* @default Infinity
*/
maxMagnitude: Infinity,
/**
* This property determines the rate of change to new maximum amount of velocities.
*
* @property maxMagnitudeDelta
* @type number
* @default 0
*/
maxMagnitudeDelta: 0,
/**
* This property determines whether orientation changes should apply external velocities from pre-change momentum.
*
* @property reorientVelocities
* @type Boolean
* @default true
*/
reorientVelocities: true
},
/**
* This component handles entity motion via velocity and acceleration changes. This is useful for directional movement, gravity, bounce-back collision reactions, jumping, etc.
*
* @memberof platypus.components
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#component-added
* @listens platypus.Entity#component-removed
* @listens platypus.Entity#handle-movement
* @listens platypus.Entity#handle-post-collision-logic
* @listens platypus.Entity#hit-solid
* @listens platypus.Entity#load
* @listens platypus.Entity#pause-movment
* @listens platypus.Entity#orientation-updated
* @listens platypus.Entity#set-mover
* @listens platypus.Entity#unpause-movment
*/
initialize: function () {
var maxMagnitude = Infinity,
max = this.maxMagnitude,
thisState = this.owner.state;
Vector.assign(this.owner, 'position', 'x', 'y', 'z');
Vector.assign(this.owner, 'velocity', 'dx', 'dy', 'dz');
this.position = this.owner.position;
this.velocity = this.owner.velocity;
this.lastVelocity = Vector.setUp(this.velocity);
this.collision = null;
this.pause = false;
// Copy movers so we're not re-using mover definitions
this.moversCopy = this.movers;
this.movers = arrayCache.setUp();
this.velocityChanges = arrayCache.setUp();
this.velocityDirections = arrayCache.setUp();
this.ground = Vector.setUp(this.ground);
this.state = thisState;
thisState.set('grounded', false);
Object.defineProperty(this.owner, "maxMagnitude", {
get: function () {
return maxMagnitude;
},
set: function (max) {
if (typeof max === 'number') {
this.clamp = clampNumber;
maxMagnitude = max;
if (!this.maxMagnitudeDelta) {
this.maxMagnitudeInterim = max;
}
} else {
this.clamp = clampObject;
if (typeof maxMagnitude === 'number') {
maxMagnitude = {
up: maxMagnitude,
right: maxMagnitude,
down: maxMagnitude,
left: maxMagnitude
};
}
if (typeof max.up === 'number') {
maxMagnitude.up = max.up;
}
if (typeof max.right === 'number') {
maxMagnitude.right = max.right;
}
if (typeof max.down === 'number') {
maxMagnitude.down = max.down;
}
if (typeof max.left === 'number') {
maxMagnitude.left = max.left;
}
if (typeof this.maxMagnitudeInterim === 'number') {
if (this.maxMagnitudeDelta) {
this.maxMagnitudeInterim = {
up: this.maxMagnitudeInterim,
right: this.maxMagnitudeInterim,
down: this.maxMagnitudeInterim,
left: this.maxMagnitudeInterim
};
} else {
this.maxMagnitudeInterim = {
up: maxMagnitude.up,
right: maxMagnitude.right,
down: maxMagnitude.down,
left: maxMagnitude.left
};
}
} else if (!this.maxMagnitudeDelta) {
this.maxMagnitudeInterim.up = maxMagnitude.up;
this.maxMagnitudeInterim.right = maxMagnitude.right;
this.maxMagnitudeInterim.down = maxMagnitude.down;
this.maxMagnitudeInterim.left = maxMagnitude.left;
}
}
}.bind(this)
});
this.maxMagnitudeInterim = 0;
this.maxMagnitude = max;
},
events: {
"component-added": function (component) {
if (component.type === 'Motion') {
this.movers.push(component);
}
},
"component-removed": function (component) {
var i = 0;
if (component.type === 'Motion') {
i = this.movers.indexOf(component);
if (i >= 0) {
greenSplice(this.movers, i);
}
}
},
"load": function () {
var i = 0,
movs = this.moversCopy;
delete this.moversCopy;
for (i = 0; i < movs.length; i++) {
this.addMover(movs[i]);
}
this.externalForces = this.addMover({
velocity: [0, 0, 0],
orient: false
}).velocity;
// Set up speed property if supplied.
if (this.speed) {
if (!isNaN(this.speed)) {
this.speed = [this.speed, 0, 0];
}
this.speed = this.addMover({
velocity: this.speed,
controlState: "moving"
}).velocity;
}
// Set up gravity property if supplied.
if (this.gravity) {
if (!isNaN(this.gravity)) {
this.gravity = [0, this.gravity, 0];
}
this.gravity = this.addMover({
acceleration: this.gravity,
orient: false,
aliases: {
"gravitate": "control-acceleration"
}
}).acceleration;
}
// Set up jump property if supplied.
if (this.jump) {
if (!isNaN(this.jump)) {
this.jump = [0, this.jump, 0];
}
this.jump = this.addMover({
velocity: this.jump,
instant: true,
controlState: "grounded",
state: "jumping",
instantSuccess: "just-jumped",
instantDecay: 0.2,
aliases: {
"jump": "instant-motion"
}
}).instant;
}
},
"handle-movement": function (tick) {
var delta = tick.delta,
m = null,
thisState = this.state,
vect = null,
velocity = this.velocity,
position = this.position,
movers = this.movers,
i = movers.length;
if (thisState.get('paused') || this.paused) {
return;
}
if (!velocity.equals(this.lastVelocity, 2)) {
this.externalForces.addVector(velocity).subtractVector(this.lastVelocity);
}
velocity.setXYZ(0, 0, 0);
while (i--) {
m = movers[i].update(delta);
if (m) {
if (this.grounded) { // put this in here to match earlier behavior
if (movers[i].friction !== -1) {
m.multiply(1 - movers[i].friction);
} else {
m.multiply(1 - this.friction);
}
} else if (movers[i].drag !== -1) {
m.multiply(1 - movers[i].drag);
} else {
m.multiply(1 - this.drag);
}
velocity.add(m);
}
}
this.clamp(velocity, delta);
this.lastVelocity.setVector(velocity);
vect = Vector.setUp(velocity).multiply(delta);
position.add(vect);
vect.recycle();
thisState.set('grounded', this.grounded);
this.grounded = false;
},
/**
* On receiving this message, this component stops all velocities along the axis of the collision direction and sets "grounded" to `true` if colliding with the ground.
*
* @event platypus.Entity#hit-solid
* @param collisionInfo {Object}
* @param collisionInfo.direction {platypus.Vector} The direction of collision from the entity's position.
*/
"hit-solid": function (collisionInfo) {
var s = 0,
e = 0,
entityV = collisionInfo.entity && collisionInfo.entity.velocity,
direction = collisionInfo.direction,
add = true,
vc = this.velocityChanges,
vd = this.velocityDirections,
i = vc.length;
if (direction.dot(this.ground) > 0) {
this.grounded = true;
}
s = this.velocity.scalarProjection(direction);
if (s > 0) {
if (entityV) {
e = Math.max(entityV.scalarProjection(direction), 0);
if (e < s) {
s = e;
} else {
s = 0;
}
} else {
s = 0;
}
while (i--) {
if ((s < vc[i]) && (vd[i].dot(direction) > 0)) {
vc[i] = s;
vd[i].setVector(direction);
add = false;
break;
}
}
if (add) {
vc.push(s);
vd.push(Vector.setUp(direction));
}
}
},
"handle-post-collision-logic": function () {
var direction = null,
ms = this.movers,
vc = this.velocityChanges,
vd = this.velocityDirections,
i = vc.length,
j = ms.length,
m = null,
s = 0,
sdi = 0,
soc = null,
v = tempVector;
if (i) {
soc = arrayCache.setUp();
while (j--) {
m = ms[j];
if (m.stopOnCollision) {
soc.push(m);
}
}
while (i--) {
direction = vd[i];
s = vc[i];
j = soc.length;
sdi = s / j;
while (j--) {
m = soc[j];
v.setVector(direction).normalize().multiply(sdi - m.velocity.scalarProjection(direction));
m.velocity.add(v);
}
direction.recycle();
}
vc.length = 0;
vd.length = 0;
arrayCache.recycle(soc);
}
},
/**
* Update mover properties.
*
* @event platypus.Entity#set-mover
* @param mover {Object}
* @param [mover.maxMagnitude] {Number|Object} New maximums for magnitude.
* @param [mover.magnitude] {Number} Delta for the change in maximums.
*/
"set-mover": function (mover) {
if (typeof mover.maxMagnitudeDelta === 'number') {
this.maxMagnitudeDelta = mover.maxMagnitudeDelta;
}
if (mover.maxMagnitude) {
this.maxMagnitude = mover.maxMagnitude;
}
},
/**
* Stops all movement on the Entity.
*
* @event platypus.Entity#pause-movment
*/
"pause-movement": function () {
this.paused = true;
},
/**
* Unpauses all movement on the Entity.
*
* @event platypus.Entity#unpause-movment
*/
"unpause-movement": function () {
this.paused = false;
},
"orientation-updated": function (matrix) {
if (!this.reorientVelocities) {
this.lastVelocity.multiply(matrix);
}
}
},
methods: {
destroy: function () {
var i = 0,
max = this.maxMagnitude;
for (i = this.movers.length - 1; i >= 0; i--) {
this.removeMover(this.movers[i]);
}
arrayCache.recycle(this.movers);
this.ground.recycle();
this.lastVelocity.recycle();
arrayCache.recycle(this.velocityChanges);
arrayCache.recycle(this.velocityDirections);
delete this.owner.maxMagnitude; // remove property handlers
this.owner.maxMagnitude = max;
this.state = null;
}
},
publicMethods: {
/**
* This method adds a mover to the entity in the form of a ["Motion"]("Motion"%20Component.html) component definition.
*
* @method platypus.components.Mover#addMover
* @param mover {Object} For motion definition properties, see the ["Motion"]("Motion"%20Component.html) component.
* @return motion {Motion}
*/
addMover: function (mover) {
var m = this.owner.addComponent(new platypus.components.Motion(this.owner, mover));
return m;
},
/**
* This method removes a mover from the entity.
*
* @method platypus.components.Mover#removeMover
* @param motion {Motion}
*/
removeMover: function (m) {
this.owner.removeComponent(m);
}
}
});