/**
* Arena5 HTML canvas game.
* Scenes for CanvasMark Rendering Benchmark - March 2013
*
* (C) 2013 Kevin Roast kevtoast@yahoo.com @kevinroast
*
* Please see: license.txt
* You are welcome to use this code, but I would appreciate an email or tweet
* if you do anything interesting with it!
*/
/**
* Arena root namespace.
*
* @namespace Arena
*/
if (typeof Arena == "undefined" || !Arena)
{
var Arena = {};
}
/**
* Arena prerenderer class.
*
* @namespace Arena
* @class Arena.Prerenderer
*/
(function()
{
Arena.Prerenderer = function()
{
this.images = [];
this._renderers = [];
return this;
};
Arena.Prerenderer.prototype =
{
/**
* Image list. Keyed by renderer ID - returning an array also. So to get
* the first image output by prerenderer with id "default": images["default"][0]
*
* @public
* @property images
* @type Array
*/
images: null,
_renderers: null,
/**
* Add a renderer function to the list of renderers to execute
*
* @param fn {function} Callback to execute to perform prerender
* Passed canvas element argument - to execute against - the
* callback is responsible for setting appropriate width/height
* of the buffer and should not assume it is cleared.
* Should return Array of images from prerender process
* @param id {string} Id of the prerender - used to lookup images later
*/
addRenderer: function addRenderer(fn, id)
{
this._renderers[id] = fn;
},
/**
* Execute all prerender functions - call once all renderers have been added
*/
execute: function execute()
{
var buffer = document.createElement('canvas');
for (var id in this._renderers)
{
this.images[id] = this._renderers[id].call(this, buffer);
}
}
};
})();
/**
* Arena main Benchmark Test class.
*
* @namespace Arena
* @class Arena.Test
*/
(function()
{
Arena.Test = function(benchmark)
{
// generate the single player actor - available across all scenes
this.player = new Arena.Player(new Vector(GameHandler.width / 2, GameHandler.height / 2), new Vector(0, 0), 0);
// add benchmark scene
benchmark.addBenchmarkScene(new Arena.BenchmarkScene(this, benchmark.scenes.length, 0));
// perform prerender steps - create bitmap graphics to use later
var pr = new Arena.Prerenderer();
// function to generate a set of point particle images
var fnPointRenderer = function(buffer, colour)
{
var imgs = [];
for (var size=4; size<=10; size+=2)
{
var width = size << 1;
buffer.width = buffer.height = width;
var ctx = buffer.getContext('2d');
var radgrad = ctx.createRadialGradient(size, size, size >> 1, size, size, size);
radgrad.addColorStop(0, colour);
radgrad.addColorStop(1, "#000");
ctx.fillStyle = radgrad;
ctx.fillRect(0, 0, width, width);
var img = new Image();
img.src = buffer.toDataURL("image/png");
imgs.push(img);
}
return imgs;
};
// add the various point particle image prerenderers based on above function
// default explosion colour
pr.addRenderer(function(buffer) {
return fnPointRenderer.call(this, buffer, "rgb(255,125,50)");
}, "points_rgb(255,125,50)");
// Tracker: enemy particles
pr.addRenderer(function(buffer) {
return fnPointRenderer.call(this, buffer, "rgb(255,96,0)");
}, "points_rgb(255,96,0)");
// Borg: enemy particles
pr.addRenderer(function(buffer) {
return fnPointRenderer.call(this, buffer, "rgb(0,255,64)");
}, "points_rgb(0,255,64)");
// Splitter: enemy particles
pr.addRenderer(function(buffer) {
return fnPointRenderer.call(this, buffer, "rgb(148,0,255)");
}, "points_rgb(148,0,255)");
// Bomber: enemy particles
pr.addRenderer(function(buffer) {
return fnPointRenderer.call(this, buffer, "rgb(255,0,255)");
}, "points_rgb(255,0,255)");
// add the smudge explosion particle image prerenderer
pr.addRenderer(function(buffer) {
var imgs = [];
for (var size=8; size<=64; size+=8)
{
var width = size << 1;
buffer.width = buffer.height = width;
var ctx = buffer.getContext('2d');
var radgrad = ctx.createRadialGradient(size, size, size >> 3, size, size, size);
radgrad.addColorStop(0, "rgb(255,125,50)");
radgrad.addColorStop(1, "#000");
ctx.fillStyle = radgrad;
ctx.fillRect(0, 0, width, width);
var img = new Image();
img.src = buffer.toDataURL("image/png");
imgs.push(img);
}
return imgs;
}, "smudges");
pr.addRenderer(function(buffer) {
var imgs = [];
var size = 40;
buffer.width = buffer.height = size;
var ctx = buffer.getContext('2d');
// draw bullet primary weapon
var rf = function(width, height)
{
ctx.beginPath();
ctx.moveTo(0, height);
ctx.lineTo(width, 0);
ctx.lineTo(width, -height);
ctx.lineTo(0, -height*0.5);
ctx.lineTo(-width, -height);
ctx.lineTo(-width, 0);
ctx.closePath();
ctx.fill();
};
ctx.shadowBlur = 8;
ctx.globalCompositeOperation = "lighter";
ctx.translate(size/2, size/2);
ctx.shadowColor = "rgb(255,255,255)";
ctx.fillStyle = "rgb(255,220,75)";
rf.call(this, 10, 15)
ctx.shadowColor = "rgb(255,100,100)";
ctx.fillStyle = "rgb(255,50,50)";
rf.call(this, 10 * 0.75, 15 * 0.75);
var img = new Image();
img.src = buffer.toDataURL("image/png");
imgs.push(img);
return imgs;
}, "playerweapon");
pr.execute();
GameHandler.prerenderer = pr;
};
Arena.Test.prototype =
{
/**
* Reference to the single game player actor
*/
player: null,
/**
* Lives count
*/
lives: 1,
/**
* Current game score
*/
score: 0,
/**
* High score
*/
highscore: 0,
/**
* Last score
*/
lastscore: 0,
/**
* Current multipler
*/
scoreMultiplier: 1
};
})();
/**
* Arena Game scene class.
*
* @namespace Arena
* @class Arena.BenchmarkScene
*/
(function()
{
Arena.BenchmarkScene = function(game, test, feature)
{
this.game = game;
this.test = test;
this.feature = feature;
this.player = game.player;
var msg = "Test " + test + " - Arena5 - Vectors, shadows, bitmaps, text";
var interval = new Game.Interval(msg, this.intervalRenderer);
Arena.BenchmarkScene.superclass.constructor.call(this, true, interval);
};
extend(Arena.BenchmarkScene, Game.Scene,
{
test: 0,
game: null,
/**
* Local reference to the game player actor
*/
player: null,
/**
* Top-level list of game actors sub-lists
*/
actors: null,
/**
* List of player fired bullet actors
*/
playerBullets: null,
/**
* List of enemy actors (asteroids, ships etc.)
*/
enemies: null,
/**
* List of enemy fired bullet actors
*/
enemyBullets: null,
/**
* List of effect actors
*/
effects: null,
/**
* List of collectables actors
*/
collectables: null,
/**
* Displayed score (animates towards actual score)
*/
scoredisplay: 0,
/**
* Scene init event handler
*/
onInitScene: function onInitScene()
{
// generate the actors and add the actor sub-lists to the main actor list
this.actors = [];
this.actors.push(this.enemies = []);
this.actors.push(this.playerBullets = []);
this.actors.push(this.enemyBullets = []);
this.actors.push(this.effects = []);
this.actors.push(this.collectables = []);
// start view centered in the game world
this.world.viewx = this.world.viewy = (this.world.size / 2) - (this.world.viewsize / 2);
this.testScore = 10;
// reset player
this.resetPlayerActor();
},
/**
* Restore the player to the game - reseting position etc.
*/
resetPlayerActor: function resetPlayerActor(persistPowerUps)
{
this.actors.push([this.player]);
// reset the player position - centre of world
with (this.player)
{
position.x = position.y = this.world.size / 2;
vector.x = vector.y = heading = 0;
reset(persistPowerUps);
}
},
/**
* Scene before rendering event handler
*/
onBeforeRenderScene: function onBeforeRenderScene(benchmark)
{
var p = this.player,
w = this.world;
// update view position based on new player position
var viewedge = w.viewsize * 0.2;
if (p.position.x > viewedge && p.position.x < w.size - viewedge)
{
w.viewx = p.position.x - w.viewsize * 0.5;
}
if (p.position.y > viewedge && p.position.y < w.size - viewedge)
{
w.viewy = p.position.y - w.viewsize * 0.5;
}
if (benchmark)
{
if (Date.now() - this.sceneStartTime > this.testState)
{
this.testState += 500;
for (var i=0; i<2; i++)
{
this.enemies.push(new Arena.EnemyShip(this, (this.enemies.length % 6) + 1));
}
this.enemies[0].damageBy(100);
this.destroyEnemy(this.enemies[0], new Vector(0,1), this.player)
}
}
// update all actors using their current vector in the game world
this.updateActors();
},
/**
* Scene rendering event handler
*/
onRenderScene: function onRenderScene(ctx)
{
ctx.clearRect(0, 0, GameHandler.width, GameHandler.height);
// glowing vector effect shadow
ctx.shadowBlur = (DEBUG && DEBUG.DISABLEGLOWEFFECT) ? 0 : 8;
// render background effect - wire grid
this.renderBackground(ctx);
// render the game actors
this.renderActors(ctx);
// render info overlay graphics
this.renderOverlay(ctx);
// detect bullet collisions
this.collisionDetectBullets();
},
/**
* Render background effects for the scene
*/
renderBackground: function renderBackground(ctx)
{
// render background effect - wire grid
// manually transform world to screen for this effect and therefore
// assume there is a horizonal and vertical "wire" every N units
ctx.save();
ctx.strokeStyle = "rgb(0,30,60)";
ctx.lineWidth = 1.0;
ctx.shadowBlur = 0;
ctx.beginPath();
var UNIT = 100;
var w = this.world;
xoff = UNIT - w.viewx % UNIT,
yoff = UNIT - w.viewy % UNIT,
// calc top left edge of world (prescaled)
x1 = (w.viewx >= 0 ? 0 : -w.viewx) * w.scale,
y1 = (w.viewy >= 0 ? 0 : -w.viewy) * w.scale,
// calc bottom right edge of world (prescaled)
x2 = (w.viewx < w.size - w.viewsize ? w.viewsize : w.size - w.viewx) * w.scale,
y2 = (w.viewy < w.size - w.viewsize ? w.viewsize : w.size - w.viewy) * w.scale;
// plot the grid wires that make up the background
for (var i=0, j=w.viewsize/UNIT; i 0 && xoff + w.viewx < w.size)
{
ctx.moveTo(xoff * w.scale, y1);
ctx.lineTo(xoff * w.scale, y2);
}
if (yoff + w.viewy > 0 && yoff + w.viewy < w.size)
{
ctx.moveTo(x1, yoff * w.scale);
ctx.lineTo(x2, yoff * w.scale);
}
xoff += UNIT;
yoff += UNIT;
}
ctx.closePath();
ctx.stroke();
// render world edges
ctx.strokeStyle = "rgb(60,128,90)";
ctx.lineWidth = 1;
ctx.beginPath();
if (w.viewx <= 0)
{
xoff = -w.viewx;
ctx.moveTo(xoff * w.scale, y1);
ctx.lineTo(xoff * w.scale, y2);
}
else if (w.viewx >= w.size - w.viewsize)
{
xoff = w.size - w.viewx;
ctx.moveTo(xoff * w.scale, y1);
ctx.lineTo(xoff * w.scale, y2);
}
if (w.viewy <= 0)
{
yoff = -w.viewy;
ctx.moveTo(x1, yoff * w.scale);
ctx.lineTo(x2, yoff * w.scale);
}
else if (w.viewy >= w.size - w.viewsize)
{
yoff = w.size - w.viewy;
ctx.moveTo(x1, yoff * w.scale);
ctx.lineTo(x2, yoff * w.scale);
}
ctx.closePath();
ctx.stroke();
ctx.restore();
},
/**
* Update the scene actors based on current vectors and expiration.
*/
updateActors: function updateActors()
{
for (var i = 0, j = this.actors.length; i < j; i++)
{
var actorList = this.actors[i];
for (var n = 0; n < actorList.length; n++)
{
var actor = actorList[n];
// call onUpdate() event for each actor
actor.onUpdate(this);
// expiration test first
if (actor.expired())
{
actorList.splice(n, 1);
}
else
{
// update actor using its current vector
actor.position.add(actor.vector);
// TODO: different behavior for traversing out of the world space?
// add behavior flag to Actor i.e. bounce, invert, disipate etc.
// - could add method to actor itself - so would handle internally...
if (actor === this.player)
{
if (actor.position.x >= this.world.size ||
actor.position.x < 0 ||
actor.position.y >= this.world.size ||
actor.position.y < 0)
{
actor.vector.invert();
actor.vector.scale(0.75);
actor.position.add(actor.vector);
}
}
else
{
var bounceX = false,
bounceY = false;
if (actor.position.x >= this.world.size)
{
actor.position.x = this.world.size;
bounceX = true;
}
else if (actor.position.x < 0)
{
actor.position.x = 0;
bounceX = true;
}
if (actor.position.y >= this.world.size)
{
actor.position.y = this.world.size;
bounceY = true;
}
else if (actor.position.y < 0)
{
actor.position.y = 0;
bounceY = true
}
// bullets don't bounce - create an effect at the arena boundry instead
if ((bounceX || bounceY) &&
((actor instanceof Arena.Bullet && !this.player.bounceWeapons) ||
actor instanceof Arena.EnemyBullet))
{
// replace bullet with a particle effect at the same position and vector
var vec = actor.vector.nscale(0.5);
this.effects.push(new Arena.BulletImpactEffect(actor.position.clone(), vec));
// remove bullet actor from play
actorList.splice(n, 1);
}
else
{
if (bounceX)
{
var h = actor.vector.thetaTo2(new Vector(0, 1));
actor.vector.rotate(h*2);
actor.vector.scale(0.9);
actor.position.add(actor.vector);
// TODO: add "interface" for actor with heading?
// or is hasProperty() more "javascript"
if (actor.hasOwnProperty("heading")) actor.heading += (h*2)/RAD;
}
if (bounceY)
{
var h = actor.vector.thetaTo2(new Vector(1, 0));
actor.vector.rotate(h*2);
actor.vector.scale(0.9);
actor.position.add(actor.vector);
if (actor.hasOwnProperty("heading")) actor.heading += (h*2)/RAD;
}
}
}
}
}
}
},
/**
* Detect bullet collisions with enemy actors.
*/
collisionDetectBullets: function collisionDetectBullets()
{
var bullet, bulletRadius, bulletPos;
// collision detect player bullets with enemies
// NOTE: test length each loop as list length can change
for (var i = 0; i < this.playerBullets.length; i++)
{
bullet = this.playerBullets[i];
bulletRadius = bullet.radius;
bulletPos = bullet.position;
// test circle intersection with each enemy actor
for (var n = 0, m = this.enemies.length, enemy, z; n < m; n++)
{
enemy = this.enemies[n];
// test the distance against the two radius combined
if (bulletPos.distance(enemy.position) <= bulletRadius + enemy.radius)
{
// test for area effect bomb weapon
var effectRad = bullet.effectRadius();
//if (effectRad === 0)
{
// impact the enemy with the bullet - may destroy it or just damage it
if (enemy.damageBy(bullet.power()))
{
// destroy the enemy under the bullet
this.destroyEnemy(enemy, bullet.vector, true);
this.generateMultiplier(enemy);
this.generatePowerUp(enemy);
}
else
{
// add bullet impact effect to show the bullet hit
var effect = new Arena.EnemyImpact(
bullet.position.clone(),
bullet.vector.nscale(0.5 + Rnd() * 0.5), enemy);
this.effects.push(effect);
}
}
// remove this bullet from the actor list as it has been destroyed
this.playerBullets.splice(i, 1);
break;
}
}
}
},
/**
* Destroy an enemy. Replace with appropriate effect.
* Also applies the score for the destroyed item if the player caused it.
*
* @param enemy {Game.EnemyActor} The enemy to destory and add score for
* @param parentVector {Vector} The vector of the item that hit the enemy
* @param player {boolean} If true, the player was the destroyer
*/
destroyEnemy: function destroyEnemy(enemy, parentVector, player)
{
// add an explosion actor at the enemy position and vector
var vec = enemy.vector.clone();
// add scaled parent vector - to give some momentum from the impact
vec.add(parentVector.nscale(0.2));
this.effects.push(new Arena.EnemyExplosion(enemy.position.clone(), vec, enemy));
if (player)
{
// increment score
var inc = (enemy.scoretype + 1) * 5 * this.game.scoreMultiplier;
this.game.score += inc;
// generate a score effect indicator at the destroyed enemy position
var vec = new Vector(0, -5.0).add(enemy.vector.nscale(0.5));
this.effects.push(new Arena.ScoreIndicator(
new Vector(enemy.position.x, enemy.position.y - 16), vec, inc));
// call event handler for enemy
enemy.onDestroyed(this, player);
}
},
/**
* Generate score multiplier(s) for player to collect after enemy is destroyed
*/
generateMultiplier: function generateMultiplier(enemy)
{
if (enemy.dropsMutliplier)
{
var count = randomInt(1, (enemy.type < 5 ? enemy.type : 4));
for (var i=0; i= 0; n--)
{
actorList[n].onRender(ctx, this.world);
}
}
},
/**
* Render player information HUD overlay graphics.
*
* @param ctx {object} Canvas rendering context
*/
renderOverlay: function renderOverlay(ctx)
{
var w = this.world,
width = GameHandler.width,
height = GameHandler.height;
ctx.save();
ctx.shadowBlur = 0;
// energy bar (scaled down from player energy max)
var ewidth = ~~(100 * w.scale * 2),
eheight = ~~(4 * w.scale * 2);
ctx.strokeStyle = "rgb(128,128,50)";
ctx.strokeRect(4, 4, ewidth+1, 4 + eheight);
ctx.fillStyle = "rgb(255,255,150)";
ctx.fillRect(5, 5, (this.player.energy / (this.player.ENERGY_INIT / ewidth)), 3 + eheight);
// score display - update towards the score in increments to animate it
var font12pt = Game.fontFamily(w, 12),
font12size = Game.fontSize(w, 12);
var score = this.game.score,
inc = (score - this.scoredisplay) * 0.1;
this.scoredisplay += inc;
if (this.scoredisplay > score)
{
this.scoredisplay = score;
}
var sscore = Ceil(this.scoredisplay).toString();
// pad with zeros
for (var i=0, j=8-sscore.length; i this.game.highscore)
{
this.game.highscore = score;
}
sscore = this.game.highscore.toString();
// pad with zeros
for (var i=0, j=8-sscore.length; i