Created
January 18, 2013 15:22
-
-
Save andymason/4565267 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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