Holy-Unblocker/public/vibeOS/appfiles/breakout/game.js
TheEmeraldStarr f5d9539d09 Fixed vibeOS
2020-10-15 20:28:24 -07:00

676 lines
22 KiB
JavaScript

//=============================================================================
//
// We need some ECMAScript 5 methods but we need to implement them ourselves
// for older browsers (compatibility: http://kangax.github.com/es5-compat-table/)
//
// Function.bind: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
// Object.create: http://javascript.crockford.com/prototypal.html
// Object.extend: (defacto standard like jquery $.extend or prototype's Object.extend)
//
// Object.construct: our own wrapper around Object.create that ALSO calls
// an initialize constructor method if one exists
//
//=============================================================================
if (!Function.prototype.bind) {
Function.prototype.bind = function(obj) {
var slice = [].slice,
args = slice.call(arguments, 1),
self = this,
nop = function () {},
bound = function () {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
};
nop.prototype = self.prototype;
bound.prototype = new nop();
return bound;
};
}
if (!Object.create) {
Object.create = function(base) {
function F() {};
F.prototype = base;
return new F();
}
}
if (!Object.construct) {
Object.construct = function(base) {
var instance = Object.create(base);
if (instance.initialize)
instance.initialize.apply(instance, [].slice.call(arguments, 1));
return instance;
}
}
if (!Object.extend) {
Object.extend = function(destination, source) {
for (var property in source) {
if (source.hasOwnProperty(property))
destination[property] = source[property];
}
return destination;
};
}
/* NOT READY FOR PRIME TIME
if (!window.requestAnimationFrame) {// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimationFrame = window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, element) {
window.setTimeout(callback, 1000 / 60);
}
}
*/
//=============================================================================
// Minimal DOM Library ($)
//=============================================================================
Element = function() {
var instance = {
_extended: true,
showIf: function(on) { if (on) this.show(); else this.hide(); },
show: function() { this.style.display = ''; },
hide: function() { this.style.display = 'none'; },
update: function(content) { this.innerHTML = content; },
hasClassName: function(name) { return (new RegExp("(^|\s*)" + name + "(\s*|$)")).test(this.className) },
addClassName: function(name) { this.toggleClassName(name, true); },
removeClassName: function(name) { this.toggleClassName(name, false); },
toggleClassName: function(name, on) {
var classes = this.className.split(' ');
var n = classes.indexOf(name);
on = (typeof on == 'undefined') ? (n < 0) : on;
if (on && (n < 0))
classes.push(name);
else if (!on && (n >= 0))
classes.splice(n, 1);
this.className = classes.join(' ');
}
};
var get = function(ele) {
if (typeof ele == 'string')
ele = document.getElementById(ele);
if (!ele._extended)
Object.extend(ele, instance);
return ele;
};
return get;
}();
$ = Element;
//=============================================================================
// State Machine
//=============================================================================
StateMachine = {
//---------------------------------------------------------------------------
create: function(cfg) {
var target = cfg.target || {};
var events = cfg.events;
var n, event, name, can = {};
for(n = 0 ; n < events.length ; n++) {
event = events[n];
name = event.name;
can[name] = (can[name] || []).concat(event.from);
target[name] = this.buildEvent(name, event.from, event.to, target);
}
target.current = 'none';
target.is = function(state) { return this.current == state; };
target.can = function(event) { return can[event].indexOf(this.current) >= 0; };
target.cannot = function(event) { return !this.can(event); };
if (cfg.initial) { // see "initial" qunit tests for examples
var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow single string to represent initial state, or complex object to configure { state: 'first', event: 'init', defer: true|false }
name = initial.event || 'startup';
can[name] = ['none'];
event = this.buildEvent(name, 'none', initial.state, target);
if (initial.defer)
target[name] = event; // allow caller to trigger initial transition event
else
event.call(target);
}
return target;
},
//---------------------------------------------------------------------------
buildEvent: function(name, from, to, target) {
return function() {
if (this.cannot(name))
throw "event " + name + " innapropriate in current state " + this.current;
var beforeEvent = this['onbefore' + name];
if (beforeEvent && (false === beforeEvent.apply(this, arguments)))
return;
if (this.current != to) {
var exitState = this['onleave' + this.current];
if (exitState)
exitState.apply(this, arguments);
this.current = to;
var enterState = this['onenter' + to] || this['on' + to];
if (enterState)
enterState.apply(this, arguments);
}
var afterEvent = this['onafter' + name] || this['on' + name];
if (afterEvent)
afterEvent.apply(this, arguments);
}
}
//---------------------------------------------------------------------------
};
//=============================================================================
// GAME
//=============================================================================
Game = {
compatible: function() {
return Object.create &&
Object.extend &&
Function.bind &&
document.addEventListener && // HTML5 standard, all modern browsers that support canvas should also support add/removeEventListener
Game.ua.hasCanvas
},
start: function(id, game, cfg) {
if (Game.compatible())
return Game.current = Object.construct(Game.Runner, id, game, cfg).game; // return the game instance, not the runner (caller can always get at the runner via game.runner)
},
ua: function() { // should avoid user agent sniffing... but sometimes you just gotta do what you gotta do
var ua = navigator.userAgent.toLowerCase();
var key = ((ua.indexOf("opera") > -1) ? "opera" : null);
key = key || ((ua.indexOf("firefox") > -1) ? "firefox" : null);
key = key || ((ua.indexOf("chrome") > -1) ? "chrome" : null);
key = key || ((ua.indexOf("safari") > -1) ? "safari" : null);
key = key || ((ua.indexOf("msie") > -1) ? "ie" : null);
try {
var re = (key == "ie") ? "msie (\\d)" : key + "\\/(\\d\\.\\d)"
var matches = ua.match(new RegExp(re, "i"));
var version = matches ? parseFloat(matches[1]) : null;
} catch (e) {}
return {
full: ua,
name: key + (version ? " " + version.toString() : ""),
version: version,
isFirefox: (key == "firefox"),
isChrome: (key == "chrome"),
isSafari: (key == "safari"),
isOpera: (key == "opera"),
isIE: (key == "ie"),
hasCanvas: (document.createElement('canvas').getContext),
hasAudio: (typeof(Audio) != 'undefined'),
hasTouch: ('ontouchstart' in window)
}
}(),
addEvent: function(obj, type, fn) { $(obj).addEventListener(type, fn, false); },
removeEvent: function(obj, type, fn) { $(obj).removeEventListener(type, fn, false); },
windowWidth: function() { return window.innerWidth || /* ie */ document.documentElement.offsetWidth; },
windowHeight: function() { return window.innerHeight || /* ie */ document.documentElement.offsetHeight; },
ready: function(fn) {
if (Game.compatible())
Game.addEvent(document, 'DOMContentLoaded', fn);
},
renderToCanvas: function(width, height, render, canvas) { // http://kaioa.com/node/103
canvas = canvas || document.createElement('canvas');
canvas.width = width;
canvas.height = height;
render(canvas.getContext('2d'));
return canvas;
},
loadScript: function(src, cb) {
var head = document.getElementsByTagName('head')[0];
var s = document.createElement('script');
head.appendChild(s);
if (Game.ua.isIE) {
s.onreadystatechange = function(e) {
if (e.currentTarget.readyState == 'loaded')
cb(e.currentTarget);
}
}
else {
s.onload = function(e) { cb(e.currentTarget); }
}
s.type = 'text/javascript';
s.src = src;
},
loadImages: function(sources, callback) { /* load multiple images and callback when ALL have finished loading */
var images = {};
var count = sources ? sources.length : 0;
if (count == 0) {
callback(images);
}
else {
for(var n = 0 ; n < sources.length ; n++) {
var source = sources[n];
var image = document.createElement('img');
images[source] = image;
Game.addEvent(image, 'load', function() { if (--count == 0) callback(images); });
image.src = source;
}
}
},
loadSounds: function(cfg) {
cfg = cfg || {};
if (typeof soundManager == 'undefined') {
var path = cfg.path || 'sound/soundmanager2-nodebug-jsmin.js';
var swf = cfg.swf || 'sound/swf';
window.SM2_DEFER = true;
Game.loadScript(path, function() {
window.soundManager = new SoundManager();
soundManager.useHighPerformance = true;
soundManager.useFastPolling = true;
soundManager.url = swf;
soundManager.defaultOptions.volume = 50; // shhh!
soundManager.onready(function() {
Game.loadSounds(cfg);
});
soundManager.beginDelayedInit();
});
}
else {
var sounds = [];
for(var id in cfg.sounds) {
sounds.push(soundManager.createSound({id: id, url: cfg.sounds[id]}));
}
if (cfg.onload)
cfg.onload(sounds);
}
},
random: function(min, max) {
return (min + (Math.random() * (max - min)));
},
randomChoice: function(choices) {
return choices[Math.round(Game.random(0, choices.length-1))];
},
randomBool: function() {
return Game.randomChoice([true, false]);
},
timestamp: function() {
return new Date().getTime();
},
THREESIXTY: Math.PI * 2,
KEY: {
BACKSPACE: 8,
TAB: 9,
RETURN: 13,
ESC: 27,
SPACE: 32,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
DELETE: 46,
HOME: 36,
END: 35,
PAGEUP: 33,
PAGEDOWN: 34,
INSERT: 45,
ZERO: 48,
ONE: 49,
TWO: 50,
A: 65,
D: 68,
L: 76,
P: 80,
Q: 81,
TILDA: 192
},
//-----------------------------------------------------------------------------
Math: {
bound: function(box) {
if (box.radius) {
box.w = 2 * box.radius;
box.h = 2 * box.radius;
box.left = box.x - box.radius;
box.right = box.x + box.radius;
box.top = box.y - box.radius;
box.bottom = box.y + box.radius;
}
else {
box.left = box.x;
box.right = box.x + box.w;
box.top = box.y;
box.bottom = box.y + box.h;
}
return box;
},
overlap: function(box1, box2, returnOverlap) {
if ((box1.right < box2.left) ||
(box1.left > box2.right) ||
(box1.top > box2.bottom) ||
(box1.bottom < box2.top)) {
return false;
}
else {
if (returnOverlap) {
var left = Math.max(box1.left, box2.left);
var right = Math.min(box1.right, box2.right);
var top = Math.max(box1.top, box2.top);
var bottom = Math.min(box1.bottom, box2.bottom);
return {x: left, y: top, w: right-left, h: bottom-top, left: left, right: right, top: top, bottom: bottom };
}
else {
return true;
}
}
},
normalize: function(vec, m) {
vec.m = this.magnitude(vec.x, vec.y);
if (vec.m == 0) {
vec.x = vec.y = vec.m = 0;
}
else {
vec.m = vec.m / (m || 1);
vec.x = vec.x / vec.m;
vec.y = vec.y / vec.m;
vec.m = vec.m / vec.m;
}
return vec;
},
magnitude: function(x, y) {
return Math.sqrt(x*x + y*y);
},
move: function(x, y, dx, dy, dt) {
var nx = dx * dt;
var ny = dy * dt;
return { x: x + nx, y: y + ny, dx: dx, dy: dy, nx: nx, ny: ny };
},
accelerate: function(x, y, dx, dy, accel, dt) {
var x2 = x + (dt * dx) + (accel * dt * dt * 0.5);
var y2 = y + (dt * dy) + (accel * dt * dt * 0.5);
var dx2 = dx + (accel * dt) * (dx > 0 ? 1 : -1);
var dy2 = dy + (accel * dt) * (dy > 0 ? 1 : -1);
return { nx: (x2-x), ny: (y2-y), x: x2, y: y2, dx: dx2, dy: dy2 };
},
intercept: function(x1, y1, x2, y2, x3, y3, x4, y4, d) {
var denom = ((y4-y3) * (x2-x1)) - ((x4-x3) * (y2-y1));
if (denom != 0) {
var ua = (((x4-x3) * (y1-y3)) - ((y4-y3) * (x1-x3))) / denom;
if ((ua >= 0) && (ua <= 1)) {
var ub = (((x2-x1) * (y1-y3)) - ((y2-y1) * (x1-x3))) / denom;
if ((ub >= 0) && (ub <= 1)) {
var x = x1 + (ua * (x2-x1));
var y = y1 + (ua * (y2-y1));
return { x: x, y: y, d: d};
}
}
}
return null;
},
ballIntercept: function(ball, rect, nx, ny) {
var pt;
if (nx < 0) {
pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
rect.right + ball.radius,
rect.top - ball.radius,
rect.right + ball.radius,
rect.bottom + ball.radius,
"right");
}
else if (nx > 0) {
pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
rect.left - ball.radius,
rect.top - ball.radius,
rect.left - ball.radius,
rect.bottom + ball.radius,
"left");
}
if (!pt) {
if (ny < 0) {
pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
rect.left - ball.radius,
rect.bottom + ball.radius,
rect.right + ball.radius,
rect.bottom + ball.radius,
"bottom");
}
else if (ny > 0) {
pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
rect.left - ball.radius,
rect.top - ball.radius,
rect.right + ball.radius,
rect.top - ball.radius,
"top");
}
}
return pt;
}
},
//-----------------------------------------------------------------------------
Runner: {
initialize: function(id, game, cfg) {
this.cfg = Object.extend(game.Defaults || {}, cfg || {}); // use game defaults (if any) and extend with custom cfg (if any)
this.fps = this.cfg.fps || 60;
this.interval = 1000.0 / this.fps;
this.canvas = $(id);
this.bounds = this.canvas.getBoundingClientRect();
this.width = this.cfg.width || this.canvas.offsetWidth;
this.height = this.cfg.height || this.canvas.offsetHeight;
this.front = this.canvas;
this.front.width = this.width;
this.front.height = this.height;
this.front2d = this.front.getContext('2d');
this.addEvents();
this.resetStats();
this.resize();
this.game = Object.construct(game, this, this.cfg); // finally construct the game object itself
if (this.cfg.state)
StateMachine.create(Object.extend({target: this.game}, this.cfg.state));
this.initCanvas();
},
start: function() { // game instance should call runner.start() when its finished initializing and is ready to start the game loop
this.lastFrame = Game.timestamp();
this.timer = setInterval(this.loop.bind(this), this.interval);
},
stop: function() {
clearInterval(this.timer);
},
loop: function() {
this._start = Game.timestamp(); this.update((this._start - this.lastFrame)/1000.0); // send dt as seconds
this._middle = Game.timestamp(); this.draw();
this._end = Game.timestamp();
this.updateStats(this._middle - this._start, this._end - this._middle);
this.lastFrame = this._start;
},
initCanvas: function() {
if (this.game && this.game.initCanvas)
this.game.initCanvas(this.front2d);
},
update: function(dt) {
this.game.update(dt);
},
draw: function() {
this.game.draw(this.front2d);
this.drawStats(this.front2d);
},
resetStats: function() {
this.stats = {
count: 0,
fps: 0,
update: 0,
draw: 0,
frame: 0 // update + draw
};
},
updateStats: function(update, draw) {
if (this.cfg.stats) {
this.stats.update = Math.max(1, update);
this.stats.draw = Math.max(1, draw);
this.stats.frame = this.stats.update + this.stats.draw;
this.stats.count = this.stats.count == this.fps ? 0 : this.stats.count + 1;
this.stats.fps = Math.min(this.fps, 1000 / this.stats.frame);
}
},
strings: {
frame: "frame: ",
fps: "fps: ",
update: "update: ",
draw: "draw: ",
ms: "ms"
},
drawStats: function(ctx) {
if (this.cfg.stats) {
ctx.fillText(this.strings.frame + Math.round(this.stats.count), this.width - 100, this.height - 60);
ctx.fillText(this.strings.fps + Math.round(this.stats.fps), this.width - 100, this.height - 50);
ctx.fillText(this.strings.update + Math.round(this.stats.update) + this.strings.ms, this.width - 100, this.height - 40);
ctx.fillText(this.strings.draw + Math.round(this.stats.draw) + this.strings.ms, this.width - 100, this.height - 30);
}
},
addEvents: function() {
Game.addEvent(document, 'keydown', this.onkeydown.bind(this));
Game.addEvent(document, 'keyup', this.onkeyup.bind(this));
Game.addEvent(window, 'resize', this.onresize.bind(this));
},
onresize: function() {
this.stop();
if (this.onresizeTimer)
clearTimeout(this.onresizeTimer);
this.onresizeTimer = setTimeout(this.onresizeend.bind(this), 50); // dont fire resize event until 50ms after user has stopped resizing (avoid flickering)
},
onresizeend: function() {
this.resize();
this.start();
},
resize: function() {
if ((this.width != this.canvas.offsetWidth) || (this.height != this.front.offsetHeight)) {
// console.log("CANVAS RESIZED " + this.front.offsetWidth + ", " + this.front.offsetHeight);
this.width = this.front.width = this.front.offsetWidth;
this.height = this.front.height = this.front.offsetHeight;
if (this.game && this.game.onresize)
this.game.onresize(this.width, this.height);
this.initCanvas(); // when canvas is really resized, its state is reset so we need to re-initialize
}
},
onkeydown: function(ev) {
if (this.game.onkeydown)
return this.game.onkeydown(ev.keyCode);
else if (this.cfg.keys)
return this.onkey(ev.keyCode, 'down');
},
onkeyup: function(ev) {
if (this.game.onkeyup)
return this.game.onkeyup(ev.keyCode);
else if (this.cfg.keys)
return this.onkey(ev.keyCode, 'up');
},
onkey: function(keyCode, mode) {
var n, k, i, state = this.game.current; // avoid same key event triggering in 2 different states by remembering current state so that even if an earlier keyhandler changes state, the later keyhandler wont kick in.
for(n = 0 ; n < this.cfg.keys.length ; n++) {
k = this.cfg.keys[n];
k.mode = k.mode || 'up';
if ((k.key == keyCode) || (k.keys && (k.keys.indexOf(keyCode) >= 0))) {
if (!k.state || (k.state == state)) {
if (k.mode == mode) {
k.action.call(this.game);
}
}
}
}
},
storage: function() {
try {
return this.localStorage = this.localStorage || window.localStorage || {};
}
catch(e) { // IE localStorage throws exceptions when using non-standard port (e.g. during development)
return this.localStorage = {};
}
},
alert: function(msg) {
this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update
result = window.alert(msg);
this.start();
return result;
},
confirm: function(msg) {
this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update
result = window.confirm(msg);
this.start();
return result;
}
//-------------------------------------------------------------------------
} // Game.Runner
} // Game