phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
1,171 lines (929 loc) • 37 kB
JavaScript
var Particle = require('../../../src/gameobjects/particles/Particle');
// ---------------------------------------------------------------------------
// Mock helpers
// ---------------------------------------------------------------------------
function createMockEmitter ()
{
return {
scene: {},
anims: [],
emit: function (event, a1, a2, a3, a4, a5)
{
return true;
}
};
}
function createMockOp (returnValue)
{
var val = (returnValue !== undefined) ? returnValue : 0;
return {
active: false,
steps: 0,
onEmit: function (particle, key) { return val; },
onUpdate: function (particle, key, t, current) { return current; }
};
}
// An op that always returns its fixed value from both onEmit and onUpdate
function createFixedOp (returnValue)
{
var val = (returnValue !== undefined) ? returnValue : 0;
return {
active: false,
steps: 0,
onEmit: function (particle, key) { return val; },
onUpdate: function (particle, key, t, current) { return val; }
};
}
function createMockOps ()
{
return {
x: createMockOp(0),
y: createMockOp(0),
lifespan: createMockOp(1000),
delay: createMockOp(0),
hold: createMockOp(0),
scaleX: createMockOp(1),
scaleY: createMockOp(1),
rotate: createMockOp(0),
speedX: createMockOp(100),
speedY: createMockOp(100),
angle: createMockOp(0),
moveToX: createMockOp(0),
moveToY: createMockOp(0),
accelerationX: createMockOp(0),
accelerationY: createMockOp(0),
maxVelocityX: createMockOp(10000),
maxVelocityY: createMockOp(10000),
bounce: createMockOp(0),
alpha: createMockOp(1),
tint: createMockOp(0xffffff),
color: createMockOp(0xffffff)
};
}
function createMockFrame ()
{
return {
width: 32,
height: 32,
texture: { key: 'test' }
};
}
function createIdentityMatrix ()
{
return {
scaleX: 1,
scaleY: 1,
transformPoint: function (x, y, point)
{
point.x = x;
point.y = y;
return point;
}
};
}
function createFullMockEmitter ()
{
var frame = createMockFrame();
return {
scene: {},
anims: [],
ops: createMockOps(),
emit: function () { return true; },
getAnim: function () { return null; },
getFrame: function () { return frame; },
getEmitZone: function (particle) {},
getDeathZone: function (particle) { return false; },
worldMatrix: {
transformPoint: function (x, y, point)
{
point.x = x;
point.y = y;
return point;
}
},
getWorldTransformMatrix: function ()
{
return createIdentityMatrix();
},
radial: false,
moveTo: false,
acceleration: false,
gravityX: 0,
gravityY: 0
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('Particle', function ()
{
describe('constructor', function ()
{
it('should store the emitter reference', function ()
{
var emitter = createMockEmitter();
var particle = new Particle(emitter);
expect(particle.emitter).toBe(emitter);
});
it('should default x and y to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.x).toBe(0);
expect(particle.y).toBe(0);
});
it('should default velocities to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.velocityX).toBe(0);
expect(particle.velocityY).toBe(0);
});
it('should default accelerations to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.accelerationX).toBe(0);
expect(particle.accelerationY).toBe(0);
});
it('should default maxVelocity to 10000', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.maxVelocityX).toBe(10000);
expect(particle.maxVelocityY).toBe(10000);
});
it('should default bounce to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.bounce).toBe(0);
});
it('should default scale to one', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.scaleX).toBe(1);
expect(particle.scaleY).toBe(1);
});
it('should default alpha to one', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.alpha).toBe(1);
});
it('should default angle and rotation to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.angle).toBe(0);
expect(particle.rotation).toBe(0);
});
it('should default tint to white', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.tint).toBe(0xffffff);
});
it('should default life and lifeCurrent to 1000', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.life).toBe(1000);
expect(particle.lifeCurrent).toBe(1000);
});
it('should default delayCurrent and holdCurrent to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.delayCurrent).toBe(0);
expect(particle.holdCurrent).toBe(0);
});
it('should default lifeT to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.lifeT).toBe(0);
});
it('should default texture and frame to null', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.texture).toBeNull();
expect(particle.frame).toBeNull();
});
it('should initialise worldPosition as a Vector2 at origin', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.worldPosition).toBeDefined();
expect(particle.worldPosition.x).toBe(0);
expect(particle.worldPosition.y).toBe(0);
});
it('should initialise bounds as a Rectangle', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.bounds).toBeDefined();
expect(typeof particle.bounds.x).toBe('number');
expect(typeof particle.bounds.y).toBe('number');
});
it('should set scene from emitter.scene', function ()
{
var scene = { key: 'myScene' };
var emitter = createMockEmitter();
emitter.scene = scene;
var particle = new Particle(emitter);
expect(particle.scene).toBe(scene);
});
it('should leave anims null when emitter has no anims', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.anims).toBeNull();
});
it('should initialise data with expected keys', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.data).toBeDefined();
expect(particle.data.tint).toBeDefined();
expect(particle.data.alpha).toBeDefined();
expect(particle.data.rotate).toBeDefined();
expect(particle.data.scaleX).toBeDefined();
expect(particle.data.scaleY).toBeDefined();
expect(particle.data.x).toBeDefined();
expect(particle.data.y).toBeDefined();
expect(particle.data.bounce).toBeDefined();
});
});
// -----------------------------------------------------------------------
describe('isAlive', function ()
{
it('should return true when lifeCurrent is positive', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = 500;
expect(particle.isAlive()).toBe(true);
});
it('should return true when lifeCurrent is 1', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = 1;
expect(particle.isAlive()).toBe(true);
});
it('should return false when lifeCurrent is zero', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = 0;
expect(particle.isAlive()).toBe(false);
});
it('should return false when lifeCurrent is negative', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = -100;
expect(particle.isAlive()).toBe(false);
});
});
// -----------------------------------------------------------------------
describe('kill', function ()
{
it('should set lifeCurrent to zero', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.lifeCurrent).toBe(1000);
particle.kill();
expect(particle.lifeCurrent).toBe(0);
});
it('should cause isAlive to return false', function ()
{
var particle = new Particle(createMockEmitter());
particle.kill();
expect(particle.isAlive()).toBe(false);
});
it('should work when particle is already dead', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = 0;
particle.kill();
expect(particle.lifeCurrent).toBe(0);
});
});
// -----------------------------------------------------------------------
describe('setPosition', function ()
{
it('should set x and y to the given values', function ()
{
var particle = new Particle(createMockEmitter());
particle.setPosition(100, 200);
expect(particle.x).toBe(100);
expect(particle.y).toBe(200);
});
it('should reset x and y to zero when called with no arguments', function ()
{
var particle = new Particle(createMockEmitter());
particle.x = 50;
particle.y = 75;
particle.setPosition();
expect(particle.x).toBe(0);
expect(particle.y).toBe(0);
});
it('should handle negative coordinates', function ()
{
var particle = new Particle(createMockEmitter());
particle.setPosition(-300, -400);
expect(particle.x).toBe(-300);
expect(particle.y).toBe(-400);
});
it('should handle floating point coordinates', function ()
{
var particle = new Particle(createMockEmitter());
particle.setPosition(1.5, 2.7);
expect(particle.x).toBeCloseTo(1.5);
expect(particle.y).toBeCloseTo(2.7);
});
it('should default x to zero when only y is undefined', function ()
{
var particle = new Particle(createMockEmitter());
particle.setPosition(undefined, 50);
expect(particle.x).toBe(0);
expect(particle.y).toBe(50);
});
});
// -----------------------------------------------------------------------
describe('emit', function ()
{
it('should delegate to emitter.emit and return its result', function ()
{
var emitter = createMockEmitter();
var called = false;
var capturedArgs = [];
emitter.emit = function (event, a1, a2, a3, a4, a5)
{
called = true;
capturedArgs = [ event, a1, a2, a3, a4, a5 ];
return true;
};
var particle = new Particle(emitter);
var result = particle.emit('test-event', 1, 2, 3, 4, 5);
expect(called).toBe(true);
expect(result).toBe(true);
expect(capturedArgs[0]).toBe('test-event');
expect(capturedArgs[1]).toBe(1);
expect(capturedArgs[2]).toBe(2);
expect(capturedArgs[3]).toBe(3);
expect(capturedArgs[4]).toBe(4);
expect(capturedArgs[5]).toBe(5);
});
it('should return false when emitter.emit returns false', function ()
{
var emitter = createMockEmitter();
emitter.emit = function () { return false; };
var particle = new Particle(emitter);
expect(particle.emit('no-listeners')).toBe(false);
});
});
// -----------------------------------------------------------------------
describe('setSizeToFrame', function ()
{
it('should be a no-op and not throw', function ()
{
var particle = new Particle(createMockEmitter());
expect(function () { particle.setSizeToFrame(); }).not.toThrow();
});
it('should return undefined', function ()
{
var particle = new Particle(createMockEmitter());
expect(particle.setSizeToFrame()).toBeUndefined();
});
});
// -----------------------------------------------------------------------
describe('fire', function ()
{
it('should return true when particle fires successfully', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
var result = particle.fire(100, 200);
expect(result).toBe(true);
});
it('should set frame and texture from emitter.getFrame', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.frame).not.toBeNull();
expect(particle.texture).not.toBeNull();
expect(particle.texture.key).toBe('test');
});
it('should set life and lifeCurrent from ops.lifespan', function ()
{
var emitter = createFullMockEmitter();
emitter.ops.lifespan = createMockOp(2000);
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.life).toBe(2000);
expect(particle.lifeCurrent).toBe(2000);
});
it('should reset lifeT to zero', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.lifeT = 0.9;
particle.fire(0, 0);
expect(particle.lifeT).toBe(0);
});
it('should set delayCurrent from ops.delay', function ()
{
var emitter = createFullMockEmitter();
emitter.ops.delay = createMockOp(500);
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.delayCurrent).toBe(500);
});
it('should set alpha from ops.alpha', function ()
{
var emitter = createFullMockEmitter();
emitter.ops.alpha = createMockOp(0.5);
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.alpha).toBeCloseTo(0.5);
});
it('should set scaleX from ops.scaleX', function ()
{
var emitter = createFullMockEmitter();
emitter.ops.scaleX = createMockOp(2);
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.scaleX).toBe(2);
});
it('should copy scaleX to scaleY when scaleY op is inactive', function ()
{
var emitter = createFullMockEmitter();
emitter.ops.scaleX = createMockOp(3);
emitter.ops.scaleY.active = false;
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.scaleY).toBe(3);
});
it('should use scaleY op value when scaleY op is active', function ()
{
var emitter = createFullMockEmitter();
emitter.ops.scaleX = createMockOp(2);
emitter.ops.scaleY = {
active: true,
steps: 0,
onEmit: function () { return 4; },
onUpdate: function (p, k, t, c) { return c; }
};
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.scaleX).toBe(2);
expect(particle.scaleY).toBe(4);
});
it('should set non-radial velocities directly from speedX and speedY ops', function ()
{
var emitter = createFullMockEmitter();
emitter.radial = false;
emitter.ops.speedX = createMockOp(150);
emitter.ops.speedY = {
active: true,
steps: 0,
onEmit: function () { return 75; },
onUpdate: function (p, k, t, c) { return c; }
};
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.velocityX).toBe(150);
expect(particle.velocityY).toBe(75);
});
it('should copy speedX to velocityY when speedY op is inactive', function ()
{
var emitter = createFullMockEmitter();
emitter.radial = false;
emitter.ops.speedX = createMockOp(200);
emitter.ops.speedY.active = false;
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.velocityX).toBe(200);
expect(particle.velocityY).toBe(200);
});
it('should set radial velocity using angle op when radial is true', function ()
{
var emitter = createFullMockEmitter();
emitter.radial = true;
// angle = 0 degrees -> cos(0)=1, sin(0)=0
emitter.ops.angle = {
active: false,
steps: 0,
onEmit: function () { return 0; },
onUpdate: function (p, k, t, c) { return c; }
};
emitter.ops.speedX = createMockOp(100);
emitter.ops.speedY.active = false;
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.velocityX).toBeCloseTo(100);
expect(particle.velocityY).toBeCloseTo(0);
});
it('should set moveTo velocities when emitter.moveTo is true', function ()
{
var emitter = createFullMockEmitter();
emitter.moveTo = true;
emitter.ops.lifespan = createMockOp(1000); // 1 second
emitter.ops.moveToX = {
active: false,
steps: 0,
onEmit: function () { return 200; },
onUpdate: function (p, k, t, c) { return c; }
};
emitter.ops.moveToY = {
active: false,
steps: 0,
onEmit: function () { return 300; },
onUpdate: function (p, k, t, c) { return c; }
};
var particle = new Particle(emitter);
particle.fire(0, 0);
// life = 1000ms = 1s, so velocity = distance/time
// x starts at 0 (no emit zone offset), moveToX=200 -> vx = 200/1 = 200
expect(particle.velocityX).toBeCloseTo(200);
expect(particle.velocityY).toBeCloseTo(300);
});
it('should return false when particle spawns inside a death zone', function ()
{
var emitter = createFullMockEmitter();
emitter.getDeathZone = function () { return true; };
var particle = new Particle(emitter);
var result = particle.fire(0, 0);
expect(result).toBe(false);
expect(particle.lifeCurrent).toBe(0);
});
it('should set acceleration when emitter.acceleration is true', function ()
{
var emitter = createFullMockEmitter();
emitter.acceleration = true;
emitter.ops.accelerationX = createMockOp(50);
emitter.ops.accelerationY = createMockOp(80);
var particle = new Particle(emitter);
particle.fire(0, 0);
expect(particle.accelerationX).toBe(50);
expect(particle.accelerationY).toBe(80);
});
it('should throw when no texture frame is available', function ()
{
var emitter = createFullMockEmitter();
emitter.getFrame = function () { return null; };
var particle = new Particle(emitter);
expect(function () { particle.fire(0, 0); }).toThrow();
});
});
// -----------------------------------------------------------------------
describe('update', function ()
{
it('should return true immediately when lifeCurrent is zero and holdCurrent is zero', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = 0;
particle.holdCurrent = 0;
expect(particle.update(16, 0.016, [])).toBe(true);
});
it('should return false and decrement holdCurrent when lifeCurrent is zero but holdCurrent is positive', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = 0;
particle.holdCurrent = 500;
var result = particle.update(100, 0.1, []);
expect(result).toBe(false);
expect(particle.holdCurrent).toBe(400);
});
it('should return true when holdCurrent expires after decrement', function ()
{
var particle = new Particle(createMockEmitter());
particle.lifeCurrent = 0;
particle.holdCurrent = 50;
var result = particle.update(100, 0.1, []);
expect(result).toBe(true);
expect(particle.holdCurrent).toBeLessThanOrEqual(0);
});
it('should return false and decrement delayCurrent when delay is active', function ()
{
var particle = new Particle(createMockEmitter());
particle.delayCurrent = 300;
var result = particle.update(100, 0.1, []);
expect(result).toBe(false);
expect(particle.delayCurrent).toBe(200);
});
it('should return false and not advance life while delay is pending', function ()
{
var particle = new Particle(createMockEmitter());
particle.delayCurrent = 100;
var lifeBefore = particle.lifeCurrent;
particle.update(16, 0.016, []);
expect(particle.lifeCurrent).toBe(lifeBefore);
});
it('should decrement lifeCurrent by delta on normal update', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
particle.update(16, 0.016, []);
expect(particle.lifeCurrent).toBe(1000 - 16);
});
it('should return false when particle is still alive after update', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
var result = particle.update(16, 0.016, []);
expect(result).toBe(false);
});
it('should return true when lifeCurrent reaches zero', function ()
{
var emitter = createFullMockEmitter();
emitter.ops.lifespan = createMockOp(16);
var particle = new Particle(emitter);
particle.fire(0, 0);
var result = particle.update(16, 0.016, []);
expect(result).toBe(true);
});
it('should update lifeT proportionally', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
// lifeT is computed at the top of update before decrementing lifeCurrent.
// Set lifeCurrent to 500 so that t = 1 - (500/1000) = 0.5.
particle.lifeCurrent = 500;
particle.update(16, 0.016, []);
expect(particle.lifeT).toBeCloseTo(0.5);
});
it('should kill the particle and return true when entering a death zone during update', function ()
{
var emitter = createFullMockEmitter();
var callCount = 0;
emitter.getDeathZone = function ()
{
callCount++;
// First call is from fire(), second is from update()
return callCount > 1;
};
var particle = new Particle(emitter);
particle.fire(0, 0);
var result = particle.update(16, 0.016, []);
expect(result).toBe(true);
expect(particle.lifeCurrent).toBe(0);
});
it('should call active processor update during normal update', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
var processorCalled = false;
var processors = [
{
active: true,
update: function (p, delta, step, t)
{
processorCalled = true;
}
}
];
particle.update(16, 0.016, processors);
expect(processorCalled).toBe(true);
});
it('should skip inactive processors during update', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
var processorCalled = false;
var processors = [
{
active: false,
update: function ()
{
processorCalled = true;
}
}
];
particle.update(16, 0.016, processors);
expect(processorCalled).toBe(false);
});
});
// -----------------------------------------------------------------------
describe('computeVelocity', function ()
{
it('should update position based on velocity and step', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.velocityX = 100;
particle.velocityY = 50;
particle.computeVelocity(emitter, 16, 0.016, [], 0);
expect(particle.x).toBeCloseTo(100 * 0.016);
expect(particle.y).toBeCloseTo(50 * 0.016);
});
it('should apply gravity to velocity', function ()
{
var emitter = createFullMockEmitter();
emitter.gravityY = 100;
var particle = new Particle(emitter);
particle.velocityX = 0;
particle.velocityY = 0;
particle.computeVelocity(emitter, 1000, 1, [], 0);
// vy = 0 + (100 * 1) = 100
expect(particle.velocityY).toBeCloseTo(100);
// y = 0 + 100 * 1 = 100
expect(particle.y).toBeCloseTo(100);
});
it('should apply horizontal gravity to velocity', function ()
{
var emitter = createFullMockEmitter();
emitter.gravityX = 200;
var particle = new Particle(emitter);
particle.velocityX = 0;
particle.velocityY = 0;
particle.computeVelocity(emitter, 1000, 1, [], 0);
expect(particle.velocityX).toBeCloseTo(200);
expect(particle.x).toBeCloseTo(200);
});
it('should clamp velocity to maxVelocityX and maxVelocityY', function ()
{
var emitter = createFullMockEmitter();
emitter.gravityX = 0;
emitter.gravityY = 0;
// Use createFixedOp so onUpdate always returns 50, not particle.maxVelocityX
emitter.ops.maxVelocityX = createFixedOp(50);
emitter.ops.maxVelocityY = createFixedOp(50);
var particle = new Particle(emitter);
particle.velocityX = 999;
particle.velocityY = -999;
particle.computeVelocity(emitter, 16, 0.016, [], 0);
expect(particle.velocityX).toBe(50);
expect(particle.velocityY).toBe(-50);
});
it('should update worldPosition after integrating position', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.velocityX = 100;
particle.velocityY = 200;
particle.computeVelocity(emitter, 1000, 1, [], 0);
expect(particle.worldPosition.x).toBeCloseTo(100);
expect(particle.worldPosition.y).toBeCloseTo(200);
});
it('should call active processor.update with correct arguments', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
var capturedArgs = null;
var processors = [
{
active: true,
update: function (p, delta, step, t)
{
capturedArgs = { p: p, delta: delta, step: step, t: t };
}
}
];
particle.computeVelocity(emitter, 16, 0.016, processors, 0.5);
expect(capturedArgs).not.toBeNull();
expect(capturedArgs.p).toBe(particle);
expect(capturedArgs.delta).toBe(16);
expect(capturedArgs.step).toBeCloseTo(0.016);
expect(capturedArgs.t).toBe(0.5);
});
it('should skip inactive processors', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
var called = false;
var processors = [
{
active: false,
update: function () { called = true; }
}
];
particle.computeVelocity(emitter, 16, 0.016, processors, 0);
expect(called).toBe(false);
});
it('should handle an empty processors array', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
expect(function ()
{
particle.computeVelocity(emitter, 16, 0.016, [], 0);
}).not.toThrow();
});
});
// -----------------------------------------------------------------------
describe('getBounds', function ()
{
it('should return the bounds rectangle', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
var result = particle.getBounds(createIdentityMatrix());
expect(result).toBe(particle.bounds);
});
it('should compute correct bounds for an axis-aligned unrotated particle at origin', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
particle.rotation = 0;
// frame is 32x32, scaleX/Y = 1, identity matrix
// half-width = 16, half-height = 16
particle.getBounds(createIdentityMatrix());
expect(particle.bounds.x).toBeCloseTo(-16);
expect(particle.bounds.y).toBeCloseTo(-16);
expect(particle.bounds.width).toBeCloseTo(32);
expect(particle.bounds.height).toBeCloseTo(32);
});
it('should scale bounds by scaleX and scaleY', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
particle.scaleX = 2;
particle.scaleY = 2;
particle.rotation = 0;
// frame 32x32, scale 2 -> half = 32
particle.getBounds(createIdentityMatrix());
expect(particle.bounds.width).toBeCloseTo(64);
expect(particle.bounds.height).toBeCloseTo(64);
});
it('should offset bounds when particle is not at origin', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
particle.x = 100;
particle.y = 50;
particle.rotation = 0;
particle.getBounds(createIdentityMatrix());
// x=100 ± 16 -> min=84, width=32
expect(particle.bounds.x).toBeCloseTo(84);
expect(particle.bounds.y).toBeCloseTo(34);
expect(particle.bounds.width).toBeCloseTo(32);
expect(particle.bounds.height).toBeCloseTo(32);
});
it('should produce a larger bounding box when particle is rotated 45 degrees', function ()
{
var emitter = createFullMockEmitter();
var particle = new Particle(emitter);
particle.fire(0, 0);
particle.rotation = Math.PI / 4; // 45 degrees
var unrotated = particle.getBounds(createIdentityMatrix());
var unrotatedWidth = unrotated.width;
particle.rotation = 0;
particle.getBounds(createIdentityMatrix());
var straightWidth = particle.bounds.width;
particle.rotation = Math.PI / 4;
particle.getBounds(createIdentityMatrix());
var rotatedWidth = particle.bounds.width;
// At 45 degrees a square's bounding box is larger than its unrotated form
expect(rotatedWidth).toBeGreaterThan(straightWidth - 0.001);
});
it('should use emitter world transform matrix when no matrix argument is given', function ()
{
var emitter = createFullMockEmitter();
var matrixUsed = false;
emitter.getWorldTransformMatrix = function ()
{
matrixUsed = true;
return createIdentityMatrix();
};
var particle = new Particle(emitter);
particle.fire(0, 0);
particle.getBounds();
expect(matrixUsed).toBe(true);
});
});
// -----------------------------------------------------------------------
describe('destroy', function ()
{
it('should null the emitter reference', function ()
{
var particle = new Particle(createMockEmitter());
particle.destroy();
expect(particle.emitter).toBeNull();
});
it('should null the texture reference', function ()
{
var particle = new Particle(createMockEmitter());
particle.destroy();
expect(particle.texture).toBeNull();
});
it('should null the frame reference', function ()
{
var particle = new Particle(createMockEmitter());
particle.destroy();
expect(particle.frame).toBeNull();
});
it('should null the scene reference', function ()
{
var particle = new Particle(createMockEmitter());
particle.destroy();
expect(particle.scene).toBeNull();
});
it('should null the anims reference', function ()
{
var particle = new Particle(createMockEmitter());
particle.destroy();
expect(particle.anims).toBeNull();
});
it('should not throw when called on an already-destroyed particle', function ()
{
var particle = new Particle(createMockEmitter());
particle.destroy();
expect(function () { particle.destroy(); }).not.toThrow();
});
});
});