Skip to content

Instantly share code, notes, and snippets.

@andymason
Created January 18, 2013 15:22
Show Gist options
  • Save andymason/4565267 to your computer and use it in GitHub Desktop.
Save andymason/4565267 to your computer and use it in GitHub Desktop.
// IMPORTS ------------------------------------------------------------
// http://ejohn.org/blog/simple-javascript-inheritance/
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*/
// Inspired by base2 and Prototype
(function(){
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function(){};
// Create a new Class that inherits from this class
Class.extend = function(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = arguments.callee;
return Class;
};
})();
// http://ejohn.org/projects/flexible-javascript-events/
function addEvent( obj, type, fn ) {
if ( obj.attachEvent ) {
obj['e'+type+fn] = fn;
obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
obj.attachEvent( 'on'+type, obj[type+fn] );
} else
obj.addEventListener( type, fn, false );
};
function removeEvent( obj, type, fn ) {
if ( obj.detachEvent ) {
obj.detachEvent( 'on'+type, obj[type+fn] );
obj[type+fn] = null;
} else
obj.removeEventListener( type, fn, false );
};
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = (function() {
// we don't really need all the extra frames
return function(callback, element) { window.setTimeout(callback, 1000 / 30); };
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(/* function */ callback, /* DOMElement */ element)
{
window.setTimeout(callback, 1000 / 60);
};
})();
// http://mir.aculo.us/2010/06/04/making-an-ipad-html5-app-making-it-really-fast/
var supportsTouch = 'createTouch' in document;
// FRAMEWORK BEGINS ---------------------------------------------------
// Types //////////////////////////////////////////////////////////////
var YES = true;
var NO = false;
var Size = function(w,h) { return {'width':w, 'height':h}; };
var Point = function(x,y) { return {'x':x, 'y':y}; };
var Rect = function(x,y,w,h) { return {'origin':Point(x,y), 'size':Size(w,h)}; };
// Util ///////////////////////////////////////////////////////////////
function $(id) { return document.getElementById(id); };
function clone(o) { return JSON.parse(JSON.stringify(o)); };
function isEqual(o1,o2) { return JSON.stringify(o1)==JSON.stringify(o2); };
// Sort ///////////////////////////////////////////////////////////////
function compareZIndex(a, b) {
return a.zIndex - b.zIndex;
};
function compareY(a, b) {
return a.body.origin.y - b.body.origin.y;
};
// Geometry ///////////////////////////////////////////////////////////
function rectContainsPoint(r, p)
{
var h = (p.x >= r.origin.x && p.x < r.origin.x + r.size.width);
var v = (p.y >= r.origin.y && p.y < r.origin.y + r.size.height);
return (h && v);
};
function rectIsEmpty(r)
{
return (r.size.width && r.size.height) ? NO : YES;
};
// based on http://stackoverflow.com/questions/2752349/fast-rectangle-to-rectangle-intersection
function rectIntersectsRect(r1, r2)
{
if (!r1 || !r2) return false;
return !(r2.origin.x > r1.origin.x+r1.size.width || // r2 is right of r1
r2.origin.x+r2.size.width < r1.origin.x || // r2 is left of r1
r2.origin.y > r1.origin.y+r1.size.height || // r2 is below r1
r2.origin.y+r2.size.height < r1.origin.y); // r2 is above r1
};
function intersectingRect(r1, r2)
{
var r = Rect(0,0,0,0);
if (rectIntersectsRect(r1,r2))
{
var x1 = r1.origin.x;
var x2 = r2.origin.x;
var xw1 = r1.origin.x + r1.size.width;
var xw2 = r2.origin.x + r2.size.width;
var y1 = r1.origin.y;
var y2 = r2.origin.y;
var yh1 = r1.origin.y + r1.size.height;
var yh2 = r2.origin.y + r2.size.height;
var l = x1 > x2 ? x1 : x2;
var r = xw1 < xw2 ? xw1 : xw2;
var t = y1 > y2 ? y1 : y2;
var b = yh1 < yh2 ? yh1 : yh2;
r = Rect(l,t, r-l, b-t);
};
return r;
};
// Base Classes ///////////////////////////////////////////////////////
var Textures = // Singleton
{
srcCanvas : null,
dstCanvas : null,
srcCtx : null,
dstCtx : null,
waiting : {}, // src:[texture]
cache : {}, // src:img
scaled : {}, // scale:{src:img}
initialize : function()
{
this.srcCanvas = document.createElement('canvas');
this.dstCanvas = document.createElement('canvas');
this.srcCtx = this.srcCanvas.getContext('2d');
this.dstCtx = this.dstCanvas.getContext('2d');
this.scaled[1] = {};
},
preload : function(srcs)
{
for (var i=0; i<srcs.length; i++)
{
new Texture(srcs[i]);
};
},
isReady : function(texture)
{
// not scaled yet
if (!this.scaled[System._scale] || !this.scaled[System._scale][texture.src])
{
if (this.cache[texture.src] && this.cache[texture.src].isReady) // ready to be scaled
{
this.scale(texture.src);
};
return false;
}
else
{
return this.scaled[System._scale][texture.src].isReady;
};
},
img : function(texture)
{
return this.scaled[System._scale][texture.src];
},
load : function(texture)
{
if (this.cache[texture.src]) // already cached
{
if (this.cache[texture.src].isReady) // already ready
{
texture.onload(this.cache[texture.src]);
}
else // not ready yet
{
this.waiting[texture.src].push(texture);
};
return;
};
// prepare the wait list if necessary
if (!this.waiting[texture.src]) this.waiting[texture.src] = [];
var img = new Image();
img.isReady = NO;
img.onload = function()
{
img.isReady = YES;
for (var i=0;i<Textures.waiting[texture.src].length; i++)
{
Textures.waiting[texture.src][i].onload(this);
};
Textures.waiting[texture.src] = [];
Textures.scale(texture.src);
};
this.cache[texture.src] = img;
this.scaled[1][texture.src] = img;
this.waiting[texture.src].push(texture);
img.src = texture.src;
},
scale : function(src)
{
var scale = System._scale;
if (!this.scaled[scale]) this.scaled[scale] = {};
if (this.scaled[scale][src]) return; // already scaled
var img = this.cache[src];
var dstImg = new Image();
dstImg.isReady = NO;
dstImg.onload = function() { this.isReady = YES; };
this.scaled[scale][src] = dstImg; // add as soon as possible to prevent duplicate scaling
// set up source
this.srcCanvas.width = img.width;
this.srcCanvas.height = img.height;
this.srcCtx.clearRect(0,0,img.width,img.height);
this.srcCtx.drawImage(img,0,0);
var srcData = this.srcCtx.getImageData(0,0,img.width,img.height).data;
var sw = scale * img.width;
var sh = scale * img.height;
// set up destination
this.dstCanvas.width = sw;
this.dstCanvas.height = sh;
this.dstCtx.clearRect(0,0,sw,sh);
var dstImgData = this.dstCtx.getImageData(0,0,sw,sh);
var dstData = dstImgData.data;
// scale
var srcP = 0;
var dstP = 0;
for (var y = 0; y < img.height; ++y) {
for (var i = 0; i < scale; ++i) {
for (var x = 0; x < img.width; ++x) {
var srcP = 4 * (y * img.width + x);
for (var j = 0; j < scale; ++j) {
var tmp = srcP;
dstData[dstP++] = srcData[tmp++];
dstData[dstP++] = srcData[tmp++];
dstData[dstP++] = srcData[tmp++];
dstData[dstP++] = srcData[tmp++];
};
};
};
};
this.dstCtx.putImageData(dstImgData,0,0);
dstImg.src = this.dstCanvas.toDataURL();
}
};
Textures.initialize();
var Texture = Class.extend(
{
init : function(src,w,h) // w,h are tileSize not imgSize
{
this.isReady = NO;
this.src = src;
this.tileSize = Size(w,h);
this.imgSize = Size(0,0);
this.sizeInTiles= Size(1,1);
this.totalTiles = 1;
Textures.load(this);
},
onload : function(img)
{
this.imgSize = Size(img.width, img.height);
if (isNaN(this.tileSize.width) || isNaN(this.tileSize.height)) this.tileSize = Size(img.width, img.height);
var wt = img.width / this.tileSize.width;
var ht = img.height / this.tileSize.height;
this.sizeInTiles = Size(wt,ht);
this.totalTiles = wt * ht;
this.isReady = YES;
},
draw : function(x,y)
{
if (!Textures.isReady(this)) return;
var s = System._scale;
System.screen.drawImage(Textures.img(this),Math.round(x)*s,Math.round(y)*s);
},
drawTile : function(tile,x,y)
{
if (!Textures.isReady(this) || tile >= this.totalTiles) return;
var s = System._scale;
var w = this.tileSize.width;
var h = this.tileSize.height;
var ty = Math.floor(tile/this.sizeInTiles.width);
var tx = tile - (ty * this.sizeInTiles.width);
var sx = tx * w;
var sy = ty * h;
System.screen.drawImage(Textures.img(this),
sx*s,sy*s,
w*s,h*s,
Math.round(x)*s,Math.round(y)*s,
w*s,h*s);
}
});
var Surface = Class.extend(
{
init : function(x,y,w,h)
{
this.body = new Rect(x,y,w,h);
this.crop = null;
this.parent = null;
this.children = [];
this.zIndex = 0;
this.isVisible = YES;
},
_crop : function()
{
if (this.crop) return this.crop;
else if (this.parent) return this.parent._crop();
else return Rect(0,0,this.body.size.width,this.body.size.height);
},
_relPos : function()
{
var offset = clone(this.body.origin);
// commented out because Sinkhole was designed without relative positioning
// if (this.parent)
// {
// var pOffset = this.parent._relPos();
// offset.x += pOffset.x;
// offset.y += pOffset.y;
// };
return offset;
},
center : function()
{
var x = this.body.origin.x;
var y = this.body.origin.y;
var hw = this.body.size.width/2;
var hh = this.body.size.height/2;
switch (arguments.length)
{
case 0:
return Point(x+hw,y+hh);
break;
case 1:
this.body.origin = Point(arguments[0].x-hw,arguments[0].y-hh);
break;
default:
this.body.origin = Point(arguments[0]-hw,arguments[1]-hh);
break;
};
},
addChild : function(child)
{
child.parent = this;
this.children.push(child);
this.children.sort(compareZIndex);
},
removeChild : function(child)
{
for (var i in this.children)
{
if (this.children[i] == child)
{
System.input.unregisterTarget(child);
child.parent = null;
this.children.splice(i,1);
break;
};
};
},
removeAllChildren : function()
{
for (var i in this.children)
{
var child = this.children[i];
System.input.unregisterTarget(child);
child.parent = null;
};
this.children = [];
},
update : function(elapsed)
{
for (var i in this.children)
{
this.children[i].update(elapsed);
};
},
render : function()
{
for (var i in this.children)
{
if (this.children[i].isVisible)
{
this.children[i].render();
};
};
}
});
// sorts by body.origin.y instead of zIndex - PERSPECTIVE!
var SurfaceY = Surface.extend(
{
addChild : function(child)
{
child.parent = this;
this.children.push(child);
this.children.sort(compareY);
}
});
// Surfaces ///////////////////////////////////////////////////////////
var State = Surface.extend(
{
init : function()
{
this._super(0,0,System.body.size.width,System.body.size.height);
}
});
var Rectangle = Surface.extend(
{
init : function(x,y,w,h,color)
{
this._super(x,y,w,h);
this.color = color;
},
render : function()
{
var s = System._scale;
var crop = this._crop();
var offset = this._relPos();
System.screen.fillStyle = this.color;
System.screen.fillRect(Math.round(offset.x-crop.origin.x)*s,
Math.round(offset.y-crop.origin.y)*s,
this.body.size.width*s, this.body.size.height*s);
}
});
var Tile = Surface.extend(
{
init : function(x,y,w,h,src)
{
this._super(x,y,w,h);
this.texture = new Texture(src,w,h);
this.currentTile = 0;
},
render : function()
{
var crop = this._crop();
var offset = this._relPos();
this.texture.drawTile(this.currentTile,
offset.x-crop.origin.x,
offset.y-crop.origin.y);
}
});
var Wallpaper = Surface.extend(
{
init : function(x,y,w,h,src)
{
this._super(x,y,w,h);
this.texture = new Texture(src);
},
render : function()
{
var x = 0;
var y = 0;
var crop = this._crop();
var offset = this._relPos();
while (x < this.body.size.width)
{
y = 0;
while (y < this.body.size.height)
{
var nx = x + offset.x - crop.origin.x;
var ny = y + offset.y - crop.origin.y;
if (nx+this.texture.tileSize.width >= this.body.origin.x &&
ny+this.texture.tileSize.height >= this.body.origin.y &&
nx <= -crop.origin.x + crop.size.width &&
ny <= -crop.origin.y + crop.size.height)
{
this.texture.draw(nx,ny);
};
y += this.texture.tileSize.height;
};
x += this.texture.tileSize.width;
};
}
});
var TileMap = Surface.extend(
{
init : function(x,y,w,h,src,tiles) // w/h refer to tileSize, not mapSize
{
//x,y must be 0,0 or behavior is hinky
this._super(x,y,w*tiles[0].length,h*tiles.length);
this.texture = new Texture(src,w,h);
this.tiles = tiles;
this.offset = 0;
},
render : function()
{
var x = 0;
var y = 0;
var crop = this._crop();
var offset = this._relPos();
// will only ever draw 176 tiles max (perfect is 15x10, bleed is 16x11)
while (x < this.tiles[0].length)
{
y = 0;
while (y < this.tiles.length)
{
var tile = this.tiles[y][x];
var nx = offset.x + x*this.texture.tileSize.width;
var ny = offset.y + y*this.texture.tileSize.height;
if (nx+this.texture.tileSize.width >= crop.origin.x &&
ny+this.texture.tileSize.height >= crop.origin.y &&
nx < crop.origin.x + crop.size.width &&
ny < crop.origin.y + crop.size.height
)
{
this.texture.drawTile(tile+this.offset,
nx-crop.origin.x,
ny-crop.origin.y);
};
y++;
};
x++;
};
}
});
var Sprite = Tile.extend(
{
init : function(x,y,w,h,src)
{
this._super(x,y,w,h,src);
this.sequences = {};
this.frameDuration = 0.2;
this.frameOffset = 0;
this.sequenceElapsed = 0;
this.currentSequence = null;
this.lastSequence = null;
this.currentFrame = 0;
this.shouldPlayOnce = NO;
},
update : function(elapsed)
{
if (!this.isVisible) return;
if (this.currentSequence != this.lastSequence)
{
this.currentFrame = 0;
this.sequenceElapsed = 0;
};
this.sequenceElapsed += elapsed;
if (this.sequenceElapsed > this.frameDuration)
{
this.sequenceElapsed = 0;
this.currentFrame++;
if (this.currentFrame >= this.sequences[this.currentSequence].length)
{
if (this.shouldPlayOnce) this.currentFrame--; // undo, stay on last frame
else this.currentFrame = 0;
};
};
this.currentTile = this.sequences[this.currentSequence][this.currentFrame] + this.frameOffset;
this.lastSequence = this.currentSequence;
}
});
var Mask = Surface.extend(
{
init : function()
{
this._super();
this.body = System.body;
this.s = System._scale;
this.buffer = document.createElement('canvas');
this.buffer.width = this.s * this.body.size.width;
this.buffer.height = this.s * this.body.size.height;
},
render : function()
{
if (this.s != System._scale)
{
this.s = System._scale;
this.buffer.width = this.s * this.body.size.width;
this.buffer.height = this.s * this.body.size.height;
};
// return this._super(); // masks are not the reason for slow performance on iPod touch
System.screen = this.buffer.getContext('2d'); // swap to buffer so children can draw to mask
System.screen.clearRect(0,0,this.buffer.width,this.buffer.height);
this._super(); // render children
System.screen = System.canvas.getContext('2d'); // swap back to actual screen to resume normal drawing
// apply mask
System.screen.save();
System.screen.globalCompositeOperation = 'destination-in'; // retina iPad doesn't have enough memory to composite
System.screen.drawImage(this.buffer,0,0);
System.screen.restore();
}
});
// conflates the separate Font and TextMap classes of SI2d proper
// (distinction is unnecessary here) and lacks a few of features
var TextMap = Surface.extend(
{
init : function()
{
this._super(0,0,0,0);
this.texture = null;
this.chars = {};
this.charWidth = 0;
this.charWidths = {};
this.offsetChar = '*';
this.offsetTile = 0;
this.offsetSize = 0;
this.maxWidth = 0;
this.text = '';
this.cache = [];
this.currentLength = -1;
this.delay = 0;
this.delayElapsed = 0;
this.hasFinished = NO;
this.isCentered = NO;
this.alignment = TextMap.alignmentTypes.leftAligned;
},
setText : function(text)
{
if (this.text == text) return;
this.text = text;
var chars = text.split('');
this.cache = [];
var x = 0;
var y = 0;
var t = 0;
var mw = 0;
var spaces = 0;
for (var i=0; i<chars.length; i++)
{
var c = chars[i];
if (c == this.offsetChar)
{
this.offsetTile = (this.offsetTile == this.offsetSize) ? 0 : this.offsetSize;
continue;
};
this.cache[t] = {'tile':this.chars[c], 'offset':this.offsetTile, 'origin':Point(x,y)};
x += this.charWidths[c] ? this.charWidths[c] : this.charWidth;
if (x > mw) mw = x;
t++;
// wrap from most recent space
if (this.maxWidth && x > this.maxWidth)
{
x = 0;
y += this.texture.tileSize.height;
// find the space
var s = this.chars[' '];
for (var j=this.cache.length-1; j>0; j--)
{
if (this.cache[j].tile == s)
{
// not sure why this convoluted addition is necessary
i = j+(spaces?1:0);
t = j+(spaces?1:0);
break;
};
};
spaces++;
};
};
this.body.size = Size(mw, this.texture.tileSize.height);
var w = this.body.size.width;
var pw = this.parent.body.size.width;
if (this.alignment == TextMap.alignmentTypes.centered)
{
this.body.origin.x = pw/2 - w/2;
}
else if (this.alignment == TextMap.alignmentTypes.rightAligned)
{
this.body.origin.x = pw - w;
};
this.currentLength = -1;
this.delayedElapsed = 0;
this.delayDuration = this.delay;
this.hasFinished = NO;
},
advance : function()
{
this.currentLength = this.cache.length;
},
update : function(elapsed)
{
this.delayedElapsed += elapsed;
if (this.delayedElapsed >= this.delayDuration && this.currentLength < this.cache.length)
{
this.currentLength++;
this.delayedElapsed -= this.delayDuration;
};
},
render : function()
{
var offset = this._relPos();
for (var i=0; i<this.cache.length; i++)
{
if (i > this.currentLength && this.delayDuration) break;
this.texture.drawTile(this.cache[i].tile+this.cache[i].offset,
offset.x+this.cache[i].origin.x,
offset.y+this.cache[i].origin.y);
};
if (this.currentLength >= this.cache.length)
{
this.hasFinished = YES;
}
}
});
TextMap.alignmentTypes =
{
leftAligned : 0,
rightAligned : 1,
centered : 2
};
// Input //////////////////////////////////////////////////////////////
var InputEvent = function(origin, type, key) { return {'type':type, 'origin':origin, 'key':key}; }; // types currently: began, moved, ended, keydown, keyup
var InputManager = Class.extend(
{
init : function(target)
{
this.targets = [];
this.hasTouch = NO;
this.lastTouch = Point(0,0);
addEvent(document, 'keydown', function(e){System.input.handleEvent(e)});
addEvent(document, 'keyup', function(e){System.input.handleEvent(e)});
addEvent(window, 'MozGamepadButtonDown', function(e){System.input.handleEvent(e)});
addEvent(window, 'MozGamepadButtonUp', function(e){System.input.handleEvent(e)});
addEvent(window, 'MozGamepadAxisMove', function(e){System.input.handleEvent(e)});
addEvent(target, supportsTouch ? 'touchstart':'mousedown', function(e){System.input.handleEvent(e)});
addEvent(target, supportsTouch ? 'touchmove':'mousemove', function(e){System.input.handleEvent(e)});
addEvent(target, supportsTouch ? 'touchend':'mouseup', function(e){System.input.handleEvent(e)});
if (supportsTouch) addEvent(target, 'touchcancel', function(e){System.input.handleEvent(e)});
},
pointInEvent : function(event)
{
var px = 0;
var py = 0;
if (event.touches && event.touches.length)
{
px = event.touches[0].pageX;
py = event.touches[0].pageY;
}
else if (event.pageX == null && event.clientX != null)
{
var eventDocument = event.target.ownerDocument || document,
doc = eventDocument.documentElement,
body = eventDocument.body;
px = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
py = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
}
else
{
px = event.pageX;
py = event.pageY;
};
var e = System.canvas;
while (e.offsetParent)
{
px -= e.offsetLeft;
py -= e.offsetTop;
e = e.offsetParent;
};
if (System._zoom > 1)
{
px = Math.floor(px/System._zoom);
py = Math.floor(py/System._zoom);
};
if (System._scale > 1)
{
px = Math.floor(px/System._scale);
py = Math.floor(py/System._scale);
};
return Point(px, py);
},
handleEvent : function(event)
{
var origin = this.pointInEvent(event);
var type;
var key = InputManager.KEY.NONE;
switch(event.type)
{
case 'MozGamepadAxisMove':
if (event.axis == 0) // X
{
switch(event.value)
{
case -1: // left
key = InputManager.KEY.LEFT;
type = 'keydown';
break;
case 1: // right
key = InputManager.KEY.RIGHT;
type = 'keydown';
break;
default: // release
key = this.xAxis == 1 ? InputManager.KEY.RIGHT : InputManager.KEY.LEFT;
type = 'keyup';
break;
};
this.xAxis = Math.round(event.value);
}
else // Y
{
switch(event.value)
{
case -1: // up
key = InputManager.KEY.UP;
type = 'keydown';
break;
case 1: // right
key = InputManager.KEY.DOWN;
type = 'keydown';
break;
default: // release
key = this.yAxis == 1 ? InputManager.KEY.DOWN : InputManager.KEY.UP;
type = 'keyup';
break;
};
this.yAxis = Math.round(event.value);
};
break;
case 'MozGamepadButtonDown':
key = InputManager.KEY.ACTION;
type = 'keyup';
break;
case 'MozGamepadButtonDown':
key = InputManager.KEY.ACTION;
type = 'keydown';
break;
case 'keydown':
case 'keyup':
// alert(event.keyCode);
if (event.ctrlKey || event.metaKey) break;
switch(event.keyCode)
{
case 87: // w
case 38: // up
key = InputManager.KEY.UP;
break;
case 68: // d
case 39: // right
key = InputManager.KEY.RIGHT;
break;
case 83: // s
case 40: // down
key = InputManager.KEY.DOWN;
break;
case 65: // a
case 37: // left
key = InputManager.KEY.LEFT;
break;
case 32 : // space
key = InputManager.KEY.ACTION;
break;
}
if (key!=InputManager.KEY.NONE) type = event.type;
break;
case 'mousedown':
case 'touchstart':
type = 'began';
this.lastTouch = origin;
this.hasTouch = YES;
break;
case 'mousemove':
case 'touchmove':
if (this.hasTouch)
{
type = 'moved';
this.lastTouch = origin;
}
else type = NO;
break;
case 'mouseup':
case 'touchend':
case 'touchcancel':
this.hasTouch = NO;
if (event.type == 'mouseup') // touchend and touchcancel don't provide pageX/pageY so leave existing value
{
this.lastTouch = origin;
};
type = 'ended';
break;
default:
this.hasTouch = NO;
type = NO;
};
if (type)
{
event.preventDefault();
// send to targets
for (var i in this.targets)
{
if (this.targets[i].handleEvent)
{
this.targets[i].handleEvent(InputEvent(origin,type,key));
};
};
};
},
registerTarget : function(target)
{
this.targets.push(target);
},
unregisterTarget : function(target)
{
for (var i in this.targets)
{
if (this.targets[i] == target)
{
this.targets.splice(i,1);
break;
};
};
},
unregisterAllTargets : function()
{
this.targets = [];
}
});
InputManager.KEY =
{
NONE : -1,
UP : 0,
RIGHT : 1,
DOWN : 2,
LEFT : 3,
ACTION : 4
};
// Speaker ////////////////////////////////////////////////////////////
var Sound = Class.extend( // don't use Sounds directly
{
init : function(src)
{
this.src = src;
this.buffer = null; // cached
this.source = null; // single-use
this.isReady = NO;
this.isLoop = NO;
this.startTime = 0;
this.loopTime = 0;
this.duration = 0;
this.onload = null;
this.timeoutId = null;
var sound = this;
var request = new XMLHttpRequest();
request.addEventListener('load', function(e)
{
System.speaker.context.decodeAudioData(request.response, function(decoded_data)
{
// cache
sound.buffer = decoded_data;
sound.duration = sound.buffer.duration;
sound.isReady = YES;
if (typeof sound.onload == 'function') sound.onload();
}, function(e)
{
console.log(e);
});
}, false);
request.open('GET', src, true);
request.responseType = 'arraybuffer';
request.send();
},
renewSource : function()
{
if (this.source)
{
this.source.noteOff(0);
this.source = null;
};
this.source = System.speaker.context.createBufferSource();
this.source.buffer = this.buffer;
this.source.connect(System.speaker.context.destination);
},
play : function()
{
this.renewSource();
this.source.noteOn(0);
this.startTime = System.speaker.context.currentTime;
this.loopTime = 0;
if (this.isLoop) this.loop();
},
loop : function()
{
if (this.isLoop)
{
var sound = this;
this.timeoutId = window.setTimeout(function()
{
sound.play();
}, this.duration*1000);
};
},
pause : function()
{
if (!this.isLoop) return;
window.clearTimeout(this.timeoutId);
this.loopTime += System.speaker.context.currentTime - this.startTime;
this.source.noteOff(0);
},
resume : function()
{
if (!this.isLoop) return;
var timeout = this.duration - this.loopTime;
this.renewSource();
this.source.noteGrainOn(0, this.loopTime, timeout);
this.startTime = System.speaker.context.currentTime;
var sound = this;
this.timeoutId = window.setTimeout(function()
{
sound.play();
}, timeout*1000);
}
});
var Speaker = Class.extend(
{
init : function()
{
this.sounds = {}; // by src
this.loopSrc = '';
this.context = NO;
if (typeof AudioContext == "function")
{
this.context = new AudioContext();
}
else if (typeof webkitAudioContext == "function")
{
this.context = new webkitAudioContext();
};
},
preload : function(srcs)
{
if (!this.context) return;
for (var src in srcs)
{
if (!this.sounds[src])
{
var sound = new Sound(src);
this.sounds[src] = sound;
};
};
},
play : function(src, isLoop)
{
var speaker = this;
if (this.sounds[src]) // cached and ready
{
var sound = this.sounds[src];
if (sound.isReady)
{
sound.isLoop = isLoop;
sound.play();
}
else
{
sound.onload = function()
{
speaker.play(src, isLoop);
};
};
}
else // load then play
{
var sound = new Sound(src);
sound.onload = function()
{
speaker.play(src, isLoop);
};
this.sounds[src] = sound;
};
},
loop : function(src)
{
if (!this.context) return;
if (this.loopSrc != src)
{
this.loopSrc = src;
this.play(src, YES);
};
},
pause : function()
{
if (!this.context) return;
if (this.loopSrc && this.sounds[this.loopSrc])
{
this.sounds[this.loopSrc].pause();
};
},
resume : function()
{
if (!this.context) return;
if (this.loopSrc && this.sounds[this.loopSrc])
{
this.sounds[this.loopSrc].resume();
};
},
fx : function(src)
{
if (!this.context) return;
this.play(src, NO);
}
});
// System /////////////////////////////////////////////////////////////
var GameSystem = Surface.extend(
{
init : function(id, w, h)
{
this._super(0,0,w,h);
this.RAM = {}; // global storage
document.getElementById(id).innerHTML = '<canvas id="'+id+'-canvas" width="'+w+'" height="'+h+'"></canvas>';
this.canvas = document.getElementById(id+'-canvas');
this.zoom(1);
this.scale(1);
this.screen = this.canvas.getContext('2d');
this.debug = document.getElementById('debug');
this.speaker = new Speaker;
this.input = new InputManager(this.canvas);
this.fps = 0;
this._fps = 0;
this.second = 0;
this.isPaused = NO;
this.lastTime = (new Date()).getTime();
this.loop();
addEvent(window, 'blur', function(){System.pause();});
addEvent(window, 'focus', function(){System.unpause();});
if (window.devicePixelRatio && window.devicePixelRatio >= 2)
{
// trigger hi-res retina graphics by doubling window size
// System.autozoom() then does the right thing
document.getElementById('viewport').content = 'initial-scale=0.5;user-scalable=no';
};
},
pause : function()
{
this.isPaused = YES;
this.speaker.pause();
},
unpause : function()
{
if (this.isPaused)
{
this.isPaused = NO;
this.speaker.resume();
this.lastTime = (new Date()).getTime();
};
},
zoom : function(z) // zoom with CSS, blurry in Safari
{
if (this._zoom == z) return;
this._zoom = z;
var p = 1/z*100;
var w = z * this.body.size.width;
var h = z * this.body.size.height;
var css = '';
var prefixes = ['','-webkit-','-moz-','-o-','-ms-'];
for (var i=0; i<prefixes.length; i++)
{
var prefix = prefixes[i];
if (prefix == '-webkit-')
{
css += 'zoom:'+z+';';
}
else
{
css += prefix+'transform: scale('+z+','+z+');'; // translateZ(0) not necessary, canvas is already hardware accelerated
css += prefix+'transform-origin: 0 0;'
};
};
this.canvas.style.cssText = css;
this.canvas.parentNode.style.cssText = 'width:'+w*this._scale+'px;height:'+h*this._scale+'px;';
},
autozoom : function()
{
this._autozoom();
addEvent(window, 'resize', function(){System._autozoom();});
},
_autozoom : function()
{
var wr = Math.floor(window.innerWidth / this.body.size.width);
var hr = Math.floor(window.innerHeight / this.body.size.height);
if (wr < 1) wr = 1;
if (hr < 1) hr = 1;
var scale = wr<hr?wr:hr;
this.zoom(scale);
},
scale : function(s) // scale on canvas, crisp in Safari, significant fps hit in Firefox/heat on retina device
{
if (this._scale == s) return;
// compositing operations break down above a scale of 5 on retina iPad
if (navigator.userAgent.match(/mobile/i) && navigator.userAgent.match(/webkit/i) && s > 5)
{
// TODO: hard code to 8?
// iPad can accommodate x8 so scale to x4 then zoom x2
s = 4;
this._scale = s;
this.zoom(2);
};
this._scale = s;
var w = s * this.body.size.width;
var h = s * this.body.size.height;
this.canvas.width = w;
this.canvas.height = h;
this.canvas.parentNode.style.cssText = 'width:'+w*this._zoom+'px;height:'+h*this._zoom+'px;';
},
autoscale : function()
{
this._autoscale();
addEvent(window, 'resize', function(){System._autoscale();});
},
_autoscale : function()
{
var wr = Math.floor(window.innerWidth / this.body.size.width);
var hr = Math.floor(window.innerHeight / this.body.size.height);
if (wr < 1) wr = 1;
if (hr < 1) hr = 1;
var scale = wr<hr?wr:hr;
this.scale(scale);
},
log : function(html)
{
this.debug.innerHTML = html + '\n' + this.debug.innerHTML;
},
loop : function()
{
if (this.isPaused) return requestAnimFrame(function(){System.loop();}, this.canvas);
var now = (new Date()).getTime();
var elapsed = (now - this.lastTime) / 1000;
this.lastTime = now;
this._fps++;
var second = Math.floor(now/1000);
if (second != this.second)
{
this.second = second;
this.fps = this._fps;
this._fps = 0;
};
// this.debug.innerHTML = this.fps+' fps';
// this.debug.innerHTML = this._scale;
this.update(elapsed);
requestAnimFrame(function(){System.loop();}, this.canvas); // schedule next before render
this.screen.clearRect(0,0,this.body.size.width*this._zoom*this._scale,this.body.size.height*this._zoom*this._scale);
this.render();
},
screenshot : function()
{
return this.canvas.toDataURL();
},
state : function()
{
return this.children[0];
},
loadState : function(newState)
{
this.removeAllChildren();
this.addChild(newState);
}
});
// GAME BEGINS --------------------------------------------------------
// Utils //////////////////////////////////////////////////////////////
function zeroPad(num, len) {
var str = num+'';
while (str.length < len)
{
str = '0' + str;
};
return str;
};
function removeIndex(arr, index) {
var retArr = [];
for (var i in arr)
{
if (i == index) continue;
retArr.push(arr[i]);
};
return retArr;
};
// http://dev.kanngard.net/Permalinks/ID_20030114184548.html
function contains(a, e) {
for(j=0;j<a.length;j++)if(a[j]==e)return true;
return false;
}
// quick JavaScript port of http://roguebasin.roguelikedevelopment.org/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels
// this dupes *a lot* of the Mapper function
var WaterMapper = function(tw,th)
{
var TILE_WATER = 1;
var TILE_FLOOR = 0;
var generationParams = function(_r1_cutoff, _r2_cutoff, _reps)
{
return {
'r1_cutoff' : _r1_cutoff,
'r2_cutoff' : _r2_cutoff,
'reps' : _reps
};
};
var grid = [];
var grid2 = [];
var open = [];
var fillprob = 35;
var r1_cutoff = 5;
var r2_cutoff = 5;
var size_x = tw;
var size_y = th;
var params = {};
var params_set = [];
var generations;
var randpick = function()
{
return Math.random()*100 < fillprob ? TILE_FLOOR : TILE_WATER;
};
var initmap = function()
{
var xi;
var yi;
for (yi=0; yi<size_y; yi++)
{
grid [yi] = [];
grid2[yi] = [];
};
for (yi=0; yi<size_y; yi++)
{
for (xi=0; xi<size_x; xi++)
{
grid [yi][xi] = randpick();
};
};
};
var generation = function()
{
var xi, yi, ii, jj;
for(yi=1; yi<size_y-1; yi++)
{
for(xi=1; xi<size_x-1; xi++)
{
var adjcount_r1 = 0,
adjcount_r2 = 0;
for(ii=-1; ii<=1; ii++)
{
for(jj=-1; jj<=1; jj++)
{
if(grid[yi+ii][xi+jj] != TILE_WATER)
adjcount_r1++;
};
};
for(ii=yi-2; ii<=yi+2; ii++)
{
for(jj=xi-2; jj<=xi+2; jj++)
{
if(Math.abs(ii-yi)==2 && Math.abs(jj-xi)==2) continue;
if(ii<0 || jj<0 || ii>=size_y || jj>=size_x) continue;
if(grid[ii][jj] != TILE_WATER)
{
adjcount_r2++;
};
};
};
if(adjcount_r1 >= params.r1_cutoff || adjcount_r2 <= params.r2_cutoff)
{
grid2[yi][xi] = TILE_FLOOR;
}
else
{
grid2[yi][xi] = TILE_WATER;
};
};
};
for(yi=1; yi<size_y-1; yi++)
{
for(xi=1; xi<size_x-1; xi++)
{
grid[yi][xi] = grid2[yi][xi];
};
};
};
// generate
initmap();
params_set.push(generationParams(r1_cutoff,r2_cutoff,4));
params_set.push(generationParams(r1_cutoff,0,3));
generations = params_set.length;
var ii, jj;
for(ii=0; ii<generations; ii++)
{
params = params_set[ii];
for(jj=0; jj<params.reps; jj++)
{
generation();
};
};
return grid; // y arrays of xs
};
// this is so dirty, I don't _really_ understand how it works
var Mapper = function(tw,th)
{
var TILE_FLOOR = 0;
var TILE_WALL = 1;
var generationParams = function(_r1_cutoff, _r2_cutoff, _reps)
{
return {
'r1_cutoff' : _r1_cutoff,
'r2_cutoff' : _r2_cutoff,
'reps' : _reps
};
};
var grid = [];
var grid2 = [];
var open = [];
var fillprob = 40;
var r1_cutoff = 5;
var r2_cutoff = 2;
var size_x = tw;
var size_y = th;
var params = {};
var params_set = [];
var generations;
var randpick = function()
{
return Math.random()*100 < fillprob ? TILE_WALL : TILE_FLOOR;
};
var initmap = function()
{
var xi;
var yi;
for (yi=0; yi<size_y; yi++)
{
grid [yi] = [];
grid2[yi] = [];
};
for (yi=1; yi<size_y-1; yi++)
{
for (xi=1; xi<size_x-1; xi++)
{
grid [yi][xi] = randpick();
};
};
for (yi=0; yi<size_y; yi++)
{
for (xi=0; xi<size_x; xi++)
{
grid2[yi][xi] = TILE_WALL;
};
};
for (yi=0; yi<size_y; yi++)
{
grid [yi][0] = grid [yi][size_x-1] = TILE_WALL;
};
for (xi=0; xi<size_x; xi++)
{
grid [0][xi] = grid [size_y-1][xi] = TILE_WALL;
};
};
var generation = function()
{
var xi, yi, ii, jj;
for(yi=1; yi<size_y-1; yi++)
{
for(xi=1; xi<size_x-1; xi++)
{
var adjcount_r1 = 0,
adjcount_r2 = 0;
for(ii=-1; ii<=1; ii++)
{
for(jj=-1; jj<=1; jj++)
{
if(grid[yi+ii][xi+jj] != TILE_FLOOR)
adjcount_r1++;
};
};
for(ii=yi-2; ii<=yi+2; ii++)
{
for(jj=xi-2; jj<=xi+2; jj++)
{
if(Math.abs(ii-yi)==2 && Math.abs(jj-xi)==2) continue;
if(ii<0 || jj<0 || ii>=size_y || jj>=size_x) continue;
if(grid[ii][jj] != TILE_FLOOR)
{
adjcount_r2++;
};
};
};
if(adjcount_r1 >= params.r1_cutoff || adjcount_r2 <= params.r2_cutoff)
{
grid2[yi][xi] = TILE_WALL;
}
else
{
grid2[yi][xi] = TILE_FLOOR;
};
};
};
for(yi=1; yi<size_y-1; yi++)
{
for(xi=1; xi<size_x-1; xi++)
{
grid[yi][xi] = grid2[yi][xi];
};
};
};
// generate
initmap();
params_set.push(generationParams(r1_cutoff,r2_cutoff,4));
params_set.push(generationParams(r1_cutoff,0,3));
generations = params_set.length;
var ii, jj;
for(ii=0; ii<generations; ii++)
{
params = params_set[ii];
for(jj=0; jj<params.reps; jj++)
{
generation();
};
};
// identify open cell coordinates
for (var y=0; y<size_y; y++)
{
for (var x=0; x<size_x; x++)
{
if (grid[y][x] == TILE_FLOOR) open.push([x,y]);
};
};
// random start and end points
var start = -1;
var end = -1;
var isSolvable = NO;
while (!isSolvable)
{
start = Math.floor(Math.random()*open.length);
end = Math.floor(Math.random()*open.length);
key = Math.floor(Math.random()*open.length);
var epath = AStar(grid,open[start],open[end]);
var kpath = AStar(grid,open[key],open[end]);
if (kpath.length > 64 && epath.length > 64) isSolvable = YES;
};
var water = WaterMapper(tw,th);
// eliminate water behind walls
for (var y=0; y<size_y; y++)
{
for (var x=0; x<size_x; x++)
{
if (grid[y][x] == TILE_WALL) water[y][x] = TILE_FLOOR;
};
};
// then open up around the starting point
var sx = open[start][0];
var sy = open[start][1];
water[sy-1][sx] = TILE_FLOOR;
water[sy][sx-1] = TILE_FLOOR;
water[sy][sx] = TILE_FLOOR;
water[sy][sx+1] = TILE_FLOOR;
water[sy+1][sx] = TILE_FLOOR;
return {
'grid' : grid, // y arrays of xs
'water' : water, // y arrays of xs
'open' : open, // array of x,y coordinates
'start' : start, // index in open array
'end' : end, // index in open array
'key' : key // index in open array
};
};
// Surfaces ///////////////////////////////////////////////////////////
var HUDNumber = Surface.extend(
{
init : function(x,y)
{
this._super(x,y,13,7);
this.number = 0;
this.texture = new Texture('images/hud-nums.png', 7, 7);
},
render : function()
{
if (this.number > 99) this.number = 99;
var tens = Math.floor(this.number/10);
var ones = this.number - (tens*10);
this.texture.drawTile(tens?tens:ones, this.body.origin.x, this.body.origin.y);
if (tens)this.texture.drawTile(ones, this.body.origin.x+6, this.body.origin.y);
}
});
var Minimap = Surface.extend(
{
init : function(x,y,w,h,tiles)
{
this._super(x,y,w*tiles[0].length,h*tiles.length);
this.tiles = tiles; // 0 ground, 1 wall
this.reveal = []; // 0 empty, 1 opaque
for (var y=0; y<this.tiles.length; y++)
{
this.reveal[y] = [];
for (var x=0; x<this.tiles[y].length; x++)
{
this.reveal[y][x] = 0;
};
};
this.isDisabled = NO;
},
render : function()
{
if (this.isDisabled) return;
var x = 0;
var y = 0;
var s = System._scale;
while (x < this.tiles[0].length)
{
y = 0;
while (y < this.tiles.length)
{
if (this.reveal[y][x] == 1)
{
System.screen.fillStyle = this.tiles[y][x]?'#00746c':'#00e8d8'; // dark/light teal
System.screen.fillRect((this.body.origin.x+x)*s,(this.body.origin.y+y)*s,s,s);
};
y++;
};
x++;
};
}
});
var Throbber = Sprite.extend(
{
init : function(x,y,w,h,src)
{
this._super(x,y,w,h,src);
this.sequences =
{
'throb' : [0,1,2,1]
};
this.currentSequence = 'throb';
}
});
var Meter = Surface.extend(
{
init : function(x,y)
{
this._super(x,y,16,3);
this.value = 1.0;
},
updateValue : function(value)
{
this.value = value;
if (this.value<0) this.value = 0;
if (this.value>1) this.value = 1;
},
render : function()
{
var crop = this._crop();
var x = Math.round(this.body.origin.x-crop.origin.x);
var y = Math.round(this.body.origin.y-crop.origin.y);
var s = System._scale;
// outline
System.screen.fillStyle = '#000'; // black
System.screen.fillRect(x*s,y*s,this.body.size.width*s,this.body.size.height*s);
// inset
System.screen.fillStyle = '#004058'; // dark blue
System.screen.fillRect((x+1)*s,(y+1)*s,14*s,s);
// bar
var w = Math.round(this.value*14);
System.screen.fillStyle = '#fce0a8'; // yellow
System.screen.fillRect((x+1+(14-w))*s,(y+1)*s,w*s,s);
}
});
// Entities ///////////////////////////////////////////////////////////
var Player = Sprite.extend(
{
init : function(x,y)
{
this._super(x,y,16,16,'images/tomo.png');
this.vx = 0;
this.vy = 0;
// used for beam direction, field of vision rectangle, and animation
this.facing = 2; // 0=up,1=right,2=down,3=left
this.lightsource = 0; // 0=none,1=flashlight,2=matches
this.sequences =
{
'stand' : [10,10,10,10,10,10,10,10,11,11,12,12,12,12,13,13,13,13,12,12,12,12],
'face-up' : [0],
'face-right' : [2],
'face-down' : [4],
'face-left' : [6],
'walk-up' : [0,1],
'walk-right' : [2,3],
'walk-down' : [4,5],
'walk-left' : [6,7],
'climb' : [8,9],
'dead' : [10]
};
this.currentSequence = 'face-down';
this.isClimbing = NO;
this.isDead = NO;
this.isInWater = NO;
this.isStanding = NO;
},
stand : function()
{
this.currentSequence = 'stand';
this.isStanding = YES;
this.shouldPlayOnce = YES;
},
move : function()
{
this.isStanding = NO;
this.shouldPlayOnce = NO;
},
climb : function()
{
if (this.isClimbing) return;
this.isClimbing = YES;
this.vy = -16;
System.state().queueDialog(localize('Up I go...'));
},
die : function()
{
if (this.isClimbing) return; // invincible bitches!
this.isDead = YES;
this.vx = 0;
this.vy = 0;
System.state().queueDialog(localize('Ugh!'));
},
revive : function()
{
this.isDead = NO;
},
view : function() // only used by Stalker currently
{
var w = this.body.size.width;
var h = this.body.size.height;
var x = this.body.origin.x;
var y = this.body.origin.y;
var view;
switch(this.lightsource)
{
case 2: // flashlight, 3 wide, 3 deep
switch(this.facing)
{
case 0: // up
x -= w;
y -= h*3;
break;
case 1: // right
x += w;
y -= h;
break;
case 2: // down
x -= w;
y += h;
break;
case 3: // left
x -= w*3;
y -= h;
break;
};
w *= 3;
h *= 3;
view = Rect(x,y,w,h);
break;
case 1: // matches, 2 wide, 1 deep
if (this.facing == 0 || this.facing == 2) // wide
{
x -= w/2;
w *= 2;
if (this.facing == 0) // up
{
y -= h;
}
else
{
y += h;
};
}
else // tall
{
y -= h/2;
h *= 2;
if (this.facing == 1) // right
{
x += w;
}
else
{
x -= w;
};
};
view = Rect(x,y,w,h);
break;
default: // no source of light, no protection
view = Rect(0,0,0,0);
break;
};
return view;
},
update : function(elapsed)
{
if (this.isStanding)
{
if (this.currentFrame == this.sequences['stand'].length-1)
{
this.move(); // triggers appropriate lightsource
};
}
else
{
var seq = (this.vx || this.vy) ? 'walk-' : 'face-';
switch(this.facing)
{
case 0:
seq += 'up';
break;
case 1:
seq += 'right';
break;
case 2:
seq += 'down';
break;
case 3:
seq += 'left';
break;
};
this.frameOffset = this.isInWater ? 16 : 0;
if (this.isClimbing) seq = 'climb';
if (this.isDead) seq = 'dead';
this.currentSequence = seq;
};
this._super(elapsed);
}
});
var Slasher = Sprite.extend(
{
init : function()
{
this._super(0,0,16,24,'images/slash.png');
this.sequences =
{
'slash' : [0,1,1,2,2,2,3,]
};
this.currentSequence = 'slash';
this.shouldPlayOnce = YES;
this.frameDuration = 0.1;
System.speaker.fx('audio/slash.ogg');
}
});
var Stalker = Sprite.extend(
{
init : function()
{
this._super(0,0,18,15,'images/alert.png');
this.sequenceNames =
[
'growl',
'snap',
'hiss',
'screech'
];
this.sequences =
{
'growl' : [1,0],
'snap' : [2,0],
'hiss' : [3,0],
'screech' : [4,0]
};
this.frameDuration = 1.0;
this.currentSequence = this.sequenceNames[Math.floor(Math.random()*this.sequenceNames.length)];
this.shouldPlayOnce = YES;
this.dormantPeriod = 20 + Math.random()*4;
this.dormantElapsed = 0;
this.vx = 0;
this.vy = 0;
this.stalkSpeed = 24;
this.stalkPeriod = 2;
this.stalkElapsed = 0;
this.isVisible = NO;
this.stalkee = null;
},
update : function(elapsed)
{
this.dormantElapsed += elapsed;
this.stalkElapsed += elapsed;
if (this.isVisible)
{
var s = this.stalkee.center();
var c = this.center();
var shouldStalk = YES;
switch (this.stalkee.facing)
{
case 0: // up
if (c.y < s.y) shouldStalk = NO;
break;
case 1: // right
if (c.x > s.x) shouldStalk = NO;
break;
case 2: // down
if (c.y > s.y) shouldStalk = NO;
break;
case 3: // left
if (c.x < s.x) shouldStalk = NO;
break;
};
if (!this.stalkee.lightsource) shouldStalk = YES; // lights out! go! go! go!
if (shouldStalk)
{
var d = Point(s.x-c.x, s.y-c.y);
var ad = Point(Math.abs(d.x), Math.abs(d.y));
var adjustedStalkSpeed = this.stalkSpeed;
switch(this.stalkee.lightsource)
{
case 1: // flashlight
adjustedStalkSpeed *= 1.0;
break;
case 2: // matches
adjustedStalkSpeed *= 0.7;
break;
case 0:
adjustedStalkSpeed *= 0.4;
break;
};
if (ad.x > ad.y)
{
this.vx = d.x > 0 ? adjustedStalkSpeed : -adjustedStalkSpeed;
this.vy = 0;
}
else
{
this.vx = 0;
this.vy = d.y > 0 ? adjustedStalkSpeed : -adjustedStalkSpeed;
};
}
else
{
// deer in headlights
this.vx = 0;
this.vy = 0;
};
if (rectIntersectsRect(this.stalkee.view(), this.body))
{
this.flee();
};
}
else if (this.dormantElapsed >= this.dormantPeriod)
{
this.stalk();
};
var p = this.body.origin
this.body.origin = Point(p.x+this.vx*elapsed, p.y+this.vy*elapsed);
if (this.isVisible && rectContainsPoint(this.body, this.stalkee.center()))
{
this.flee();
var slasher = new Slasher();
slasher.center(this.stalkee.center());
this.parent.addChild(slasher);
System.state().kill();
};
this._super(elapsed);
},
stalk : function()
{
// randomly position at edge of crop
// but outside of the player's view
var crop = this._crop();
var isInPlayerView = YES;
var view = this.stalkee.view();
var p;
while (isInPlayerView)
{
var rx = Math.random()*crop.size.width + crop.origin.x;
var ry = Math.random()*crop.size.height + crop.origin.y;
this.center(Point(rx, ry));
if (!rectIntersectsRect(view, this.body))
{
isInPlayerView = NO;
}
};
this.isVisible = YES;
this.stalkElapsed = 0;
this.currentSequence = this.sequenceNames[Math.floor(Math.random()*this.sequenceNames.length)];
this.currentFrame = 0;
this.sequenceElapsed = 0;
System.state().queueDialog(localize('What was that!'));
System.speaker.fx('audio/stalker.ogg');
},
flee : function()
{
this.isVisible = NO;
this.dormantElapsed = 0;
System.speaker.fx('audio/flee.ogg');
System.state().queueDialog(localize('I must be hearing things...'));
if (this.parent.children.length >= 8) return;
var junior = new Stalker();
junior.dormantPeriod = this.dormantPeriod*0.75 + Math.random()*4;
junior.stalkSpeed = this.stalkSpeed + Math.random()*this.stalkSpeed*0.25;
if (junior.stalkSpeed > 48) junior.stalkSpeed = 48; // throttle to player speed
junior.stalkee = this.stalkee;
this.parent.addChild(junior);
}
})
var Vine = Surface.extend(
{
init : function(x,y)
{
this._super(x,y,16,16);
// this.addChild(new Rectangle(x,y,16,16,'rgb(200,0,0)'));
this.addChild(new Tile(x+2,y-40,12,56,'images/vine.png'));
}
});
var Compass = Throbber.extend(
{
init : function(x,y)
{
this._super(x+2,y+2,12,11,'images/compass.png');
}
});
var Battery = Throbber.extend(
{
init : function(x,y)
{
this._super(x+4,y+2,8,12,'images/battery.png');
}
});
var Matchbook = Throbber.extend(
{
init : function(x,y)
{
this._super(x+1,y+3,13,10,'images/matchbook.png');
}
});
var Talisman = Throbber.extend(
{
init : function(x,y)
{
this._super(x+3,y+2,10,12,'images/talisman.png');
}
});
var Campfire = Sprite.extend(
{
init : function(x,y)
{
this._super(x,y,16,16,'images/campfire.png');
this.sequences =
{
'idle' : [0],
'burn' : [3,4]
};
this.currentSequence = 'burn';
this.frameDuration = 0.1;
}
});
// Font ///////////////////////////////////////////////////////////////
var SentrySans = TextMap.extend(
{
init : function()
{
this._super();
this.texture = new Texture('images/sentry-sans+katakana.png',8,10);
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?\'"・アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモラリルレロガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポヤユヨワヲンァィゥェォャュョッー。 ').split('');
this.chars = {};
for (var i=0; i<chars.length; i++)
{
this.chars[chars[i]] = i;
};
this.charWidth = 5; // default
this.charWidthMap =
[
'', // 0
'', // 1
'il.,!'+"'", // 2
' ', // 3
'Ifjtx1"・トェョ。', // 4
'', // 5 (default)
'MNTVWXYZmvwアイウエオカキケサシスセソタチツテナニネノハフヘホマミムメモラルレロドピヤユワヲン', // 6
'グゴビパプペポ', // 7
'ガギゲザジズゼゾダヂヅデバブベボ', // 8
];
this.charWidths = {};
for (var i=0; i<this.charWidthMap.length; i++)
{
if (this.charWidthMap[i] == '') continue;
var chars = this.charWidthMap[i].split('');
for (var j=0; j<chars.length; j++)
{
this.charWidths[chars[j]] = i;
};
};
this.offsetSize = 160;
this.delay = 0.06;
}
});
// States /////////////////////////////////////////////////////////////
var PlayState = State.extend(
{
disableMinimap : function(isDisabled)
{
this.minimap.isDisabled = isDisabled;
},
init : function()
{
this._super();
this.hasFoundExit = NO;
this.isEnding = NO;
this.isKilling = NO;
this.isPaused = NO;
this.isGameOver = NO;
this.joystick = Point(0,0);
// static properties
this.batteryDuration = 60; // not the freshes batteries
this.batteryDelay = 2; // changing batteries takes a while
this.matchDuration = 3; // burns down fast
this.matchDelay = 1; // time to strike
this.lightsourceBattery = 2;
this.lightsourceMatch = 1;
this.lightsourceEyes = 0;
this.lightsourceNone = -1;
// inventory
if (!System.RAM['inventory'])
{
System.RAM.inventory =
{
talismanCount : 3,
batteryCount : 3, // drops to 2 instantly
matchCount : 4, // per matchbook
elapsedLight : 0,
lightDuration : 0,
lightsource : this.lightsourceNone
};
};
this.talismanCount = System.RAM.inventory.talismanCount;
this.batteryCount = System.RAM.inventory.batteryCount;
this.matchCount = System.RAM.inventory.matchCount;
this.elapsedLight = System.RAM.inventory.elapsedLight;
this.lightDuration = System.RAM.inventory.lightDuration;
this.lightsource = System.RAM.inventory.lightsource;
if (!System.RAM['level'])
{
System.RAM.level = 1;
};
this.room = new Surface();
this.room.zIndex = 0;
this.room.crop = System.body;
this.addChild(this.room);
this.hud = new Surface();
this.hud.zIndex = 1;
this.hud.addChild(new Tile(0,0,240,160,'images/hud.png'));
this.addChild(this.hud);
this.data = Mapper(60,40);
var tiles = clone(this.data.grid);
// autotile walls
for (var y=0; y<tiles.length; y++)
{
for (var x=0; x<tiles[y].length; x++)
{
if (!tiles[y][x]) continue; // ignore floor for now
var hasAbove = YES;
var hasRight = YES;
var hasBelow = YES;
var hasLeft = YES;
if (y>0 && !tiles[y-1][x]) hasAbove = NO;
if (x<tiles[y].length-1 && !tiles[y][x+1]) hasRight = NO;
if (y<tiles.length-1 && !tiles[y+1][x]) hasBelow = NO;
if (x>0 && !tiles[y][x-1]) hasLeft = NO;
if (!hasAbove && hasRight && hasBelow && hasLeft)
{
tiles[y][x] = 2; // edge up
}
else if (hasAbove && !hasRight && hasBelow && hasLeft)
{
tiles[y][x] = 3; // edge right
}
else if (hasAbove && hasRight && !hasBelow && hasLeft)
{
tiles[y][x] = 4; // edge bottom
}
else if (hasAbove && hasRight && hasBelow && !hasLeft)
{
tiles[y][x] = 5; // edge left
}
else if (hasAbove && !hasRight && hasBelow && !hasLeft)
{
tiles[y][x] = 6; // vertical
}
else if (!hasAbove && hasRight && !hasBelow && hasLeft)
{
tiles[y][x] = 7; // horizontal
}
else if (!hasAbove && !hasRight && hasBelow && !hasLeft)
{
tiles[y][x] = 8; // cap up
}
else if (!hasAbove && !hasRight && !hasBelow && hasLeft)
{
tiles[y][x] = 9; // cap right
}
else if (hasAbove && !hasRight && !hasBelow && !hasLeft)
{
tiles[y][x] = 10; // cap down
}
else if (!hasAbove && hasRight && !hasBelow && !hasLeft)
{
tiles[y][x] = 11; // cap left
}
else if (!hasAbove && hasRight && hasBelow && !hasLeft)
{
tiles[y][x] = 12; // corner top left
}
else if (!hasAbove && !hasRight && hasBelow && hasLeft)
{
tiles[y][x] = 13; // corner top right
}
else if (hasAbove && !hasRight && !hasBelow && hasLeft)
{
tiles[y][x] = 14; // corner bottom right
}
else if (hasAbove && hasRight && !hasBelow && !hasLeft)
{
tiles[y][x] = 15; // corner bottom left
}
else if (!hasAbove && !hasRight && !hasBelow && !hasLeft)
{
tiles[y][x] = 16; // standalone
}
};
};
this.map = new TileMap(0,0,16,16,'images/tiles.png',tiles);
this.room.addChild(this.map);
var water = clone(this.data.water);
// autotile water
for (var y=0; y<water.length; y++)
{
for (var x=0; x<water[y].length; x++)
{
if (!water[y][x]) continue; // ignore empty tiles
// has refers to ground not water (so the water's edge)
var hasAbove = NO;
var hasRight = NO;
var hasBelow = NO;
var hasLeft = NO;
if (y>0 && !water[y-1][x]) hasAbove = YES;
if (x<water[y].length-1 && !water[y][x+1]) hasRight = YES;
if (y<water.length-1 && !water[y+1][x]) hasBelow = YES;
if (x>0 && !water[y][x-1]) hasLeft = YES;
var hasCornerAL = NO;
var hasCornerAR = NO;
var hasCornerBL = NO;
var hasCornerBR = NO;
if (y>0) // above corners
{
if (x>0 && !water[y-1][x-1]) hasCornerAL = YES;
if (x<water[y-1].length-1 && !water[y-1][x+1]) hasCornerAR = YES;
};
if (y<water.length-1) // below
{
if (x>0 && !water[y+1][x-1]) hasCornerBL = YES;
if (x<water[y+1].length-1 && !water[y+1][x+1]) hasCornerBR = YES;
};
// four closed
if (hasAbove && hasRight && hasBelow && hasLeft)
{
water[y][x] = 36;
}
// three closed
else if (hasAbove && hasRight && !hasBelow && hasLeft)
{
water[y][x] = 6;
}
else if (hasAbove && hasRight && hasBelow && !hasLeft)
{
water[y][x] = 7;
}
else if (!hasAbove && hasRight && hasBelow && hasLeft)
{
water[y][x] = 8;
}
else if (hasAbove && !hasRight && hasBelow && hasLeft)
{
water[y][x] = 9;
}
// two closed, parallel
else if (!hasAbove && hasRight && !hasBelow && hasLeft)
{
water[y][x] = 10;
}
else if (hasAbove && !hasRight && hasBelow && !hasLeft)
{
water[y][x] = 11;
}
// two closed, perpendicular
else if (hasAbove && !hasRight && !hasBelow && hasLeft)
{
if (hasCornerBR)
{
water[y][x] = 16;
}
else
{
water[y][x] = 12;
};
}
else if (hasAbove && hasRight && !hasBelow && !hasLeft)
{
if (hasCornerBL)
{
water[y][x] = 17;
}
else
{
water[y][x] = 13;
};
}
else if (!hasAbove && !hasRight && hasBelow && hasLeft)
{
if (hasCornerAR)
{
water[y][x] = 18;
}
else
{
water[y][x] = 14;
};
}
else if (!hasAbove && hasRight && hasBelow && !hasLeft)
{
if (hasCornerAL)
{
water[y][x] = 19;
}
else
{
water[y][x] = 15;
};
}
// one closed
else if (hasAbove && !hasRight && !hasBelow && !hasLeft)
{
if (hasCornerBL && hasCornerBR)
{
water[y][x] = 32;
}
else if (hasCornerBL)
{
water[y][x] = 29;
}
else if (hasCornerBR)
{
water[y][x] = 28;
}
else
{
water[y][x] = 2;
}
}
else if (!hasAbove && hasRight && !hasBelow && !hasLeft)
{
if (hasCornerAL && hasCornerBL)
{
water[y][x] = 33;
}
else if (hasCornerAL)
{
water[y][x] = 27;
}
else if (hasCornerBL)
{
water[y][x] = 25;
}
else
{
water[y][x] = 3;
}
}
else if (!hasAbove && !hasRight && hasBelow && !hasLeft)
{
if (hasCornerAL && hasCornerAR)
{
water[y][x] = 34;
}
else if (hasCornerAL)
{
water[y][x] = 31;
}
else if (hasCornerAR)
{
water[y][x] = 30;
}
else
{
water[y][x] = 4;
}
}
else if (!hasAbove && !hasRight && !hasBelow && hasLeft)
{
if (hasCornerAR && hasCornerBR)
{
water[y][x] = 35;
}
else if (hasCornerAR)
{
water[y][x] = 26;
}
else if (hasCornerBR)
{
water[y][x] = 24;
}
else
{
water[y][x] = 5;
}
}
// no closed
else if (!hasAbove && !hasRight && !hasBelow && !hasLeft)
{
// four corners
if (hasCornerAL && hasCornerAR && hasCornerBL && hasCornerBR)
{
water[y][x] = 37;
}
// three corners
else if (hasCornerAL && hasCornerAR && !hasCornerBL && hasCornerBR)
{
water[y][x] = 38;
}
else if (hasCornerAL && hasCornerAR && hasCornerBL && !hasCornerBR)
{
water[y][x] = 39;
}
else if (!hasCornerAL && hasCornerAR && hasCornerBL && hasCornerBR)
{
water[y][x] = 40;
}
else if (hasCornerAL && !hasCornerAR && hasCornerBL && hasCornerBR)
{
water[y][x] = 41;
}
// two corners, opposite
else if (hasCornerAL && !hasCornerAR && !hasCornerBL && hasCornerBR)
{
water[y][x] = 42;
}
else if (!hasCornerAL && hasCornerAR && hasCornerBL && !hasCornerBR)
{
water[y][x] = 43;
}
// two corners, adjacent
else if (hasCornerAL && hasCornerAR && !hasCornerBL && !hasCornerBR)
{
water[y][x] = 44;
}
else if (!hasCornerAL && hasCornerAR && !hasCornerBL && hasCornerBR)
{
water[y][x] = 45;
}
else if (!hasCornerAL && !hasCornerAR && hasCornerBL && hasCornerBR)
{
water[y][x] = 46;
}
else if (hasCornerAL && !hasCornerAR && hasCornerBL && !hasCornerBR)
{
water[y][x] = 47;
}
// one corner
else if (!hasCornerAL && !hasCornerAR && !hasCornerBL && hasCornerBR)
{
water[y][x] = 20;
}
else if (!hasCornerAL && !hasCornerAR && hasCornerBL && !hasCornerBR)
{
water[y][x] = 21;
}
else if (!hasCornerAL && hasCornerAR && !hasCornerBL && !hasCornerBR)
{
water[y][x] = 22;
}
else if (hasCornerAL && !hasCornerAR && !hasCornerBL && !hasCornerBR)
{
water[y][x] = 23;
}
};
};
};
this.water = new TileMap(0,0,16,16,'images/tiles-water.png',water);
this.room.addChild(this.water);
this.waterElapsed = 0;
this.exit = new Vine(this.data.open[this.data.end][0]*16,
this.data.open[this.data.end][1]*16);
this.exit.zIndex = 1;
this.room.addChild(this.exit);
this.compass = new Compass(this.data.open[this.data.key][0]*16,
this.data.open[this.data.key][1]*16);
this.room.addChild(this.compass);
// random collectibles, not concerned with whether they are actually accessible to player
this.collectibles = new Surface();
var rb = 1 + Math.round(Math.random()*2); // 1-3 per level
var rm = 6 + Math.round(Math.random()*4); // 6-10 per level
// talismans
if (Math.round(Math.random()))
{
var o = Math.floor(Math.random()*this.data.open.length);
if (o == this.data.start || o == this.data.end || o == this.data.key)
{
// too lazy to rewrite the conditional. I know, right?
}
else
{
var talisman = new Talisman(this.data.open[o][0]*16,
this.data.open[o][1]*16);
this.collectibles.addChild(talisman);
};
};
// batteries
for (var i=0; i<rb; i++)
{
var o = Math.floor(Math.random()*this.data.open.length);
if (o == this.data.start || o == this.data.end || o == this.data.key) continue;
var collectible = new Battery(this.data.open[o][0]*16,
this.data.open[o][1]*16);
this.collectibles.addChild(collectible);
};
// matchbooks
for (var i=0; i<rm; i++)
{
var o = Math.floor(Math.random()*this.data.open.length);
if (o == this.data.start || o == this.data.end || o == this.data.key) continue;
var collectible = new Matchbook(this.data.open[o][0]*16,
this.data.open[o][1]*16);
this.collectibles.addChild(collectible);
};
this.room.addChild(this.collectibles);
var hole = new Tile(this.data.open[this.data.start][0]*16,
this.data.open[this.data.start][1]*16,
16,16,
'images/vine-hole.png');
hole.zIndex = 1;
hole.currentTile = System.RAM.level > 1 ? 1 : 0;
this.room.addChild(hole);
this.player = new Player(this.data.open[this.data.start][0]*16,
this.data.open[this.data.start][1]*16);
this.player.zIndex = 2;
this.room.addChild(this.player);
if (System.RAM.level==1)
{
this.player.stand();
}
this.mask = new Mask;
this.mask.zIndex = 3;
this.room.addChild(this.mask);
this.beam = new Tile(0,0,132,132,'images/beam-mask.png');
this.beam.currentTile = 9; // unadjusted eyes
this.beam.center(this.player.center());
this.mask.addChild(this.beam);
// this.darkness = new Rectangle(0,0,480,320,'rgba(0,0,0,0.1)');
// this.darkness.center(this.player.center());
// this.mask.addChild(this.darkness);
// this.mask.isVisible = NO;
// stalkers, self-replicating
this.stalkers = new Surface();
this.stalkers.zIndex = 4;
var stalker = new Stalker();
stalker.stalkee = this.player;
this.stalkers.addChild(stalker);
this.room.addChild(this.stalkers);
// light meter
this.meter = new Meter(0,0);
this.meter.zIndex = 5;
this.room.addChild(this.meter);
// hud
this.batteryNum = new HUDNumber(11,11);
this.hud.addChild(this.batteryNum);
this.matchNum = new HUDNumber(31,11);
this.hud.addChild(this.matchNum);
this.talismanNum = new HUDNumber(64,11);
this.hud.addChild(this.talismanNum);
this.levelNum = new HUDNumber(32, 150);
this.levelNum.number = System.RAM.level;
this.hud.addChild(this.levelNum);
this.frameNum = new HUDNumber(224, 48);
// this.hud.addChild(this.frameNum);
// this.scaleNum = new HUDNumber(212, 48);
// this.scaleNum.number = System._scale;
// this.hud.addChild(this.scaleNum);
this.pauseRect = Rect(220,140,20,20);
// mini map
this.minimap = new Minimap(174,4,1,1, clone(this.data.grid));
this.hud.addChild(this.minimap);
this.miniplayer = new Tile(this.data.open[this.data.start][0]+this.minimap.body.origin.x-1,
this.data.open[this.data.start][1]+this.minimap.body.origin.x-1,
3,3,
'images/miniplayer.png');
this.hud.addChild(this.miniplayer);
this.miniexit = new Tile(this.data.open[this.data.end][0]+this.minimap.body.origin.x-2,
this.data.open[this.data.end][1]+this.minimap.body.origin.y-3,
5,6,
'images/minivine.png');
this.miniexit.isVisible = NO;
this.hud.addChild(this.miniexit);
// touch point
this.touch = new Tile(0,0,32,32,'images/touch.png');
this.touch.isVisible = NO;
this.hud.addChild(this.touch);
// dialog
this.sentry = new SentrySans;
this.sentry.body.origin = Point(54, 149);
this.hud.addChild(this.sentry);
this.pausedElapsed = 0;
this.paused = new Tile(97,50,51,51,'images/paused.png');
this.paused.isVisible = NO;
this.addChild(this.paused);
this.gameOverElapsed = 0;
this.gameOver = new Tile(97,50,51,51,'images/gameover.png');
this.gameOver.isVisible = NO;
this.addChild(this.gameOver);
this.dialogs = [];
this.dialogElapsed = 0;
switch (System.RAM.level)
{
case 1:
this.queueDialog(localize('How did I survive that fall?'));
this.queueDialog(localize('I need to find a way out of here.'));
this.queueDialog(localize('My arm is definitely broken and'));
this.queueDialog(localize('these batteries won\'t last forever...'));
break;
case 2:
this.queueDialog(localize('I wonder how far down I am...'));
break;
case 3:
this.queueDialog(localize('This is starting to look familiar.'));
break;
}
System.input.registerTarget(this);
// this.beam.isVisible = NO;
},
queueDialog : function(str)
{
if (contains(this.dialogs, str)) return;
if (!this.dialogs.length) this.dialogElapsed = 10;
this.dialogs.push(str);
},
displayNextDialog : function()
{
if (this.dialogs.length)
{
this.sentry.setText(this.dialogs.shift());
this.dialogElapsed = 0;
}
else if (this.sentry.text)
{
this.sentry.setText('');
};
},
kill : function()
{
if (this.isKilling) return;
this.isKilling = YES;
this.touch.isVisible = NO;
this.talismanCount--;
this.isGameOver = this.talismanCount < 0;
if (this.isGameOver) this.talismanCount = 0; // can't display -1
var player = this.player;
var gameOver = this.gameOver;
var isGameOver = this.isGameOver;
window.setTimeout(function()
{
player.die();
if (isGameOver) gameOver.isVisible = YES;
window.setTimeout(function()
{
if (isGameOver)
{
System.state().gameover();
}
else
{
System.state().reset();
}
}, (isGameOver?5:2) * 1000);
}, 500);
},
gameover : function()
{
System.RAM['inventory'] = null;
System.loadState(new TitleState);
},
reset : function()
{
// reset stalkers
this.stalkers.removeAllChildren();
var stalker = new Stalker();
stalker.stalkee = this.player;
this.stalkers.addChild(stalker);
// reset collectibles (except compass which carries over)
for (var i=0; i<this.collectibles.children.length; i++)
{
this.collectibles.children[i].isVisible = YES;
};
// reset player
this.player.body.origin = Point(this.data.open[this.data.start][0]*16,
this.data.open[this.data.start][1]*16);
this.player.revive();
// reload inventory from RAM
this.batteryCount = System.RAM.inventory.batteryCount;
this.matchCount = System.RAM.inventory.matchCount;
this.elapsedLight = System.RAM.inventory.elapsedLight;
this.lightDuration = System.RAM.inventory.lightDuration;
this.lightsource = System.RAM.inventory.lightsource;
// reset dialog
this.dialogs = [];
// clear killing variables
this.isKilling = NO;
},
clear : function()
{
this.isEnding = YES;
this.touch.isVisible = NO;
this.player.body.origin = clone(this.exit.body.origin);
this.player.climb();
System.RAM.inventory.talismanCount = this.talismanCount;
System.RAM.inventory.batteryCount = this.batteryCount;
System.RAM.inventory.matchCount = this.matchCount;
System.RAM.inventory.elapsedLight = this.elapsedLight;
System.RAM.inventory.lightDuration = this.lightDuration;
System.RAM.inventory.lightsource = this.lightsource;
// TODO: store total number of stalkers?
window.setTimeout(function()
{
System.RAM.level++;
System.loadState(new PlayState);
}, 2 * 1000);
},
updateCamera : function()
{
var c = this.player.center();
var o = Point(c.x-System.body.size.width/2, c.y-System.body.size.height/2);
if (o.x < 0) o.x = 0;
if (o.y < 0) o.y = 0;
if (o.x+System.body.size.width > this.map.body.size.width) o.x = this.map.body.size.width-System.body.size.width;
if (o.y+System.body.size.height > this.map.body.size.height) o.y = this.map.body.size.height-System.body.size.height;
this.room.crop.origin = o;
},
update: function(elapsed)
{
this.frameNum.number = System.fps;
if (this.isPaused)
{
this.pausedElapsed += elapsed;
var dur = 0.4;
if (this.pausedElapsed >= dur)
{
this.paused.isVisible = !this.paused.isVisible;
this.pausedElapsed -= dur;
};
return;
};
if (this.isGameOver)
{
this.gameOverElapsed += elapsed;
var dur = 0.4;
if (this.gameOverElapsed >= dur)
{
this.gameOver.isVisible = !this.gameOver.isVisible;
this.gameOverElapsed -= dur;
};
// return; // continue playing animations
};
this.waterElapsed += elapsed;
if (this.waterElapsed > 0.4)
{
this.water.offset = this.water.offset?0:48;
this.waterElapsed -= 0.4;
};
this.dialogElapsed += elapsed;
if (this.dialogElapsed >= 4)
{
this.displayNextDialog();
};
if (this.isEnding)
{
this.meter.isVisible = NO;
this.player.update(elapsed);
this.sentry.update(elapsed);
this.player.body.origin.y += this.player.vy*elapsed;
this.beam.center(this.player.center());
return;
};
this._super(elapsed);
// drain current light source
if (!this.isKilling && !this.player.isStanding) this.elapsedLight += elapsed;
this.meter.value = 0; // default
if (this.elapsedLight >= 0) // not during the delay period
{
// ready for a new light source, batteries available
if (this.lightsource < this.lightsourceBattery && this.batteryCount)
{
this.elapsedLight = 0;
this.lightsource = this.lightsourceBattery;
this.lightDuration = this.batteryDuration;
this.batteryCount--;
}
// current battery has drained
else if (this.lightsource == this.lightsourceBattery && this.elapsedLight >= this.lightDuration)
{
this.lightsource = this.lightsourceNone;
this.elapsedLight = -this.batteryDelay;
if (this.batteryCount)
{
this.queueDialog(localize('Time for a new battery.'));
}
else
{
this.queueDialog(localize('No more batteries.'));
};
}
// ready for a new light source, matches available
else if (this.lightsource < this.lightsourceMatch && this.matchCount)
{
this.elapsedLight = 0;
this.lightsource = this.lightsourceMatch;
this.lightDuration = this.matchDuration;
this.matchCount--;
}
// current match has burnt out
else if (this.lightsource == this.lightsourceMatch && this.elapsedLight >= this.lightDuration)
{
this.lightsource = this.lightsourceNone;
this.elapsedLight = -this.matchDelay;
if (this.matchCount)
{
this.queueDialog(localize('Hot! Hot! Hot!'));
}
else
{
this.queueDialog(localize('Out of matches...'));
};
}
else if (this.elapsedLight >= this.lightDuration)
{
this.lightsource = this.lightsourceEyes;
};
if (this.lightsource > this.lightsourceEyes)
{
this.meter.updateValue(1 - this.elapsedLight/this.lightDuration);
};
};
// move player
var o = this.player.body.origin;
var n = Point(o.x + this.player.vx*elapsed*(this.player.isInWater?0.6:1.0), o.y + this.player.vy*elapsed*(this.player.isInWater?0.6:1.0));
// collide against 4 adjacent/overlapped tiles
var ts = 16;
var sx = Math.floor(n.x/ts); // left tile
var sy = Math.floor(n.y/ts); // top tile
var nr = clone(this.player.body);
var dx = this.player.vx > 0 ? 1 : this.player.vx < 0 ? -1 : 0;
var dy = this.player.vy > 0 ? 1 : this.player.vy < 0 ? -1 : 0;
var c = this.player.center();
c.y += 4; // set lower since Tomo is half-head
this.player.isInWater = NO;
var totalCollisions = 0;
var softness = 7.9; // must be less than half tile size
var softx = 0;
var softy = 0;
for (var y=0; y<2; y++)
{
for (var x=0; x<2; x++)
{
var nx = sx+x;
var ny = sy+y;
if (this.data.grid[ny] && this.data.grid[ny][nx]) // solid
{
nr.origin = n;
var tr = Rect(nx*ts, ny*ts, ts, ts);
if (rectIntersectsRect(this.player.body, tr))
{
totalCollisions++;
};
var xr = intersectingRect(nr, tr);
if (!rectIsEmpty(xr)) // overlapping
{
if (dx) // moving left or right
{
var vy = Math.abs(this.player.vx);
n.x -= dx * xr.size.width;
if (xr.size.height < softness) // soften corners
{
var d = (n.y < tr.origin.y) ? 1 : -1;
var s = xr.size.height > 1 ? 1 : xr.size.height;
softy = d * s;
};
}
else if (dy) // moving up or down
{
n.y -= dy * xr.size.height;
if (xr.size.width < softness) // soften corners
{
var d = (n.x < tr.origin.x) ? 1 : -1;
var s = xr.size.width > 1 ? 1 : xr.size.width;
softx = d * s;
};
};
};
};
if (this.data.water[ny] && this.data.water[ny][nx]) // water
{
var tr = Rect(nx*ts, ny*ts, ts, ts);
if (rectContainsPoint(tr, c))
{
this.player.isInWater = YES;
};
};
};
};
if (totalCollisions == 1)
{
n.x -= softx;
n.y -= softy;
};
if (this.player.vy < 0) this.player.facing = 0;
if (this.player.vx > 0) this.player.facing = 1;
if (this.player.vy > 0) this.player.facing = 2;
if (this.player.vx < 0) this.player.facing = 3;
this.player.body.origin = n;
this.meter.body.origin = Point(n.x,n.y-8);
this.updateCamera();
this.beam.center(this.player.center());
if (!this.isKilling) // can't collect things when you're dead
{
// collide with random collectibles
for (var i=0; i<this.collectibles.children.length; i++)
{
var collectible = this.collectibles.children[i];
if (!collectible.isVisible) continue;
if (rectContainsPoint(this.player.body, collectible.center()))
{
if (collectible instanceof Battery)
{
if (this.batteryCount < 6)
{
this.batteryCount++;
collectible.isVisible = NO;
this.queueDialog(localize('Found a *battery*!'));
System.speaker.fx('audio/battery.ogg');
}
else
{
this.queueDialog(localize('Can\'t carry any more batteries.'));
};
}
else if (collectible instanceof Matchbook)
{
if (this.matchCount < 32)
{
this.matchCount += 4;
if (this.matchCount > 32) this.matchCount = 32;
collectible.isVisible = NO;
this.queueDialog(localize('Found a *matchbook*!'));
System.speaker.fx('audio/matchbook.ogg');
}
else
{
this.queueDialog(localize('Can\'t carry any more matches.'));
};
}
else if (collectible instanceof Talisman)
{
if (this.talismanCount < 4)
{
this.talismanCount++;
collectible.isVisible = NO;
this.queueDialog(localize('Found a *talisman*!'));
this.queueDialog(localize('These things creep me out.'));
System.speaker.fx('audio/talisman.ogg');
}
else
{
this.queueDialog(localize('Can\'t carry any more creepy talismen.'));
};
};
};
};
}
var shouldSwapZIndex = NO;
// check for exit collisions, piggy back onto the swap zIndex checks
if (rectContainsPoint(this.player.body, this.exit.center()))
{
this.clear();
if (this.player.zIndex < this.exit.zIndex) shouldSwapZIndex = YES;
};
if (this.player.body.origin.y >= this.exit.body.origin.y)
{
if (this.player.zIndex < this.exit.zIndex) shouldSwapZIndex = YES;
}
else
{
if (this.player.zIndex > this.exit.zIndex) shouldSwapZIndex = YES;
};
if (shouldSwapZIndex)
{
var z = this.exit.zIndex;
this.exit.zIndex = this.player.zIndex;
this.player.zIndex = z;
this.player.parent.children.sort(compareZIndex);
};
// update HUD counts
this.talismanNum.number = this.talismanCount;
this.batteryNum.number = this.batteryCount;
this.matchNum.number = this.matchCount;
if (this.elapsedLight < 0)
{
this.beam.currentTile = 9; // unadjusted eyes
}
else
{
if (this.lightsource == this.lightsourceBattery)
{
this.beam.currentTile = this.player.facing;
}
else if (this.lightsource == this.lightsourceMatch)
{
this.beam.currentTile = this.player.facing + 4;
}
else
{
if (this.beam.currentTile != 8)
{
this.queueDialog(localize('Eyes finally adjusted to the dark.'));
};
this.beam.currentTile = 8; // adjusted eyes
}
}
if (this.player.isStanding) this.beam.currentTile = 8;
// wtf is this?
if (this.isEnding || this.player.isDead) this.beam.currentTile = 8; // override
// and this?
if (this.beam.currentTile >= 8) this.player.lightsource = 0; // none
else if (this.beam.currentTile >=4) this.player.lightsource = 1; // matches
else this.player.lightsource = 2; // flashlight
// update miniplayer
x1 = Math.floor(n.x/ts);
y1 = Math.floor(n.y/ts);
this.miniplayer.body.origin = Point(x1+this.minimap.body.origin.x-1, y1+this.minimap.body.origin.y-1);
// check for compass collisions
if (this.compass.isVisible)
{
if (rectContainsPoint(this.player.body, this.compass.center()))
{
this.hasFoundExit = YES;
this.queueDialog(localize('Found a *compass*!'));
System.speaker.fx('audio/compass.ogg');
};
}
if (!this.hasFoundExit)
{
var fov = Rect( this.player.body.origin.x-this.player.body.size.width,
this.player.body.origin.y-this.player.body.size.height,
this.player.body.size.width*3,
this.player.body.size.height*3);
if (rectIntersectsRect(fov, this.exit.body))
{
this.hasFoundExit = YES;
};
};
if (this.hasFoundExit && !this.miniexit.isVisible)
{
this.compass.isVisible = NO;
this.miniexit.isVisible = YES;
};
// + for no lightsource
// []| for matches
// []|| for flash light
// update minimap reveal
if (y1-1 >= 0) // row above
{
if (this.player.lightsource && x1-1 >= 0) this.minimap.reveal[y1-1][x1-1] = 1; // top left
this.minimap.reveal[y1-1][x1] = 1; // top
if (this.player.lightsource && x1+1 < this.minimap.reveal[y1-1].length) this.minimap.reveal[y1-1][x1+1] = 1; // top right
};
// this row
if (x1-1 >= 0) this.minimap.reveal[y1][x1-1] = 1;
this.minimap.reveal[y1][x1] = 1;
if (x1+1 < this.minimap.reveal[y1].length) this.minimap.reveal[y1][x1+1] = 1;
// row below
if (y1+1 < this.minimap.reveal.length) // row above
{
if (this.player.lightsource && x1-1 >= 0) this.minimap.reveal[y1+1][x1-1] = 1; // bottom left
this.minimap.reveal[y1+1][x1] = 1;
if (this.player.lightsource && x1+1 < this.minimap.reveal[y1+1].length) this.minimap.reveal[y1+1][x1+1] = 1; // bottom right
};
if (this.player.lightsource)
{
// add extra row or column in front of player
switch (this.player.facing)
{
case 0: // up
if (y1-2 >= 0)
{
if (x1-1 >= 0) this.minimap.reveal[y1-2][x1-1] = 1; // top left
this.minimap.reveal[y1-2][x1] = 1; // top
if (x1+1 < this.minimap.reveal[y1-2].length) this.minimap.reveal[y1-2][x1+1] = 1; // top right
};
break;
case 1: // right
if (x1+2 >= 0)
{
if (y1-1 >= 0) this.minimap.reveal[y1-1][x1+2] = 1; // top right
this.minimap.reveal[y1][x1+2] = 1; // right
if (y1+1 < this.minimap.reveal.length) this.minimap.reveal[y1+1][x1+2] = 1; // bottom right
};
break;
case 2: // down
if (y1+2 < this.minimap.reveal.length)
{
if (x1-1 >= 0) this.minimap.reveal[y1+2][x1-1] = 1; // bottom left
this.minimap.reveal[y1+2][x1] = 1;
if (x1+1 < this.minimap.reveal[y1+2].length) this.minimap.reveal[y1+2][x1+1] = 1; // bottom right
};
break;
case 3: // left
if (x1-2 >= 0)
{
if (y1-1 >= 0) this.minimap.reveal[y1-1][x1-2] = 1; // top left
this.minimap.reveal[y1][x1-2] = 1; // left
if (y1+1 < this.minimap.reveal.length) this.minimap.reveal[y1+1][x1-2] = 1; // bottom left
};
break;
};
if (this.player.lightsource == 2)
{
// add another extra row or column in front of player
switch (this.player.facing)
{
case 0: // up
if (y1-3 >= 0)
{
if (x1-1 >= 0) this.minimap.reveal[y1-3][x1-1] = 1; // top left
this.minimap.reveal[y1-3][x1] = 1; // top
if (x1+1 < this.minimap.reveal[y1-3].length) this.minimap.reveal[y1-3][x1+1] = 1; // top right
};
break;
case 1: // right
if (x1+3 >= 0)
{
if (y1-1 >= 0) this.minimap.reveal[y1-1][x1+3] = 1; // top right
this.minimap.reveal[y1][x1+3] = 1; // right
if (y1+1 < this.minimap.reveal.length) this.minimap.reveal[y1+1][x1+3] = 1; // bottom right
};
break;
case 2: // down
if (y1+3 < this.minimap.reveal.length)
{
if (x1-1 >= 0) this.minimap.reveal[y1+3][x1-1] = 1; // bottom left
this.minimap.reveal[y1+3][x1] = 1;
if (x1+1 < this.minimap.reveal[y1+3].length) this.minimap.reveal[y1+3][x1+1] = 1; // bottom right
};
break;
case 3: // left
if (x1-3 >= 0)
{
if (y1-1 >= 0) this.minimap.reveal[y1-1][x1-3] = 1; // top left
this.minimap.reveal[y1][x1-3] = 1; // left
if (y1+1 < this.minimap.reveal.length) this.minimap.reveal[y1+1][x1-3] = 1; // bottom left
};
break;
};
};
};
},
togglePause : function()
{
this.isPaused = !this.isPaused;
this.paused.isVisible = this.isPaused;
this.hud.isVisible = !this.isPaused;
this.pausedElapsed = 0;
// if (this.isPaused) System.speaker.pause();
// else System.speaker.resume();
},
handleEvent : function(ie)
{
if (this.isEnding) return;
if (this.player.isDead) return;
var speed = 48;
switch(ie.type)
{
case 'keydown':
this.player.move();
switch(ie.key)
{
case InputManager.KEY.UP:
this.player.vx = 0;
this.player.vy = -speed;
this.joystick.y = -1;
break;
case InputManager.KEY.RIGHT:
this.player.vx = speed;
this.player.vy = 0;
this.joystick.x = 1;
break;
case InputManager.KEY.DOWN:
this.player.vx = 0;
this.player.vy = speed;
this.joystick.y = 1;
break;
case InputManager.KEY.LEFT:
this.player.vx = -speed;
this.player.vy = 0;
this.joystick.x = -1;
break;
}
break;
case 'keyup':
switch(ie.key)
{
case InputManager.KEY.UP:
this.player.vy = 0;
if (this.joystick.y==-1) this.joystick.y = 0;
break;
case InputManager.KEY.RIGHT:
this.player.vx = 0;
if (this.joystick.x==1) this.joystick.x = 0;
break;
case InputManager.KEY.DOWN:
this.player.vy = 0;
if (this.joystick.y==1) this.joystick.y = 0;
break;
case InputManager.KEY.LEFT:
this.player.vx = 0;
if (this.joystick.x==-1) this.joystick.x = 0;
break;
case InputManager.KEY.ACTION:
this.togglePause();
break;
}
// resume previous direction
if (this.player.vx == 0)
{
this.player.vy = speed * this.joystick.y;
}
if (this.player.vy == 0)
{
this.player.vx = speed * this.joystick.x;
};
break;
// touch doesn't use this.joystick
case 'began':
this.player.move();
this.restPoint = System.input.lastTouch;
if (!this.isPaused)
{
this.touch.currentTile = 0;
this.touch.center(this.restPoint);
this.touch.isVisible = YES;
};
break;
case 'moved':
var diff = Point(System.input.lastTouch.x - this.restPoint.x, System.input.lastTouch.y - this.restPoint.y);
var dist = diff.x*diff.x + diff.y*diff.y;
var sensitivity = 2;
var adiff = Point(Math.abs(diff.x), Math.abs(diff.y));
// http://stackoverflow.com/a/3309658/145965
var theta = Math.atan2(-diff.y, diff.x);
if (theta < 0) theta += 2 * Math.PI;
var angle = theta * 180 / Math.PI;
if (dist >= sensitivity)
{
if (angle <= 45 || angle > 315) // right
{
this.player.vx = speed;
this.player.vy = 0;
this.touch.currentTile = 1;
}
else if (angle > 45 && angle <= 135) // up
{
this.player.vx = 0;
this.player.vy = -speed;
this.touch.currentTile = 2;
}
else if (angle > 135 && angle <= 225) // left
{
this.player.vx = -speed;
this.player.vy = 0;
this.touch.currentTile = 3;
}
else // down
{
this.player.vx = 0;
this.player.vy = speed;
this.touch.currentTile = 4;
};
};
break;
case 'ended':
this.player.vx = 0;
this.player.vy = 0;
this.touch.isVisible = NO;
if (rectContainsPoint(this.pauseRect,System.input.lastTouch) || this.isPaused)
{
this.togglePause();
};
break;
};
}
});
var TitleState = State.extend(
{
init : function()
{
this._super();
this.addChild(new Tile(0,0,240,160,'images/sinkhole.png'));
this.sentry = new SentrySans;
this.sentry.delay = 0;
this.addChild(this.sentry);
this.sentry.alignment = TextMap.alignmentTypes.centered;
this.sentry.setText(localize('Press *Any Arrow Key* to Start'));
this.sentry.maxWidth = 240;
this.sentry.body.origin.y = 132;
this.flashElapsed = 0;
System.speaker.loop('audio/loop.ogg');
System.input.registerTarget(this);
},
update : function(elapsed)
{
this.flashElapsed += elapsed;
var dur = 0.4;
if (this.flashElapsed >= dur)
{
this.sentry.isVisible = !this.sentry.isVisible;
this.flashElapsed -= dur;
}
},
handleEvent : function(ie)
{
var wasHandled = NO;
switch(ie.type)
{
case 'ended':
wasHandled = YES;
break;
case 'keyup':
switch(ie.key)
{
case InputManager.KEY.UP:
case InputManager.KEY.RIGHT:
case InputManager.KEY.DOWN:
case InputManager.KEY.LEFT:
case InputManager.KEY.ACTION:
wasHandled = YES;
break;
}
break;
};
if (wasHandled) System.loadState(new PrologueState);
},
});
var PrologueState = State.extend(
{
init : function()
{
this._super();
// dialog
this.sentry = new SentrySans;
this.sentry.body.origin = Point(32, 75);
this.addChild(this.sentry);
this.dialogs = [];
this.dialogElapsed = 0;
this.queueDialog(localize('Tomo was on a routine survey mission'));
this.queueDialog(localize('when the earth opened up'));
this.queueDialog(localize('and swallowed him whole.'));
this.queueDialog(localize('Guide Tomo out of the sinkhole'));
this.queueDialog(localize('before he runs out of batteries'));
this.queueDialog(localize('or burns through all his matches.'));
this.queueDialog(localize('It will be a grueling, lonely ascent. '));
this.queueDialog(localize('At least, Tomo thinks he\'s alone...'));
this.queueDialog(localize('He *hopes* he\'s alone.'));
this.displayNextDialog();
System.input.registerTarget(this);
System.speaker.fx('audio/start.ogg');
},
queueDialog : function(str)
{
if (contains(this.dialogs, str)) return;
if (!this.dialogs.length) this.dialogElapsed = 10;
this.dialogs.push(str);
},
displayNextDialog : function()
{
if (this.dialogs.length)
{
this.sentry.setText(this.dialogs.shift());
this.dialogElapsed = 0;
}
else {
System.loadState(new PlayState);
};
},
update : function(elapsed)
{
this.dialogElapsed += elapsed;
if (this.dialogElapsed >= 4)
{
this.displayNextDialog();
};
this._super(elapsed);
},
handleEvent : function(ie)
{
var wasHandled = NO;
var skipAll = NO;
switch(ie.type)
{
case 'ended':
wasHandled = YES;
break;
case 'keyup':
switch(ie.key)
{
case InputManager.KEY.ACTION:
skipAll = YES;
case InputManager.KEY.UP:
case InputManager.KEY.RIGHT:
case InputManager.KEY.DOWN:
case InputManager.KEY.LEFT:
wasHandled = YES;
break;
}
break;
};
if (wasHandled)
{
if (skipAll)
{
System.loadState(new PlayState);
}
else if (!this.sentry.hasFinished)
{
this.sentry.advance();
}
else
{
this.displayNextDialog();
};
};
},
});
var DebugState = State.extend(
{
init : function()
{
this._super();
this.addChild(new Rectangle(0,0,240,160,'red'));
this.mask = new Mask;
this.mask.zIndex = 3;
this.addChild(this.mask);
this.beam = new Tile(0,0,132,132,'images/beam-mask.png');
this.mask.addChild(this.beam);
},
});
var strings = {};
function localize(str)
{
var lang = location.hash.replace('#', '') || window.navigator.userLanguage || window.navigator.language;
if (lang.match(/(ja|jp)/) && strings['ja'][str]) {
str = strings['ja'][str];
};
return str;
};
// Thanks @ourmaninjapan!
strings['ja'] = {
'Sinkhole' : 'シンクホール',
'Press *Any Arrow Key* to Start' : 'ヤジルシキー ヲ オシテスタート',
'Paused' : 'テイシ',
'Game Over' : 'ゲームオーバー',
'Tomo was on a routine survey mission' : 'ノボル ガ ソクリョウ ヲ シテイタラ',
'when the earth opened up' : 'ジメン ニ アイタ オオキナ アナ ガ',
'and swallowed him whole.' : 'ノボル ヲ スッポリ ノミコンダ。',
'Guide Tomo out of the sinkhole' : 'ノボル ノ ダッシュツ ヲ タスケテホシイ',
'before he runs out of batteries' : 'デンチ ガ キレルカ',
'or burns through all his matches.' : 'マッチ ガ ナクナル マエニ。',
'It will be a grueling, lonely ascent. ' : 'ミチノリ ハ ケワシク ノポル ハ ヒトリボッチダ。',
'At least, Tomo thinks he\'s alone...' : 'ノボル ハ ヒトリボッチ ト オモッテイル・・・',
'He *hopes* he\'s alone.' : 'ソウダト イイト オモッテイル。',
'How did I survive that fall?' : 'ドウシタラ ハイアガレルカナ?',
'I need to find a way out of here.' : 'デグチ ヲ サガサナキャ',
'My arm is definitely broken and' : 'ウデ ハ オレタシ',
'these batteries won\'t last forever...' : 'デンチ モ キレソウダ・・・',
'I wonder how far down I am...' : 'チカ ドノアタリダロウ・・・',
'This is starting to look familiar.' : 'ミオボエ ガ アルヨウナ',
'Up I go...' : 'ウエ ヘ イクゾ・・・',
'Ugh!' : 'ウワッ!',
'What was that!' : 'アレ ハ ナンダ?',
'I must be hearing things...' : 'ソラミミ ガ スルゾ・・・',
'Found a *battery*!' : '*デンチ* ヲ ミツケタ! ',
'Can\'t carry any more batteries.' : 'コレイジョウ デンチ ハ モテナイ',
'Found a *matchbook*!' : '*マッチブック* ヲ ミツケタ!',
'Can\'t carry any more matches.' : 'コレイジョウ マッチ ハ モテナイ',
'Found a *talisman*!' : '*オマモリ* ヲ ミツケタ!',
'These things creep me out.' : 'ホントニ ゾットスル',
'Can\'t carry any more creepy talismen.' : 'コレイジョウ オマモリ ハ モテナイヤ',
'Found a *compass*!' : '*コンパス* ヲ ミツケタ!',
'Time for a new battery.' : 'デンチ ヲ カエナキャ',
'No more batteries.' : 'モウ デンチ ガ ナイ',
'Hot! Hot! Hot!' : 'アチチチ!', // fingers burnt by match burning down
'Out of matches...' : 'マッチ ガ ナイ・・・',
'Eyes finally adjusted to the dark.' : 'ヤット メ ガ クラヤミ ニ ナレタ'
};
// BOOT ---------------------------------------------------------------
var System = new GameSystem('game', 240, 160);
if (navigator.userAgent.match(/webkit/i))
{
System.autoscale(); // Safari (non-nightly) needs and can handle it.
}
else
{
System.autozoom(); // Firefox doesn't and couldn't if it needed to.
};
Textures.preload([
'images/alert.png',
'images/battery.png',
'images/beam-mask.png',
'images/campfire.png',
'images/compass.png',
'images/hud-nums.png',
'images/hud.png',
'images/matchbook.png',
'images/minimap-border.png',
'images/miniplayer.png',
'images/minivine.png',
'images/paused.png',
'images/sentry-sans+katakana.png',
'images/sinkhole.png',
'images/slash.png',
'images/start.png',
'images/talisman.png',
'images/tiles-water.png',
'images/tiles.png',
'images/tomo.png',
'images/touch.png',
'images/vine-hole.png',
'images/vine.png'
]);
System.speaker.preload(
[
'audio/loop.ogg',
'audio/battery.ogg',
'audio/compass.ogg',
'audio/flee.ogg',
'audio/matchbook.ogg',
'audio/slash.ogg',
'audio/stalker.ogg',
'audio/start.ogg',
'audio/talisman.ogg'
]);
System.loadState(new TitleState);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment