CanvasMark/scripts/canvasmark_v6.js
2013-07-23 23:57:10 +01:00

6480 lines
195 KiB
JavaScript

/**
* Game class library, utility functions and globals.
*
* @author Kevin Roast
*
* 30/04/09 Initial version.
* 12/05/09 Refactored to remove globals into GameHandler instance and added FPS controller game loop.
* 17/01/11 Full screen resizable canvas
* 26/01/11 World to screen transformation - no longer unit=pixel
* 03/08/11 Modified version for CanvasMark usage
*/
var KEY = { SHIFT:16, CTRL:17, ESC:27, RIGHT:39, UP:38, LEFT:37, DOWN:40, SPACE:32,
A:65, D:68, E:69, G:71, L:76, P:80, R:82, S:83, W:87, Z:90, OPENBRACKET:219, CLOSEBRACKET:221 };
var iOS = (navigator.userAgent.indexOf("iPhone;") != -1 ||
navigator.userAgent.indexOf("iPod;") != -1 ||
navigator.userAgent.indexOf("iPad;") != -1);
/**
* Game Handler.
*
* Singleton instance responsible for managing the main game loop and
* maintaining a few global references such as the canvas and frame counters.
*/
var GameHandler =
{
/**
* The single Game.Main derived instance
*/
game: null,
/**
* True if the game is in pause state, false if running
*/
paused: false,
/**
* The single canvas play field element reference
*/
canvas: null,
/**
* Width of the canvas play field
*/
width: 0,
/**
* Height of the canvas play field
*/
height: 0,
offsetX: 0,
offsetY: 0,
/**
* Frame counter
*/
frameCount: 0,
sceneStartTime: 0,
benchmarkScoreCount: 0,
benchmarkScores: [],
FPSMS: 60,
FRAME_TIME_MAX: 1000/30,
MAX_GLITCH_COUNT: 10,
/**
* Debugging output
*/
maxfps: 0,
frametime: 0,
frameInterval: 0,
/**
* Init function called once by your window.onload handler
*/
init: function()
{
this.canvas = document.getElementById('canvas');
this.width = this.canvas.height;
this.height = this.canvas.width;
var me = GameHandler;
var el = me.canvas, x = 0, y = 0;
do
{
y += el.offsetTop;
x += el.offsetLeft;
} while (el = el.offsetParent);
// compute canvas offset including page view position
me.offsetX = x - window.pageXOffset;
me.offsetY = y - window.pageYOffset;
},
/**
* Game start method - begins the main game loop.
* Pass in the object that represent the game to execute.
* Also called each frame by the main game loop unless paused.
*
* @param {Game.Main} game main derived object handler
*/
start: function(game)
{
if (game) this.game = game;
GameHandler.game.frame();
},
/**
* Game pause toggle method.
*/
pause: function()
{
if (this.paused)
{
this.paused = false;
GameHandler.game.frame();
}
else
{
this.paused = true;
}
}
};
/**
* Game root namespace.
*
* @namespace Game
*/
if (typeof Game == "undefined" || !Game)
{
var Game = {};
}
/**
* Transform a vector from world coordinates to screen
*
* @method worldToScreen
* @return Vector or null if non visible
*/
Game.worldToScreen = function worldToScreen(vector, world, radiusx, radiusy)
{
// transform a vector from the world to the screen
radiusx = (radiusx ? radiusx : 0);
radiusy = (radiusy ? radiusy : radiusx);
var screenvec = null,
viewx = vector.x - world.viewx,
viewy = vector.y - world.viewy;
if (viewx < world.viewsize + radiusx && viewy < world.viewsize + radiusy &&
viewx > -radiusx && viewy > -radiusy)
{
screenvec = new Vector(viewx, viewy).scale(world.scale);
}
return screenvec;
};
/**
* Game main loop class.
*
* @namespace Game
* @class Game.Main
*/
(function()
{
Game.Main = function()
{
var me = this;
document.onkeydown = function(event)
{
var keyCode = (event === null ? window.event.keyCode : event.keyCode);
if (me.sceneIndex !== -1)
{
if (me.scenes[me.sceneIndex].onKeyDownHandler(keyCode))
{
// if the key is handled, prevent any further events
if (event)
{
event.preventDefault();
event.stopPropagation();
}
}
}
};
document.onkeyup = function(event)
{
var keyCode = (event === null ? window.event.keyCode : event.keyCode);
if (me.sceneIndex !== -1)
{
if (me.scenes[me.sceneIndex].onKeyUpHandler(keyCode))
{
// if the key is handled, prevent any further events
if (event)
{
event.preventDefault();
event.stopPropagation();
}
}
}
};
};
Game.Main.prototype =
{
scenes: [],
startScene: null,
endScene: null,
currentScene: null,
sceneIndex: -1,
lastFrameStart: 0,
interval: null,
/**
* Game frame method - called by window timeout.
*/
frame: function frame()
{
var frameStart = Date.now();
GameHandler.frameInterval = frameStart - GameHandler.frameStart;
if (GameHandler.frameInterval === 0) GameHandler.frameInterval = 1;
// calculate scene transition and current scene
var currentScene = this.currentScene;
if (currentScene === null)
{
// set to scene zero (game init)
this.sceneIndex = 0;
currentScene = this.scenes[0];
currentScene._onInitScene();
currentScene.onInitScene();
}
if ((currentScene.interval === null || currentScene.interval.complete) && currentScene.isComplete())
{
if (this.sceneIndex === 0)
{
// reset total score recorded during the benchmark
GameHandler.benchmarkScoreCount = 0;
}
this.sceneIndex++;
if (this.sceneIndex < this.scenes.length)
{
currentScene = this.scenes[this.sceneIndex];
}
else
{
this.sceneIndex = 0;
currentScene = this.scenes[0];
}
currentScene._onInitScene();
currentScene.onInitScene();
}
// get canvas context for a render pass
var ctx = GameHandler.canvas.getContext('2d');
// calculate viewport transform and offset against the world
// we want to show a fixed number of world units in our viewport
// so calculate the scaling factor to transform world to view
currentScene.world.scale = GameHandler.width / currentScene.world.viewsize;
// render the game and current scene
if (currentScene.interval === null || currentScene.interval.complete)
{
currentScene.onBeforeRenderScene(currentScene._onBeforeRenderScene());
currentScene.onRenderScene(ctx);
}
else
{
// for the benchmark we just clear the canvas
ctx.clearRect(0, 0, GameHandler.width, GameHandler.height);
currentScene.interval.intervalRenderer.call(currentScene, currentScene.interval, ctx);
}
// update global frame counter and current scene reference
this.currentScene = currentScene;
GameHandler.frameCount++;
// calculate frame time and frame multiplier required for smooth animation
var now = Date.now();
GameHandler.frametime = now - frameStart;
GameHandler.frameMultipler = GameHandler.frameInterval / GameHandler.FPSMS;
GameHandler.frameStart = frameStart;
// update last fps every few frames for debugging output
if (GameHandler.frameCount % 16 === 0) GameHandler.lastfps = ~~(1000 / GameHandler.frameInterval);
// IE9 does not support requestAnimationFrame so need to calc interval manually
var ieinterval = 17 - (GameHandler.frametime);
requestAnimFrame(GameHandler.start, ieinterval > 0 ? ieinterval : 1);
},
isGameOver: function isGameOver()
{
return false;
}
};
})();
// requestAnimFrame shim
window.requestAnimFrame = (function()
{
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, frameOffset)
{
window.setTimeout(callback, frameOffset);
};
})();
/**
* Game scene base class. Adapted for Benchmark scoring.
*
* @namespace Game
* @class Game.Scene
*/
(function()
{
Game.Scene = function(playable, interval)
{
this.playable = playable;
this.interval = interval;
};
Game.Scene.prototype =
{
playable: true,
interval: null,
sceneStartTime: null,
sceneCompletedTime: null,
sceneGlitchCount: 0,
testState: 0,
testScore: 0,
world:
{
size: 1500, // total units vertically and horizontally
viewx: 0, // current view left corner xpos
viewy: 0, // current view left corner ypos
viewsize: 1500, // size of the viewable area
scale: 1 // scale for world->view transformation - calculated based on physical viewport size
},
/**
* Return true if this scene should update the actor list.
*/
isPlayable: function isPlayable()
{
return this.playable;
},
_onInitScene: function _onInitScene()
{
this.sceneGlitchCount = this.testScore = this.testState = 0;
this.sceneStartTime = Date.now();
this.sceneCompletedTime = null;
},
onInitScene: function onInitScene()
{
if (this.interval !== null)
{
// reset interval flag
this.interval.reset();
}
},
_onBeforeRenderScene: function _onBeforeRenderScene()
{
// calculate if the scene shoud render in benchmark mode or not
if (this.playable)
{
if (!this.sceneCompletedTime)
{
if (GameHandler.frameInterval > GameHandler.FRAME_TIME_MAX)
{
this.sceneGlitchCount++;
}
if (this.sceneGlitchCount < GameHandler.MAX_GLITCH_COUNT)
{
return true;
}
else
{
// too many FPS glitches! so benchmark scene completed (allow to run visually for a few seconds)
this.sceneCompletedTime = Date.now();
var score = ~~(((this.sceneCompletedTime - this.sceneStartTime) * this.testScore) / 100);
GameHandler.benchmarkScoreCount += score;
GameHandler.benchmarkScores.push(score);
if (typeof console !== "undefined")
{
console.log(score + " [" + this.interval.label + "]");
}
}
}
}
return false;
},
getTransientTestScore: function getTransientTestScore()
{
var score = ((this.sceneCompletedTime ? this.sceneCompletedTime : Date.now()) - this.sceneStartTime) * this.testScore;
return ~~(score/100);
},
onBeforeRenderScene: function onBeforeRenderScene()
{
},
onRenderScene: function onRenderScene(ctx)
{
},
onRenderInterval: function onRenderInterval(ctx)
{
},
onKeyDownHandler: function onKeyDownHandler(keyCode)
{
},
onKeyUpHandler: function onKeyUpHandler(keyCode)
{
},
isComplete: function isComplete()
{
return this.sceneCompletedTime && (Date.now() - this.sceneCompletedTime > 2000);
},
intervalRenderer: function intervalRenderer(interval, ctx)
{
if (interval.framecounter++ < 100)
{
Game.centerFillText(ctx, interval.label, "14pt Courier New", GameHandler.height/2 - 8, "white");
}
else
{
interval.complete = true;
}
}
};
})();
(function()
{
Game.Interval = function(label, intervalRenderer)
{
this.label = label;
this.intervalRenderer = intervalRenderer;
this.framecounter = 0;
this.complete = false;
};
Game.Interval.prototype =
{
label: null,
intervalRenderer: null,
framecounter: 0,
complete: false,
reset: function reset()
{
this.framecounter = 0;
this.complete = false;
}
};
})();
/**
* Actor base class.
*
* Game actors have a position in the game world and a current vector to indicate
* direction and speed of travel per frame. They each support the onUpdate() and
* onRender() event methods, finally an actor has an expired() method which should
* return true when the actor object should be removed from play.
*
* An actor can be hit and destroyed by bullets or similar. The class supports a hit()
* method which should return true when the actor should be removed from play.
*
* @namespace Game
* @class Game.Actor
*/
(function()
{
Game.Actor = function(p, v)
{
this.position = p;
this.vector = v;
return this;
};
Game.Actor.prototype =
{
/**
* Actor position
*
* @property position
* @type Vector
*/
position: null,
/**
* Actor vector
*
* @property vector
* @type Vector
*/
vector: null,
/**
* Alive flag
*
* @property alive
* @type boolean
*/
alive: true,
/**
* Radius - default is zero to imply that it is not affected by collision tests etc.
*
* @property radius
* @type int
*/
radius: 0,
/**
* Actor expiration test
*
* @method expired
* @return true if expired and to be removed from the actor list, false if still in play
*/
expired: function expired()
{
return !(this.alive);
},
/**
* Hit by bullet
*
* @param force of the impacting bullet (as the actor may support health)
* @return true if destroyed, false otherwise
*/
hit: function hit(force)
{
this.alive = false;
return true;
},
/**
* Transform current position vector from world coordinates to screen.
* Applies the appropriate translation and scaling to the canvas context.
*
* @method worldToScreen
* @return Vector or null if non visible
*/
worldToScreen: function worldToScreen(ctx, world, radius)
{
var viewposition = Game.worldToScreen(this.position, world, radius);
if (viewposition)
{
// scale ALL graphics... - translate to position apply canvas scaling
ctx.translate(viewposition.x, viewposition.y);
ctx.scale(world.scale, world.scale);
}
return viewposition;
},
/**
* Actor game loop update event method. Called for each actor
* at the start of each game loop cycle.
*
* @method onUpdate
*/
onUpdate: function onUpdate()
{
},
/**
* Actor rendering event method. Called for each actor to
* render for each frame.
*
* @method onRender
* @param ctx {object} Canvas rendering context
* @param world {object} World metadata
*/
onRender: function onRender(ctx, world)
{
}
};
})();
/**
* SpriteActor base class.
*
* An actor that can be rendered by a bitmap. The sprite handling code deals with the increment
* of the current frame within the supplied bitmap sprite strip image, based on animation direction,
* animation speed and the animation length before looping. Call renderSprite() each frame.
*
* NOTE: by default sprites source images are 64px wide 64px by N frames high and scaled to the
* appropriate final size. Any other size input source should be set in the constructor.
*
* @namespace Game
* @class Game.SpriteActor
*/
(function()
{
Game.SpriteActor = function(p, v, s)
{
Game.SpriteActor.superclass.constructor.call(this, p, v);
if (s) this.frameSize = s;
return this;
};
extend(Game.SpriteActor, Game.Actor,
{
/**
* Size in pixels of the width/height of an individual frame in the image
*/
frameSize: 64,
/**
* Animation image sprite reference.
* Sprite image sources are all currently 64px wide 64px by N frames high.
*/
animImage: null,
/**
* Length in frames of the sprite animation
*/
animLength: 0,
/**
* Animation direction, true for forward, false for reverse.
*/
animForward: true,
/**
* Animation frame inc/dec speed.
*/
animSpeed: 1.0,
/**
* Current animation frame index
*/
animFrame: 0,
/**
* Render sprite graphic based on current anim image, frame and anim direction
* Automatically updates the current anim frame.
*
* Optionally this method will automatically correct for objects moving on/off
* a cyclic canvas play area - if so it will render the appropriate stencil
* sections of the sprite top/bottom/left/right as needed to complete the image.
* Note that this feature can only be used if the sprite is absolutely positioned
* and not translated/rotated into position by canvas operations.
*/
renderSprite: function renderSprite(ctx, x, y, w, cyclic)
{
var offset = this.animFrame << 6,
fs = this.frameSize;
ctx.drawImage(this.animImage, 0, offset, fs, fs, x, y, w, w);
if (cyclic)
{
if (x < 0 || y < 0)
{
ctx.drawImage(this.animImage, 0, offset, fs, fs,
(x < 0 ? (GameHandler.width + x) : x),
(y < 0 ? (GameHandler.height + y) : y),
w, w);
}
if (x + w >= GameHandler.width || y + w >= GameHandler.height)
{
ctx.drawImage(this.animImage, 0, offset, fs, fs,
(x + w >= GameHandler.width ? (x - GameHandler.width) : x),
(y + w >= GameHandler.height ? (y - GameHandler.height) : y),
w, w);
}
}
// update animation frame index
if (this.animForward)
{
this.animFrame += this.animSpeed;
if (this.animFrame >= this.animLength)
{
this.animFrame = 0;
}
}
else
{
this.animFrame -= this.animSpeed;
if (this.animFrame < 0)
{
this.animFrame = this.animLength - 1;
}
}
}
});
})();
/**
* EffectActor base class.
*
* An actor representing a transient effect in the game world. An effect is nothing more than
* a special graphic that does not play any direct part in the game and does not interact with
* any other objects. It automatically expires after a set lifespan, generally the rendering of
* the effect is based on the remaining lifespan.
*
* @namespace Game
* @class Game.EffectActor
*/
(function()
{
Game.EffectActor = function(p, v, lifespan)
{
Game.EffectActor.superclass.constructor.call(this, p, v);
this.lifespan = lifespan;
return this;
};
extend(Game.EffectActor, Game.Actor,
{
/**
* Effect lifespan remaining
*/
lifespan: 0,
/**
* Actor expiration test
*
* @return true if expired and to be removed from the actor list, false if still in play
*/
expired: function expired()
{
// deduct lifespan from the explosion
return (--this.lifespan === 0);
}
});
})();
/**
* Image Preloader class. Executes the supplied callback function once all
* registered images are loaded by the browser.
*
* @namespace Game
* @class Game.Preloader
*/
(function()
{
Game.Preloader = function()
{
this.images = [];
return this;
};
Game.Preloader.prototype =
{
/**
* Image list
*
* @property images
* @type Array
*/
images: null,
/**
* Callback function
*
* @property callback
* @type Function
*/
callback: null,
/**
* Images loaded so far counter
*/
counter: 0,
/**
* Add an image to the list of images to wait for
*/
addImage: function addImage(img, url)
{
var me = this;
img.url = url;
// attach closure to the image onload handler
img.onload = function()
{
me.counter++;
if (me.counter === me.images.length)
{
// all images are loaded - execute callback function
me.callback.call(me);
}
};
this.images.push(img);
},
/**
* Load the images and call the supplied function when ready
*/
onLoadCallback: function onLoadCallback(fn)
{
this.counter = 0;
this.callback = fn;
// load the images
for (var i=0, j=this.images.length; i<j; i++)
{
this.images[i].src = this.images[i].url;
}
}
};
})();
/**
* Render text into the canvas context.
* Compatible with FF3, FF3.5, SF4, GC4, OP10
*
* @method Game.drawText
* @static
*/
Game.drawText = function(g, txt, font, x, y, col)
{
g.save();
if (col) g.strokeStyle = col;
g.font = font;
g.strokeText(txt, x, y);
g.restore();
};
Game.fillText = function(g, txt, font, x, y, col)
{
g.save();
if (col) g.fillStyle = col;
g.font = font;
g.fillText(txt, x, y);
g.restore();
};
Game.centerFillText = function(g, txt, font, y, col)
{
g.save();
if (col) g.fillStyle = col;
g.font = font;
g.fillText(txt, (GameHandler.width - g.measureText(txt).width) / 2, y);
g.restore();
};
Game.fontSize = function fontSize(world, size)
{
var s = ~~(size * world.scale * 2);
if (s > 20) s = 20;
else if (s < 8) s = 8;
return s;
};
Game.fontFamily = function fontFamily(world, size, font)
{
return Game.fontSize(world, size) + "pt " + (font ? font : "Courier New");
};/**
* Feature test 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!
*/
/**
* Feature root namespace.
*
* @namespace Feature
*/
if (typeof Feature == "undefined" || !Feature)
{
var Feature = {};
}
Feature.textureImage = new Image();
Feature.blurImage = new Image();
/**
* Feature main Benchmark Test class.
*
* @namespace Feature
* @class Feature.Test
*/
(function()
{
Feature.Test = function(benchmark, loader)
{
loader.addImage(Feature.textureImage, "./images/texture5.png");
loader.addImage(Feature.blurImage, "./images/fruit.jpg");
// add benchmark scenes
var t = benchmark.scenes.length;
for (var i=0; i<3; i++)
{
benchmark.addBenchmarkScene(new Feature.GameScene(this, t+i, i));
}
};
Feature.Test.prototype =
{
};
})();
(function()
{
/**
* Feature.K3DController constructor
*/
Feature.K3DController = function()
{
Feature.K3DController.superclass.constructor.call(this);
};
extend(Feature.K3DController, K3D.BaseController,
{
/**
* Render tick - should be called from appropriate scene renderer
*/
render: function(ctx)
{
// execute super class method to process render pipelines
ctx.save();
ctx.translate(GameHandler.width/2, GameHandler.height/2);
this.processFrame(ctx);
ctx.restore();
}
});
})();
/**
* Feature Game scene class.
*
* @namespace Feature
* @class Feature.GameScene
*/
(function()
{
Feature.GameScene = function(game, test, feature)
{
this.game = game;
this.test = test;
this.feature = feature;
var msg = "Test " + test + " - ";
switch (feature)
{
case 0: msg += "Plasma - Maths, canvas shapes"; break;
case 1: msg += "3D Rendering - Maths, polygons, image transforms"; break;
case 2: msg += "Pixel blur - Math, getImageData, putImageData"; break;
}
var interval = new Game.Interval(msg, this.intervalRenderer);
Feature.GameScene.superclass.constructor.call(this, true, interval);
};
extend(Feature.GameScene, Game.Scene,
{
feature: 0,
index: 0,
game: null,
/**
* Scene init event handler
*/
onInitScene: function onInitScene()
{
switch (this.feature)
{
case 0:
{
// generate plasma palette
var palette = [];
for (var i=0,r,g,b; i<256; i++)
{
r = ~~(128 + 128 * Math.sin(Math.PI * i / 32));
g = ~~(128 + 128 * Math.sin(Math.PI * i / 64));
b = ~~(128 + 128 * Math.sin(Math.PI * i / 128));
palette[i] = "rgb(" + ~~r + "," + ~~g + "," + ~~b + ")";
}
this.paletteoffset = 0;
this.palette = palette;
// size of the plasma pixels ratio - bigger = more calculations and rendering
this.plasmasize = 8;
this.testScore = 10;
break;
}
case 1:
{
// K3D controller
this.k3d = new Feature.K3DController();
// generate 3D objects
for (var i=0; i<10; i++)
{
this.add3DObject(i);
}
this.testScore = 10;
break;
}
case 2:
{
this.testScore = 25;
break;
}
}
},
add3DObject: function add3DObject(offset)
{
var gap = 360/20;
var obj = new K3D.K3DObject();
obj.ophi = (360 / gap) * offset;
obj.otheta = (180 / gap / 2) * offset;
obj.textures.push(Feature.textureImage);
with (obj)
{
drawmode = "solid"; // one of "point", "wireframe", "solid"
shademode = "lightsource"; // one of "plain", "depthcue", "lightsource" (solid drawing mode only)
addgamma = 0.5; addtheta = -1.0; addphi = -0.75;
aboutx = 150; abouty = -150; aboutz = -50;
perslevel = 512;
scale = 13;
init(
// describe the points of a simple unit cube
[{x:-1,y:1,z:-1}, {x:1,y:1,z:-1}, {x:1,y:-1,z:-1}, {x:-1,y:-1,z:-1}, {x:-1,y:1,z:1}, {x:1,y:1,z:1}, {x:1,y:-1,z:1}, {x:-1,y:-1,z:1}],
// describe the edges of the cube
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
// describe the polygon faces of the cube
[{color:[255,0,0],vertices:[0,1,2,3],texture:0},{color:[0,255,0],vertices:[0,4,5,1]},{color:[0,0,255],vertices:[1,5,6,2]},{color:[255,255,0],vertices:[2,6,7,3]},{color:[0,255,255],vertices:[3,7,4,0]},{color:[255,0,255],vertices:[7,6,5,4],texture:0}]
);
}
// add another 3D object to the controller
this.k3d.addK3DObject(obj);
},
/**
* Scene before rendering event handler
*/
onBeforeRenderScene: function onBeforeRenderScene(benchmark)
{
if (benchmark)
{
switch (this.feature)
{
case 0:
{
if (Date.now() - this.sceneStartTime > this.testState)
{
this.testState+=100;
this.plasmasize++;
}
break;
}
case 1:
{
if (Date.now() - this.sceneStartTime > this.testState)
{
this.testState+=100;
this.add3DObject(this.k3d.objects.length);
}
break;
}
case 2:
{
if (Date.now() - this.sceneStartTime > this.testState)
{
this.testState+=2;
}
break;
}
}
}
},
/**
* Scene rendering event handler
*/
onRenderScene: function onRenderScene(ctx)
{
ctx.clearRect(0, 0, GameHandler.width, GameHandler.height);
// render feature benchmark
var width = GameHandler.width, height = GameHandler.height;
switch (this.feature)
{
case 0:
{
var dist = function dist(a, b, c, d)
{
return Math.sqrt((a - c) * (a - c) + (b - d) * (b - d));
}
// plasma source width and height - variable benchmark state
var pwidth = this.plasmasize;
var pheight = pwidth * (height/width);
// scale the plasma source to the canvas width/height
var vpx = width/pwidth, vpy = height/pheight;
var time = Date.now() / 64;
var colour = function colour(x, y)
{
// plasma function
return (128 + (128 * Math.sin(x * 0.0625)) +
128 + (128 * Math.sin(y * 0.03125)) +
128 + (128 * Math.sin(dist(x + time, y - time, width, height) * 0.125)) +
128 + (128 * Math.sin(Math.sqrt(x * x + y * y) * 0.125)) ) * 0.25;
}
// render plasma effect
for (var y=0,x; y<pheight; y++)
{
for (x=0; x<pwidth; x++)
{
// map plasma pixels to canvas pixels using the virtual pixel size
ctx.fillStyle = this.palette[~~(colour(x, y) + this.paletteoffset) % 256];
ctx.fillRect(Math.floor(x * vpx), Math.floor(y * vpy), Math.ceil(vpx), Math.ceil(vpy));
}
}
// palette cycle speed
this.paletteoffset++;
break;
}
case 1:
{
this.k3d.render(ctx);
break;
}
case 2:
{
//
// TODO: add more interesting image!
//
var s = this.testState < GameHandler.width ? this.testState : GameHandler.width;
ctx.drawImage(Feature.blurImage, 0, 0, GameHandler.width, GameHandler.height);
boxBlurCanvasRGBA( ctx, 0, 0, s, s, s >> 4 + 1, 1 );
break;
}
}
ctx.save();
ctx.shadowBlur = 0;
// Benchmark - information output
if (this.sceneCompletedTime)
{
Game.fillText(ctx, "TEST "+this.test+" COMPLETED: "+this.getTransientTestScore(), "20pt Courier New", 4, 40, "white");
}
Game.fillText(ctx, "SCORE: " + this.getTransientTestScore(), "12pt Courier New", 0, GameHandler.height - 42, "lightblue");
Game.fillText(ctx, "TSF: " + Math.round(GameHandler.frametime) + "ms", "12pt Courier New", 0, GameHandler.height - 22, "lightblue");
Game.fillText(ctx, "FPS: " + GameHandler.lastfps, "12pt Courier New", 0, GameHandler.height - 2, "lightblue");
ctx.restore();
}
});
})();
/*
Superfast Blur - a fast Box Blur For Canvas
Version: 0.5
Author: Mario Klingemann
Contact: mario@quasimondo.com
Website: http://www.quasimondo.com/BoxBlurForCanvas
Twitter: @quasimondo
In case you find this class useful - especially in commercial projects -
I am not totally unhappy for a small donation to my PayPal account
mario@quasimondo.de
Or support me on flattr:
https://flattr.com/thing/140066/Superfast-Blur-a-pretty-fast-Box-Blur-Effect-for-CanvasJavascript
Copyright (c) 2011 Mario Klingemann
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
var mul_table = [ 1,57,41,21,203,34,97,73,227,91,149,62,105,45,39,137,241,107,3,173,39,71,65,238,219,101,187,87,81,151,141,133,249,117,221,209,197,187,177,169,5,153,73,139,133,127,243,233,223,107,103,99,191,23,177,171,165,159,77,149,9,139,135,131,253,245,119,231,224,109,211,103,25,195,189,23,45,175,171,83,81,79,155,151,147,9,141,137,67,131,129,251,123,30,235,115,113,221,217,53,13,51,50,49,193,189,185,91,179,175,43,169,83,163,5,79,155,19,75,147,145,143,35,69,17,67,33,65,255,251,247,243,239,59,29,229,113,111,219,27,213,105,207,51,201,199,49,193,191,47,93,183,181,179,11,87,43,85,167,165,163,161,159,157,155,77,19,75,37,73,145,143,141,35,138,137,135,67,33,131,129,255,63,250,247,61,121,239,237,117,29,229,227,225,111,55,109,216,213,211,209,207,205,203,201,199,197,195,193,48,190,47,93,185,183,181,179,178,176,175,173,171,85,21,167,165,41,163,161,5,79,157,78,154,153,19,75,149,74,147,73,144,143,71,141,140,139,137,17,135,134,133,66,131,65,129,1];
var shg_table = [0,9,10,10,14,12,14,14,16,15,16,15,16,15,15,17,18,17,12,18,16,17,17,19,19,18,19,18,18,19,19,19,20,19,20,20,20,20,20,20,15,20,19,20,20,20,21,21,21,20,20,20,21,18,21,21,21,21,20,21,17,21,21,21,22,22,21,22,22,21,22,21,19,22,22,19,20,22,22,21,21,21,22,22,22,18,22,22,21,22,22,23,22,20,23,22,22,23,23,21,19,21,21,21,23,23,23,22,23,23,21,23,22,23,18,22,23,20,22,23,23,23,21,22,20,22,21,22,24,24,24,24,24,22,21,24,23,23,24,21,24,23,24,22,24,24,22,24,24,22,23,24,24,24,20,23,22,23,24,24,24,24,24,24,24,23,21,23,22,23,24,24,24,22,24,24,24,23,22,24,24,25,23,25,25,23,24,25,25,24,22,25,25,25,24,23,24,25,25,25,25,25,25,25,25,25,25,25,25,23,25,23,24,25,25,25,25,25,25,25,25,25,24,22,25,25,23,25,25,20,24,25,24,25,25,22,24,25,24,25,24,25,25,24,25,25,25,25,22,25,25,25,24,25,24,25,18];
function boxBlurCanvasRGBA( context, top_x, top_y, width, height, radius, iterations ){
radius |= 0;
var imageData = context.getImageData( top_x, top_y, width, height );
var pixels = imageData.data;
var rsum,gsum,bsum,asum,x,y,i,p,p1,p2,yp,yi,yw,idx,pa;
var wm = width - 1;
var hm = height - 1;
var wh = width * height;
var rad1 = radius + 1;
var mul_sum = mul_table[radius];
var shg_sum = shg_table[radius];
var r = [];
var g = [];
var b = [];
var a = [];
var vmin = [];
var vmax = [];
while ( iterations-- > 0 ){
yw = yi = 0;
for ( y=0; y < height; y++ ){
rsum = pixels[yw] * rad1;
gsum = pixels[yw+1] * rad1;
bsum = pixels[yw+2] * rad1;
asum = pixels[yw+3] * rad1;
for( i = 1; i <= radius; i++ ){
p = yw + (((i > wm ? wm : i )) << 2 );
rsum += pixels[p++];
gsum += pixels[p++];
bsum += pixels[p++];
asum += pixels[p]
}
for ( x = 0; x < width; x++ ) {
r[yi] = rsum;
g[yi] = gsum;
b[yi] = bsum;
a[yi] = asum;
if( y==0) {
vmin[x] = ( ( p = x + rad1) < wm ? p : wm ) << 2;
vmax[x] = ( ( p = x - radius) > 0 ? p << 2 : 0 );
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
rsum += pixels[p1++] - pixels[p2++];
gsum += pixels[p1++] - pixels[p2++];
bsum += pixels[p1++] - pixels[p2++];
asum += pixels[p1] - pixels[p2];
yi++;
}
yw += ( width << 2 );
}
for ( x = 0; x < width; x++ ) {
yp = x;
rsum = r[yp] * rad1;
gsum = g[yp] * rad1;
bsum = b[yp] * rad1;
asum = a[yp] * rad1;
for( i = 1; i <= radius; i++ ) {
yp += ( i > hm ? 0 : width );
rsum += r[yp];
gsum += g[yp];
bsum += b[yp];
asum += a[yp];
}
yi = x << 2;
for ( y = 0; y < height; y++) {
pixels[yi+3] = pa = (asum * mul_sum) >>> shg_sum;
if ( pa > 0 )
{
pa = 255 / pa;
pixels[yi] = ((rsum * mul_sum) >>> shg_sum) * pa;
pixels[yi+1] = ((gsum * mul_sum) >>> shg_sum) * pa;
pixels[yi+2] = ((bsum * mul_sum) >>> shg_sum) * pa;
} else {
pixels[yi] = pixels[yi+1] = pixels[yi+2] = 0;
}
if( x == 0 ) {
vmin[y] = ( ( p = y + rad1) < hm ? p : hm ) * width;
vmax[y] = ( ( p = y - radius) > 0 ? p * width : 0 );
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += r[p1] - r[p2];
gsum += g[p1] - g[p2];
bsum += b[p1] - b[p2];
asum += a[p1] - a[p2];
yi += width << 2;
}
}
}
context.putImageData( imageData, top_x, top_y );
}/**
* Asteroids HTML5 Canvas Game
* Scenes for CanvasMark Rendering Benchmark - March 2013
*
* @email kevtoast at yahoo dot com
* @twitter kevinroast
*
* (C) 2013 Kevin Roast
*
* 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!
*/
// Globals
var BITMAPS = true;
var GLOWEFFECT = false;
var g_asteroidImg1 = new Image();
var g_asteroidImg2 = new Image();
var g_asteroidImg3 = new Image();
var g_asteroidImg4 = new Image();
var g_shieldImg = new Image();
var g_backgroundImg = new Image();
var g_playerImg = new Image();
var g_enemyshipImg = new Image();
/**
* Asteroids root namespace.
*
* @namespace Asteroids
*/
if (typeof Asteroids == "undefined" || !Asteroids)
{
var Asteroids = {};
}
/**
* Asteroids benchmark test class.
*
* @namespace Asteroids
* @class Asteroids.Test
*/
(function()
{
Asteroids.Test = function(benchmark, loader)
{
// get the image graphics loading
loader.addImage(g_backgroundImg, './images/bg3_1.jpg');
loader.addImage(g_playerImg, './images/player.png');
loader.addImage(g_asteroidImg1, './images/asteroid1.png');
loader.addImage(g_asteroidImg2, './images/asteroid2.png');
loader.addImage(g_asteroidImg3, './images/asteroid3.png');
loader.addImage(g_asteroidImg4, './images/asteroid4.png');
loader.addImage(g_enemyshipImg, './images/enemyship1.png');
// generate the single player actor - available across all scenes
this.player = new Asteroids.Player(new Vector(GameHandler.width / 2, GameHandler.height / 2), new Vector(0.0, 0.0), 0.0);
// add the Asteroid game benchmark scenes
for (var level, i=0, t=benchmark.scenes.length; i<4; i++)
{
level = new Asteroids.BenchMarkScene(this, t+i, i+1);// NOTE: asteroids indexes feature from 1...
benchmark.addBenchmarkScene(level);
}
};
Asteroids.Test.prototype =
{
/**
* Reference to the single game player actor
*/
player: null,
/**
* Lives count (only used to render overlay graphics during benchmark mode)
*/
lives: 3
};
})();
/**
* Asteroids Benchmark scene class.
*
* @namespace Asteroids
* @class Asteroids.BenchMarkScene
*/
(function()
{
Asteroids.BenchMarkScene = function(game, test, feature)
{
this.game = game;
this.test = test;
this.feature = feature;
this.player = game.player;
var msg = "Test " + test + " - Asteroids - ";
switch (feature)
{
case 1: msg += "Bitmaps"; break;
case 2: msg += "Vectors"; break;
case 3: msg += "Bitmaps, shapes, text"; break;
case 4: msg += "Shapes, shadows, blending"; break;
}
var interval = new Game.Interval(msg, this.intervalRenderer);
Asteroids.BenchMarkScene.superclass.constructor.call(this, true, interval);
// generate background starfield
for (var star, i=0; i<this.STARFIELD_SIZE; i++)
{
star = new Asteroids.Star();
star.init();
this.starfield.push(star);
}
};
extend(Asteroids.BenchMarkScene, Game.Scene,
{
STARFIELD_SIZE: 32,
game: null,
test: 0,
/**
* 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,
/**
* Background scrolling bitmap x position
*/
backgroundX: 0,
/**
* Background starfield star list
*/
starfield: [],
/**
* Update each individual star in the starfield background
*/
updateStarfield: function updateStarfield(ctx)
{
for (var s, i=0, j=this.starfield.length; i<j; i++)
{
s = this.starfield[i];
s.render(ctx);
s.z -= s.VELOCITY * 0.1;
if (s.z < 0.1 || s.prevx > GameHandler.height || s.prevy > GameHandler.width)
{
s.init();
}
}
},
/**
* Scene init event handler
*/
onInitScene: function onInitScene()
{
// generate the actors and add the actor sub-lists to the main actor list
this.actors = [];
this.enemies = [];
this.actors.push(this.enemies);
this.actors.push(this.playerBullets = []);
this.actors.push(this.enemyBullets = []);
this.actors.push(this.effects = []);
this.actors.push([this.player]);
// reset the player position
with (this.player)
{
position.x = GameHandler.width / 2;
position.y = GameHandler.height / 2;
vector.x = 0.0;
vector.y = 0.0;
heading = 0.0;
}
// tests 1-2 display asteroids in various modes
switch (this.feature)
{
case 1:
{
// start with 10 asteroids - more will be added if framerate is acceptable
for (var i=0; i<10; i++)
{
this.enemies.push(this.generateAsteroid(Math.random()+1.0, ~~(Math.random()*4) + 1));
}
this.testScore = 10;
break;
}
case 2:
{
// start with 10 asteroids - more will be added if framerate is acceptable
for (var i=0; i<10; i++)
{
this.enemies.push(this.generateAsteroid(Math.random()+1.0, ~~(Math.random()*4) + 1));
}
this.testScore = 20;
break;
}
case 3:
{
// test 3 generates lots of enemy ships that fire
for (var i=0; i<10; i++)
{
this.enemies.push(new Asteroids.EnemyShip(this, i%2));
}
this.testScore = 10;
break;
}
case 4:
{
this.testScore = 25;
break;
}
}
// tests 2 in wireframe, all others are bitmaps
BITMAPS = !(this.feature === 2);
// reset interval flag
this.interval.reset();
},
/**
* Scene before rendering event handler
*/
onBeforeRenderScene: function onBeforeRenderScene(benchmark)
{
// add items to the test
if (benchmark)
{
switch (this.feature)
{
case 1:
case 2:
{
var count;
switch (this.feature)
{
case 1:
count = 10;
break;
case 2:
count = 5;
break;
}
for (var i=0; i<count; i++)
{
this.enemies.push(this.generateAsteroid(Math.random()+1.0, ~~(Math.random()*4) + 1));
}
break;
}
case 3:
{
if (Date.now() - this.sceneStartTime > this.testState)
{
this.testState += 20;
for (var i=0; i<2; i++)
{
this.enemies.push(new Asteroids.EnemyShip(this, i%2));
}
this.enemies[0].hit();
this.destroyEnemy(this.enemies[0], new Vector(0, 1));
}
break;
}
case 4:
{
if (Date.now() - this.sceneStartTime > this.testState)
{
this.testState += 25;
// spray forward guns
for (var i=0; i<=~~(this.testState/500); i++)
{
h = this.player.heading - 15;
t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector);
this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h));
h = this.player.heading;
t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector);
this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h));
h = this.player.heading + 15;
t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector);
this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h));
}
// side firing guns also
h = this.player.heading - 90;
t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector);
this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25));
h = this.player.heading + 90;
t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector);
this.playerBullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25));
// update player heading to rotate
this.player.heading += 8;
}
break;
}
}
}
// update all actors using their current vector
this.updateActors();
},
/**
* Scene rendering event handler
*/
onRenderScene: function onRenderScene(ctx)
{
// setup canvas for a render pass and apply background
if (BITMAPS)
{
// draw a scrolling background image
ctx.drawImage(g_backgroundImg, this.backgroundX++, 0, GameHandler.width, GameHandler.height, 0, 0, GameHandler.width, GameHandler.height);
if (this.backgroundX == (g_backgroundImg.width / 2))
{
this.backgroundX = 0;
}
ctx.shadowBlur = 0;
}
else
{
// clear the background to black
ctx.fillStyle = "black";
ctx.fillRect(0, 0, GameHandler.width, GameHandler.height);
// glowing vector effect shadow
ctx.shadowBlur = GLOWEFFECT ? 8 : 0;
// update and render background starfield effect
this.updateStarfield(ctx);
}
// render the game actors
this.renderActors(ctx);
// render info overlay graphics
this.renderOverlay(ctx);
},
/**
* Randomly generate a new large asteroid. Ensures the asteroid is not generated
* too close to the player position!
*
* @param speedFactor {number} Speed multiplier factor to apply to asteroid vector
*/
generateAsteroid: function generateAsteroid(speedFactor, size)
{
while (true)
{
// perform a test to check it is not too close to the player
var apos = new Vector(Math.random()*GameHandler.width, Math.random()*GameHandler.height);
if (this.player.position.distance(apos) > 125)
{
var vec = new Vector( ((Math.random()*2)-1)*speedFactor, ((Math.random()*2)-1)*speedFactor );
var asteroid = new Asteroids.Asteroid(
apos, vec, size ? size : 4);
return asteroid;
}
}
},
/**
* Update the actors position 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);
// handle traversing out of the coordinate space and back again
if (actor.position.x >= GameHandler.width)
{
actor.position.x = 0;
}
else if (actor.position.x < 0)
{
actor.position.x = GameHandler.width - 1;
}
if (actor.position.y >= GameHandler.height)
{
actor.position.y = 0;
}
else if (actor.position.y < 0)
{
actor.position.y = GameHandler.height - 1;
}
}
}
}
},
/**
* Blow up an enemy.
*
* An asteroid may generate new baby asteroids and leave an explosion
* in the wake.
*
* Also applies the score for the destroyed item.
*
* @param enemy {Game.Actor} 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 destroyed
*/
destroyEnemy: function destroyEnemy(enemy, parentVector)
{
if (enemy instanceof Asteroids.Asteroid)
{
// generate baby asteroids
this.generateBabyAsteroids(enemy, parentVector);
// add an explosion actor at the asteriod position and vector
var boom = new Asteroids.Explosion(enemy.position.clone(), enemy.vector.clone(), enemy.size);
this.effects.push(boom);
// generate a score effect indicator at the destroyed enemy position
var vec = new Vector(0, -(Math.random()*2 + 0.5));
var effect = new Asteroids.ScoreIndicator(
new Vector(enemy.position.x, enemy.position.y), vec, Math.floor(100 + (Math.random()*100)));
this.effects.push(effect);
}
else if (enemy instanceof Asteroids.EnemyShip)
{
// add an explosion actor at the asteriod position and vector
var boom = new Asteroids.Explosion(enemy.position.clone(), enemy.vector.clone(), 4);
this.effects.push(boom);
// generate a score text effect indicator at the destroyed enemy position
var vec = new Vector(0, -(Math.random()*2 + 0.5));
var effect = new Asteroids.ScoreIndicator(
new Vector(enemy.position.x, enemy.position.y), vec, Math.floor(100 + (Math.random()*100)));
this.effects.push(effect);
}
},
/**
* Generate a number of baby asteroids from a detonated parent asteroid. The number
* and size of the generated asteroids are based on the parent size. Some of the
* momentum of the parent vector (e.g. impacting bullet) is applied to the new asteroids.
*
* @param asteroid {Asteroids.Asteroid} The parent asteroid that has been destroyed
* @param parentVector {Vector} Vector of the impacting object e.g. a bullet
*/
generateBabyAsteroids: function generateBabyAsteroids(asteroid, parentVector)
{
// generate some baby asteroid(s) if bigger than the minimum size
if (asteroid.size > 1)
{
for (var x=0, xc=Math.floor(asteroid.size/2); x<xc; x++)
{
var babySize = asteroid.size - 1;
var vec = asteroid.vector.clone();
// apply a small random vector in the direction of travel
var t = new Vector(0.0, -(Math.random() * 3));
// rotate vector by asteroid current heading - slightly randomized
t.rotate(asteroid.vector.theta() * (Math.random()*Math.PI));
vec.add(t);
// add the scaled parent vector - to give some momentum from the impact
vec.add(parentVector.clone().scale(0.2));
// create the asteroid - slightly offset from the centre of the old one
var baby = new Asteroids.Asteroid(
new Vector(asteroid.position.x + (Math.random()*5)-2.5, asteroid.position.y + (Math.random()*5)-2.5),
vec, babySize, asteroid.type);
this.enemies.push(baby);
}
}
},
/**
* Render each actor to the canvas.
*
* @param ctx {object} Canvas rendering context
*/
renderActors: function renderActors(ctx)
{
for (var i = 0, j = this.actors.length; i < j; i++)
{
// walk each sub-list and call render on each object
var actorList = this.actors[i];
for (var n = actorList.length - 1; n >= 0; n--)
{
actorList[n].onRender(ctx);
}
}
},
/**
* Render player information HUD overlay graphics.
*
* @param ctx {object} Canvas rendering context
*/
renderOverlay: function renderOverlay(ctx)
{
ctx.save();
// energy bar (100 pixels across, scaled down from player energy max)
ctx.strokeStyle = "rgb(50,50,255)";
ctx.strokeRect(4, 4, 101, 6);
ctx.fillStyle = "rgb(100,100,255)";
var energy = this.player.energy;
if (energy > this.player.ENERGY_INIT)
{
// the shield is on for "free" briefly when he player respawns
energy = this.player.ENERGY_INIT;
}
ctx.fillRect(5, 5, (energy / (this.player.ENERGY_INIT / 100)), 5);
// lives indicator graphics
for (var i=0; i<this.game.lives; i++)
{
if (BITMAPS)
{
ctx.drawImage(g_playerImg, 0, 0, 64, 64, 350+(i*20), 0, 16, 16);
}
else
{
ctx.save();
ctx.shadowColor = ctx.strokeStyle = "rgb(255,255,255)";
ctx.translate(360+(i*16), 8);
ctx.beginPath();
ctx.moveTo(-4, 6);
ctx.lineTo(4, 6);
ctx.lineTo(0, -6);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
// score display
Game.fillText(ctx, "00000000", "12pt Courier New", 120, 12, "white");
// high score
Game.fillText(ctx, "HI: 00000000", "12pt Courier New", 220, 12, "white");
// Benchmark - information output
if (this.sceneCompletedTime)
{
Game.fillText(ctx, "TEST " + this.feature + " COMPLETED: " + this.getTransientTestScore(), "20pt Courier New", 4, 40, "white");
}
Game.fillText(ctx, "SCORE: " + this.getTransientTestScore(), "12pt Courier New", 0, GameHandler.height - 42, "lightblue");
Game.fillText(ctx, "TSF: " + Math.round(GameHandler.frametime) + "ms", "12pt Courier New", 0, GameHandler.height - 22, "lightblue");
Game.fillText(ctx, "FPS: " + GameHandler.lastfps, "12pt Courier New", 0, GameHandler.height - 2, "lightblue");
ctx.restore();
}
});
})();
/**
* Starfield star class.
*
* @namespace Asteroids
* @class Asteroids.Star
*/
(function()
{
Asteroids.Star = function()
{
return this;
};
Asteroids.Star.prototype =
{
MAXZ: 12.0,
VELOCITY: 1.5,
MAXSIZE: 5,
x: 0,
y: 0,
z: 0,
prevx: 0,
prevy: 0,
init: function init()
{
// select a random point for the initial location
this.x = (Math.random() * GameHandler.width - (GameHandler.width * 0.5)) * this.MAXZ;
this.y = (Math.random() * GameHandler.height - (GameHandler.height * 0.5)) * this.MAXZ;
this.z = this.MAXZ;
},
render: function render(ctx)
{
var xx = this.x / this.z;
var yy = this.y / this.z;
var size = 1.0 / this.z * this.MAXSIZE + 1;
ctx.save();
ctx.fillStyle = "rgb(200,200,200)";
ctx.beginPath();
ctx.arc(xx + (GameHandler.width / 2), yy +(GameHandler.height / 2), size/2, 0, TWOPI, true);
ctx.closePath();
ctx.fill();
ctx.restore();
this.prevx = xx;
this.prevy = yy;
}
};
})();
/**
* Player actor class.
*
* @namespace Asteroids
* @class Asteroids.Player
*/
(function()
{
Asteroids.Player = function(p, v, h)
{
Asteroids.Player.superclass.constructor.call(this, p, v);
this.heading = h;
this.energy = this.ENERGY_INIT;
// setup SpriteActor values - used for shield sprite
this.animImage = g_shieldImg;
this.animLength = this.SHIELD_ANIM_LENGTH;
// setup weapons
this.primaryWeapons = [];
this.primaryWeapons["main"] = new Asteroids.PrimaryWeapon(this);
return this;
};
extend(Asteroids.Player, Game.SpriteActor,
{
MAX_PLAYER_VELOCITY: 10.0,
PLAYER_RADIUS: 10,
SHIELD_RADIUS: 14,
SHIELD_ANIM_LENGTH: 100,
SHIELD_MIN_PULSE: 20,
ENERGY_INIT: 200,
THRUST_DELAY: 1,
BOMB_RECHARGE: 20,
BOMB_ENERGY: 40,
/**
* Player heading
*/
heading: 0.0,
/**
* Player energy (shield and bombs)
*/
energy: 0,
/**
* Player shield active counter
*/
shieldCounter: 0,
/**
* Player 'alive' flag
*/
alive: true,
/**
* Primary weapon list
*/
primaryWeapons: null,
/**
* Bomb fire recharging counter
*/
bombRecharge: 0,
/**
* Engine thrust recharge counter
*/
thrustRecharge: 0,
/**
* True if the engine thrust graphics should be rendered next frame
*/
engineThrust: false,
/**
* Frame that the player was killed on - to cause a delay before respawning the player
*/
killedOnFrame: 0,
/**
* Power up setting - can fire when shielded
*/
fireWhenShield: false,
/**
* Player rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
var headingRad = this.heading * RAD;
// render engine thrust?
if (this.engineThrust)
{
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.rotate(headingRad);
ctx.globalAlpha = 0.4 + Math.random()/2;
if (BITMAPS)
{
ctx.fillStyle = "rgb(25,125,255)";
}
else
{
ctx.shadowColor = ctx.strokeStyle = "rgb(25,125,255)";
}
ctx.beginPath();
ctx.moveTo(-5, 8);
ctx.lineTo(5, 8);
ctx.lineTo(0, 15 + Math.random()*7);
ctx.closePath();
if (BITMAPS) ctx.fill(); else ctx.stroke();
ctx.restore();
this.engineThrust = false;
}
// render player graphic
if (BITMAPS)
{
var size = (this.PLAYER_RADIUS * 2) + 4;
var normAngle = this.heading % 360;
if (normAngle < 0)
{
normAngle = 360 + normAngle;
}
ctx.drawImage(g_playerImg, 0, normAngle * 16, 64, 64, this.position.x - (size / 2), this.position.y - (size / 2), size, size);
}
else
{
ctx.save();
ctx.shadowColor = ctx.strokeStyle = "rgb(255,255,255)";
ctx.translate(this.position.x, this.position.y);
ctx.rotate(headingRad);
ctx.beginPath();
ctx.moveTo(-6, 8);
ctx.lineTo(6, 8);
ctx.lineTo(0, -8);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
// shield up? if so render a shield graphic around the ship
if (this.shieldCounter > 0 && this.energy > 0)
{
if (BITMAPS)
{
// render shield graphic bitmap
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.rotate(headingRad);
this.renderSprite(ctx, -this.SHIELD_RADIUS-1, -this.SHIELD_RADIUS-1, (this.SHIELD_RADIUS * 2) + 2);
ctx.restore();
}
else
{
// render shield as a simple circle around the ship
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.rotate(headingRad);
ctx.shadowColor = ctx.strokeStyle = "rgb(100,100,255)";
ctx.beginPath();
ctx.arc(0, 2, this.SHIELD_RADIUS, 0, TWOPI, true);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
this.shieldCounter--;
this.energy--;
}
},
/**
* Execute player forward thrust request
* Automatically a delay is used between each application - to ensure stable thrust on all machines.
*/
thrust: function thrust()
{
// now test we did not thrust too recently - to stop fast key repeat issues
if (GameHandler.frameCount - this.thrustRecharge > this.THRUST_DELAY)
{
// update last frame count
this.thrustRecharge = GameHandler.frameCount;
// generate a small thrust vector
var t = new Vector(0.0, -0.55);
// rotate thrust vector by player current heading
t.rotate(this.heading * RAD);
// test player won't then exceed maximum velocity
var pv = this.vector.clone();
if (pv.add(t).length() < this.MAX_PLAYER_VELOCITY)
{
// add to current vector (which then gets applied during each render loop)
this.vector.add(t);
}
}
// mark so that we know to render engine thrust graphics
this.engineThrust = true;
},
/**
* Execute player active shield request
* If energy remaining the shield will be briefly applied - or until key is release
*/
activateShield: function activateShield()
{
// ensure shield stays up for a brief pulse between key presses!
if (this.energy > 0)
{
this.shieldCounter = this.SHIELD_MIN_PULSE;
}
},
isShieldActive: function isShieldActive()
{
return (this.shieldCounter > 0 && this.energy > 0);
},
radius: function radius()
{
return (this.isShieldActive() ? this.SHIELD_RADIUS : this.PLAYER_RADIUS);
},
expired: function expired()
{
return !(this.alive);
},
kill: function kill()
{
this.alive = false;
this.killedOnFrame = GameHandler.frameCount;
},
/**
* Fire primary weapon(s)
* @param bulletList {Array} to add bullet(s) to on success
*/
firePrimary: function firePrimary(bulletList)
{
// attempt to fire the primary weapon(s)
// first ensure player is alive and the shield is not up
if (this.alive && (!this.isShieldActive() || this.fireWhenShield))
{
for (var w in this.primaryWeapons)
{
var b = this.primaryWeapons[w].fire();
if (b)
{
if (isArray(b))
{
for (var i=0; i<b.length; i++)
{
bulletList.push(b[i]);
}
}
else
{
bulletList.push(b);
}
}
}
}
},
/**
* Fire secondary weapon.
* @param bulletList {Array} to add bullet to on success
*/
fireSecondary: function fireSecondary(bulletList)
{
// attempt to fire the secondary weapon and generate bomb object if successful
// first ensure player is alive and the shield is not up
if (this.alive && (!this.isShieldActive() || this.fireWhenShield) && this.energy > this.BOMB_ENERGY)
{
// now test we did not fire too recently
if (GameHandler.frameCount - this.bombRecharge > this.BOMB_RECHARGE)
{
// ok, update last fired frame and we can now generate a bomb
this.bombRecharge = GameHandler.frameCount;
// decrement energy supply
this.energy -= this.BOMB_ENERGY;
// generate a vector rotated to the player heading and then add the current player
// vector to give the bomb the correct directional momentum
var t = new Vector(0.0, -6.0);
t.rotate(this.heading * RAD);
t.add(this.vector);
bulletList.push(new Asteroids.Bomb(this.position.clone(), t));
}
}
},
onUpdate: function onUpdate()
{
// slowly recharge the shield - if not active
if (!this.isShieldActive() && this.energy < this.ENERGY_INIT)
{
this.energy += 0.1;
}
},
reset: function reset(persistPowerUps)
{
// reset energy, alive status, weapons and power up flags
this.alive = true;
if (!persistPowerUps)
{
this.primaryWeapons = [];
this.primaryWeapons["main"] = new Asteroids.PrimaryWeapon(this);
this.fireWhenShield = false;
}
this.energy = this.ENERGY_INIT + this.SHIELD_MIN_PULSE; // for shield as below
// active shield briefly
this.activateShield();
}
});
})();
/**
* Asteroid actor class.
*
* @namespace Asteroids
* @class Asteroids.Asteroid
*/
(function()
{
Asteroids.Asteroid = function(p, v, s, t)
{
Asteroids.Asteroid.superclass.constructor.call(this, p, v);
this.size = s;
this.health = s;
// randomly select an asteroid image bitmap
if (t === undefined)
{
t = randomInt(1, 4);
}
eval("this.animImage=g_asteroidImg" + t);
this.type = t;
// randomly setup animation speed and direction
this.animForward = (Math.random() < 0.5);
this.animSpeed = 0.25 + Math.random();
this.animLength = this.ANIMATION_LENGTH;
this.rotation = randomInt(0, 180);
this.rotationSpeed = randomInt(-1, 1) / 25;
return this;
};
extend(Asteroids.Asteroid, Game.SpriteActor,
{
ANIMATION_LENGTH: 180,
/**
* Asteroid size - values from 1-4 are valid.
*/
size: 0,
/**
* Asteroid type i.e. which bitmap it is drawn from
*/
type: 1,
/**
* Asteroid health before it's destroyed
*/
health: 0,
/**
* Retro graphics mode rotation orientation and speed
*/
rotation: 0,
rotationSpeed: 0,
/**
* Asteroid rendering method
*/
onRender: function onRender(ctx)
{
var rad = this.size * 8;
ctx.save();
if (BITMAPS)
{
// render asteroid graphic bitmap
// bitmap is rendered slightly large than the radius as the raytraced asteroid graphics do not
// quite touch the edges of the 64x64 sprite - this improves perceived collision detection
this.renderSprite(ctx, this.position.x - rad - 2, this.position.y - rad - 2, (rad * 2)+4, true);
}
else
{
// draw asteroid outline circle
ctx.shadowColor = ctx.strokeStyle = "white";
ctx.translate(this.position.x, this.position.y);
ctx.scale(this.size * 0.8, this.size * 0.8);
ctx.rotate(this.rotation += this.rotationSpeed);
ctx.lineWidth = (0.8 / this.size) * 2;
ctx.beginPath();
// asteroid wires
switch (this.type)
{
case 1:
ctx.moveTo(0,10);
ctx.lineTo(8,6);
ctx.lineTo(10,-4);
ctx.lineTo(4,-2);
ctx.lineTo(6,-6);
ctx.lineTo(0,-10);
ctx.lineTo(-10,-3);
ctx.lineTo(-10,5);
break;
case 2:
ctx.moveTo(0,10);
ctx.lineTo(8,6);
ctx.lineTo(10,-4);
ctx.lineTo(4,-2);
ctx.lineTo(6,-6);
ctx.lineTo(0,-10);
ctx.lineTo(-8,-8);
ctx.lineTo(-6,-3);
ctx.lineTo(-8,-4);
ctx.lineTo(-10,5);
break;
case 3:
ctx.moveTo(-4,10);
ctx.lineTo(1,8);
ctx.lineTo(7,10);
ctx.lineTo(10,-4);
ctx.lineTo(4,-2);
ctx.lineTo(6,-6);
ctx.lineTo(0,-10);
ctx.lineTo(-10,-3);
ctx.lineTo(-10,5);
break;
case 4:
ctx.moveTo(-8,10);
ctx.lineTo(7,8);
ctx.lineTo(10,-2);
ctx.lineTo(6,-10);
ctx.lineTo(-2,-8);
ctx.lineTo(-6,-10);
ctx.lineTo(-10,-6);
ctx.lineTo(-7,0);
break;
}
ctx.closePath();
ctx.stroke();
}
ctx.restore();
},
radius: function radius()
{
return this.size * 8;
},
/**
* Asteroid hit by player bullet
*
* @param force of the impacting bullet, -1 for instant kill
* @return true if destroyed, false otherwise
*/
hit: function hit(force)
{
if (force !== -1)
{
this.health -= force;
}
else
{
// instant kill
this.health = 0;
}
return !(this.alive = (this.health > 0));
}
});
})();
/**
* Enemy Ship actor class.
*
* @namespace Asteroids
* @class Asteroids.EnemyShip
*/
(function()
{
Asteroids.EnemyShip = function(scene, size)
{
this.size = size;
// small ship, alter settings slightly
if (this.size === 1)
{
this.BULLET_RECHARGE = 45;
this.RADIUS = 8;
}
// randomly setup enemy initial position and vector
// ensure the enemy starts in the opposite quadrant to the player
var p, v;
if (scene.player.position.x < GameHandler.width / 2)
{
// player on left of the screen
if (scene.player.position.y < GameHandler.height / 2)
{
// player in top left of the screen
p = new Vector(GameHandler.width-48, GameHandler.height-48);
}
else
{
// player in bottom left of the screen
p = new Vector(GameHandler.width-48, 48);
}
v = new Vector(-(Math.random() + 1 + size), Math.random() + 0.5 + size);
}
else
{
// player on right of the screen
if (scene.player.position.y < GameHandler.height / 2)
{
// player in top right of the screen
p = new Vector(0, GameHandler.height-48);
}
else
{
// player in bottom right of the screen
p = new Vector(0, 48);
}
v = new Vector(Math.random() + 1 + size, Math.random() + 0.5 + size);
}
// setup SpriteActor values
this.animImage = g_enemyshipImg;
this.animLength = this.SHIP_ANIM_LENGTH;
Asteroids.EnemyShip.superclass.constructor.call(this, p, v);
return this;
};
extend(Asteroids.EnemyShip, Game.SpriteActor,
{
SHIP_ANIM_LENGTH: 90,
RADIUS: 16,
BULLET_RECHARGE: 60,
/**
* Enemy ship size - 0 = large (slow), 1 = small (fast)
*/
size: 0,
/**
* Bullet fire recharging counter
*/
bulletRecharge: 0,
onUpdate: function onUpdate(scene)
{
// change enemy direction randomly
if (this.size === 0)
{
if (Math.random() < 0.01)
{
this.vector.y = -(this.vector.y + (0.25 - (Math.random()/2)));
}
}
else
{
if (Math.random() < 0.02)
{
this.vector.y = -(this.vector.y + (0.5 - Math.random()));
}
}
// regular fire a bullet at the player
if (GameHandler.frameCount - this.bulletRecharge > this.BULLET_RECHARGE && scene.player.alive)
{
// ok, update last fired frame and we can now generate a bullet
this.bulletRecharge = GameHandler.frameCount;
// generate a vector pointed at the player
// by calculating a vector between the player and enemy positions
var v = scene.player.position.clone().sub(this.position);
// scale resulting vector down to bullet vector size
var scale = (this.size === 0 ? 5.0 : 6.0) / v.length();
v.x *= scale;
v.y *= scale;
// slightly randomize the direction (big ship is less accurate also)
v.x += (this.size === 0 ? (Math.random() * 2 - 1) : (Math.random() - 0.5));
v.y += (this.size === 0 ? (Math.random() * 2 - 1) : (Math.random() - 0.5));
// - could add the enemy motion vector for correct momentum
// - but problem is this leads to slow bullets firing back from dir of travel
// - so pretend that enemies are clever enough to account for this...
//v.add(this.vector);
var bullet = new Asteroids.EnemyBullet(this.position.clone(), v);
scene.enemyBullets.push(bullet);
}
},
/**
* Enemy rendering method
*/
onRender: function onRender(ctx)
{
if (BITMAPS)
{
// render enemy graphic bitmap
var rad = this.RADIUS + 2;
this.renderSprite(ctx, this.position.x - rad, this.position.y - rad, rad * 2, true);
}
else
{
ctx.save();
ctx.translate(this.position.x, this.position.y);
if (this.size === 0)
{
ctx.scale(2, 2);
}
ctx.beginPath();
ctx.moveTo(0, -4);
ctx.lineTo(8, 3);
ctx.lineTo(0, 8);
ctx.lineTo(-8, 3);
ctx.lineTo(0, -4);
ctx.closePath();
ctx.shadowColor = ctx.strokeStyle = "rgb(100,150,100)";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, -8);
ctx.lineTo(4, -4);
ctx.lineTo(0, 0);
ctx.lineTo(-4, -4);
ctx.lineTo(0, -8);
ctx.closePath();
ctx.shadowColor = ctx.strokeStyle = "rgb(150,200,150)";
ctx.stroke();
ctx.restore();
}
},
radius: function radius()
{
return this.RADIUS;
}
});
})();
/**
* Weapon system base class for the player actor.
*
* @namespace Asteroids
* @class Asteroids.Weapon
*/
(function()
{
Asteroids.Weapon = function(player)
{
this.player = player;
return this;
};
Asteroids.Weapon.prototype =
{
WEAPON_RECHARGE: 3,
weaponRecharge: 0,
player: null,
fire: function()
{
// now test we did not fire too recently
if (GameHandler.frameCount - this.weaponRecharge > this.WEAPON_RECHARGE)
{
// ok, update last fired frame and we can now generate a bullet
this.weaponRecharge = GameHandler.frameCount;
return this.doFire();
}
},
doFire: function()
{
}
};
})();
/**
* Basic primary weapon for the player actor.
*
* @namespace Asteroids
* @class Asteroids.PrimaryWeapon
*/
(function()
{
Asteroids.PrimaryWeapon = function(player)
{
Asteroids.PrimaryWeapon.superclass.constructor.call(this, player);
return this;
};
extend(Asteroids.PrimaryWeapon, Asteroids.Weapon,
{
doFire: function()
{
// generate a vector rotated to the player heading and then add the current player
// vector to give the bullet the correct directional momentum
var t = new Vector(0.0, -8.0);
t.rotate(this.player.heading * RAD);
t.add(this.player.vector);
return new Asteroids.Bullet(this.player.position.clone(), t, this.player.heading);
}
});
})();
/**
* Twin Cannons primary weapon for the player actor.
*
* @namespace Asteroids
* @class Asteroids.TwinCannonsWeapon
*/
(function()
{
Asteroids.TwinCannonsWeapon = function(player)
{
Asteroids.TwinCannonsWeapon.superclass.constructor.call(this, player);
return this;
};
extend(Asteroids.TwinCannonsWeapon, Asteroids.Weapon,
{
doFire: function()
{
var t = new Vector(0.0, -8.0);
t.rotate(this.player.heading * RAD);
t.add(this.player.vector);
return new Asteroids.BulletX2(this.player.position.clone(), t, this.player.heading);
}
});
})();
/**
* V Spray Cannons primary weapon for the player actor.
*
* @namespace Asteroids
* @class Asteroids.VSprayCannonsWeapon
*/
(function()
{
Asteroids.VSprayCannonsWeapon = function(player)
{
this.WEAPON_RECHARGE = 5;
Asteroids.VSprayCannonsWeapon.superclass.constructor.call(this, player);
return this;
};
extend(Asteroids.VSprayCannonsWeapon, Asteroids.Weapon,
{
doFire: function()
{
var t, h;
var bullets = [];
h = this.player.heading - 15;
t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector);
bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h));
h = this.player.heading;
t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector);
bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h));
h = this.player.heading + 15;
t = new Vector(0.0, -7.0).rotate(h * RAD).add(this.player.vector);
bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h));
return bullets;
}
});
})();
/**
* Side Guns additional primary weapon for the player actor.
*
* @namespace Asteroids
* @class Asteroids.SideGunWeapon
*/
(function()
{
Asteroids.SideGunWeapon = function(player)
{
this.WEAPON_RECHARGE = 5;
Asteroids.SideGunWeapon.superclass.constructor.call(this, player);
return this;
};
extend(Asteroids.SideGunWeapon, Asteroids.Weapon,
{
doFire: function()
{
var t, h;
var bullets = [];
h = this.player.heading - 90;
t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector);
bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25));
h = this.player.heading + 90;
t = new Vector(0.0, -8.0).rotate(h * RAD).add(this.player.vector);
bullets.push(new Asteroids.Bullet(this.player.position.clone(), t, h, 25));
return bullets;
}
});
})();
/**
* Rear Gun additional primary weapon for the player actor.
*
* @namespace Asteroids
* @class Asteroids.RearGunWeapon
*/
(function()
{
Asteroids.RearGunWeapon = function(player)
{
this.WEAPON_RECHARGE = 5;
Asteroids.RearGunWeapon.superclass.constructor.call(this, player);
return this;
};
extend(Asteroids.RearGunWeapon, Asteroids.Weapon,
{
doFire: function()
{
var t = new Vector(0.0, -8.0);
var h = this.player.heading + 180;
t.rotate(h * RAD);
t.add(this.player.vector);
return new Asteroids.Bullet(this.player.position.clone(), t, h, 25);
}
});
})();
/**
* Player Bullet actor class.
*
* @namespace Asteroids
* @class Asteroids.Bullet
*/
(function()
{
Asteroids.Bullet = function(p, v, h, lifespan)
{
Asteroids.Bullet.superclass.constructor.call(this, p, v);
this.heading = h;
if (lifespan)
{
this.lifespan = lifespan;
}
return this;
};
extend(Asteroids.Bullet, Game.Actor,
{
BULLET_WIDTH: 2,
BULLET_HEIGHT: 6,
FADE_LENGTH: 5,
/**
* Bullet heading
*/
heading: 0.0,
/**
* Bullet lifespan remaining
*/
lifespan: 40,
/**
* Bullet power energy
*/
powerLevel: 1,
/**
* Bullet rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
var width = this.BULLET_WIDTH;
var height = this.BULLET_HEIGHT;
ctx.save();
ctx.globalCompositeOperation = "lighter";
if (this.lifespan < this.FADE_LENGTH)
{
// fade out
ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
}
if (BITMAPS)
{
ctx.shadowBlur = 8;
ctx.shadowColor = ctx.fillStyle = "rgb(50,255,50)";
}
else
{
ctx.shadowColor = ctx.strokeStyle = "rgb(50,255,50)";
}
// rotate the little bullet rectangle into the correct heading
ctx.translate(this.position.x, this.position.y);
ctx.rotate(this.heading * RAD);
var x = -(width / 2);
var y = -(height / 2);
if (BITMAPS)
{
ctx.fillRect(x, y, width, height);
ctx.fillRect(x, y+1, width, height-1);
}
else
{
ctx.strokeRect(x, y-1, width, height+1);
ctx.strokeRect(x, y, width, height);
}
ctx.restore();
},
/**
* Actor expiration test
*
* @return true if expired and to be removed from the actor list, false if still in play
*/
expired: function expired()
{
// deduct lifespan from the bullet
return (--this.lifespan === 0);
},
/**
* Area effect weapon radius - zero for primary bullets
*/
effectRadius: function effectRadius()
{
return 0;
},
radius: function radius()
{
// approximate based on average between width and height
return (this.BULLET_HEIGHT + this.BULLET_WIDTH) / 2;
},
power: function power()
{
return this.powerLevel;
}
});
})();
/**
* Player BulletX2 actor class. Used by the Twin Cannons primary weapon.
*
* @namespace Asteroids
* @class Asteroids.BulletX2
*/
(function()
{
Asteroids.BulletX2 = function(p, v, h)
{
Asteroids.BulletX2.superclass.constructor.call(this, p, v, h);
this.lifespan = 50;
this.powerLevel = 2;
return this;
};
extend(Asteroids.BulletX2, Asteroids.Bullet,
{
/**
* Bullet rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
var width = this.BULLET_WIDTH;
var height = this.BULLET_HEIGHT;
ctx.save();
ctx.globalCompositeOperation = "lighter";
if (this.lifespan < this.FADE_LENGTH)
{
// fade out
ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
}
if (BITMAPS)
{
ctx.shadowBlur = 8;
ctx.shadowColor = ctx.fillStyle = "rgb(50,255,128)";
}
else
{
ctx.shadowColor = ctx.strokeStyle = "rgb(50,255,128)";
}
// rotate the little bullet rectangle into the correct heading
ctx.translate(this.position.x, this.position.y);
ctx.rotate(this.heading * RAD);
var x = -(width / 2);
var y = -(height / 2);
if (BITMAPS)
{
ctx.fillRect(x - 4, y, width, height);
ctx.fillRect(x - 4, y+1, width, height-1);
ctx.fillRect(x + 4, y, width, height);
ctx.fillRect(x + 4, y+1, width, height-1);
}
else
{
ctx.strokeRect(x - 4, y-1, width, height+1);
ctx.strokeRect(x - 4, y, width, height);
ctx.strokeRect(x + 4, y-1, width, height+1);
ctx.strokeRect(x + 4, y, width, height);
}
ctx.restore();
},
radius: function radius()
{
// double width bullets - so bigger hit area than basic ones
return (this.BULLET_HEIGHT);
}
});
})();
/**
* Bomb actor class.
*
* @namespace Asteroids
* @class Asteroids.Bomb
*/
(function()
{
Asteroids.Bomb = function(p, v)
{
Asteroids.Bomb.superclass.constructor.call(this, p, v);
return this;
};
extend(Asteroids.Bomb, Asteroids.Bullet,
{
BOMB_RADIUS: 4,
FADE_LENGTH: 5,
EFFECT_RADIUS: 45,
/**
* Bomb lifespan remaining
*/
lifespan: 80,
/**
* Bomb rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
var rad = this.BOMB_RADIUS;
ctx.save();
ctx.globalCompositeOperation = "lighter";
var alpha = 0.8;
if (this.lifespan < this.FADE_LENGTH)
{
// fade out
alpha = (0.8 / this.FADE_LENGTH) * this.lifespan;
rad = (this.BOMB_RADIUS / this.FADE_LENGTH) * this.lifespan;
}
ctx.globalAlpha = alpha;
if (BITMAPS)
{
ctx.fillStyle = "rgb(155,255,155)";
}
else
{
ctx.shadowColor = ctx.strokeStyle = "rgb(155,255,155)";
}
ctx.translate(this.position.x, this.position.y);
ctx.rotate(GameHandler.frameCount % 360);
// account for the fact that stroke() draws around the shape
if (!BITMAPS) ctx.scale(0.8, 0.8);
ctx.beginPath()
ctx.moveTo(rad * 2, 0);
for (var i=0; i<15; i++)
{
ctx.rotate(Math.PI / 8);
if (i % 2 == 0)
{
ctx.lineTo((rad * 2 / 0.525731) * 0.200811, 0);
}
else
{
ctx.lineTo(rad * 2, 0);
}
}
ctx.closePath();
if (BITMAPS) ctx.fill(); else ctx.stroke();
ctx.restore();
},
/**
* Area effect weapon radius
*/
effectRadius: function effectRadius()
{
return this.EFFECT_RADIUS;
},
radius: function radius()
{
var rad = this.BOMB_RADIUS;
if (this.lifespan <= this.FADE_LENGTH)
{
rad = (this.BOMB_RADIUS / this.FADE_LENGTH) * this.lifespan;
}
return rad;
}
});
})();
/**
* Enemy Bullet actor class.
*
* @namespace Asteroids
* @class Asteroids.EnemyBullet
*/
(function()
{
Asteroids.EnemyBullet = function(p, v)
{
Asteroids.EnemyBullet.superclass.constructor.call(this, p, v);
return this;
};
extend(Asteroids.EnemyBullet, Game.Actor,
{
BULLET_RADIUS: 4,
FADE_LENGTH: 5,
/**
* Bullet lifespan remaining
*/
lifespan: 60,
/**
* Bullet rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
var rad = this.BULLET_RADIUS;
ctx.save();
ctx.globalCompositeOperation = "lighter";
var alpha = 0.7;
if (this.lifespan < this.FADE_LENGTH)
{
// fade out and make smaller
alpha = (0.7 / this.FADE_LENGTH) * this.lifespan;
rad = (this.BULLET_RADIUS / this.FADE_LENGTH) * this.lifespan;
}
ctx.globalAlpha = alpha;
if (BITMAPS)
{
ctx.fillStyle = "rgb(150,255,150)";
}
else
{
ctx.shadowColor = ctx.strokeStyle = "rgb(150,255,150)";
}
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, (rad-1 > 0 ? rad-1 : 0.1), 0, TWOPI, true);
ctx.closePath();
if (BITMAPS) ctx.fill(); else ctx.stroke();
ctx.translate(this.position.x, this.position.y);
ctx.rotate((GameHandler.frameCount % 720) / 2);
ctx.beginPath()
ctx.moveTo(rad * 2, 0);
for (var i=0; i<7; i++)
{
ctx.rotate(Math.PI/4);
if (i%2 == 0)
{
ctx.lineTo((rad * 2/0.525731) * 0.200811, 0);
}
else
{
ctx.lineTo(rad * 2, 0);
}
}
ctx.closePath();
if (BITMAPS) ctx.fill(); else ctx.stroke();
ctx.restore();
},
/**
* Actor expiration test
*
* @return true if expired and to be removed from the actor list, false if still in play
*/
expired: function expired()
{
// deduct lifespan from the bullet
return (--this.lifespan === 0);
},
radius: function radius()
{
var rad = this.BULLET_RADIUS;
if (this.lifespan <= this.FADE_LENGTH)
{
rad = (rad / this.FADE_LENGTH) * this.lifespan;
}
return rad;
}
});
})();
/**
* Basic explosion effect actor class.
*
* @namespace Asteroids
* @class Asteroids.Explosion
*/
(function()
{
Asteroids.Explosion = function(p, v, s)
{
Asteroids.Explosion.superclass.constructor.call(this, p, v, this.FADE_LENGTH);
this.size = s;
return this;
};
extend(Asteroids.Explosion, Game.EffectActor,
{
FADE_LENGTH: 10,
/**
* Explosion size
*/
size: 0,
/**
* Explosion rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
// fade out
var brightness = Math.floor((255 / this.FADE_LENGTH) * this.lifespan);
var rad = (this.size * 8 / this.FADE_LENGTH) * this.lifespan;
var rgb = brightness.toString();
ctx.save();
ctx.globalAlpha = 0.75;
ctx.fillStyle = "rgb(" + rgb + ",0,0)";
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, rad, 0, TWOPI, true);
ctx.closePath();
ctx.fill();
ctx.restore();
}
});
})();
/**
* Player Explosion effect actor class.
*
* @namespace Asteroids
* @class Asteroids.PlayerExplosion
*/
(function()
{
Asteroids.PlayerExplosion = function(p, v)
{
Asteroids.PlayerExplosion.superclass.constructor.call(this, p, v, this.FADE_LENGTH);
return this;
};
extend(Asteroids.PlayerExplosion, Game.EffectActor,
{
FADE_LENGTH: 15,
/**
* Explosion rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
ctx.save();
var alpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
ctx.globalCompositeOperation = "lighter";
ctx.globalAlpha = alpha;
var rad;
if (this.lifespan > 5 && this.lifespan <= 15)
{
var offset = this.lifespan - 5;
rad = (48 / this.FADE_LENGTH) * offset;
ctx.fillStyle = "rgb(255,170,30)";
ctx.beginPath();
ctx.arc(this.position.x-2, this.position.y-2, rad, 0, TWOPI, true);
ctx.closePath();
ctx.fill();
}
if (this.lifespan > 2 && this.lifespan <= 12)
{
var offset = this.lifespan - 2;
rad = (32 / this.FADE_LENGTH) * offset;
ctx.fillStyle = "rgb(255,255,50)";
ctx.beginPath();
ctx.arc(this.position.x+2, this.position.y+2, rad, 0, TWOPI, true);
ctx.closePath();
ctx.fill();
}
if (this.lifespan <= 10)
{
var offset = this.lifespan;
rad = (24 / this.FADE_LENGTH) * offset;
ctx.fillStyle = "rgb(255,70,100)";
ctx.beginPath();
ctx.arc(this.position.x+2, this.position.y-2, rad, 0, TWOPI, true);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
});
})();
/**
* Impact effect (from bullet hitting an object) actor class.
*
* @namespace Asteroids
* @class Asteroids.Impact
*/
(function()
{
Asteroids.Impact = function(p, v)
{
Asteroids.Impact.superclass.constructor.call(this, p, v, this.FADE_LENGTH);
return this;
};
extend(Asteroids.Impact, Game.EffectActor,
{
FADE_LENGTH: 12,
/**
* Impact effect rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
// fade out alpha
var alpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
ctx.save();
ctx.globalAlpha = alpha * 0.75;
if (BITMAPS)
{
ctx.fillStyle = "rgb(50,255,50)";
}
else
{
ctx.shadowColor = ctx.strokeStyle = "rgb(50,255,50)";
}
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 2, 0, TWOPI, true);
ctx.closePath();
if (BITMAPS) ctx.fill(); else ctx.stroke();
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 1, 0, TWOPI, true);
ctx.closePath();
if (BITMAPS) ctx.fill(); else ctx.stroke();
ctx.restore();
}
});
})();
/**
* Text indicator effect actor class.
*
* @namespace Asteroids
* @class Asteroids.TextIndicator
*/
(function()
{
Asteroids.TextIndicator = function(p, v, msg, textSize, colour, fadeLength)
{
this.fadeLength = (fadeLength ? fadeLength : this.DEFAULT_FADE_LENGTH);
Asteroids.TextIndicator.superclass.constructor.call(this, p, v, this.fadeLength);
this.msg = msg;
if (textSize)
{
this.textSize = textSize;
}
if (colour)
{
this.colour = colour;
}
return this;
};
extend(Asteroids.TextIndicator, Game.EffectActor,
{
DEFAULT_FADE_LENGTH: 16,
fadeLength: 0,
textSize: 12,
msg: null,
colour: "rgb(255,255,255)",
/**
* Text indicator effect rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
// fade out alpha
var alpha = (1.0 / this.fadeLength) * this.lifespan;
ctx.save();
ctx.globalAlpha = alpha;
Game.fillText(ctx, this.msg, this.textSize + "pt Courier New", this.position.x, this.position.y, this.colour);
ctx.restore();
}
});
})();
/**
* Score indicator effect actor class.
*
* @namespace Asteroids
* @class Asteroids.ScoreIndicator
*/
(function()
{
Asteroids.ScoreIndicator = function(p, v, score, textSize, prefix, colour, fadeLength)
{
var msg = score.toString();
if (prefix)
{
msg = prefix + ' ' + msg;
}
Asteroids.ScoreIndicator.superclass.constructor.call(this, p, v, msg, textSize, colour, fadeLength);
return this;
};
extend(Asteroids.ScoreIndicator, Asteroids.TextIndicator,
{
});
})();
/**
* Power up collectable.
*
* @namespace Asteroids
* @class Asteroids.PowerUp
*/
(function()
{
Asteroids.PowerUp = function(p, v)
{
Asteroids.PowerUp.superclass.constructor.call(this, p, v);
return this;
};
extend(Asteroids.PowerUp, Game.EffectActor,
{
RADIUS: 8,
pulse: 128,
pulseinc: 8,
/**
* Power up rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx)
{
ctx.save();
ctx.globalAlpha = 0.75;
var col = "rgb(255," + this.pulse.toString() + ",0)";
if (BITMAPS)
{
ctx.fillStyle = col;
ctx.strokeStyle = "rgb(255,255,128)";
}
else
{
ctx.lineWidth = 2.0;
ctx.shadowColor = ctx.strokeStyle = col;
}
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, this.RADIUS, 0, TWOPI, true);
ctx.closePath();
if (BITMAPS)
{
ctx.fill();
}
ctx.stroke();
ctx.restore();
this.pulse += this.pulseinc;
if (this.pulse > 255)
{
this.pulse = 256 - this.pulseinc;
this.pulseinc =- this.pulseinc;
}
else if (this.pulse < 0)
{
this.pulse = 0 - this.pulseinc;
this.pulseinc =- this.pulseinc;
}
},
radius: function radius()
{
return this.RADIUS;
},
collected: function collected(game, player, scene)
{
// randomly select a powerup to apply
var message = null;
switch (randomInt(0, 9))
{
case 0:
case 1:
// boost energy
message = "Energy Boost!";
player.energy += player.ENERGY_INIT/2;
if (player.energy > player.ENERGY_INIT)
{
player.energy = player.ENERGY_INIT;
}
break;
case 2:
// fire when shieled
message = "Fire When Shielded!";
player.fireWhenShield = true;
break;
case 3:
// extra life
message = "Extra Life!";
game.lives++;
break;
case 4:
// slow down asteroids
message = "Slow Down Asteroids!";
for (var n = 0, m = scene.enemies.length, enemy; n < m; n++)
{
enemy = scene.enemies[n];
if (enemy instanceof Asteroids.Asteroid)
{
enemy.vector.scale(0.75);
}
}
break;
case 5:
// smart bomb
message = "Smart Bomb!";
var effectRad = 96;
// add a BIG explosion actor at the smart bomb weapon position and vector
var boom = new Asteroids.Explosion(
this.position.clone(), this.vector.clone().scale(0.5), effectRad / 8);
scene.effects.push(boom);
// test circle intersection with each enemy actor
// we check the enemy list length each iteration to catch baby asteroids
// this is a fully fledged smart bomb after all!
for (var n = 0, enemy, pos = this.position; n < scene.enemies.length; n++)
{
enemy = scene.enemies[n];
// test the distance against the two radius combined
if (pos.distance(enemy.position) <= effectRad + enemy.radius())
{
// intersection detected!
enemy.hit(-1);
scene.generatePowerUp(enemy);
scene.destroyEnemy(enemy, this.vector, true);
}
}
break;
case 6:
// twin cannon primary weapon upgrade
message = "Twin Cannons!";
player.primaryWeapons["main"] = new Asteroids.TwinCannonsWeapon(player);
break;
case 7:
// v spray cannons
message = "Spray Cannons!";
player.primaryWeapons["main"] = new Asteroids.VSprayCannonsWeapon(player);
break;
case 8:
// rear guns
message = "Rear Gun!";
player.primaryWeapons["rear"] = new Asteroids.RearGunWeapon(player);
break;
case 9:
// side guns
message = "Side Guns!";
player.primaryWeapons["side"] = new Asteroids.SideGunWeapon(player);
break;
}
if (message)
{
// generate a effect indicator at the destroyed enemy position
var vec = new Vector(0, -3.0);
var effect = new Asteroids.TextIndicator(
new Vector(this.position.x, this.position.y - this.RADIUS), vec, message, null, null, 32);
scene.effects.push(effect);
}
}
});
})();
/**
* 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<j; i++)
{
// check we are in bounds of the visible world before drawing grid line segments
if (xoff + w.viewx > 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<count; i++)
{
this.collectables.push(new Arena.Multiplier(enemy.position.clone(),
enemy.vector.nscale(0.2).rotate(Rnd() * TWOPI)));
}
}
},
/**
* Generate powerup for player to collect after enemy is destroyed
*/
generatePowerUp: function generatePowerUp(enemy)
{
if (this.player.energy !== this.player.ENERGY_INIT && Rnd() < 0.1)
{
this.collectables.push(new Arena.EnergyBoostPowerup(enemy.position.clone(),
enemy.vector.nscale(0.5).rotate(Rnd() * TWOPI)));
}
},
/**
* Render each actor to the canvas.
*
* @param ctx {object} Canvas rendering context
*/
renderActors: function renderActors(ctx)
{
for (var i = 0, j = this.actors.length; i < j; i++)
{
// walk each sub-list and call render on each object
var actorList = this.actors[i];
for (var n = actorList.length - 1; n >= 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<j; i++)
{
sscore = "0" + sscore;
}
Game.fillText(ctx, sscore, font12pt, width * 0.2 + width * 0.1, font12size + 2, "white");
// high score
// TODO: add method for incrementing score so this is not done here
if (score > this.game.highscore)
{
this.game.highscore = score;
}
sscore = this.game.highscore.toString();
// pad with zeros
for (var i=0, j=8-sscore.length; i<j; i++)
{
sscore = "0" + sscore;
}
Game.fillText(ctx, "HI: " + sscore, font12pt, width * 0.4 + width * 0.1, font12size + 2, "white");
// score multiplier indicator
Game.fillText(ctx, "x" + this.game.scoreMultiplier, font12pt, width * 0.7 + width * 0.1, font12size + 2, "white");
// Benchmark - information output
if (this.sceneCompletedTime)
{
Game.fillText(ctx, "TEST " + this.test + " COMPLETED: " + this.getTransientTestScore(), "20pt Courier New", 4, 40, "white");
}
Game.fillText(ctx, "SCORE: " + this.getTransientTestScore(), "12pt Courier New", 0, GameHandler.height - 42, "lightblue");
Game.fillText(ctx, "TSF: " + Math.round(GameHandler.frametime) + "ms", "12pt Courier New", 0, GameHandler.height - 22, "lightblue");
Game.fillText(ctx, "FPS: " + GameHandler.lastfps, "12pt Courier New", 0, GameHandler.height - 2, "lightblue");
ctx.restore();
},
screenCenterVector: function screenCenterVector()
{
// transform to world position - to get the center of the game screen
var m = new Vector(GameHandler.width*0.5, GameHandler.height*0.5);
m.scale(1 / this.world.scale);
m.x += this.world.viewx;
m.y += this.world.viewy;
return m;
}
});
})();
/**
* Arena.K3DController class.
*
* Arena impl of a K3D controller. One per sprite.
*/
(function()
{
/**
* Arena.Controller constructor
*
* @param canvas {Object} The canvas to render the object list into.
*/
Arena.Controller = function()
{
Arena.Controller.superclass.constructor.call(this);
};
extend(Arena.Controller, K3D.BaseController,
{
/**
* Render tick - should be called from appropriate sprite renderer
*/
render: function(ctx)
{
// execute super class method to process render pipelines
this.processFrame(ctx);
}
});
})();
/**
* K3DActor class.
*
* An actor that can be rendered by K3D. The code implements a K3D controller.
* Call renderK3D() each frame.
*
* @namespace Arena
* @class Arena.K3DActor
*/
(function()
{
Arena.K3DActor = function(p, v)
{
Arena.K3DActor.superclass.constructor.call(this, p, v);
this.k3dController = new Arena.Controller();
return this;
};
extend(Arena.K3DActor, Game.Actor,
{
k3dController: null,
k3dObject: null,
/**
* Render K3D graphic.
*/
renderK3D: function renderK3D(ctx)
{
this.k3dController.render(ctx);
},
setK3DObject: function setK3DObject(obj)
{
this.k3dObject = obj;
this.k3dController.addK3DObject(obj);
}
});
})();/**
* Player actor class.
*
* @namespace Arena
* @class Arena.Player
*/
(function()
{
Arena.Player = function(p, v, h)
{
Arena.Player.superclass.constructor.call(this, p, v);
this.energy = this.ENERGY_INIT;
this.radius = 20;
this.heading = h;
// setup weapons
this.primaryWeapons = [];
this.primaryWeapons["main"] = new Arena.PrimaryWeapon(this);
// 3D sprite object - must be created after constructor call
var obj = new K3D.K3DObject();
with (obj)
{
drawmode = "wireframe";
shademode = "depthcue";
depthscale = 32;
linescale = 3;
perslevel = 256;
addphi = -1.0; //addphi = 1.0; addtheta = -1.0; addgamma = -0.75;
scale = 0.8; // TODO: pre-scale points? (this is only done once for player, but enemies...)
init(
[{x:-30,y:-20,z:0}, {x:-15,y:-25,z:20}, {x:15,y:-25,z:20}, {x:30,y:-20,z:0}, {x:15,y:-25,z:-20}, {x:-15,y:-25,z:-20}, {x:0,y:35,z:0}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:4}, {a:4,b:5}, {a:5,b:0}, {a:1,b:6}, {a:2,b:6}, {a:4,b:6}, {a:5,b:6}, {a:0,b:6}, {a:3,b:6}],
[{vertices:[0,1,6]}, {vertices:[1,2,6]}, {vertices:[2,3,6]}, {vertices:[3,4,6]}, {vertices:[4,5,6]}, {vertices:[5,0,6]}, {vertices:[0,1,2,3,4,5]}]
);
}
this.setK3DObject(obj);
return this;
};
extend(Arena.Player, Arena.K3DActor,
{
MAX_PLAYER_VELOCITY: 15.0,
THRUST_DELAY: 1,
ENERGY_INIT: 100,
/**
* Player heading
*/
heading: 0,
/**
* Player energy level
*/
energy: 0,
/**
* Primary weapon list
*/
primaryWeapons: null,
/**
* Engine thrust recharge counter
*/
thrustRecharge: 0,
/**
* True if the engine thrust graphics should be rendered next frame
*/
engineThrust: false,
/**
* Frame that the player was killed on - to cause a delay before respawning the player
*/
killedOnFrame: 0,
/**
* Power up settings - primary weapon bounce
*/
bounceWeapons: false,
/**
* Player rendering method
*
* @param ctx {object} Canvas rendering context
* @param world {object} World metadata
*/
onRender: function onRender(ctx, world)
{
var headingRad = this.heading * RAD;
// transform world to screen - non-visible returns null
var viewposition = Game.worldToScreen(this.position, world, this.radius);
if (viewposition)
{
// render engine thrust?
if (this.engineThrust)
{
ctx.save();
// scale ALL graphics... - translate to position apply canvas scaling
ctx.translate(viewposition.x, viewposition.y);
ctx.scale(world.scale, world.scale);
ctx.rotate(headingRad);
ctx.translate(0, -4); // slight offset so that collision radius is centered
ctx.globalAlpha = 0.4 + Rnd() * 0.5;
ctx.shadowColor = ctx.fillStyle = "rgb(25,125,255)";
ctx.beginPath();
ctx.moveTo(-12, 20);
ctx.lineTo(12, 20);
ctx.lineTo(0, 50 + Rnd() * 20);
ctx.closePath();
ctx.fill();
ctx.restore();
this.engineThrust = false;
}
// render player graphic
ctx.save();
ctx.shadowColor = "rgb(255,255,255)";
ctx.translate(viewposition.x, viewposition.y);
ctx.scale(world.scale, world.scale);
ctx.rotate(headingRad);
ctx.translate(0, -4); // slight offset so that collision radius is centered
// render 3D sprite
this.renderK3D(ctx);
ctx.restore();
}
//if (DEBUG && !viewposition) console.log("non-visible: " + new Date().getTime());
},
/**
* Handle key input to rotate and move the player
*/
handleInput: function handleInput(input)
{
var h = this.heading % 360;
// TODO: hack, fix this to maintain +ve heading or change calculation below...
if (h < 0) h += 360;
// first section tweens the current rendered heading of the player towards
// the desired heading - but the next section actually applies a vector
// TODO: this seems over complicated - must be an easier way to do this...
if (input.left)
{
if (h > 270 || h < 90)
{
if (h > 270) this.heading -= ((h - 270) * 0.2);
else this.heading -= ((h + 90) * 0.2);
}
else this.heading += ((270 - h) * 0.2);
}
if (input.right)
{
if (h < 90 || h > 270)
{
if (h < 90) this.heading += ((90 - h) * 0.2);
else this.heading += ((h - 90) * 0.2);
}
else this.heading -= ((h - 90) * 0.2);
}
if (input.up)
{
if (h < 180)
{
this.heading -= (h * 0.2);
}
else this.heading += ((360 - h) * 0.2);
}
if (input.down)
{
if (h < 180)
{
this.heading += ((180 - h) * 0.2);
}
else this.heading -= ((h - 180) * 0.2);
}
// second section applies the direct thrust angled vector
// this ensures a snappy control method with the above heading effect
var angle = null;
if (input.left)
{
if (input.up) angle = 315;
else if (input.down) angle = 225;
else angle = 270;
}
else if (input.right)
{
if (input.up) angle = 45;
else if (input.down) angle = 135;
else angle = 90;
}
else if (input.up)
{
if (input.left) angle = 315;
else if (input.right) angle = 45;
else angle = 0;
}
else if (input.down)
{
if (input.left) angle = 225;
else if (input.right) angle = 135;
else angle = 180;
}
if (angle !== null)
{
this.thrust(angle);
}
else
{
// reduce thrust over time if player isn't actively moving
this.vector.scale(0.9);
}
},
/**
* Execute player forward thrust request
* Automatically a delay is used between each application - to ensure stable thrust on all machines.
*/
thrust: function thrust(angle)
{
// now test we did not thrust too recently - to stop fast key repeat issues
if (GameHandler.frameCount - this.thrustRecharge > this.THRUST_DELAY)
{
// update last frame count
this.thrustRecharge = GameHandler.frameCount;
// generate a small thrust vector
var t = new Vector(0.0, -2.00);
// rotate thrust vector by player current heading
t.rotate(angle * RAD);
// add player thrust vector to position
this.vector.add(t);
// player can't exceed maximum velocity - scale vector down if
// this occurs - do this rather than not adding the thrust at all
// otherwise the player cannot turn and thrust at max velocity
if (this.vector.length() > this.MAX_PLAYER_VELOCITY)
{
this.vector.scale(this.MAX_PLAYER_VELOCITY / this.vector.length());
}
}
// mark so that we know to render engine thrust graphics
this.engineThrust = true;
},
damageBy: function damageBy(enemy)
{
this.energy -= enemy.playerDamage;
if (this.energy <= 0)
{
this.energy = 0;
this.kill();
}
},
kill: function kill()
{
this.alive = false;
this.killedOnFrame = GameHandler.frameCount;
},
/**
* Fire primary weapon(s)
* @param bulletList {Array} to add bullet(s) to on success
* @param heading {Number} bullet heading
*/
firePrimary: function firePrimary(bulletList, vector, heading)
{
// attempt to fire the primary weapon(s)
// first ensure player is alive
if (this.alive)
{
for (var w in this.primaryWeapons)
{
var b = this.primaryWeapons[w].fire(vector, heading);
if (b)
{
for (var i=0; i<b.length; i++)
{
bulletList.push(b[i]);
}
}
}
}
},
reset: function reset(persistPowerUps)
{
// reset energy, alive status, weapons and power up flags
this.alive = true;
if (!persistPowerUps)
{
// reset weapons
this.primaryWeapons = [];
this.primaryWeapons["main"] = new Arena.PrimaryWeapon(this);
// reset powerup settings
this.bounceWeapons = false;
}
this.energy = this.ENERGY_INIT;
}
});
})();
/**
* Player score multiplier collectable class.
*
* @namespace Arena
* @class Arena.Multiplier
*/
(function()
{
Arena.Multiplier = function(p, v, h)
{
Arena.Multiplier.superclass.constructor.call(this, p, v, this.LIFESPAN);
this.radius = 10;
this.rotation = 0;
return this;
};
extend(Arena.Multiplier, Game.EffectActor,
{
LIFESPAN: 250,
FADE_LENGTH: 16,
rotation: 0,
/**
* Multiplier collectable rendering method
*
* @param ctx {object} Canvas rendering context
* @param world {object} World metadata
*/
onRender: function onRender(ctx, world)
{
// transform world to screen - non-visible returns null
var viewposition = Game.worldToScreen(this.position, world, this.radius);
if (viewposition)
{
var r = this.radius * 0.6;
ctx.save();
ctx.globalCompositeOperation = "lighter";
if (this.lifespan < this.FADE_LENGTH)
{
ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
}
ctx.strokeStyle = ctx.shadowColor = "rgb(255,180,0)";
ctx.translate(viewposition.x, viewposition.y);
ctx.scale(world.scale, world.scale);
ctx.rotate(this.rotation);
ctx.strokeRect(-r, -r, this.radius*1.2, this.radius*1.2);
ctx.restore();
this.rotation += 0.02;
}
},
collected: function collected(game, player, scene)
{
if (++game.scoreMultiplier % 10 === 0)
{
// display multiplier to player every large increment
var vec = new Vector(0, -5.0).add(this.vector);
scene.effects.push(new Arena.TextIndicator(
this.position.clone(), vec, "x" + game.scoreMultiplier, 32, "white", 32));
}
}
});
})();
/**
* Player energy boost powerup collectable class.
*
* @namespace Arena
* @class Arena.EnergyBoostPowerup
*/
(function()
{
Arena.EnergyBoostPowerup = function(p, v, h)
{
Arena.EnergyBoostPowerup.superclass.constructor.call(this, p, v, this.LIFESPAN);
this.radius = 12;
this.rotation = 0;
return this;
};
extend(Arena.EnergyBoostPowerup, Game.EffectActor,
{
LIFESPAN: 350,
FADE_LENGTH: 16,
rotation: 0,
/**
* EnergyBoostPowerup collectable rendering method
*
* @param ctx {object} Canvas rendering context
* @param world {object} World metadata
*/
onRender: function onRender(ctx, world)
{
// transform world to screen - non-visible returns null
var viewposition = Game.worldToScreen(this.position, world, this.radius);
if (viewposition)
{
var r = this.radius * 0.6;
ctx.save();
ctx.globalCompositeOperation = "lighter";
if (this.lifespan < this.FADE_LENGTH)
{
ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
}
ctx.lineWidth = 2.0;
ctx.strokeStyle = ctx.shadowColor = "rgb(100,255,0)";
ctx.translate(viewposition.x, viewposition.y);
ctx.scale(world.scale, world.scale);
ctx.rotate(this.rotation);
ctx.strokeRect(-r, -r, this.radius*1.2, this.radius*1.2);
ctx.restore();
this.rotation += 0.05;
}
},
collected: function collected(game, player, scene)
{
// increment player energy
player.energy += 25;
if (player.energy > player.ENERGY_INIT)
{
player.energy = player.ENERGY_INIT;
}
// display indicator
var vec = new Vector(0, -5.0).add(this.vector);
scene.effects.push(new Arena.TextIndicator(
this.position.clone(), vec, "Energy Boost!", 32, "white", 32));
}
});
})();
/**
* Weapon system base class for the player actor.
*
* @namespace Arena
* @class Arena.Weapon
*/
(function()
{
Arena.Weapon = function(player)
{
this.player = player;
return this;
};
Arena.Weapon.prototype =
{
rechargeTime: 3,
weaponRecharged: 0,
player: null,
fire: function(v, h)
{
// now test we did not fire too recently
if (GameHandler.frameCount - this.weaponRecharged > this.rechargeTime)
{
// ok, update last fired frame and we can now generate a bullet
this.weaponRecharged = GameHandler.frameCount;
return this.doFire(v, h);
}
},
doFire: function(v, h)
{
}
};
})();
/**
* Basic primary weapon for the player actor.
*
* @namespace Arena
* @class Arena.PrimaryWeapon
*/
(function()
{
Arena.PrimaryWeapon = function(player)
{
Arena.PrimaryWeapon.superclass.constructor.call(this, player);
this.rechargeTime = this.DEFAULT_RECHARGE;
return this;
};
extend(Arena.PrimaryWeapon, Arena.Weapon,
{
DEFAULT_RECHARGE: 5,
bulletCount: 1, // increase this to output more intense bullet stream
doFire: function(vector, heading)
{
var bullets = [],
count = this.bulletCount,
total = (count > 2 ? randomInt(count - 1, count) : count);
for (var i=0; i<total; i++)
{
// slightly randomize the spread based on bullet count
var offset = (count > 1 ? Rnd() * PIO16 * (count-1) : 0),
h = heading + offset - (PIO32 * (count-1)),
v = vector.nrotate(offset - (PIO32 * (count-1))).scale(1 + Rnd() * 0.1 - 0.05);
v.add(this.player.vector);
bullets.push(new Arena.Bullet(this.player.position.clone(), v, h));
}
return bullets;
}
});
})();
/**
* Player Bullet actor class.
*
* @namespace Arena
* @class Arena.Bullet
*/
(function()
{
Arena.Bullet = function(p, v, h, lifespan)
{
Arena.Bullet.superclass.constructor.call(this, p, v);
this.heading = h;
this.lifespan = (lifespan ? lifespan : this.BULLET_LIFESPAN);
this.radius = this.BULLET_RADIUS;
return this;
};
extend(Arena.Bullet, Game.Actor,
{
BULLET_RADIUS: 12,
BULLET_LIFESPAN: 30,
FADE_LENGTH: 5,
/**
* Bullet heading
*/
heading: 0,
/**
* Bullet lifespan remaining
*/
lifespan: 0,
/**
* Bullet power energy
*/
powerLevel: 1,
/**
* Bullet rendering method
*
* @param ctx {object} Canvas rendering context
* @param world {object} World metadata
*/
onRender: function onRender(ctx, world)
{
ctx.save();
ctx.shadowBlur = 0;
ctx.globalCompositeOperation = "lighter";
if (this.worldToScreen(ctx, world, this.BULLET_RADIUS) &&
this.lifespan < this.BULLET_LIFESPAN - 1) // hack - to stop draw over player ship
{
if (this.lifespan < this.FADE_LENGTH)
{
ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
}
// rotate into the correct heading
ctx.rotate(this.heading * RAD);
// draw bullet primary weapon
try
{
ctx.drawImage(GameHandler.prerenderer.images["playerweapon"][0], -20, -20);
}
catch (error)
{
if (console !== undefined) console.log(error.message);
}
}
ctx.restore();
},
/**
* Actor expiration test
*
* @return true if expired and to be removed from the actor list, false if still in play
*/
expired: function expired()
{
// deduct lifespan from the bullet
return (--this.lifespan === 0);
},
/**
* Area effect weapon radius - zero for primary bullets
*/
effectRadius: function effectRadius()
{
return 0;
},
power: function power()
{
return this.powerLevel;
}
});
})();
/**
* Enemy Bullet actor class.
*
* @namespace Arena
* @class Arena.EnemyBullet
*/
(function()
{
Arena.EnemyBullet = function(p, v, power)
{
Arena.EnemyBullet.superclass.constructor.call(this, p, v);
this.powerLevel = this.playerDamage = power;
this.lifespan = this.BULLET_LIFESPAN;
this.radius = this.BULLET_RADIUS;
return this;
};
extend(Arena.EnemyBullet, Game.Actor,
{
BULLET_LIFESPAN: 75,
BULLET_RADIUS: 10,
FADE_LENGTH: 8,
powerLevel: 0,
playerDamage: 0,
/**
* Bullet lifespan remaining
*/
lifespan: 0,
/**
* Bullet rendering method
*
* @param ctx {object} Canvas rendering context
* @param world {object} World metadata
*/
onRender: function onRender(ctx, world)
{
ctx.save();
ctx.globalCompositeOperation = "lighter";
if (this.worldToScreen(ctx, world, this.BULLET_RADIUS) &&
this.lifespan < this.BULLET_LIFESPAN - 1) // hack - to stop draw over enemy
{
if (this.lifespan < this.FADE_LENGTH)
{
ctx.globalAlpha = (1.0 / this.FADE_LENGTH) * this.lifespan;
}
ctx.shadowColor = ctx.fillStyle = "rgb(150,255,150)";
var rad = this.BULLET_RADIUS - 2;
ctx.beginPath();
ctx.arc(0, 0, (rad-1 > 0 ? rad-1 : 0.1), 0, TWOPI, true);
ctx.closePath();
ctx.fill();
ctx.rotate((GameHandler.frameCount % 1800) / 5);
ctx.beginPath()
ctx.moveTo(rad * 2, 0);
for (var i=0; i<7; i++)
{
ctx.rotate(PIO4);
if (i%2 === 0)
{
ctx.lineTo((rad * 2 / 0.5) * 0.2, 0);
}
else
{
ctx.lineTo(rad * 2, 0);
}
}
ctx.closePath();
ctx.fill();
}
ctx.restore();
},
/**
* Actor expiration test
*
* @return true if expired and to be removed from the actor list, false if still in play
*/
expired: function expired()
{
// deduct lifespan from the bullet
return (--this.lifespan === 0);
},
power: function power()
{
return this.powerLevel;
}
});
})();
/**
* Enemy Ship actor class.
*
* @namespace Arena
* @class Arena.EnemyShip
*/
(function()
{
Arena.EnemyShip = function(scene, type)
{
// enemy score multiplier based on type buy default - but some enemies
// will tweak this in the individual setup code later
this.type = this.scoretype = type;
// generate enemy at start position - not too close to the player
var p, v = null;
while (!v)
{
p = new Vector(Rnd() * scene.world.size, Rnd() * scene.world.size);
if (scene.player.position.distance(p) > 220)
{
v = new Vector(0,0);
}
}
Arena.EnemyShip.superclass.constructor.call(this, p, v);
// 3D sprite object - must be created after constructor call
var me = this;
var obj = new K3D.K3DObject();
with (obj)
{
drawmode = "wireframe";
shademode = "depthcue";
depthscale = 32;
linescale = 3;
perslevel = 256;
switch (type)
{
case 0:
// Dumbo: blue stretched cubiod
me.radius = 22;
me.playerDamage = 10;
me.colorRGB = "rgb(0,128,255)";
color = [0,128,255];
addphi = -1.0; addgamma = -0.75;
init(
[{x:-20,y:-20,z:12}, {x:-20,y:20,z:12}, {x:20,y:20,z:12}, {x:20,y:-20,z:12}, {x:-10,y:-10,z:-12}, {x:-10,y:10,z:-12}, {x:10,y:10,z:-12}, {x:10,y:-10,z:-12}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
[]);
break;
case 1:
// Zoner: yellow diamond
me.radius = 22;
me.playerDamage = 10;
me.colorRGB = "rgb(255,255,0)";
color = [255,255,0];
addphi = 0.5; addgamma = -0.5; addtheta = -1.0;
init(
[{x:-20,y:-20,z:0}, {x:-20,y:20,z:0}, {x:20,y:20,z:0}, {x:20,y:-20,z:0}, {x:0,y:0,z:-20}, {x:0,y:0,z:20}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:0,b:4}, {a:1,b:4}, {a:2,b:4}, {a:3,b:4}, {a:0,b:5}, {a:1,b:5}, {a:2,b:5}, {a:3,b:5}],
[]);
break;
case 2:
// Tracker: red flattened square
me.radius = 22;
me.health = 2;
me.playerDamage = 15;
me.colorRGB = "rgb(255,96,0)";
color = [255,96,0];
addgamma = 1.0;
init(
[{x:-20,y:-20,z:5}, {x:-20,y:20,z:5}, {x:20,y:20,z:5}, {x:20,y:-20,z:5}, {x:-15,y:-15,z:-5}, {x:-15,y:15,z:-5}, {x:15,y:15,z:-5}, {x:15,y:-15,z:-5}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
[]);
break;
case 3:
// Borg: big green cube
me.radius = 52;
me.health = 5;
me.playerDamage = 25;
me.colorRGB = "rgb(0,255,64)";
color = [0,255,64];
depthscale = 96; // tweak for larger object
addphi = -1.5;
init(
[{x:-40,y:-40,z:40}, {x:-40,y:40,z:40}, {x:40,y:40,z:40}, {x:40,y:-40,z:40}, {x:-40,y:-40,z:-40}, {x:-40,y:40,z:-40}, {x:40,y:40,z:-40}, {x:40,y:-40,z:-40}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
[]);
break;
case 4:
// Dodger: small cyan cube
me.radius = 25;
me.playerDamage = 10;
me.colorRGB = "rgb(0,255,255)";
color = [0,255,255];
addphi = 0.5; addtheta = -3.0;
init(
[{x:-20,y:-20,z:20}, {x:-20,y:20,z:20}, {x:20,y:20,z:20}, {x:20,y:-20,z:20}, {x:-20,y:-20,z:-20}, {x:-20,y:20,z:-20}, {x:20,y:20,z:-20}, {x:20,y:-20,z:-20}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
[]);
break;
case 5:
// Splitter: medium purple pyrimid (converts to 2x smaller versions when hit)
me.radius = 25;
me.health = 3;
me.playerDamage = 20;
me.colorRGB = "rgb(148,0,255)";
color = [148,0,255];
depthscale = 56; // tweak for larger object
addphi = 3.0;
init(
[{x:-30,y:-20,z:0}, {x:0,y:-20,z:30}, {x:30,y:-20,z:0}, {x:0,y:-20,z:-30}, {x:0,y:30,z:0}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:0,b:4}, {a:1,b:4}, {a:2,b:4}, {a:3,b:4}],
[]);
break;
case 6:
// Bomber: medium magenta star - dodge bullets, dodge player!
me.radius = 28;
me.health = 5;
me.playerDamage = 20;
me.colorRGB = "rgb(255,0,255)";
color = [255,0,255];
depthscale = 56; // tweak for larger object
addgamma = -5.0;
init(
[{x:-30,y:-30,z:10}, {x:-30,y:30,z:10}, {x:30,y:30,z:10}, {x:30,y:-30,z:10}, {x:-15,y:-15,z:-15}, {x:-15,y:15,z:-15}, {x:15,y:15,z:-15}, {x:15,y:-15,z:-15}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:4,b:5}, {a:5,b:6}, {a:6,b:7}, {a:7,b:4}, {a:0,b:4}, {a:1,b:5}, {a:2,b:6}, {a:3,b:7}],
[]);
break;
case 99:
// Splitter-mini: see Splitter above
me.scoretype = 4; // override default score type setting
me.dropsMutliplier = false;
me.radius = 12;
me.health = 1;
me.playerDamage = 5;
me.colorRGB = "rgb(148,0,211)";
color = [148,0,211];
depthscale = 16; // tweak for smaller object
addphi = 5.0;
init(
[{x:-15,y:-10,z:0}, {x:0,y:-10,z:15}, {x:15,y:-10,z:0}, {x:0,y:-10,z:-15}, {x:0,y:15,z:0}],
[{a:0,b:1}, {a:1,b:2}, {a:2,b:3}, {a:3,b:0}, {a:0,b:4}, {a:1,b:4}, {a:2,b:4}, {a:3,b:4}],
[]);
break;
}
}
this.setK3DObject(obj);
return this;
};
extend(Arena.EnemyShip, Arena.K3DActor,
{
BULLET_RECHARGE: 50,
SPAWN_LENGTH: 20, // TODO: replace this with anim state machine
aliveTime: 0, // TODO: replace this with anim state machine
type: 0,
scoretype: 0,
dropsMutliplier: true,
health: 1,
colorRGB: null,
playerDamage: 0,
bulletRecharge: 0,
hit: false, // TODO: replace with state? - "extends" default render state...?
onUpdate: function onUpdate(scene)
{
// TODO: replace this with anim state machine
if (++this.aliveTime < this.SPAWN_LENGTH)
{
// TODO: needs enemy state implemented so can test for "alive" state
// for collision detection?
// other methods can then test state such as onRender()
// SPAWNED->ALIVE->DEAD
return;
}
else if (this.aliveTime === this.SPAWN_LENGTH)
{
// initial vector needed for some enemy types - others will set later
this.vector = new Vector(4 * (Rnd < 0.5 ? 1 : -1), 4 * (Rnd < 0.5 ? 1 : -1));
}
switch (this.type)
{
case 0:
// dumb - change direction randomly
if (Rnd() < 0.01)
{
this.vector.y = -(this.vector.y + (0.5 - Rnd()));
}
break;
case 1:
// randomly reorientate towards player ("perception level")
// so player can avade by moving around them
if (Rnd() < 0.04)
{
// head towards player - generate a vector pointed at the player
// by calculating a vector between the player and enemy positions
var v = scene.player.position.nsub(this.position);
// scale resulting vector down to fixed vector size i.e. speed
this.vector = v.scaleTo(4);
}
break;
case 2:
// very perceptive and faster - this one is mean
if (Rnd() < 0.2)
{
var v = scene.player.position.nsub(this.position);
this.vector = v.scaleTo(8);
}
break;
case 3:
// fast dash towards player, otherwise it slows down
if (Rnd() < 0.03)
{
var v = scene.player.position.nsub(this.position);
this.vector = v.scaleTo(12);
}
else
{
this.vector.scale(0.95);
}
break;
case 4:
// perceptive and fast - and tries to dodgy bullets!
var dodged = false;
// if we are close to the player then don't try and dodge,
// otherwise enemy might dash away rather than go for the kill
if (scene.player.position.nsub(this.position).length() > 150)
{
var p = this.position,
r = this.radius + 50; // bullet "distance" perception
// look at player bullets list - are any about to hit?
for (var i=0, j=scene.playerBullets.length, bullet, n; i < j; i++)
{
bullet = scene.playerBullets[i];
// test the distance against the two radius combined
if (bullet.position.distance(p) <= bullet.radius + r)
{
// if so attempt a fast sideways dodge!
var v = bullet.position.nsub(p).scaleTo(12);
// randomise dodge direction a bit
v.rotate((n = Rnd()) < 0.5 ? n*PIO4 : -n*PIO4);
v.invert();
this.vector = v;
dodged = true;
break;
}
}
}
if (!dodged && Rnd() < 0.04)
{
var v = scene.player.position.nsub(this.position);
this.vector = v.scaleTo(8);
}
break;
case 5:
if (Rnd() < 0.04)
{
var v = scene.player.position.nsub(this.position);
this.vector = v.scaleTo(5);
}
break;
case 6:
// if we are near the player move away
// if we are far from the player move towards
var v = scene.player.position.nsub(this.position);
if (v.length() > 400)
{
// move closer
if (Rnd() < 0.08) this.vector = v.scaleTo(8);
}
else if (v.length() < 350)
{
// move away
if (Rnd() < 0.08) this.vector = v.invert().scaleTo(8);
}
else
{
// slow down into a firing position
this.vector.scale(0.8);
// reguarly fire at the player
if (GameHandler.frameCount - this.bulletRecharge > this.BULLET_RECHARGE && scene.player.alive)
{
// update last fired frame and generate a bullet
this.bulletRecharge = GameHandler.frameCount;
// generate a vector pointed at the player
// by calculating a vector between the player and enemy positions
// then scale to a fixed size - i.e. bullet speed
var v = scene.player.position.nsub(this.position).scaleTo(10);
// slightly randomize the direction to apply some accuracy issues
v.x += (Rnd() * 2 - 1);
v.y += (Rnd() * 2 - 1);
var bullet = new Arena.EnemyBullet(this.position.clone(), v, 10);
scene.enemyBullets.push(bullet);
}
}
break;
case 99:
if (Rnd() < 0.04)
{
var v = scene.player.position.nsub(this.position);
this.vector = v.scaleTo(8);
}
break;
}
},
/**
* Enemy rendering method
*
* @param ctx {object} Canvas rendering context
* @param world {object} World metadata
*/
onRender: function onRender(ctx, world)
{
ctx.save();
if (this.worldToScreen(ctx, world, this.radius))
{
// render 3D sprite
if (!this.hit)
{
ctx.shadowColor = this.colorRGB;
}
else
{
// override colour with plain white for "hit" effect
ctx.shadowColor = "white";
var oldColor = this.k3dObject.color;
this.k3dObject.color = [255,255,255];
this.k3dObject.shademode = "plain";
}
// TODO: replace this with anim state machine test...
// TODO: adjust RADIUS for collision etc. during spawn!
if (this.aliveTime < this.SPAWN_LENGTH)
{
// nifty scaling effect as an enemy spawns into position
var scale = 1 - (this.SPAWN_LENGTH - this.aliveTime) / this.SPAWN_LENGTH;
if (scale <= 0) scale = 0.01;
else if (scale > 1) scale = 1;
ctx.scale(scale, scale);
}
this.renderK3D(ctx);
if (this.hit)
{
// restore colour and depthcue rendering mode
this.k3dObject.color = oldColor;
this.k3dObject.shademode = "depthcue";
this.hit = false;
}
}
ctx.restore();
},
damageBy: function damageBy(force)
{
// record hit - will change enemy colour for a single frame
this.hit = true;
if (force === -1 || (this.health -= force) <= 0)
{
this.alive = false;
}
return !this.alive;
},
onDestroyed: function onDestroyed(scene, player)
{
if (this.type === 5)
{
// Splitter enemy divides into two smaller ones
var enemy = new Arena.EnemyShip(scene, 99);
// update position and vector
// TODO: move this as option in constructor
enemy.vector = this.vector.nrotate(PIO2);
enemy.position = this.position.nadd(enemy.vector);
scene.enemies.push(enemy);
enemy = new Arena.EnemyShip(scene, 99);
enemy.vector = this.vector.nrotate(-PIO2);
enemy.position = this.position.nadd(enemy.vector);
scene.enemies.push(enemy);
}
}
});
})();
/**
* Particle emitter effect actor class.
*
* A simple particle emitter, that does not recycle particles, but sets itself as expired() once
* all child particles have expired.
*
* Requires a function known as the emitter that is called per particle generated.
*
* @namespace Arena
* @class Arena.Particles
*/
(function()
{
/**
* Constructor
*
* @param p {Vector} Emitter position
* @param v {Vector} Emitter velocity
* @param count {Integer} Number of particles
* @param fnEmitter {Function} Emitter function to call per particle generated
*/
Arena.Particles = function(p, v, count, fnEmitter)
{
Arena.Particles.superclass.constructor.call(this, p, v);
// generate particles based on the supplied emitter function
this.particles = new Array(count);
for (var i=0; i<count; i++)
{
this.particles[i] = fnEmitter.call(this, i);
}
return this;
};
extend(Arena.Particles, Game.Actor,
{
particles: null,
/**
* Particle effect rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx, world)
{
ctx.save();
ctx.shadowBlur = 0;
ctx.globalCompositeOperation = "lighter";
for (var i=0, particle, viewposition; i<this.particles.length; i++)
{
particle = this.particles[i];
// update particle and test for lifespan
if (particle.update())
{
viewposition = Game.worldToScreen(particle.position, world, particle.size);
if (viewposition)
{
ctx.save();
ctx.translate(viewposition.x, viewposition.y);
ctx.scale(world.scale, world.scale);
particle.render(ctx);
ctx.restore();
}
}
else
{
// particle no longer alive, remove from list
this.particles.splice(i, 1);
}
}
ctx.restore();
},
expired: function expired()
{
return (this.particles.length === 0);
}
});
})();
/**
* Default Arena Particle structure.
* Currently supports three particle types; dot, line and smudge.
*/
function ArenaParticle(position, vector, size, type, lifespan, fadelength, colour)
{
this.position = position;
this.vector = vector;
this.size = size;
this.type = type;
this.lifespan = lifespan;
this.fadelength = fadelength;
this.colour = colour ? colour : "rgb(255,125,50)"; // default colour if none set
// randomize rotation speed and angle for line particle
if (type === 1)
{
this.rotate = Rnd() * TWOPI;
this.rotationv = Rnd() - 0.5;
}
this.update = function()
{
this.position.add(this.vector);
return (--this.lifespan !== 0);
};
this.render = function(ctx)
{
// NOTE: the try/catch here is to handle where FireFox gets
// upset when rendering images outside the canvas area
try
{
ctx.globalAlpha = (this.lifespan < this.fadelength ? ((1 / this.fadelength) * this.lifespan) : 1);
switch (this.type)
{
case 0: // point (prerendered image)
// prerendered images for each enemy colour with health > 1
// lookup based on particle colour e.g. points_rgb(x,y,z)
ctx.drawImage(
GameHandler.prerenderer.images["points_" + this.colour][this.size], 0, 0);
break;
case 1: // line
var s = this.size;
ctx.rotate(this.rotate);
this.rotate += this.rotationv;
// specific line colour - for enemy explosion pieces
ctx.strokeStyle = this.colour;
ctx.lineWidth = 2.0;
ctx.beginPath();
ctx.moveTo(-s, -s);
ctx.lineTo(s, s);
ctx.closePath();
ctx.stroke();
break;
case 2: // smudge (prerendered image)
ctx.drawImage(GameHandler.prerenderer.images["smudges"][this.size - 4], 0, 0);
break;
}
}
catch (error)
{
if (console !== undefined) console.log(error.message);
}
};
}
/**
* Enemy explosion - Particle effect actor class.
*
* @namespace Arena
* @class Arena.EnemyExplosion
*/
(function()
{
/**
* Constructor
*/
Arena.EnemyExplosion = function(p, v, enemy)
{
Arena.EnemyExplosion.superclass.constructor.call(this, p, v, 16, function()
{
// randomise start position slightly
var pos = p.clone();
pos.x += randomInt(-5, 5);
pos.y += randomInt(-5, 5);
// randomise radial direction vector - speed and angle, then add parent vector
switch (randomInt(0, 2))
{
case 0:
var t = new Vector(0, randomInt(20, 25));
t.rotate(Rnd() * TWOPI);
t.add(v);
return new ArenaParticle(
pos, t, ~~(Rnd() * 4), 0, 20, 15);
case 1:
var t = new Vector(0, randomInt(5, 10));
t.rotate(Rnd() * TWOPI);
t.add(v);
// create line particle - size based on enemy type
return new ArenaParticle(
pos, t, (enemy.type !== 3 ? Rnd() * 5 + 5 : Rnd() * 10 + 10), 1, 20, 15, enemy.colorRGB);
case 2:
var t = new Vector(0, randomInt(2, 4));
t.rotate(Rnd() * TWOPI);
t.add(v);
return new ArenaParticle(
pos, t, ~~(Rnd() * 4 + 4), 2, 20, 15);
}
});
return this;
};
extend(Arena.EnemyExplosion, Arena.Particles);
})();
/**
* Enemy impact effect - Particle effect actor class.
* Used when an enemy is hit by player bullet but not destroyed.
*
* @namespace Arena
* @class Arena.EnemyImpact
*/
(function()
{
/**
* Constructor
*/
Arena.EnemyImpact = function(p, v, enemy)
{
Arena.EnemyImpact.superclass.constructor.call(this, p, v, 5, function()
{
// slightly randomise vector angle - then add parent vector
var t = new Vector(0, Rnd() < 0.5 ? randomInt(-5, -10) : randomInt(5, 10));
t.rotate(Rnd() * PIO2 - PIO4);
t.add(v);
return new ArenaParticle(
p.clone(), t, ~~(Rnd() * 4), 0, 15, 10, enemy.colorRGB);
});
return this;
};
extend(Arena.EnemyImpact, Arena.Particles);
})();
/**
* Bullet impact effect - Particle effect actor class.
* Used when an bullet hits an object and is destroyed.
*
* @namespace Arena
* @class Arena.BulletImpactEffect
*/
(function()
{
/**
* Constructor
*/
Arena.BulletImpactEffect = function(p, v, enemy)
{
Arena.BulletImpactEffect.superclass.constructor.call(this, p, v, 3, function()
{
return new ArenaParticle(
p.clone(), v.nrotate(Rnd()*PIO8), ~~(Rnd() * 4), 0, 15, 10);
});
return this;
};
extend(Arena.BulletImpactEffect, Arena.Particles);
})();
/**
* Player explosion - Particle effect actor class.
*
* @namespace Arena
* @class Arena.PlayerExplosion
*/
(function()
{
/**
* Constructor
*/
Arena.PlayerExplosion = function(p, v)
{
Arena.PlayerExplosion.superclass.constructor.call(this, p, v, 20, function()
{
// randomise start position slightly
var pos = p.clone();
pos.x += randomInt(-5, 5);
pos.y += randomInt(-5, 5);
// randomise radial direction vector - speed and angle, then add parent vector
switch (randomInt(1,2))
{
case 1:
var t = new Vector(0, randomInt(5, 8));
t.rotate(Rnd() * TWOPI);
t.add(v);
return new ArenaParticle(
pos, t, Rnd() * 5 + 5, 1, 25, 15, "white");
case 2:
var t = new Vector(0, randomInt(5, 10));
t.rotate(Rnd() * TWOPI);
t.add(v);
return new ArenaParticle(
pos, t, ~~(Rnd() * 4 + 4), 2, 25, 15);
}
});
return this;
};
extend(Arena.PlayerExplosion, Arena.Particles);
})();
/**
* Text indicator effect actor class.
*
* @namespace Arena
* @class Arena.TextIndicator
*/
(function()
{
Arena.TextIndicator = function(p, v, msg, textSize, colour, fadeLength)
{
this.fadeLength = (fadeLength ? fadeLength : this.DEFAULT_FADE_LENGTH);
Arena.TextIndicator.superclass.constructor.call(this, p, v, this.fadeLength);
this.msg = msg;
if (textSize)
{
this.textSize = textSize;
}
if (colour)
{
this.colour = colour;
}
return this;
};
extend(Arena.TextIndicator, Game.EffectActor,
{
DEFAULT_FADE_LENGTH: 16,
fadeLength: 0,
textSize: 22,
msg: null,
colour: "rgb(255,255,255)",
/**
* Text indicator effect rendering method
*
* @param ctx {object} Canvas rendering context
*/
onRender: function onRender(ctx, world)
{
ctx.save();
if (this.worldToScreen(ctx, world, 128))
{
var alpha = (1.0 / this.fadeLength) * this.lifespan;
ctx.globalAlpha = alpha;
ctx.shadowBlur = 0;
Game.fillText(ctx, this.msg, this.textSize + "pt Courier New", 0, 0, this.colour);
}
ctx.restore();
}
});
})();
/**
* Score indicator effect actor class.
*
* @namespace Arena
* @class Arena.ScoreIndicator
*/
(function()
{
Arena.ScoreIndicator = function(p, v, score, textSize, prefix, colour, fadeLength)
{
var msg = score.toString();
if (prefix)
{
msg = prefix + ' ' + msg;
}
Arena.ScoreIndicator.superclass.constructor.call(this, p, v, msg, textSize, colour, fadeLength);
return this;
};
extend(Arena.ScoreIndicator, Arena.TextIndicator,
{
});
})();
/**
* CanvasMark HTML5 Canvas Rendering Benchmark - March 2013
*
* @email kevtoast at yahoo dot com
* @twitter kevinroast
*
* (C) 2013 Kevin Roast
*
* 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!
*/
window.addEventListener('load', onloadHandler, false);
/**
* Global window onload handler
*/
var g_splashImg = new Image();
function onloadHandler()
{
// once the slash screen is loaded, bootstrap the main benchmark class
g_splashImg.src = 'images/canvasmark2013.jpg';
g_splashImg.onload = function()
{
// init our game with Game.Main derived instance
GameHandler.init();
GameHandler.start(new Benchmark.Main());
};
}
/**
* Benchmark root namespace.
*
* @namespace Benchmark
*/
if (typeof Benchmark == "undefined" || !Benchmark)
{
var Benchmark = {};
}
/**
* Benchmark main class.
*
* @namespace Benchmark
* @class Benchmark.Main
*/
(function()
{
Benchmark.Main = function()
{
Benchmark.Main.superclass.constructor.call(this);
// create the scenes that are directly part of the Benchmark container
var infoScene = new Benchmark.InfoScene(this);
// add the info scene - must be added first
this.scenes.push(infoScene);
// create the Test instances that the benchmark should manage
// each Test instance will add child scenes to the benchmark
var loader = new Game.Preloader();
this.asteroidsTest = new Asteroids.Test(this, loader);
this.arenaTest = new Arena.Test(this, loader);
this.featureTest = new Feature.Test(this, loader);
// add benchmark completed scene
this.scenes.push(new Benchmark.CompletedScene(this));
// the benchmark info scene is displayed first and responsible for allowing the
// benchmark to start once images required by the game engines have been loaded
loader.onLoadCallback(function() {
infoScene.ready();
});
};
extend(Benchmark.Main, Game.Main,
{
asteroidsTest: null,
arenaTest: null,
featureTest: null,
addBenchmarkScene: function addBenchmarkScene(scene)
{
this.scenes.push(scene);
}
});
})();
/**
* Benchmark Benchmark Info Scene scene class.
*
* @namespace Benchmark
* @class Benchmark.InfoScene
*/
(function()
{
Benchmark.InfoScene = function(game)
{
this.game = game;
// allow start via mouse click - also for starting benchmark on touch devices
var me = this;
var fMouseDown = function(e)
{
if (e.button == 0)
{
if (me.imagesLoaded)
{
me.start = true;
return true;
}
}
};
GameHandler.canvas.addEventListener("mousedown", fMouseDown, false);
Benchmark.InfoScene.superclass.constructor.call(this, false, null);
};
extend(Benchmark.InfoScene, Game.Scene,
{
game: null,
start: false,
imagesLoaded: false,
sceneStarted: null,
loadingMessage: false,
/**
* Scene completion polling method
*/
isComplete: function isComplete()
{
return this.start;
},
onInitScene: function onInitScene()
{
this.playable = false;
this.start = false;
this.yoff = 1;
},
onRenderScene: function onRenderScene(ctx)
{
ctx.save();
if (this.imagesLoaded)
{
// splash logo image dimensions
var w = 640, h = 640;
if (this.yoff < h - 1)
{
// liquid fill bg effect
ctx.drawImage(g_splashImg, 0, 0, w, this.yoff, 0, 0, w, this.yoff);
ctx.drawImage(g_splashImg, 0, this.yoff, w, 2, 0, this.yoff, w, h-this.yoff);
this.yoff++;
}
else
{
var toff = (GameHandler.height/2 + 196), tsize = 40;
ctx.drawImage(g_splashImg, 0, toff-tsize+12, w, tsize, 0, toff-tsize+12, w, tsize);
ctx.shadowBlur = 6;
ctx.shadowColor = "#000";
// alpha fade bounce in a single tertiary statement using a single counter
// first 64 values of 128 perform a fade in, for second 64 values, fade out
ctx.globalAlpha = (this.yoff % 128 < 64) ? ((this.yoff % 64) / 64) : (1 - ((this.yoff % 64) / 64));
Game.centerFillText(ctx, "Click or press SPACE to run CanvasMark", "18pt Helvetica", toff, "#fff");
}
this.yoff++;
}
else if (!this.loadingMessage)
{
Game.centerFillText(ctx, "Please wait... Loading Images...", "18pt Helvetica", GameHandler.height/2, "#eee");
this.loadingMessage = true;
}
ctx.restore();
},
/**
* Callback from image preloader when all images are ready
*/
ready: function ready()
{
this.imagesLoaded = true;
if (location.search === "?auto=true")
{
this.start = true;
}
},
onKeyDownHandler: function onKeyDownHandler(keyCode)
{
switch (keyCode)
{
case KEY.SPACE:
{
if (this.imagesLoaded)
{
this.start = true;
}
return true;
break;
}
}
}
});
})();
/**
* Benchmark CompletedScene scene class.
*
* @namespace Benchmark
* @class Benchmark.CompletedScene
*/
(function()
{
Benchmark.CompletedScene = function(game)
{
this.game = game;
// construct the interval to represent the Game Over text effect
var interval = new Game.Interval("CanvasMark Completed!", this.intervalRenderer);
Benchmark.CompletedScene.superclass.constructor.call(this, false, interval);
};
extend(Benchmark.CompletedScene, Game.Scene,
{
game: null,
exit: false,
/**
* Scene init event handler
*/
onInitScene: function onInitScene()
{
this.game.fps = 1;
this.interval.reset();
this.exit = false;
},
/**
* Scene completion polling method
*/
isComplete: function isComplete()
{
return true;
},
intervalRenderer: function intervalRenderer(interval, ctx)
{
ctx.clearRect(0, 0, GameHandler.width, GameHandler.height);
var score = GameHandler.benchmarkScoreCount;
if (interval.framecounter === 0)
{
var browser = BrowserDetect.browser + " " + BrowserDetect.version;
var OS = BrowserDetect.OS;
if (location.search === "?auto=true")
{
alert(score);
}
else
{
// write results to browser
$("#results").html("<p>CanvasMark Score: " + score + " (" + browser + " on " + OS + ")</p>");
// tweet this result link
var tweet = "http://twitter.com/home/?status=" + browser + " (" + OS + ") scored " + score + " in the CanvasMark HTML5 benchmark! Test your browser: http://bit.ly/canvasmark %23javascript %23html5";
$("#tweetlink").attr('href', tweet.replace(/ /g, "%20"));
$("#results-wrapper").fadeIn();
}
}
Game.centerFillText(ctx, interval.label, "18pt Helvetica", GameHandler.height/2 - 32, "white");
Game.centerFillText(ctx, "Benchmark Score: " + score, "14pt Helvetica", GameHandler.height/2, "white");
interval.complete = (this.exit || interval.framecounter++ > 400);
},
onKeyDownHandler: function onKeyDownHandler(keyCode)
{
switch (keyCode)
{
case KEY.SPACE:
{
this.exit = true;
return true;
break;
}
}
}
});
})();
var BrowserDetect = {
init: function () {
this.browser = this.searchString(this.dataBrowser) || "Unknown Browser";
this.version = this.searchVersion(navigator.userAgent)
|| this.searchVersion(navigator.appVersion)
|| "an unknown version";
this.OS = this.searchString(this.dataOS) || "Unknown OS";
},
searchString: function (data) {
for (var i=0;i<data.length;i++) {
var dataString = data[i].string;
var dataProp = data[i].prop;
this.versionSearchString = data[i].versionSearch || data[i].identity;
if (dataString) {
if (dataString.indexOf(data[i].subString) != -1)
return data[i].identity;
}
else if (dataProp)
return data[i].identity;
}
},
searchVersion: function (dataString) {
var index = dataString.indexOf(this.versionSearchString);
if (index == -1) return;
return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
},
dataBrowser: [
{
string: navigator.userAgent,
subString: "Chrome",
identity: "Chrome"
},
{ string: navigator.userAgent,
subString: "OmniWeb",
versionSearch: "OmniWeb/",
identity: "OmniWeb"
},
{
string: navigator.vendor,
subString: "Apple",
identity: "Safari",
versionSearch: "Version"
},
{
prop: window.opera,
identity: "Opera",
versionSearch: "Version"
},
{
string: navigator.vendor,
subString: "iCab",
identity: "iCab"
},
{
string: navigator.vendor,
subString: "KDE",
identity: "Konqueror"
},
{
string: navigator.userAgent,
subString: "Firefox",
identity: "Firefox"
},
{
string: navigator.vendor,
subString: "Camino",
identity: "Camino"
},
{ // for newer Netscapes (6+)
string: navigator.userAgent,
subString: "Netscape",
identity: "Netscape"
},
{
string: navigator.userAgent,
subString: "MSIE",
identity: "IE",
versionSearch: "MSIE"
},
{
string: navigator.userAgent,
subString: "Gecko",
identity: "Mozilla",
versionSearch: "rv"
},
{ // for older Netscapes (4-)
string: navigator.userAgent,
subString: "Mozilla",
identity: "Netscape",
versionSearch: "Mozilla"
}
],
dataOS : [
{
string: navigator.platform,
subString: "Win",
identity: "Windows"
},
{
string: navigator.platform,
subString: "Mac",
identity: "Mac"
},
{
string: navigator.userAgent,
subString: "iPhone",
identity: "iOS"
},
{
string: navigator.userAgent,
subString: "iPod",
identity: "iOS"
},
{
string: navigator.userAgent,
subString: "iPad",
identity: "iOS"
},
{
string: navigator.platform,
subString: "Linux",
identity: "Linux"
}
]
};
BrowserDetect.init();