diff --git a/tetris/Background.js b/tetris/Background.js new file mode 100644 index 00000000..fd85432b --- /dev/null +++ b/tetris/Background.js @@ -0,0 +1,59 @@ + +var Background = function (config) { + var x, y, + curTile; + + config = config || {}; + + this.originX = (config.x || 0) + FIELD_OFFSET_X; + this.originY = (config.y || 0) + FIELD_OFFSET_Y; + + this.width = 10; + this.height = 20; + + this.tiles = []; + for (x = 0; x < this.width; x += 1) { + for (y = 0; y < this.height; y += 1) { + curTile = new Block({ empty: true, blockX: x, blockY: y }); + this.tiles.push(curTile); + } + } + + this.backdrop = new jaws.Sprite({image: 'media/background/backdrop.png'}); + this.backdrop.x = 0; + this.backdrop.y = 0; + + this.topBar = new jaws.Sprite({image: 'media/background/topbar.png'}); + this.topBar.x = 181; + this.topBar.y = 0; + + this.fullRedrawNeeded = true; +}; + + +Background.prototype.draw = function (lastPaused) { + var i; + + if (this.fullRedrawNeeded || lastPaused) { + this.backdrop.draw(); + + for (i = 0; i < this.tiles.length; i += 1) { + this.tiles[i].draw(); + } + + this.fullRedrawNeeded = false; + + } else { + + this.topBar.draw(); + + // clear the swap group / previews + jaws.context.fillstyle = "#000D00"; + jaws.context.fillRect(24, 42, 118, 60); + jaws.context.fillRect(457, 18, 107, 341); + + for (i = 0; i < this.tiles.length; i += 1) { + this.tiles[i].drawIfInvalid(); + } + } +}; \ No newline at end of file diff --git a/tetris/Block.js b/tetris/Block.js new file mode 100644 index 00000000..86ef1e08 --- /dev/null +++ b/tetris/Block.js @@ -0,0 +1,111 @@ + +// TODO: constants file??? +var BLOCK_WIDTH = 24; + +function Block(config) { + var parent, key; + + config = config || {}; + + this.boX = (config.boardOriginX || 0) + FIELD_OFFSET_X; + this.boY = (config.boardOriginY || 0) + FIELD_OFFSET_Y; + this.blockX = config.blockX; + this.blockY = config.blockY; + + this.occupiedPositions = config.occupiedPositions; + this.addOccupied(this.blockX, this.blockY); + + Block.invalidSpaces[this.blockX + "," + this.blockY] = true; + + config.x = this.boX + BLOCK_WIDTH * this.blockX; + config.y = this.boY + BLOCK_WIDTH * this.blockY; + + if (config.preview) { + config.image = 'media/greyblock.png'; + } else if (config.empty) { + config.image = 'media/emptyblock.png'; + }else { + config.image = SHAPES[config.shape].image; + } + + parent = new jaws.Sprite(config); + for (key in parent) { + this[key] = parent[key]; + } +} + +Block.invalidSpaces = {}; +Block.allInvalidated = false; +Block.invalidFlushed = function() { + Block.invalidSpaces = {}; + Block.allInvalidated = false; +}; +Block.invalidateAll = function() { + Block.allInvalidated = true; +}; + +Block.prototype.setColor = function(shape, preview) { + if (preview) { + this.setImage('media/greyblock.png'); + } else { + this.setImage(SHAPES[shape].image); + } + Block.invalidSpaces[this.blockX + "," + this.blockY] = true; +}; + +Block.prototype.moveBlock = function(dx, dy) { + Block.invalidSpaces[this.blockX + "," + this.blockY] = true; + this.removeOccupied(this.blockX, this.blockY); + this.blockX += dx; + this.blockY += dy; + Block.invalidSpaces[this.blockX + "," + this.blockY] = true; + this.addOccupied(this.blockX, this.blockY); + this.x += dx * BLOCK_WIDTH; + this.y += dy * BLOCK_WIDTH; +}; + +Block.prototype.setPosition = function(blockX, blockY) { + Block.invalidSpaces[this.blockX + "," + this.blockY] = true; + this.removeOccupied(this.blockX, this.blockY); + this.blockX = blockX; + this.blockY = blockY; + Block.invalidSpaces[this.blockX + "," + this.blockY] = true; + this.addOccupied(this.blockX, this.blockY); + this.x = this.boX + blockX * BLOCK_WIDTH; + this.y = this.boY + blockY * BLOCK_WIDTH; +}; + +Block.prototype.getX = function() { return this.blockX; }; +Block.prototype.getY = function() { return this.blockY; }; + +Block.prototype.isPosition = function(x, y) { + return this.blockX === x && this.blockY === y; +}; + +Block.prototype.drawIfInvalid = function() { + if (Block.invalidSpaces[this.blockX + "," + this.blockY] || Block.allInvalidated || this.blockY < 0) { + this.draw(); + } +}; + +Block.prototype.kill = function() { + Block.invalidSpaces[this.blockX + "," + this.blockY] = true; + this.removeOccupied(this.blockX, this.blockY); +}; + +Block.prototype.removeOccupied = function(x, y) { + var posString = x + ',' + y; + if (this.occupiedPositions && this.occupiedPositions[posString]) { + this.occupiedPositions[posString] -= 1; + } +}; + +Block.prototype.addOccupied = function(x, y) { + var posString = x + ',' + y; + if (this.occupiedPositions) { + if (this.occupiedPositions[posString] === undefined) { + this.occupiedPositions[posString] = 0; + } + this.occupiedPositions[posString] += 1; + } +}; diff --git a/tetris/Button.js b/tetris/Button.js new file mode 100644 index 00000000..cacb3ca3 --- /dev/null +++ b/tetris/Button.js @@ -0,0 +1,13 @@ +function Button(config) { + + var parent = new jaws.Sprite(config), + key; + + for (key in parent) { + this[key] = parent[key]; + } +} + +Button.prototype.isClicked = function(x, y) { + return this.rect().collidePoint(x, y); +}; diff --git a/tetris/ControlGroup.js b/tetris/ControlGroup.js new file mode 100644 index 00000000..ea4468e5 --- /dev/null +++ b/tetris/ControlGroup.js @@ -0,0 +1,372 @@ + +/** +* The blocks that can be moved nby the user +* @param {Array} blocks - an array of [Block] of size 4 that can be operated on +* @param {Char} shape - the block type: i, o, j, l, s, z, t +* @param {function({Number}x, {Number}y)} isLegalCallback - a function that retursn true if a block can be moved +* to the new position +*/ +function ControlGroup(blocks, shape, isLegalCallback) { + var i, + newX, newY, + shapeConf; + + // place the blocks according to the shape + shapeConf = SHAPES[shape]; + this.pos = shapeConf.pos; + this.spin = shapeConf.spin; + this.bottomed = false; + + this.blocks = blocks; + this.baseX = shapeConf.startX; + this.baseY = shapeConf.startY; + + this.shape = shape; + this.kickOffsets = WALL_KICK_OFFSETS[shapeConf.kickType]; + this.dir = 0; + + this.isIllegalStart = false; + + this.isLegalCallback = isLegalCallback || function() {return true;}; + + this.lastWasSpin = false; + + for (i = 0; i < blocks.length; i += 1) { + newX = this.baseX + this.pos[i].x; + newY = this.baseY + this.pos[i].y; + // see if the block placement is illegal before placing + if (!this.isLegalCallback(newX, newY)) { + this.isIllegalStart = true; + } + this.blocks[i].setPosition(newX, newY); + } + + this.updateBottomedState(); +} + +/** +* if the position is legal +* @param {Number} x +* @param {Number} y +* @returns {Boolean} true iff the position is legal to move to +*/ +ControlGroup.prototype.isLegalPosition = function (x, y) { + var i, + blocks = this.blocks; + + // if it's a currently occupied, it must be legal + for (i = 0; i < 4; i += 1) { + if (blocks[i].isPosition(x, y)) { + return true; + } + } + + // if it's still not proven legal, then defer to the game to decide + return this.isLegalCallback(x, y); +}; + +/** +* Shift the block left or right +* @param {Boolean} left - true to shift left false to shift right +* @returns {Boolean} true iff the shift was successful +*/ +ControlGroup.prototype.shift = function(left) { + var dx = (left ? -1 : 1), + i; + + for (i = 0; i < 4; i += 1) { + if (!this.isLegalPosition(this.blocks[i].getX()+dx, this.blocks[i].getY())) { + return false; + } + } + + this.lastWasSpin = false; + this.baseX += dx; + + for (i = 0; i < this.blocks.length; i += 1) { + this.blocks[i].moveBlock(dx, 0); + } + this.updateBottomedState(); + + return true; +}; + +ControlGroup.prototype.updateBottomedState = function() { + var i; + + for (i = 0; i < this.blocks.length; i += 1) { + if (!this.isLegalPosition(this.blocks[i].getX(), this.blocks[i].getY() + 1)) { + this.bottomed = true; + return; + } + } + + this.bottomed = false; +}; + +/** +* Drop the block by one +*/ +ControlGroup.prototype.drop = function() { + var i; + + // don't drop if bottomed + if (this.bottomed) { + return; + } + + this.lastWasSpin = false; + this.baseY += 1; + + for (i = 0; i < this.blocks.length; i += 1) { + this.blocks[i].moveBlock(0, 1); + } + this.updateBottomedState(); +}; + +/** +* @returns {Boolean} true if the block is bottomed and another shoudl spawn +*/ +ControlGroup.prototype.isBottomed = function() { + return this.bottomed; +}; + +/** +* Turns the block +* @param {Boolean} cw - true for clockwise, false for counter-clockwise +* @returns {Boolean} true iff the block was successfully turned +*/ +ControlGroup.prototype.turn = function(cw) { + var kick, + newPos = null, + direction = cw ? 'cw' : 'ccw', + availableKicks = this.kickOffsets[this.dir][direction], + i; + + // for possible each kick offset + for (i = 0; i < availableKicks.length; i += 1) { + kick = availableKicks[i]; + newPos = this.tryTurn(cw, kick); + if (newPos) { + break; + } + } + + // if there s still no valid rotation, fail + if (!newPos) { + return false; + } + + this.lastWasSpin = true; + + // must be legal at this point move the bocks + for (i = 0; i < 4; i += 1) { + this.blocks[i].setPosition(newPos[i].x, newPos[i].y); + } + this.baseX += kick.x; + this.baseY += kick.y; + + // keep track of the direction + if (cw) { + this.dir += 1; + if (this.dir === 4) { + this.dir = 0; + } + } else { + this.dir -= 1; + if (this.dir === -1) { + this.dir = 3; + } + } + + this.updateBottomedState(); + + return true; +}; + +/** +* Checks if the given rotation and kick is valid. +* @param {Boolean} cw - true if cw, false if ccw +* @param {Object} kick - the kick offset x/y object to try +* @returns {Array} and array of x/y objects if valid, null if not valid +*/ +ControlGroup.prototype.tryTurn = function (cw, kick) { + var newX, newY, + oldX, oldY, + i, + newPos = [], + curPos; + + if (this.spin === 'block') { + for (i = 0; i < this.blocks.length; i += 1) { + newX = (cw ? -1 : 1) * (this.blocks[i].blockY - this.baseY) + this.baseX + kick.x; + newY = (cw ? 1 : -1) * (this.blocks[i].blockX - this.baseX) + this.baseY + kick.y; + + newPos[i] = {x: newX, y: newY}; + } + } else { + // point turning + for (i = 0; i < this.blocks.length; i += 1) { + oldX = this.blocks[i].blockX - this.baseX; + oldY = this.blocks[i].blockY - this.baseY; + + if (oldX >= 0) { oldX += 1; } + if (oldY >= 0) { oldY += 1; } + + newX = (cw ? -1 : 1) * oldY; + newY = (cw ? 1 : -1) * oldX; + + if (newX > 0) { newX -= 1; } + if (newY > 0) { newY -= 1; } + + newPos[i] = {x: newX + this.baseX + kick.x, y: newY + this.baseY + kick.y}; + } + } + + + // for each block + for (i = 0; i < 4; i += 1) { + curPos = newPos[i]; + if (!this.isLegalPosition(curPos.x, curPos.y)) { + return null; + } + } + + return newPos; + +}; + +/** +* Gets the positions that the block will use when it falls +* @returns {Object} {dist:{Number}, positions: {[Object]} array of hashs of {x: Number, y: Number}} +*/ +ControlGroup.prototype.getFallPositions = function () { + var res = [], + dist = 0, + i, + curBlock, + notDone = true; + + while (notDone) { + dist += 1; + + // for each block + for (i = 0; i < 4 && notDone; i += 1) { + curBlock = this.blocks[i]; + // if it's not a legal position + if (!this.isLegalPosition(curBlock.getX(), curBlock.getY() + dist)) { + // back up one and stop dropping + dist -= 1; + notDone = false; + } + } + } + + // for each block + for (i = 0; i < 4; i += 1) { + curBlock = this.blocks[i]; + res.push({x: curBlock.getX(), y: curBlock.getY() + dist}); + } + + return {dist: dist, positions: res}; +}; + +/** +* makes the block fall all the way to the bottom +* forces the next cycle to be recognized as bottomed +* @returns {Number} the distance fallen +*/ +ControlGroup.prototype.fall = function() { + var fall = this.getFallPositions(), + positions = fall.positions, + dist = fall.dist, + i, curPos; + + if (dist !== 0) { + this.lastWasSpin = false; + } + + // for each block + for (i = 0; i < 4; i += 1) { + curPos = positions[i]; + this.blocks[i].setPosition(curPos.x, curPos.y); + } + + this.bottomed = true; + return dist; +}; + +/** +* Sets the preview blocks to the approproriate positions +* @param {[Block]} previews - the 4 blocks to be modified to be put into position as preview blocks +*/ +ControlGroup.prototype.configurePreviewBlocks = function(previews) { + var positions = this.getFallPositions().positions, + i; + + for (i = 0; i < 4; i += 1) { + previews[i].setPosition(positions[i].x, positions[i].y); + } +}; + +ControlGroup.prototype.getShape = function () { + return this.shape; +}; + +ControlGroup.prototype.getBlocks = function () { + return this.blocks; +}; + +/* +* Gets the type of T spin that the group is in +* @returns {String} 'mini' for a mini-t, 'normal' for a normal t, null for not a t spin +*/ +ControlGroup.prototype.getTSpin = function() { + var i, + testPoints = [{x:-1,y:-1},{x:1,y:-1},{x:1,y:1},{x:-1,y:1}], + count = 0, + mini = false, + curPoint; + + if (!this.lastWasSpin) { + return null; + } + + // make sure it's actually a t + if (this.shape !== 't') { + return null; + } + + // t-spin mini tests + if (this.dir === 0) { + testPoints[0].miniCheck = true; + testPoints[1].miniCheck = true; + } else if (this.dir === 1) { + testPoints[1].miniCheck = true; + testPoints[2].miniCheck = true; + } else if (this.dir === 2) { + testPoints[2].miniCheck = true; + testPoints[3].miniCheck = true; + } else if (this.dir === 3) { + testPoints[3].miniCheck = true; + testPoints[0].miniCheck = true; + } + + // 3 point t test + for (i = 0; i < 4; i += 1) { + curPoint = testPoints[i] + if (!this.isLegalPosition(this.baseX + curPoint.x, this.baseY + curPoint.y)) { + count += 1; + } else if (curPoint.miniCheck) { + mini = true; + } + } + + if (count >= 3) { + if (mini) { + return 'mini'; + } + return 'normal'; + } + return null; +}; diff --git a/tetris/Game.js b/tetris/Game.js new file mode 100644 index 00000000..b2b3e1c8 --- /dev/null +++ b/tetris/Game.js @@ -0,0 +1,301 @@ +function Game(inputMapping, autoRepeat, threshold) { + var thisObject = this, + i; + + this.firstLoop = true; + + this.blocks = []; + this.controlGroup = null; + + // make the preview blocks + this.previewBlocks = []; + for (i = 0; i < 4; i += 1) { + this.previewBlocks.push(new Block({blockX: -10, blockY: -10, preview: true})); + } + + this.scoreOutput = new TtyBlock("scoreDiv", 3); + this.linesOutput = new TtyBlock("linesDiv", 3); + this.levelOutput = new TtyBlock("levelDiv", 3); + this.tickerOutput = new TtyBlock("tickerDiv", 5); + this.scoreTracker = new ScoreTracker(this.scoreOutput, this.linesOutput, this.levelOutput, this.tickerOutput); + + this.dropPeriod = this.scoreTracker.getLevelPeriod(); + this.timeToNextDrop = this.dropPeriod; + + // TODO: find the official values for these constants + this.keyChargeTime = threshold; + this.keyRepeatTime = autoRepeat; + + this.bottomTimer = null; + this.bottomLockTime = 500; + this.lastBottomedState = false; + + this.lastTime = null; + + this.gameLost = false; + + // evenly distributed random piece generator + this.previewLength = 5; + this.randBag = new RandomBag(this.previewLength); + // make the preview blocks + this.previewGroups = []; + for (i = 0; i < this.previewLength; i += 1) { + this.previewGroups.push(new PreviewGroup(330, 70 * i + 35)); + } + + this.swapGroup = null; + this.swapAllowed = true; + + // the currently occupied positions, number of blocks at a position + // indexed by the position as a string + this.occupiedPositions = {}; + + this.input = { + shiftLeft: { + autoRepeat: true, + handler: function () { + if (thisObject.controlGroup.shift(true)) { + thisObject.resetLockCounter(true); + } + } + }, + shiftRight: { + autoRepeat: true, + handler: function() { + if (thisObject.controlGroup.shift(false)) { + thisObject.resetLockCounter(true); + } + } + }, + softDrop: { + autoRepeat: true, + preCharged: true, + handler: function() { + thisObject.dropBlock(); + thisObject.scoreTracker.softDrop(); + } + }, + hardDrop: { handler: function() { + var dist = thisObject.controlGroup.fall(); + thisObject.scoreTracker.hardDrop(dist); + thisObject.lockBlocks(); + }}, + rotateLeft: { handler: function() { + if (thisObject.controlGroup.turn(false)) { + thisObject.resetLockCounter(true); + } + }}, + rotateRight: { handler: function() { + if (thisObject.controlGroup.turn(true)) { + thisObject.resetLockCounter(true); + } + }}, + swap: { handler: function() { + thisObject.swap(); + }} + }; + + this.inputMapping = inputMapping; +} + +/** +* drops a new block into the game +*/ +Game.prototype.newBlock = function (calledBySwap) { + var thisObject = this, + shape = this.randBag.popQueue(), + newBlocks = [], + curBlock, + i; + + this.dropPeriod = this.scoreTracker.getLevelPeriod(); + + // create some new blocks + for (i = 0; i < 4; i += 1) { + curBlock = new Block({blockX: -10, blockY: -10, shape: shape, occupiedPositions: this.occupiedPositions}); + newBlocks.push(curBlock); + this.blocks.push(curBlock); + } + + this.controlGroup = new ControlGroup(newBlocks, shape, function(x, y){ + return thisObject.isLegalPosition(x, y); + }); + + if (this.controlGroup.isIllegalStart) { + this.gameLost = true; + } + + if (!calledBySwap) { + // the user is allowed to swap blocks again + this.swapAllowed = true; + } + + this.updatePreviews(this.randBag.getQueue()); +}; + +/** +* processes the input keys +* @param {Number} dTime - the time in milliseconds since the last frame +*/ +Game.prototype.processInput = function(dTime) { + var curInput, + keyName, + curKeys, + pressed, + curInput, + i; + + for (actionType in this.inputMapping) { + curKeys = this.inputMapping[actionType]; + curInput = this.input[actionType]; + pressed = false; + for (i = 0; i < curKeys.length; i += 1) { + if (jaws.pressed(curKeys[i])) { + pressed = true; + } + } + + // if the key is down + if (pressed) { + // if it is a 'press' frame + if (!curInput.lastState) { + curInput.handler(); + curInput.lastState = true; + curInput.charged = (curInput.preCharged ? true : false); + curInput.holdTime = 0; + } + // if it supports auto-repeat + if (curInput.autoRepeat) { + curInput.holdTime += dTime; + + // if not charged and past the charge time + if ((!curInput.charged) && (curInput.holdTime > this.keyChargeTime)) { + // call the handler, and reset the hold time + curInput.holdTime -= this.keyChargeTime; + curInput.handler(); + curInput.charged = true; + } + // if charged and past the repeat time + if (curInput.charged && (curInput.holdTime > this.keyRepeatTime)) { + curInput.holdTime -= this.keyRepeatTime; + curInput.handler(); + } + } + } else { + // it was released + curInput.lastState = false; + } + } +}; + +Game.prototype.update = function(time) { + var curTime, + dTime, + i; + + // if the first block needs to be made + if (this.firstLoop) { + this.firstLoop = false; + + this.newBlock(); + + this.lastTime = time; + } + + curTime = time; + dTime = curTime - this.lastTime; + this.lastTime = curTime; + + this.processInput(dTime); + + if (!this.controlGroup.isBottomed()) { + this.lastBottomedState = false; + this.applyGravity(dTime); + + } else { + // if it has just touched hte bottom + if (!this.lastBottomedState) { + this.resetLockCounter(false); + } else { + this.bottomTimer -= dTime; + + if (this.bottomTimer <= 0 || this.slideCount >= 15) { + this.lockBlocks(); + } + } + this.lastBottomedState = true; + } + + // update the position of the preview blocks + if (this.controlGroup) { + // ask the control group to move the preview blocks + this.controlGroup.configurePreviewBlocks(this.previewBlocks); + } else { + // if there is no contorl group, just move them off the screen + for (i = 0; i < 4; i += 1) { + this.previewBlocks[i].setPosition(-10, -10); + } + } +}; + +/** +* Renders the entire game scene +*/ +Game.prototype.draw = function(dTime) { + var i; + + this.scoreOutput.draw(dTime); + this.linesOutput.draw(dTime); + this.levelOutput.draw(dTime); + this.tickerOutput.draw(dTime); + + // draw the preview blocks + for (i = 0; i < 4; i += 1) { + this.previewBlocks[i].drawIfInvalid(); + } + + // draw the swap block + if (this.swapGroup) { + this.swapGroup.draw(); + } + + // draw the queue + for (i = 0; i < this.previewGroups.length; i += 1) { + this.previewGroups[i].draw(); + } + + for (i = 0; i < this.blocks.length; i += 1) { + this.blocks[i].drawIfInvalid(); + } + +}; + +/** +* Returns true iff the given position can be moved into +* @param {Number} x - the x position +* @param {Number} y - the y position +* @returns {Boolean} true iff the new position is legal +*/ +Game.prototype.isLegalPosition = function (x, y) { + // if there is a block in the way + if (this.occupiedPositions[x+','+y]) { + return false; + } + + // if it's on the field + if (x >= 10 || x < 0 || y >= 20) { + return false; + } + return true; +}; + +/** +* drops the controlled blocks by one +*/ +Game.prototype.dropBlock = function (causedByGravity) { + if (!causedByGravity) { + this.timeToNextDrop = this.dropPeriod; + } + + this.controlGroup.drop(); +}; diff --git a/tetris/Game_Logic.js b/tetris/Game_Logic.js new file mode 100644 index 00000000..2d2f4cae --- /dev/null +++ b/tetris/Game_Logic.js @@ -0,0 +1,209 @@ + +/** +* @returns {[Number]} the line numbers of all the completed rows +*/ +Game.prototype.getRows = function () { + var i, + rows = [], + res = [], + curRow; + + // initialize the rows to 0 + for (i = 0; i < 20; i += 1) { + rows[i] = 0; + } + // for each block + for (i = 0; i < this.blocks.length; i += 1) { + // increment the appropriate row + curRow = this.blocks[i].getY(); + rows[curRow] += 1; + // if the row is full + if (rows[curRow] === 10) { + res.push(curRow); + } + } + + return res; +}; + +/** +* Removes the rows from the field +*/ +Game.prototype.removeRows = function (rows) { + var dropDist = {}, + i, j, + remove = {}, + curBlock, + curY; + + // initialize drops to 0 + for (i = -4; i < 20; i += 1) { + dropDist[i] = 0; + } + + // for each removed row + for (i = 0; i < rows.length; i += 1) { + remove[rows[i]] = true; + + // every row above this should be dropped another spot + for (j = -4; j < rows[i]; j += 1) { + dropDist[j] += 1; + } + } + + // for each block + for (i = 0; i < this.blocks.length; i += 1) { + curBlock = this.blocks[i]; + curY = curBlock.getY(); + + // if it is being removed + if (remove[curY]) { + // remove the block + this.removeBlock(i); + i -= 1; + } else { + // it is being dropped + curBlock.setPosition(curBlock.getX(), curBlock.getY() + dropDist[curY]); + } + } +}; + +Game.prototype.removeBlock = function(index) { + this.blocks[index].kill(); + return this.blocks.splice(index, 1); +}; + +Game.prototype.applyGravity = function (dTime) { + this.timeToNextDrop -= dTime; + + // drop until there is a positive time until the next drop time is positive, or the control group s bottomed out + while (this.timeToNextDrop < 0 && (!this.controlGroup.isBottomed())) { + this.dropBlock(true); + this.timeToNextDrop += this.dropPeriod; + } + + // if it exited through bottoming, reset the drop period + if (this.controlGroup.isBottomed()) { + this.timeToNextDrop = this.dropPeriod; + } +}; + +/** +* Changes the shapes of the preview along the side +* @param {[Char]} queue - the queue of pieces +*/ +Game.prototype.updatePreviews = function(queue) { + var i; + for (i = 0; i < queue.length; i += 1) { + this.previewGroups[i].setShape(queue[i]); + } +}; + +/** +* called when the user attempts to swap a block +*/ +Game.prototype.swap = function() { + var i, j, + newShape, + oldShape = this.controlGroup.getShape(), + oldBlocks = this.controlGroup.getBlocks(), + newBlocks = [], + thisObject = this; + + // can only be called once per drop + if (!this.swapAllowed) { + return; + } + this.swapAllowed = false; + + // Reset the locking + this.resetLockCounter(false); + + // remove the blocks + // for each block on the field + for (i = 0; i < this.blocks.length; i += 1) { + // if the block is part of the control group, remove it + for (j = 0; j < 4; j += 1) { + if (oldBlocks[j] === this.blocks[i]) { + this.removeBlock(i); + i -= 1; + } + } + } + + // if there is a block waiting + if (this.swapGroup) { + newShape = this.swapGroup.getShape(); + for (i = 0; i < 4; i += 1) { + newBlocks.push(new Block({blockX:-10, blockY:-10, shape: newShape, occupiedPositions: this.occupiedPositions})); + this.blocks.push(newBlocks[i]); + } + + this.controlGroup = new ControlGroup(newBlocks, newShape, function(x, y){ + return thisObject.isLegalPosition(x, y); + }); + + this.swapGroup.setShape(oldShape); + + return; + } + + // if there is no block waiting + this.swapGroup = new PreviewGroup(-100, 60); + this.swapGroup.setShape(oldShape); + this.newBlock(true); + +}; + +/** +* locks the currnt piece in, registers lines and makes a new block +*/ +Game.prototype.lockBlocks = function() { + // figure out if it a t-spin/t-spin mini + var tSpinType = this.controlGroup.getTSpin(), + scoreObject = {}, + rows; + + if (tSpinType === 'mini') { + scoreObject.miniT = true; + } else if (tSpinType === 'normal') { + scoreObject.normalT = true; + } + + // look for rows + rows = this.getRows(); + scoreObject.lines = rows.length; + if (rows.length > 0) { + this.removeRows(rows); + } + + // apply the score + this.scoreTracker.updateScore(scoreObject); + + this.newBlock(); + this.resetLockCounter(false); +}; + +/** +* Resets the lock counter, and the slide counter if not soft +* @param {Boolean} soft = true if a soft reset, and the slide counter should not be reset +*/ +Game.prototype.resetLockCounter = function (soft) { + if (soft) { + this.slideCount += 1; + } else { + this.slideCount = 0; + } + this.bottomTimer = this.bottomLockTime; +}; + +/** + * Determines if the game is over and returns a score object + * if it is. Otherwise, returns null + */ +Game.prototype.getResults = function() { + if (this.gameLost || this.scoreTracker.gameWon()) { + return this.scoreTracker.getResults(); + } + return null; +}; diff --git a/tetris/PreviewGroup.js b/tetris/PreviewGroup.js new file mode 100644 index 00000000..71027c0c --- /dev/null +++ b/tetris/PreviewGroup.js @@ -0,0 +1,47 @@ + +function PreviewGroup(baseX, baseY) { + var i; + + this.blocks = []; + this.shape = null; + + // create the blocks + for (i = 0; i < 4; i += 1) { + this.blocks.push(new Block({ + boardOriginX: baseX, + boardOriginY: baseY, + blockX: 0, + blockY: 0, + shape: 'i' + })); + } +} + +/** +* Sets the shape and color of the blocks +* @param {Char} shape - the letter of the new shape +* @param {Boolean} preview - true if it should have preview colors +*/ +PreviewGroup.prototype.setShape = function(shape) { + var shapeConfig = SHAPES[shape], + i; + + this.shape = shape; + + for (i = 0; i < 4; i += 1) { + this.blocks[i].setPosition(shapeConfig.pos[i].x, shapeConfig.pos[i].y); + this.blocks[i].setColor(shape, false); + } +}; + +PreviewGroup.prototype.getShape = function () { + return this.shape; +}; + +PreviewGroup.prototype.draw = function() { + var i; + for (i = 0; i < 4; i += 1) { + this.blocks[i].draw(); + } +}; + \ No newline at end of file diff --git a/tetris/RandomBag.js b/tetris/RandomBag.js new file mode 100644 index 00000000..7e17883d --- /dev/null +++ b/tetris/RandomBag.js @@ -0,0 +1,49 @@ +function RandomBag(queueSize) { + // start off empty + this.available = []; + this.queue = []; + + // initialize by refilling the queue + while (this.queue.length < queueSize) { + this.queue.push(this.nextAvailable()); + } +} + +RandomBag.initialList = ['i', 'o', 'j', 'l', 'z', 's', 't']; + +/** +* Returns the letters of the queue +* @returns {[Char]} the letters of the queue in order of oldest to newest +*/ +RandomBag.prototype.getQueue = function () { + return this.queue; +}; + +/** +* Moves the queue forward by one +* @returns {Char} the poped value +*/ +RandomBag.prototype.popQueue = function () { + var res = this.queue.shift(); + this.queue.push(this.nextAvailable()); + return res; +}; + +/** +* gets the next letter for the queue, and updates the random bag state +* @returns {Char} the next letter for the queue +* @private +*/ +RandomBag.prototype.nextAvailable = function() { + var index, res; + + // if the available needs to be rebuilt + if (this.available.length === 0) { + this.available = RandomBag.initialList.slice(0); // shallow copy + } + + index = Math.floor(Math.random()*this.available.length); + res = this.available.splice(index, 1)[0]; + + return res; +}; \ No newline at end of file diff --git a/tetris/ScoreTracker.js b/tetris/ScoreTracker.js new file mode 100644 index 00000000..252332b8 --- /dev/null +++ b/tetris/ScoreTracker.js @@ -0,0 +1,213 @@ +function ScoreTracker(scoreOutput, linesOutput, levelOutput, tickerOutput) { + this.level = 1; + this.score = 0; + this.linesRemaining = ScoreTracker.levelLines(this.level); + + this.scoreOutput = scoreOutput; + this.linesOutput = linesOutput; + this.levelOutput = levelOutput; + this.tickerOutput = tickerOutput; + + this.curCombo = -1; + this.lastWasBonus = false; + this.backToBackCount = 0; + + this.isGameWon = false; + + this.outputScore(); + this.outputLines(); + this.outputLevel(); +} + +ScoreTracker.levelLines = function (level) { + return level*5; +}; + +ScoreTracker.prototype.updateScore = function(config) { + var linesCleared = 0, + isBonus = false, + scoreDiff = 0, + tickerLines = [], + i; + + if (config.miniT) { + // mini t spin, 1 for no lines, 2 for 1 line + tickerLines.push("T Spin Mini"); + linesCleared += 1; + scoreDiff += 100 * this.level; + if (config.lines === 1) { + linesCleared += 1; + scoreDiff += 100 * this.level; + } + } else if (config.normalT) { + // normal t spin, bonus for eveything but 0 lines + switch (config.lines) { + case 0: + tickerLines.push("T Spin"); + linesCleared += 4; + scoreDiff += 400 * this.level; + break; + case 1: + tickerLines.push("T Spin Single"); + linesCleared += 8; + isBonus = true; + scoreDiff += 800 * this.level; + break; + case 2: + tickerLines.push("T Spin Double"); + linesCleared += 12; + isBonus = true; + scoreDiff += 1200 * this.level; + break; + case 3: + tickerLines.push("T SPIN TRIPLE"); + linesCleared += 16; + isBonus = true; + scoreDiff += 1600 * this.level; + break; + } + } else if (config.lines > 0) { + // plain old line clears + switch (config.lines) { + case 1: + tickerLines.push("Single"); + linesCleared += 1; + scoreDiff += 100 * this.level; + break; + case 2: + tickerLines.push("Double"); + linesCleared += 3; + scoreDiff += 300 * this.level; + break; + case 3: + tickerLines.push("Triple"); + linesCleared += 5; + scoreDiff += 500 * this.level; + break; + case 4: + tickerLines.push("TETRIS"); + linesCleared += 8; + isBonus = true; + scoreDiff += 800 * this.level; + break; + } + } + + // apply the combo + if (linesCleared > 0) { + this.curCombo += 1; + linesCleared += Math.floor(this.curCombo * 0.5); + scoreDiff += 50 * this.curCombo * this.level; + if (this.curCombo >= 1) { + tickerLines.push("Combo x" + this.curCombo); + } + } else { + this.curCombo = -1; + } + + // apply back-to-back bonus + if (this.lastWasBonus && isBonus) { + tickerLines.push("Back-to-Back"); + this.backToBackCount += 1; + linesCleared = Math.floor(linesCleared * 1.5); + scoreDiff += this.backToBackCount * 0.5 * scoreDiff; + } else { + this.backToBackCount = 0; + } + // only update the last bonus state if a single through triple was gotten + if (config.lines > 0) { + this.lastWasBonus = isBonus; + } + + // apply the lines cleared + this.linesRemaining -= linesCleared; + if (this.linesRemaining <= 0) { + if (this.level < 15) { + this.level += 1; + this.linesRemaining = ScoreTracker.levelLines(this.level); + } else { + this.isGameWon = true; + } + this.outputLevel(); + } + + if (linesCleared > 0) { + this.outputLines(); + } + + + this.score += scoreDiff; + this.outputScore(); + + if (tickerLines.length === 0) { + this.tickerOutput.addLine(""); + } else { + for (i = 0; i < tickerLines.length; i += 1) { + this.tickerOutput.addLine(tickerLines[i]); + } + } +}; + +ScoreTracker.prototype.softDrop = function() { + this.score += 1; +}; + +ScoreTracker.prototype.hardDrop = function(dist) { + this.score += 2 * dist; +}; + +ScoreTracker.prototype.getLinesRemaining = function() { return this.linesRemaining; }; +ScoreTracker.prototype.getScore = function() { return this.score; }; +ScoreTracker.prototype.getLevel = function() { return this.level; }; + +ScoreTracker.prototype.getLevelPeriod = function() { + var periods = [ + 1000, + 800, + 600, + 470, + 380, + 250, + 200, + 160, + 130, + 90, + 50, + 27, + 20, + 15, + 10 + ], + res = periods[(this.level < periods.length) ? this.level : periods.length - 1]; + return res; +}; + +ScoreTracker.prototype.gameWon = function() { + return this.isGameWon; +}; + +ScoreTracker.prototype.getResults = function() { + return { + score: this.score, + level: this.level, + won: this.isGameWon + }; +}; + +ScoreTracker.prototype.outputScore = function() { + this.scoreOutput.addLine("Score:"); + this.scoreOutput.addLine("" + this.score); + this.scoreOutput.addLine(""); +}; + +ScoreTracker.prototype.outputLines = function() { + this.linesOutput.addLine("Lines:"); + this.linesOutput.addLine("" + this.linesRemaining); + this.linesOutput.addLine(""); +}; + +ScoreTracker.prototype.outputLevel = function() { + this.levelOutput.addLine("Level:"); + this.levelOutput.addLine("" + this.level); + this.levelOutput.addLine(""); +}; diff --git a/tetris/Shapes.js b/tetris/Shapes.js new file mode 100644 index 00000000..ebebb291 --- /dev/null +++ b/tetris/Shapes.js @@ -0,0 +1,94 @@ +var SHAPES = { + i: { + spin: 'corner', + startX: 5, + startY: 0, + pos: [ + { x: -2, y: -1 }, + { x: -1, y: -1}, + { x: 0, y: -1 }, + { x: 1, y: -1 } + ], + image: 'media/cyanblock.png', + kickType: 'i_block' + }, + o: { + spin: 'corner', + startX: 5, + startY: -1, + pos: [ + { x: -1, y: 0 }, + { x: 0, y: 0}, + { x: -1, y: -1 }, + { x: 0, y: -1 } + ], + image: 'media/yellowblock.png', + kickType: 'standard' + }, + j: { + spin: 'block', + startX: 4, + startY: -1, + pos: [ + { x: -1, y: -1 }, + { x: -1, y: 0 }, + { x: 0, y: 0 }, + { x: 1, y: 0 } + ], + image: 'media/blueblock.png', + kickType: 'standard' + }, + l: { + spin: 'block', + startX: 4, + startY: -1, + pos: [ + { x: -1, y: 0 }, + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: -1 } + ], + image: 'media/orangeblock.png', + kickType: 'standard' + }, + s: { + spin: 'block', + startX: 4, + startY: -1, + pos: [ + { x: -1, y: 0 }, + { x: 0, y: 0 }, + { x: 0, y: -1 }, + { x: 1, y: -1 } + ], + image: 'media/greenblock.png', + kickType: 'standard' + }, + z: { + spin: 'block', + startX: 4, + startY: -1, + pos: [ + { x: -1, y: -1 }, + { x: 0, y: -1 }, + { x: 0, y: 0 }, + { x: 1, y: 0 } + ], + image: 'media/redblock.png', + kickType: 'standard' + }, + t: { + spin: 'block', + startX: 4, + startY: -1, + pos: [ + { x: -1, y: 0 }, + { x: 0, y: 0 }, + { x: 0, y: -1 }, + { x: 1, y: 0 } + ], + image: 'media/purpleblock.png', + kickType: 'standard' + } + +}; diff --git a/tetris/TtyBlock.js b/tetris/TtyBlock.js new file mode 100644 index 00000000..5fd1484d --- /dev/null +++ b/tetris/TtyBlock.js @@ -0,0 +1,84 @@ + +function TtyBlock (divName, numLines, rollOverLength, rollOverRemove) { + var i; + + this.elem = document.getElementById(divName); + + // slow scorlling effect variables + this.curPos = 0; + this.cursorShown = false; + + // TODO: make these random starting values + this.timePassedType = 0; + this.timePassedFlash = 0; + + // time in ms + this.typePeriod = 30; + this.flashPeriod = 300; + + this.lines = []; + for (i = 0; i < numLines; i += 1) { + this.lines.push(""); + } + + this.rollOverLength = rollOverLength || 9; + this.rollOverRemove = rollOverRemove || 3; + + this.backlog = []; +} + + +/** + updates the text block + */ +TtyBlock.prototype.draw = function (dTime) { + var i, + outputString = "", + lastLine; + + this.timePassedType += dTime; + + while (this.timePassedType > this.typePeriod) { + this.curPos += 1; + this.timePassedType -= this.typePeriod; + } + + lastLine = this.lines[this.lines.length-1]; + + if (this.curPos > lastLine.length) { + this.timePassedFlash += dTime; + while (this.timePassedFlash > this.flashPeriod) { + this.cursorShown = !this.cursorShown; + this.timePassedFlash -= this.flashPeriod; + } + } + + // if I'm past the end of the last line, and there is a backlog, shift all the lines + if (this.curPos > lastLine.length && this.backlog.length > 0) { + this.lines.shift(); + lastLine = this.backlog.shift(); + this.lines.push(lastLine); + this.curPos = 0; + } + + // print all of the lines but the last one + for (i = 0; i < this.lines.length - 1; i += 1) { + outputString += this.lines[i] + "
"; + } + outputString += lastLine.slice(0, Math.min(this.curPos, lastLine.length)); + if (this.cursorShown) { + outputString += "_"; + } + // rewirte for html gaurds + outputString.replace('>', '>'); + this.elem.innerHTML = outputString; +}; + +TtyBlock.prototype.addLine = function(str) { + // if the backlog is too long, then remove the last 3 values + if (this.backlog.length > this.rollOverLength) { + this.backlog.splice(this.backlog.length - this.rollOverRemove, this.rollOverRemove); + } + + this.backlog.push(" > " + str); +}; diff --git a/tetris/WallKicks.js b/tetris/WallKicks.js new file mode 100644 index 00000000..7fe3b526 --- /dev/null +++ b/tetris/WallKicks.js @@ -0,0 +1,67 @@ +var WALL_KICK_OFFSETS = {}; + +/* +0 -> starting orientation +1 -> 1 turn cw +2 -> 2 turns +3-> 1 turn ccw + +non-I blocks +L->2 ( 0, 0) (-1, 0) (-1,-1) ( 0,+2) (-1,+2) +L->0 ( 0, 0) (-1, 0) (-1,-1) ( 0,+2) (-1,+2) +2->R ( 0, 0) (-1, 0) (-1,+1) ( 0,-2) (-1,-2) +2->L ( 0, 0) (+1, 0) (+1,+1) ( 0,-2) (+1,-2) +R->0 ( 0, 0) (+1, 0) (+1,-1) ( 0,+2) (+1,+2) +R->2 ( 0, 0) (+1, 0) (+1,-1) ( 0,+2) (+1,+2) +0->L ( 0, 0) (+1, 0) (+1,+1) ( 0,-2) (+1,-2) +0->R ( 0, 0) (-1, 0) (-1,+1) ( 0,-2) (-1,-2) + +I block +0->R ( 0, 0) (-2, 0) (+1, 0) (-2,-1) (+1,+2) +0->L ( 0, 0) (-1, 0) (+2, 0) (-1,+2) (+2,-1) + +R->2 ( 0, 0) (-1, 0) (+2, 0) (-1,+2) (+2,-1) +R->0 ( 0, 0) (+2, 0) (-1, 0) (+2,+1) (-1,-2) + +2->L ( 0, 0) (+2, 0) (-1, 0) (+2,+1) (-1,-2) +2->R ( 0, 0) (+1, 0) (-2, 0) (+1,-2) (-2,+1) + +L->0 ( 0, 0) (+1, 0) (-2, 0) (+1,-2) (-2,+1) +L->2 ( 0, 0) (-2, 0) (+1, 0) (-2,-1) (+1,+2) +*/ + +WALL_KICK_OFFSETS.standard = [ + { + cw: [{x:0,y:0}, {x:-1,y:0}, {x:-1,y:-1}, {x:0,y:2}, {x:-1,y:2}], + ccw: [{x:0,y:0}, {x:1,y:0}, {x:1,y:-1}, {x:0,y:2}, {x:1,y:2}] + },{ + cw: [{x:0,y:0}, {x:1,y:0}, {x:1,y:1}, {x:0,y:-2}, {x:1,y:-2}], + ccw: [{x:0,y:0}, {x:1,y:0}, {x:1,y:1}, {x:0,y:-2}, {x:1,y:-2}] + },{ + cw: [{x:0, y:0}, {x:1,y:0}, {x:1,y:-1}, {x:0,y:2}, {x:1,y:2}], + ccw: [{x:0, y:0}, {x:-1, y:0}, {x:-1,y:-1}, {x:0,y:2}, {x:-1,y:2}] + },{ + cw: [{x:0,y:0}, {x:-1,y:0}, {x:-1,y:1}, {x:0,y:-2}, {x:-1,y:-2}], + ccw: [{x:0,y:0}, {x:-1,y:0}, {x:-1,y:1}, {x:0,y:-2}, {x:-1,y:-2}] + } +]; + +WALL_KICK_OFFSETS.i_block = [ + { + cw: [{x:0,y:0}, {x:-2,y:0}, {x:1,y:0}, {x:-2,y:1}, {x:1,y:-2}], + ccw: [{x:0,y:0}, {x:-1,y:0}, {x:2,y:0}, {x:-1,y:-2}, {x:2,y:1}] + },{ + cw: [{x:0,y:0}, {x:-1,y:0}, {x:2,y:0}, {x:-1,y:-2}, {x:2,y:1}], + ccw: [{x:0,y:0}, {x:2,y:0}, {x:-1,y:0}, {x:2,y:-1}, {x:-1,y:2}] + },{ + cw: [{x:0,y:0}, {x:2,y:0}, {x:-1,y:0}, {x:2,y:-1}, {x:-1,y:2}], + ccw: [{x:0,y:0}, {x:1,y:0}, {x:-2,y:0}, {x:1,y:2}, {x:-2,y:-1}] + },{ + cw: [{x:0,y:0}, {x:1,y:0}, {x:-2,y:0}, {x:1,y:2}, {x:-2,y:1}], + ccw: [{x:0,y:0}, {x:-2,y:0}, {x:1,y:0}, {x:-2,y:1}, {x:1,y:-2}] + } + +]; + + + diff --git a/tetris/about.html b/tetris/about.html new file mode 100644 index 00000000..81191369 --- /dev/null +++ b/tetris/about.html @@ -0,0 +1,66 @@ + + + TwitchTetris + + + + + + + + + + + +
+
+ > man TwitchTeteris
+

TwitchTetris is an open-source implementation of Tetris, the classic falling-block game that we all know and love, designed for those who demand the best performace from their tetris game.

+

TwitchTetris aims to be the fastest implementation of Tetris to be played within a browser. This is possible because it's implemented completely in Html5/JavaScript. It was developed out of the frustration of inconsistent performance of Flash plugins across multiple Operating Systems and Browsers, and the sad state of other available Html5 Tetris games. TwitchTetris should have the fastest reaction time to user input of any complete Tetris implementation, and should perform the most consistently over all modern browsers.

+ +

This project is still in the infancy of development. Please go to the google code page to report any bugs, problems, or suggestions that you have for the game. Contributions from other developers are also welcome.

+ +

While TwitchTetris is not deisgned to generate revenue, ads are displayed on this website in order to cover the potential cost incurring high traffic. There are no plans to expand advertising past one or 2 ads per page, and obtrusive ads which make users wait to play the game will never be shown on this website.

+ +
+
+ + + + + + \ No newline at end of file diff --git a/tetris/controls.html b/tetris/controls.html new file mode 100644 index 00000000..4fca72f1 --- /dev/null +++ b/tetris/controls.html @@ -0,0 +1,127 @@ + + + TwitchTetris + + + + + + + + + + + + + + + + +
+ +
+
+ Do you want to use a custom control scheme? +
+
+
+ +
+ +
+
+
+
+ To change your controls, select "Custom Controls" from above. +
+
+ Click on the fields on the right to set your controls. +
+
+ Press a key to set this field... +
+
+ +
+
+
+ Rotate Left: +
+
+ Rotate Right: +
+
+ Shift Left: +
+
+ Shift Right: +
+
+ Soft Drop: +
+
+ Hard Drop: +
+
+ Swap Peice: +
+
+ +
+ Auto-Repeat Times:
+ + +
+
+ AutoRepeat: ms
+ +
+
+ Repeat Charge: ms
+ +
+
+
+ +
+
+ + + + + + diff --git a/tetris/controls.js b/tetris/controls.js new file mode 100644 index 00000000..3c01fd52 --- /dev/null +++ b/tetris/controls.js @@ -0,0 +1,202 @@ +var controlsLoaded = false; +var curControl = null; + +function onControlsLoad() { + jaws.start(InputMonitor); + // check for an existing controls cookie + var customControls = readCookie('customControls'); + + // these actions will trigger the controls configurations + if (customControls === 'TRUE') { + // if there is a cookie, set up the controls for it + document.getElementById('customRadio').checked = true; + configureCustomControls(); + } else { + // if no cookie, assign defaults, create the cookie + document.getElementById('defaultRadio').checked = true; + setDefaultControls(); + } + + configureAutoRepeat(); + + controlsLoaded = true; +} + +function setDefaultControls() { + stopPollingInput(); + + document.getElementById('instructionsDefault').setAttribute('class', 'withDisplay'); + document.getElementById('instructionsCustom').setAttribute('class', 'noDisplay'); + document.getElementById('instructionsPending').setAttribute('class', 'noDisplay'); + + // set the cookies + createCookie('customControls', 'FALSE', 1000); + + // configure the gui to the default text + document.getElementById('rotateLeftValue') + .innerHTML = 'Z'; + document.getElementById('rotateRightValue') + .innerHTML = 'X, UP'; + document.getElementById('shiftLeftValue') + .innerHTML = 'LEFT'; + document.getElementById('shiftRightValue') + .innerHTML = 'RIGHT'; + document.getElementById('softDropValue') + .innerHTML = 'DOWN'; + document.getElementById('hardDropValue') + .innerHTML = 'SPACE'; + document.getElementById('swapValue') + .innerHTML = 'SHIFT, C'; +} + +function configureCustomControls(fromCookie, fromThreshold) { + stopPollingInput(); + + document.getElementById('instructionsDefault').setAttribute('class', 'noDisplay'); + document.getElementById('instructionsCustom').setAttribute('class', 'withDisplay'); + document.getElementById('instructionsPending').setAttribute('class', 'noDisplay'); + + if (controlsLoaded && !fromCookie) { + // the cookies need to be created & initialized + createCookie('rotateLeft', 'Z', 1000); + createCookie('rotateRight', 'X', 1000); + createCookie('shiftLeft', 'LEFT', 1000); + createCookie('shiftRight', 'RIGHT', 1000); + createCookie('softDrop', 'DOWN', 1000); + createCookie('hardDrop', 'SPACE', 1000); + createCookie('swap', 'C', 1000); + + createCookie('customControls', 'TRUE', 1000); + } + + // assign all of the GUI elements based on the cookie + document.getElementById('rotateLeftValue') + .innerHTML = readCookie('rotateLeft'); + document.getElementById('rotateRightValue') + .innerHTML = readCookie('rotateRight'); + document.getElementById('shiftLeftValue') + .innerHTML = readCookie('shiftLeft'); + document.getElementById('shiftRightValue') + .innerHTML = readCookie('shiftRight'); + document.getElementById('softDropValue') + .innerHTML = readCookie('softDrop'); + document.getElementById('hardDropValue') + .innerHTML = readCookie('hardDrop'); + document.getElementById('swapValue') + .innerHTML = readCookie('swap'); +} + +function controlsUnitClicked(controlName) { + // if default controls, switch to custom + if (readCookie('customControls') !== 'TRUE') { + // if no cookie, assign defaults, create the cookie + document.getElementById('customRadio').checked = true; + configureCustomControls(); + } + + document.getElementById('instructionsDefault').setAttribute('class', 'noDisplay'); + document.getElementById('instructionsCustom').setAttribute('class', 'noDisplay'); + document.getElementById('instructionsPending').setAttribute('class', 'withDisplay'); + + if (curControl !== null) { + stopPollingInput(); + } + curControl = { + name: controlName, + containerId: controlName + 'Div' + }; + + startPollingInput(); +} + +function startPollingInput() { + document.getElementById(curControl.containerId).setAttribute('class', 'controlsUnit controlsUnitPending'); + + inputPolling = true; +} + +function stopPollingInput() { + if (curControl !== null) { + inputPolling = false; + + document.getElementById(curControl.containerId).setAttribute('class', 'controlsUnit'); + curControl = null; + } +} + +function findWhereKeyUsed(key) { + var cookies = ['rotateLeft', + 'rotateRight', + 'shiftLeft', + 'shiftRight', + 'softDrop', + 'hardDrop', + 'swap'], + i; + + for (i = 0; i < cookies.length; i += 1) { + if (readCookie(cookies[i]) === key) { + return cookies[i]; + } + } + + return null; +} + +function reportKeyPressed(keyLower) { + // should never fail this case... + if (curControl !== null) { + var key = keyLower.toUpperCase(); + + // if this key is used anywhere else + var controlUsed = findWhereKeyUsed(key); + if (controlUsed !== null) { + // swap the two controls + createCookie(controlUsed, readCookie(curControl.name), 1000); + createCookie(curControl.name, key, 1000); + } else { + // set this key to the new value + createCookie(curControl.name, key, 1000); + } + + configureCustomControls(true); + + stopPollingInput(); + } +} + +function configureAutoRepeat() { + var autoRepeat = readCookie('autoRepeat'); + if (autoRepeat === null) { + autoRepeat = "50"; + createCookie('autoRepeat', autoRepeat, 1000); + } + var threshold = readCookie('threshold'); + if (threshold === null) { + threshold = "200"; + createCookie("threshold", threshold, 1000); + } + + document.getElementById('autoRepeatRange').value = autoRepeat; + document.getElementById('autoRepeatValue').innerHTML = autoRepeat; + document.getElementById('thresholdRange').value = threshold; + document.getElementById('thresholdValue').innerHTML = threshold; +} + +function updateAutoRepeat() { + var newVal = document.getElementById('autoRepeatRange').value; + document.getElementById('autoRepeatValue').innerHTML = newVal; + createCookie('autoRepeat', newVal, 1000); +} + +function updateThreshold() { + var newVal = document.getElementById('thresholdRange').value; + document.getElementById('thresholdValue').innerHTML = newVal; + createCookie('threshold', newVal, 1000); +} + +function resetAutoRepeat() { + eraseCookie('autoRepeat'); + eraseCookie('threshold'); + configureAutoRepeat(); +} \ No newline at end of file diff --git a/tetris/controlsStyles.css b/tetris/controlsStyles.css new file mode 100644 index 00000000..87b04959 --- /dev/null +++ b/tetris/controlsStyles.css @@ -0,0 +1,113 @@ +.customControlsDiv +{ + position: absolute; + top: 0px; + right: 0px; +} + +.controlsArea +{ + padding: 5px; + width: 580; + position: relative; +} + +.controlsUnit +{ + margin: 10px 5px; + font-size: 16px; + font-family: VT323; + color: #008000; + background-color: #000d00; + border: 1px solid #008000; + border-radius: 5px; + padding: 5px 10px; + width: 200px; +} + +.controlsUnit:hover +{ + cursor: pointer; + background-color: #008000; + color: #000d00; +} + +.controlsUnitPending +{ + cursor: pointer; + background-color: #008000; + color: #000d00; + font-weight: bold; +} + +.enableCustomDiv +{ + margin: 10px 5px; + font-size: 16px; + font-family: VT323; + color: #008000; + background-color: #000d00; + border: 1px solid #008000; + border-radius: 5px; + padding: 5px 10px; + width: 300px; + height: 245px; + position: absolute; + top: 0px; + left: 0px; +} + +.controlsValue +{ + float: right; +} + +.noDisplay +{ + display: none; +} + +.withDisplay +{ + display: inline; +} + +.autoRepeatDiv +{ + position: absolute; + bottom: 10px; + left: 10px; +} + +.thresholdDiv +{ + position: absolute; + bottom: 10px; + right: 10px; + width: 150px; +} + +.autoRepeatArea +{ + margin: 10px 5px; + font-size: 16px; + font-family: VT323; + color: #008000; + background-color: #000d00; + border: 1px solid #008000; + border-radius: 5px; + padding: 5px 10px; + width: 560px; + height: 50px; + position: absolute; + top: 265px; + left: 0px; +} + +.autoRepeatForm +{ + width: 350px; + position: absolute; + right: 0px; + bottom: 0px; +} \ No newline at end of file diff --git a/tetris/cookie.js b/tetris/cookie.js new file mode 100644 index 00000000..eee57b8a --- /dev/null +++ b/tetris/cookie.js @@ -0,0 +1,24 @@ +function createCookie(name,value,days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else var expires = ""; + document.cookie = name+"="+value+expires+"; path=/"; +} + +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} + +function eraseCookie(name) { + createCookie(name,"",-1); +} \ No newline at end of file diff --git a/tetris/gameControls.js b/tetris/gameControls.js new file mode 100644 index 00000000..b7ea5da6 --- /dev/null +++ b/tetris/gameControls.js @@ -0,0 +1,46 @@ + +// default input assignments +var inputAssignments = { + shiftLeft: ['left'], + shiftRight: ['right'], + softDrop: ['down'], + rotateLeft: ['z'], + rotateRight: ['x', 'up'], + swap: ['shift', 'c'], + hardDrop: ['space'] +}; + +var autoRepeatConfig = 50; +var thresholdConfig = 200; + +function loadGameControls() { + var cookies = ['rotateLeft', + 'rotateRight', + 'shiftLeft', + 'shiftRight', + 'softDrop', + 'hardDrop', + 'swap'], + i, curVal; + + // if custom controls need to be loaded + if (readCookie('customControls') === 'TRUE') { + // for each input cookie + for (i = 0; i < cookies.length; i += 1) { + // print the controls to the table + curVal = readCookie(cookies[i]); + document.getElementById(cookies[i]).innerHTML = curVal; + // pass the controls into the config object + inputAssignments[cookies[i]] = [curVal.toLowerCase()]; + } + } + + var autoRepeat = readCookie('autoRepeat'); + if (autoRepeat !== null) { + autoRepeatConfig = parseInt(autoRepeat); + } + var threshold = readCookie('threshold'); + if (threshold != null) { + thresholdConfig = parseInt(threshold); + } +} \ No newline at end of file diff --git a/tetris/highScores.html b/tetris/highScores.html new file mode 100644 index 00000000..94300ede --- /dev/null +++ b/tetris/highScores.html @@ -0,0 +1,60 @@ + + + TwitchTetris + + + + + + + + + + + + + + +
+
+ Daily High Scores: +
+
+
+
+ + diff --git a/tetris/highScores.js b/tetris/highScores.js new file mode 100644 index 00000000..fa48b8b9 --- /dev/null +++ b/tetris/highScores.js @@ -0,0 +1,39 @@ +function getXmlHttp() { + if (window.XMLHttpRequest) + {// code for IE7+, Firefox, Chrome, Opera, Safari + return new XMLHttpRequest(); + } + else + {// code for IE6, IE5 + return new ActiveXObject("Microsoft.XMLHTTP"); + } +} + +function highScoresOnLoad() { + // div called id=highScoreDiv + var xmlhttp = getXmlHttp(); + xmlhttp.onreadystatechange=function() + { + if (xmlhttp.readyState==4 && xmlhttp.status==200) + { + var response = jsonParse(xmlhttp.responseText), + dailyScoreList = response.dailyScores, + dailyOutput, + i; + + dailyOutput= ''; + + for (i = 0; i < dailyScoreList.length; i += 1) { + curScore = dailyScoreList[i]; + dailyOutput += ''; + } + + dailyOutput += '
#NameScore
' + (i+1) + '' + curScore.name + '' + curScore.score + '
'; + + document.getElementById("dailyScoreDiv").innerHTML = dailyOutput; + } + } + + xmlhttp.open("POST", "/score/tables", true); + xmlhttp.send(); +} diff --git a/tetris/index.html b/tetris/index.html index e8232fe4..d1c55f89 100644 --- a/tetris/index.html +++ b/tetris/index.html @@ -1,393 +1,121 @@ - - - - + + TwitchTetris | 3kh0 + + + - + + -
- - - Sorry, this example cannot be run because your browser does not support the <canvas> element - -
+ + + + + + + + + + + + + + + - - - + + + + + + + +
+
+
+
+
+
+
- showStats(); - addEvents(); +
+
+
- var last = now = timestamp(); - function frame() { - now = timestamp(); - update(Math.min(1, (now - last) / 1000.0)); - draw(); - stats.update(); - last = now; - requestAnimationFrame(frame, canvas); - } + + Your browser does not natively support Html5, or the Canvas Tag. Using this browser is slowing the progress of the web. Please get a modern browser, such as Google Chrome or Mozzila FireFox + +
+
+
+ Controls:
+ + + + + + + + + - resize(); - reset(); - frame(); - - } - - function showStats() { - stats.domElement.id = 'stats'; - get('menu').appendChild(stats.domElement); - } - - function addEvents() { - document.addEventListener('keydown', keydown, false); - window.addEventListener('resize', resize, false); - } - - function resize(event) { - canvas.width = canvas.clientWidth; - canvas.height = canvas.clientHeight; - ucanvas.width = ucanvas.clientWidth; - ucanvas.height = ucanvas.clientHeight; - dx = canvas.width / nx; - dy = canvas.height / ny; - invalidate(); - invalidateNext(); - } - - function keydown(ev) { - var handled = false; - if (playing) { - switch(ev.keyCode) { - case KEY.LEFT: actions.push(DIR.LEFT); handled = true; break; - case KEY.RIGHT: actions.push(DIR.RIGHT); handled = true; break; - case KEY.UP: actions.push(DIR.UP); handled = true; break; - case KEY.DOWN: actions.push(DIR.DOWN); handled = true; break; - case KEY.ESC: lose(); handled = true; break; - } - } - else if (ev.keyCode == KEY.SPACE) { - play(); - handled = true; - } - if (handled) - ev.preventDefault(); - } - function play() { hide('start'); reset(); playing = true; } - function lose() { show('start'); setVisualScore(); playing = false; } - - function setVisualScore(n) { vscore = n || score; invalidateScore(); } - function setScore(n) { score = n; setVisualScore(n); } - function addScore(n) { score = score + n; } - function clearScore() { setScore(0); } - function clearRows() { setRows(0); } - function setRows(n) { rows = n; step = Math.max(speed.min, speed.start - (speed.decrement*rows)); invalidateRows(); } - function addRows(n) { setRows(rows + n); } - function getBlock(x,y) { return (blocks && blocks[x] ? blocks[x][y] : null); } - function setBlock(x,y,type) { blocks[x] = blocks[x] || []; blocks[x][y] = type; invalidate(); } - function clearBlocks() { blocks = []; invalidate(); } - function clearActions() { actions = []; } - function setCurrentPiece(piece) { current = piece || randomPiece(); invalidate(); } - function setNextPiece(piece) { next = piece || randomPiece(); invalidateNext(); } - - function reset() { - dt = 0; - clearActions(); - clearBlocks(); - clearRows(); - clearScore(); - setCurrentPiece(next); - setNextPiece(); - } - - function update(idt) { - if (playing) { - if (vscore < score) - setVisualScore(vscore + 1); - handle(actions.shift()); - dt = dt + idt; - if (dt > step) { - dt = dt - step; - drop(); - } - } - } - - function handle(action) { - switch(action) { - case DIR.LEFT: move(DIR.LEFT); break; - case DIR.RIGHT: move(DIR.RIGHT); break; - case DIR.UP: rotate(); break; - case DIR.DOWN: drop(); break; - } - } - - function move(dir) { - var x = current.x, y = current.y; - switch(dir) { - case DIR.RIGHT: x = x + 1; break; - case DIR.LEFT: x = x - 1; break; - case DIR.DOWN: y = y + 1; break; - } - if (unoccupied(current.type, x, y, current.dir)) { - current.x = x; - current.y = y; - invalidate(); - return true; - } - else { - return false; - } - } - - function rotate() { - var newdir = (current.dir == DIR.MAX ? DIR.MIN : current.dir + 1); - if (unoccupied(current.type, current.x, current.y, newdir)) { - current.dir = newdir; - invalidate(); - } - } - - function drop() { - if (!move(DIR.DOWN)) { - addScore(10); - dropPiece(); - removeLines(); - setCurrentPiece(next); - setNextPiece(randomPiece()); - clearActions(); - if (occupied(current.type, current.x, current.y, current.dir)) { - lose(); - } - } - } - - function dropPiece() { - eachblock(current.type, current.x, current.y, current.dir, function(x, y) { - setBlock(x, y, current.type); - }); - } - - function removeLines() { - var x, y, complete, n = 0; - for(y = ny ; y > 0 ; --y) { - complete = true; - for(x = 0 ; x < nx ; ++x) { - if (!getBlock(x, y)) - complete = false; - } - if (complete) { - removeLine(y); - y = y + 1; - n++; - } - } - if (n > 0) { - addRows(n); - addScore(100*Math.pow(2,n-1)); - } - } - - function removeLine(n) { - var x, y; - for(y = n ; y >= 0 ; --y) { - for(x = 0 ; x < nx ; ++x) - setBlock(x, y, (y == 0) ? null : getBlock(x, y-1)); - } - } - var invalid = {}; - - function invalidate() { invalid.court = true; } - function invalidateNext() { invalid.next = true; } - function invalidateScore() { invalid.score = true; } - function invalidateRows() { invalid.rows = true; } - - function draw() { - ctx.save(); - ctx.lineWidth = 1; - ctx.translate(0.5, 0.5); - drawCourt(); - drawNext(); - drawScore(); - drawRows(); - ctx.restore(); - } - - function drawCourt() { - if (invalid.court) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (playing) - drawPiece(ctx, current.type, current.x, current.y, current.dir); - var x, y, block; - for(y = 0 ; y < ny ; y++) { - for (x = 0 ; x < nx ; x++) { - if (block = getBlock(x,y)) - drawBlock(ctx, x, y, block.color); - } - } - ctx.strokeRect(0, 0, nx*dx - 1, ny*dy - 1); - invalid.court = false; - } - } - - function drawNext() { - if (invalid.next) { - var padding = (nu - next.type.size) / 2; - uctx.save(); - uctx.translate(0.5, 0.5); - uctx.clearRect(0, 0, nu*dx, nu*dy); - drawPiece(uctx, next.type, padding, padding, next.dir); - uctx.strokeStyle = 'black'; - uctx.strokeRect(0, 0, nu*dx - 1, nu*dy - 1); - uctx.restore(); - invalid.next = false; - } - } - - function drawScore() { - if (invalid.score) { - html('score', ("00000" + Math.floor(vscore)).slice(-5)); - invalid.score = false; - } - } - - function drawRows() { - if (invalid.rows) { - html('rows', rows); - invalid.rows = false; - } - } - - function drawPiece(ctx, type, x, y, dir) { - eachblock(type, x, y, dir, function(x, y) { - drawBlock(ctx, x, y, type.color); - }); - } - - function drawBlock(ctx, x, y, color) { - ctx.fillStyle = color; - ctx.fillRect(x*dx, y*dy, dx, dy); - ctx.strokeRect(x*dx, y*dy, dx, dy) - } - run(); - - - \ No newline at end of file + + + + + + + + +
Move BlockSoft DropRotateSave PieceHard DropPause
Left, RightDownZ, X, UpC, ShiftSpaceEsc
+
+
+ + + +
+
+ + diff --git a/tetris/input.js b/tetris/input.js new file mode 100644 index 00000000..bbc0fa5d --- /dev/null +++ b/tetris/input.js @@ -0,0 +1,69 @@ +var availKeys = [ + "backspace", + "tab", + "enter", + "shift", + "ctrl", + "alt", + "space", + "pageup", + "pagedown", + "end", + "home", + "left", + "up", + "right", + "down", + "insert", + "delete", + "multiply", + "add", + "subtract", + "decimalpoint", + "divide", + "numlock", + "scrollock", + "semicolon", + "equalsign", + "comma", + "dash", + "period", + "forwardslash", + "openbracket", + "backslash", + "closebracket", + "singlequote", + "numpad1","numpad2","numpad3","numpad4","numpad5","numpad6","numpad7","numpad8","numpad9", + "0","1","2","3","4","5","6","7","8","9", + "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z" +]; + +var inputPolling = false; + +function InputMonitor() { + // give the program a global reference to me + this.setup = function(){ + jaws.preventDefaultKeys(availKeys); + }; + + // do nothing + this.draw = function(){}; + + // polls the keys if appropriate + this.update = function() { + var i, + pressed; + + for (i = 0; + i < availKeys.length && inputPolling; + i += 1) { + + pressed = jaws.pressed(availKeys[i]); + if (pressed) { + reportKeyPressed(availKeys[i]); + inputPolling = false; + break; + } + } + } +} \ No newline at end of file diff --git a/tetris/jawsjs.js b/tetris/jawsjs.js new file mode 100644 index 00000000..0c044674 --- /dev/null +++ b/tetris/jawsjs.js @@ -0,0 +1,1520 @@ +/* + * + * Jaws - a HTML5 canvas/javascript 2D game development framework + * + * Homepage: http://jawsjs.com/ + * Works with: Chrome 6.0+, Firefox 3.6+, 4+, IE 9+ + * License: LGPL - http://www.gnu.org/licenses/lgpl.html + * + * Formating guide: + * + * jaws.oneFunction() + * jaws.one_variable = 1 + * new jaws.OneConstructor + * + * Jaws uses the "module pattern" and exposes itself through the global "jaws". + * It should play nice with all other JS libs. + * + * Have fun! + * + * ippa. + * + */ + +var jaws = (function(jaws) { + +var title +var log_tag + +jaws.title = function(value) { + if(value) { return (title.innerHTML = value) } + return title.innerHTML +} + +/* + * Unpacks Jaws core-constructors into the global namespace + * After calling unpack you can use: + * "Sprite()" instead of "jaws.Sprite()" + * "Animation()" instead of "jaws.Animation()" + * .. and so on. + * + */ +jaws.unpack = function() { + var make_global = ["Sprite", "SpriteList", "Animation", "Viewport", "SpriteSheet", "Parallax", "TileMap", "Rect", "pressed"] + + make_global.forEach( function(item, array, total) { + if(window[item]) { jaws.log(item + "already exists in global namespace") } + else { window[item] = jaws[item] } + }); +} + + +/* + * Logger, adds text to previously found or created
+ */ +jaws.log = function(msg, add) { + if(log_tag) { + msg += "
" + if(add) { log_tag.innerHTML = log_tag.innerHTML.toString() + msg } + else { log_tag.innerHTML = msg } + } +} + +/* + * init() + * + * Initializes / creates: + * - jaws.canvas / jaws.context / jaws.dom (our drawable gamearea) + * - jaws.width / jaws.height (width/height of drawable gamearea) + * - jaws.url_parameters (hash of key/values of all parameters in current url) + * - title / log_tag (used internally by jaws) + * + * */ +jaws.init = function(options) { + /* Find tag */ + title = document.getElementsByTagName('title')[0] + jaws.url_parameters = getUrlParameters() + + /* + * If debug=1 parameter is present in the URL, let's either find <div id="jaws-log"> or create the tag. + * jaws.log(message) will use this div for debug/info output to the gamer or developer + * + */ + log_tag = document.getElementById('jaws-log') + if(jaws.url_parameters["debug"]) { + if(!log_tag) { + log_tag = document.createElement("div") + log_tag.style.cssText = "overflow: auto; color: #aaaaaa; width: 300px; height: 150px; margin: 40px auto 0px auto; padding: 5px; border: #444444 1px solid; clear: both; font: 10px verdana; text-align: left;" + document.body.appendChild(log_tag) + } + } + + jaws.canvas = document.getElementsByTagName('canvas')[0] + if(jaws.canvas) { + jaws.context = jaws.canvas.getContext('2d'); + } + else { + jaws.dom = document.getElementById("canvas") + jaws.dom.style.position = "relative" // This is needed to have sprites with position = "absolute" stay within the canvas + } + + jaws.width = jaws.canvas ? jaws.canvas.width : jaws.dom.offsetWidth + jaws.height = jaws.canvas ? jaws.canvas.height : jaws.dom.offsetHeigh +} + +/* +* +* Find the <canvas> so following draw-operations can use it. +* If the developer didn't provide a <canvas> in his HTML, let's create one. +* +*/ +function findOrCreateCanvas() { + jaws.canvas = document.getElementsByTagName('canvas')[0] + if(!jaws.canvas) { + jaws.canvas = document.createElement("canvas") + jaws.canvas.width = 500 + jaws.canvas.height = 300 + document.body.appendChild(jaws.canvas) + jaws.log("creating canvas", true) + } + else { + jaws.log("found canvas", true) + } + jaws.context = jaws.canvas.getContext('2d'); +} + +/* + * Quick and easy startup of a jaws gameloop. Can be called in different ways: + * + * jaws.start(Game) // Start game state Game() with default options + * jaws.start(Game, {fps: 30}) // Start game state Geme() with options, in this case jaws will un Game with FPS 30 + * jaws.start(window) // + * + */ +jaws.start = function(game_state, options) { + var wanted_fps = (options && options.fps) || 60 + + jaws.init() + jaws.log("setupInput()", true) + jaws.setupInput() + + /* Callback for when one single assets has been loaded */ + function assetLoaded(src, percent_done) { + jaws.log( percent_done + "%: " + src, true) + } + + /* Callback for when an asset can't be loaded*/ + function assetError(src) { + jaws.log( "Error loading: " + src) + } + + /* Callback for when all assets are loaded */ + function assetsLoaded() { + jaws.log("all assets loaded", true) + + // This makes both jaws.start() and jaws.start(MenuState) possible + // Run game state constructor (new) after all assets are loaded + if( game_state && jaws.isFunction(game_state) ) { game_state = new game_state } + if(!game_state) { game_state = window } + + jaws.gameloop = new jaws.GameLoop(game_state.setup, game_state.update, game_state.draw, wanted_fps) + jaws.game_state = game_state + jaws.gameloop.start() + } + + jaws.log("assets.loadAll()", true) + if(jaws.assets.length() > 0) { jaws.assets.loadAll({onload:assetLoaded, onerror:assetError, onfinish:assetsLoaded}) } + else { assetsLoaded() } +} + +/* + * Switch to a new active game state + * Save previous game state in jaws.previous_game_state + */ +jaws.switchGameState = function(game_state) { + jaws.gameloop.stop() + + jaws.clearKeyCallbacks() // clear out all keyboard callbacks + + if(jaws.isFunction(game_state)) { game_state = new game_state } + + jaws.previous_game_state = jaws.game_state + jaws.game_state = game_state + jaws.gameloop = new jaws.GameLoop(game_state.setup, game_state.update, game_state.draw, jaws.gameloop.fps) + jaws.gameloop.start() +} + +/* Always return obj as an array. forceArray(1) -> [1], forceArray([1,2]) -> [1,2] */ +jaws.forceArray = function(obj) { + return Array.isArray(obj) ? obj : [obj] +} + +/* Clears canvas through context.clearRect() */ +jaws.clear = function() { + jaws.context.clearRect(0,0,jaws.width,jaws.height) +} + +/* returns true if obj is an Image */ +jaws.isImage = function(obj) { + return Object.prototype.toString.call(obj) === "[object HTMLImageElement]" +} + +/* returns true of obj is a Canvas-element */ +jaws.isCanvas = function(obj) { + return Object.prototype.toString.call(obj) === "[object HTMLCanvasElement]" +} + +/* returns true of obj is either an Image or a Canvas-element */ +jaws.isDrawable = function(obj) { + return jaws.isImage(obj) || jaws.isCanvas(obj) +} + +/* returns true if obj is a String */ +jaws.isString = function(obj) { + return (typeof obj == 'string') +} + +/* returns true if obj is an Array */ +jaws.isArray = function(obj) { + return !(obj.constructor.toString().indexOf("Array") == -1) +} + +/* returns true of obj is a Function */ +jaws.isFunction = function(obj) { + return (Object.prototype.toString.call(obj) === "[object Function]") +} + +/* + * Return a hash of url-parameters and their values + * + * http://test.com/?debug=1&foo=bar -> [debug: 1, foo: bar] + */ +function getUrlParameters() { + var vars = [], hash; + var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); + for(var i = 0; i < hashes.length; i++) { + hash = hashes[i].split('='); + vars.push(hash[0]); + vars[hash[0]] = hash[1]; + } + return vars; +} + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + + var pressed_keys = {} + var keycode_to_string = [] + var on_keydown_callbacks = [] + var on_keyup_callbacks = [] + +/* + * Map all javascript keycodes to easy-to-remember letters/words + */ +jaws.setupInput = function() { + var k = [] + + k[8] = "backspace" + k[9] = "tab" + k[13] = "enter" + k[16] = "shift" + k[17] = "ctrl" + k[18] = "alt" + k[19] = "pause" + k[20] = "capslock" + k[27] = "esc" + k[32] = "space" + k[33] = "pageup" + k[34] = "pagedown" + k[35] = "end" + k[36] = "home" + k[37] = "left" + k[38] = "up" + k[39] = "right" + k[40] = "down" + k[45] = "insert" + k[46] = "delete" + + k[91] = "leftwindowkey" + k[92] = "rightwindowkey" + k[93] = "selectkey" + k[106] = "multiply" + k[107] = "add" + k[109] = "subtract" + k[110] = "decimalpoint" + k[111] = "divide" + + k[144] = "numlock" + k[145] = "scrollock" + k[186] = "semicolon" + k[187] = "equalsign" + k[188] = "comma" + k[189] = "dash" + k[190] = "period" + k[191] = "forwardslash" + k[192] = "graveaccent" + k[219] = "openbracket" + k[220] = "backslash" + k[221] = "closebracket" + k[222] = "singlequote" + + var numpadkeys = ["numpad1","numpad2","numpad3","numpad4","numpad5","numpad6","numpad7","numpad8","numpad9"] + var fkeys = ["f1","f2","f3","f4","f5","f6","f7","f8","f9"] + var numbers = ["0","1","2","3","4","5","6","7","8","9"] + var letters = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"] + for(var i = 0; numbers[i]; i++) { k[48+i] = numbers[i] } + for(var i = 0; letters[i]; i++) { k[65+i] = letters[i] } + for(var i = 0; numpadkeys[i]; i++) { k[96+i] = numpadkeys[i] } + for(var i = 0; fkeys[i]; i++) { k[112+i] = fkeys[i] } + + keycode_to_string = k + + window.onkeydown = function(e) { handleKeyDown(e) } + window.onkeyup = function(e) { handleKeyUp(e) } + window.onkeypress = function(e) {}; +} + +// handle event "onkeydown" by remembering what key was pressed +function handleKeyUp(e) { + event = (e) ? e : window.event + var human_name = keycode_to_string[event.keyCode] + pressed_keys[human_name] = false + if(on_keyup_callbacks[human_name]) { + on_keyup_callbacks[human_name]() + e.preventDefault() + } + if(prevent_default_keys[human_name]) { e.preventDefault() } +} + +// handle event "onkeydown" by remembering what key was un-pressed +function handleKeyDown(e) { + event = (e) ? e : window.event + var human_name = keycode_to_string[event.keyCode] + pressed_keys[human_name] = true + if(on_keydown_callbacks[human_name]) { + on_keydown_callbacks[human_name]() + e.preventDefault() + } + if(prevent_default_keys[human_name]) { e.preventDefault() } + + // jaws.log(event.type + " - " + event.keyCode + " " + keycode_to_string[event.keyCode]); + // e.preventDefault(); +} + + +var prevent_default_keys = [] +jaws.preventDefaultKeys = function(array_of_strings) { + array_of_strings.forEach( function(item, index) { + prevent_default_keys[item] = true + }); +} + +/* + * helper to check if a given key currently is pressed. returns true or false. + */ +jaws.pressed = function(string) { + return pressed_keys[string] +} + +jaws.on_keydown = function(key, callback) { + if(jaws.isArray(key)) { + for(var i=0; key[i]; i++) { + on_keydown_callbacks[key[i]] = callback + } + } + else { + on_keydown_callbacks[key] = callback + } +} + +jaws.on_keyup = function(key, callback) { + if(jaws.isArray(key)) { + for(var i=0; key[i]; i++) { + on_keyup_callbacks[key[i]] = callback + } + } + else { + on_keyup_callbacks[key] = callback + } +} + +/* Clean up all callbacks set by on_keydown / on_keyup */ +jaws.clearKeyCallbacks = function() { + on_keyup_callbacks = [] + on_keydown_callbacks = [] +} + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* + * jaws.Assets() + * + * Mass load / processing of assets (images, sound, video, json) + * + */ +jaws.Assets = function() { + this.loaded = [] // Hash of all URLs that's been loaded + this.loading = [] // Hash of all URLs currently loading + this.src_list = [] // Hash of all unloaded URLs that loadAll() will try to load + this.data = [] // Hash of loaded raw asset data, URLs are keys + + this.image_to_canvas = true + this.fuchia_to_transparent = true + this.root = "" + + this.file_type = {} + this.file_type["json"] = "json" + this.file_type["wav"] = "audio" + this.file_type["mp3"] = "audio" + this.file_type["ogg"] = "audio" + this.file_type["png"] = "image" + this.file_type["jpg"] = "image" + this.file_type["jpeg"] = "image" + this.file_type["gif"] = "image" + this.file_type["bmp"] = "image" + this.file_type["tiff"] = "image" + var that = this + + this.length = function() { + return this.src_list.length + } + + /* + * Get one or many resources + * + * @param String or Array of strings + * @returns The raw resource or an array of resources + * + */ + this.get = function(src) { + if(jaws.isArray(src)) { + return src.map( function(i) { return that.data[i] } ) + } + else { + if(this.loaded[src]) { return this.data[src] } + else { jaws.log("No such asset: " + src) } + } + } + + this.isLoading = function(src) { + return this.loading[src] + } + + this.isLoaded = function(src) { + return this.loaded[src] + } + + this.getPostfix = function(src) { + postfix_regexp = /\.([a-zA-Z]+)/; + return postfix_regexp.exec(src)[1] + } + + this.getType = function(src) { + var postfix = this.getPostfix(src) + return (this.file_type[postfix] ? this.file_type[postfix] : postfix) + } + + /* Add array of paths or single path to asset-list. Later load with loadAll() */ + this.add = function(src) { + if(jaws.isArray(src)) { for(var i=0; src[i]; i++) { this.add(src[i]) } } + else { src = this.root + src; this.src_list.push(src) } + return this + } + + /* Load all assets */ + this.loadAll = function(options) { + this.load_count = 0 + this.error_count = 0 + + /* With these 3 callbacks you can display progress and act when all assets are loaded */ + this.onload = options.onload + this.onerror = options.onerror + this.onfinish = options.onfinish + + for(i=0; this.src_list[i]; i++) { + this.load(this.src_list[i]) + } + } + + /* Calls onload right away if asset is available since before, otherwise try to load it */ + this.getOrLoad = function(src, onload, onerror) { + if(this.data[src]) { onload() } + else { this.load(src, onload, onerror) } + } + + /* Load one asset-object, i.e: {src: "foo.png"} */ + this.load = function(src, onload, onerror) { + var asset = {} + asset.src = src + asset.onload = onload + asset.onerror = onerror + this.loading[src] = true + + switch(this.getType(asset.src)) { + case "image": + var src = asset.src + "?" + parseInt(Math.random()*10000000) + asset.image = new Image() + asset.image.asset = asset // enables us to access asset in the callback + asset.image.onload = this.assetLoaded + asset.image.onerror = this.assetError + asset.image.src = src + break; + case "audio": + var src = asset.src + "?" + parseInt(Math.random()*10000000) + asset.audio = new Audio(src) + asset.audio.asset = asset // enables us access asset in the callback + this.data[asset.src] = asset.audio + asset.audio.addEventListener("canplay", this.assetLoaded, false); + asset.audio.addEventListener("error", this.assetError, false); + asset.audio.load() + break; + default: + var src = asset.src + "?" + parseInt(Math.random()*10000000) + var req = new XMLHttpRequest() + req.asset = asset // enables us access asset in the callback + req.onreadystatechange = this.assetLoaded + req.open('GET', src, true) + req.send(null) + break; + } + } + + /* + * Callback for all asset-loading. + * 1) Parse data depending on filetype. Images are (optionally) converted to canvas-objects. json are parsed into native objects and so on. + * 2) Save processed data in internal list for easy fetching with assets.get(src) later on + * 3) Call callbacks if defined + */ + this.assetLoaded = function(e) { + var asset = this.asset + var src = asset.src + var filetype = that.getType(asset.src) + + // Keep loading and loaded hash up to date + that.loaded[src] = true + that.loading[src] = false + + // Process data depending differently on postfix + if(filetype == "json") { + if (this.readyState != 4) { return } + that.data[asset.src] = JSON.parse(this.responseText) + } + else if(filetype == "image") { + var new_image = that.image_to_canvas ? imageToCanvas(asset.image) : asset.image + if(that.fuchia_to_transparent && that.getPostfix(asset.src) == "bmp") { new_image = fuchiaToTransparent(new_image) } + that.data[asset.src] = new_image + } + else if(filetype == "audio") { + asset.audio.removeEventListener("canplay", that.assetLoaded, false); + that.data[asset.src] = asset.audio + } + + that.load_count++ + if(asset.onload) { asset.onload() } // single asset load()-callback + that.processCallbacks(asset) + } + + this.assetError = function(e) { + var asset = this.asset + that.error_count++ + if(asset.onerror) { asset.onerror(asset) } + that.processCallbacks(asset) + } + + this.processCallbacks = function(asset) { + var percent = parseInt( (that.load_count+that.error_count) / that.src_list.length * 100) + if(that.onload) { that.onload(asset.src, percent) } // loadAll() - single asset has loaded callback + + // When loadAll() is 100%, call onfinish() and kill callbacks (reset with next loadAll()-call) + if(percent==100) { + if(that.onfinish) { that.onfinish() } + that.onload = null + that.onerror = null + that.onfinish = null + } + } +} + +/* + * Takes an image, returns a canvas. + * Benchmarks has proven canvas to be faster to work with then images. + * Returns: a canvas + */ +function imageToCanvas(image) { + var canvas = document.createElement("canvas") + canvas.src = image.src // Make canvas look more like an image + canvas.width = image.width + canvas.height = image.height + + var context = canvas.getContext("2d") + context.drawImage(image, 0, 0, image.width, image.height) + return canvas +} + +/* + * Make Fuchia (0xFF00FF) transparent + * This is the de-facto standard way to do transparency in BMPs + * Returns: a canvas + */ +function fuchiaToTransparent(image) { + canvas = jaws.isImage(image) ? imageToCanvas(image) : image + var context = canvas.getContext("2d") + var img_data = context.getImageData(0,0,canvas.width,canvas.height) + var pixels = img_data.data + for(var i = 0; i < pixels.length; i += 4) { + if(pixels[i]==255 && pixels[i+1]==0 && pixels[i+2]==255) { // Color: Fuchia + pixels[i+3] = 0 // Set total see-through transparency + } + } + context.putImageData(img_data,0,0); + return canvas +} + +/* Scale image by factor and keep jaggy retro-borders */ +function retroScale(image, factor) { + canvas = jaws.isImage(image) ? imageToCanvas(image) : image + var context = canvas.getContext("2d") + var img_data = context.getImageData(0,0,canvas.width,canvas.height) + var pixels = img_data.data + + var canvas2 = document.createElement("canvas") + canvas2.width = image.width * factor + canvas2.height = image.height * factor + var context2 = canvas.getContext("2d") + var img_data2 = context2.getImageData(0,0,canvas2.width,canvas2.height) + var pixels2 = img_data2.data + + for (var x = 0; x < canvas.width * factor; x++) { + for (var y = 0; y < canvas.height * factor; y++) { + pixels2[x*y] = pixels[x*y / factor] + pixels2[x*y+1] = pixels[x*y+1 / factor] + pixels2[x*y+2] = pixels[x*y+2 / factor] + pixels2[x*y+3] = pixels[x*y+3 / factor] + } + } + + context2.putImageData(img_data2,0,0); + return canvas2 +} + +jaws.assets = new jaws.Assets() + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* + * + * GameLoop + * + * function draw() { + * ... your stuff executed every 30 FPS ... + * } + * + * gameloop = jaws.GameLoop(setup, update, draw, 30) + * gameloop.start() + * + * gameloop.start() starts a 2-step process, where first all assets are loaded. + * Then the real gameloop is started with the userspecified FPS. + * + * If using the shorter jaws.init() a GameLoop will automatically be created and started for you. + * + */ +jaws.GameLoop = function(setup, update, draw, wanted_fps) { + this.ticks = 0 + this.tick_duration = 0 + this.fps = 0 + + var update_id + var paused = false + var that = this + var mean_value = new MeanValue(20) // let's have a smooth, non-jittery FPS-value + + this.start = function() { + jaws.log("gameloop start", true) + this.current_tick = (new Date()).getTime(); + this.last_tick = (new Date()).getTime(); + if(setup) { setup() } + update_id = setInterval(this.loop, 1000 / wanted_fps); + jaws.log("gameloop loop", true) + } + + this.loop = function() { + that.current_tick = (new Date()).getTime(); + that.tick_duration = that.current_tick - that.last_tick + //that.fps = parseInt(1000 / that.tick_duration) + that.fps = mean_value.add(1000/that.tick_duration).get() + + if(!paused) { + if(update) { update() } + if(draw) { draw() } + that.ticks++ + } + + that.last_tick = that.current_tick; + } + + this.pause = function() { paused = true } + this.unpause = function() { paused = false } + + this.stop = function() { + if(update_id) { clearInterval(update_id); } + } +} + +function MeanValue(size) { + this.size = size + this.values = new Array(this.size) + this.value + + this.add = function(value) { + if(this.values.length > this.size) { // is values filled? + this.values.splice(0,1) + this.value = 0 + for(var i=0; this.values[i]; i++) { + this.value += this.values[i] + } + this.value = this.value / this.size + } + this.values.push(value) + + return this + } + + this.get = function() { + return parseInt(this.value) + } + +} + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* + * A bread and butter Rect() - useful for basic collision detection + */ +jaws.Rect = function(x,y,width,height) { + this.x = x + this.y = y + this.width = width + this.height = height + this.right = x + width + this.bottom = y + height +} + +jaws.Rect.prototype.getPosition = function() { + return [this.x, this.y] +} + +jaws.Rect.prototype.move = function(x,y) { + this.x += x + this.y += y + this.right += x + this.bottom += y +} + +jaws.Rect.prototype.moveTo = function(x,y) { + this.x = x + this.y = y + this.right = this.x + this.width + this.bottom = this.y + this.height + return this +} + +jaws.Rect.prototype.resize = function(width,height) { + this.width += width + this.height += height + this.right = this.x + this.width + this.bottom = this.y + this.height + return this +} + +jaws.Rect.prototype.resizeTo = function(width,height) { + this.width = width + this.height = height + this.right = this.x + this.width + this.bottom = this.y + this.height + return this +} + +// Draw a red rectangle, useful for debug +jaws.Rect.prototype.draw = function() { + jaws.context.strokeStyle = "red" + jaws.context.strokeRect(this.x, this.y, this.width, this.height) + return this +} + +// Returns true if point at x, y lies within calling rect +jaws.Rect.prototype.collidePoint = function(x, y) { + return (x >= this.x && x <= this.right && y >= this.y && y <= this.bottom) +} + +// Returns true if calling rect overlaps with given rect in any way +jaws.Rect.prototype.collideRect = function(rect) { + return ((this.x >= rect.x && this.x <= rect.right) || (rect.x >= this.x && rect.x <= this.right)) && + ((this.y >= rect.y && this.y <= rect.bottom) || (rect.y >= this.y && rect.y <= this.bottom)) +} + +/* +// Possible future functions +jaws.Rect.prototype.collideRightSide = function(rect) { return(this.right >= rect.x && this.x < rect.x) } +jaws.Rect.prototype.collideLeftSide = function(rect) { return(this.x > rect.x && this.x <= rect.right) } +jaws.Rect.prototype.collideTopSide = function(rect) { return(this.y >= rect.y && this.y <= rect.bottom) } +jaws.Rect.prototype.collideBottomSide = function(rect) { return(this.bottom >= rect.y && this.y < rect.y) } +*/ + +jaws.Rect.prototype.toString = function() { return "[Rect " + this.x + ", " + this.y + ", " + this.width + ", " + this.height + "]" } + +return jaws; +})(jaws || {}); + +// Support CommonJS require() +if(typeof module !== "undefined" && ('exports' in module)) { module.exports = jaws.Rect } + +/* + * + * When we wan't to move something visible around on the screen :). + * + * + */ +var jaws = (function(jaws) { + +jaws.Sprite = function(options) { + this.options = options + this.set(options) + this.context = options.context || jaws.context + if(!this.context) { this.createDiv() } // No canvas context? Switch to DOM-based spritemode +} + +/* Call setters from JSON object. Used to parse options. */ +jaws.Sprite.prototype.set = function(options) { + this.scale_factor_x = this.scale_factor_y = (options.scale || 1) + if(!options.anchor_x == undefined) {this.anchor_x = options.anchor_x} + if(!options.anchor_y == undefined) {this.anchor_y = options.anchor_y} + this.x = options.x || 0 + this.y = options.y || 0 + this.alpha = options.alpha || 1 + this.angle = options.angle || 0 + this.flipped = options.flipped || false + this.anchor(options.anchor || "top_left") + options.image && this.setImage(options.image) + this.cacheOffsets() + return this +} + +/* +// +// Chainable setters under consideration: +// +jaws.Sprite.prototype.setFlipped = function(value) { this.flipped = value; return this } +jaws.Sprite.prototype.setAlpha = function(value) { this.alpha = value; return this } +jaws.Sprite.prototype.setAnchorX = function(value) { this.anchor_x = value; this.cacheOffsets(); return this } +jaws.Sprite.prototype.setAnchorY = function(value) { this.anchor_y = value; this.cacheOffsets(); return this } +jaws.Sprite.prototype.setAngle = function(value) { this.angle = value; return this } +jaws.Sprite.prototype.setScaleFactor = function(value) { this.scale_factor_x = this.scale_factor_y = value; this.cacheOffsets(); return this } +jaws.Sprite.prototype.setScaleFactorX = function(value) { this.scale_factor_x = value; this.cacheOffsets(); return this } +jaws.Sprite.prototype.setScaleFactorY = function(value) { this.scale_factor_y = value; this.cacheOffsets(); return this } +jaws.Sprite.prototype.moveX = function(x) { this.x += x; return this } +jaws.Sprite.prototype.moveXTo = function(x) { this.x = x; return this } +jaws.Sprite.prototype.moveY = function(y) { this.y += y; return this } +jaws.Sprite.prototype.moveYTo = function(y) { this.y = y; return this } +jaws.Sprite.prototype.scaleWidthTo = function(value) { this.scale_factor_x = value; return this.cacheOffsets() } +jaws.Sprite.prototype.scaleHeightTo = function(value) { this.scale_factor_y = value; return this.cachOfffsets() } +*/ + +/* Sprite modifiers. Modifies 1 or more properties and returns this for chainability. */ +jaws.Sprite.prototype.setImage = function(value) { + var that = this + + // An image, great, set this.image and return + if(jaws.isDrawable(value)) { + this.image = value + return this.cacheOffsets() + } + // Not an image, therefore an asset string, i.e. "ship.bmp" + else { + // Assets already loaded? Set this.image + if(jaws.assets.isLoaded(value)) { this.image = jaws.assets.get(value); this.cacheOffsets(); } + + // Not loaded? Load it with callback to set image. + else { jaws.assets.load(value, function() { that.image = jaws.assets.get(value); that.cacheOffsets(); }) } + } + return this +} +jaws.Sprite.prototype.flip = function() { this.flipped = this.flipped ? false : true; return this } +jaws.Sprite.prototype.flipTo = function(value) { this.flipped = value; return this } +jaws.Sprite.prototype.rotate = function(value) { this.angle += value; return this } +jaws.Sprite.prototype.rotateTo = function(value) { this.angle = value; return this } +jaws.Sprite.prototype.moveTo = function(x,y) { this.x = x; this.y = y; return this } +jaws.Sprite.prototype.move = function(x,y) { if(x) this.x += x; if(y) this.y += y; return this } +jaws.Sprite.prototype.scale = function(value) { this.scale_factor_x *= value; this.scale_factor_y *= value; return this.cacheOffsets() } +jaws.Sprite.prototype.scaleTo = function(value) { this.scale_factor_x = this.scale_factor_y = value; return this.cacheOffsets() } +jaws.Sprite.prototype.scaleWidth = function(value) { this.scale_factor_x *= value; return this.cacheOffsets() } +jaws.Sprite.prototype.scaleHeight = function(value) { this.scale_factor_y *= value; return this.cacheOffsets() } +jaws.Sprite.prototype.setX = function(value) { this.x = value; return this } +jaws.Sprite.prototype.setY = function(value) { this.y = value; return this } +jaws.Sprite.prototype.setWidth = function(value) { this.scale_factor_x = value/this.image.width; return this.cacheOffsets() } +jaws.Sprite.prototype.setHeight = function(value) { this.scale_factor_y = value/this.image.height; return this.cacheOffsets() } +jaws.Sprite.prototype.resize = function(width, height) { + this.scale_factor_x = (this.width + width) / this.image.width + this.scale_factor_y = (this.height + height) / this.image.height + return this.cacheOffsets() +} +jaws.Sprite.prototype.resizeTo = function(width, height) { + this.scale_factor_x = width / this.image.width + this.scale_factor_y = height / this.image.height + return this.cacheOffsets() +} + +/* +* The sprites anchor could be describe as "the part of the sprite will be placed at x/y" +* or "when rotating, what point of the of the sprite will it rotate round" +* +* For example, a topdown shooter could use anchor("center") --> Place middle of the ship on x/y +* .. and a sidescroller would probably use anchor("center_bottom") --> Place "feet" at x/y +*/ +jaws.Sprite.prototype.anchor = function(value) { + var anchors = { + top_left: [0,0], + left_top: [0,0], + center_left: [0,0.5], + left_center: [0,0.5], + bottom_left: [0,1], + left_bottom: [0,1], + top_center: [0.5,0], + center_top: [0.5,0], + center_center: [0.5,0.5], + center: [0.5,0.5], + bottom_center: [0.5,1], + center_bottom: [0.5,1], + top_right: [1,0], + right_top: [1,0], + center_right: [1,0.5], + right_center: [1,0.5], + bottom_right: [1,1], + right_bottom: [1,1] + } + + if(a = anchors[value]) { + this.anchor_x = a[0] + this.anchor_y = a[1] + if(this.image) this.cacheOffsets(); + } + return this +} + +jaws.Sprite.prototype.cacheOffsets = function() { + if(!this.image) { return } + + this.width = this.image.width * this.scale_factor_x + this.height = this.image.height * this.scale_factor_y + this.left_offset = this.width * this.anchor_x + this.top_offset = this.height * this.anchor_y + this.right_offset = this.width * (1.0 - this.anchor_x) + this.bottom_offset = this.height * (1.0 - this.anchor_y) + + if(this.cached_rect) this.cached_rect.resizeTo(this.width, this.height); + return this +} + +/* Saves a Rect() perfectly surrouning our sprite in this.cached_rect and returns it */ +jaws.Sprite.prototype.rect = function() { + if(!this.cached_rect) this.cached_rect = new jaws.Rect(this.x, this.top, this.width, this.height) + this.cached_rect.moveTo(this.x - this.left_offset, this.y - this.top_offset) + return this.cached_rect +} + +/* Make this sprite a DOM-based <div> sprite */ +jaws.Sprite.prototype.createDiv = function() { + this.div = document.createElement("div") + this.div.style.position = "absolute" + if(this.image) { + this.div.style.width = this.image.width + "px" + this.div.style.height = this.image.height + "px" + this.div.style.backgroundImage = "url(" + this.image.src + ")" + } + if(jaws.dom) { jaws.dom.appendChild(this.div) } + this.updateDiv() +} + +/* Update properties for DOM-based sprite */ +jaws.Sprite.prototype.updateDiv = function() { + this.div.style.left = this.x + "px" + this.div.style.top = this.y + "px" + + var transform = "" + transform += "rotate(" + this.angle + "deg) " + if(this.flipped) { transform += "scale(-" + this.scale_factor_x + "," + this.scale_factor_y + ")"; } + else { transform += "scale(" + this.scale_factor_x + "," + this.scale_factor_y + ")"; } + + this.div.style.MozTransform = transform + this.div.style.WebkitTransform = transform + this.div.style.transform = transform + return this +} + +// Draw the sprite on screen via its previously given context +jaws.Sprite.prototype.draw = function() { + if(!this.image) { return this } + if(jaws.dom) { return this.updateDiv() } + + this.context.save() + this.context.translate(this.x, this.y) + if(this.angle!=0) { jaws.context.rotate(this.angle * Math.PI / 180) } + this.flipped && this.context.scale(-1, 1) + this.context.globalAlpha = this.alpha + this.context.translate(-this.left_offset, -this.top_offset) // Needs to be separate from above translate call cause of flipped + this.context.drawImage(this.image, 0, 0, this.width, this.height) + this.context.restore() + return this +} + +// Create a new canvas context, draw sprite on it and return. Use to get a raw canvas copy of the current sprite state. +jaws.Sprite.prototype.asCanvasContext = function() { + var canvas = document.createElement("canvas") + canvas.width = this.width + canvas.height = this.height + + var context = canvas.getContext("2d") + context.mozImageSmoothingEnabled = jaws.context.mozImageSmoothingEnabled + + context.drawImage(this.image, 0, 0, this.width, this.height) + return context +} + +jaws.Sprite.prototype.toString = function() { return "[Sprite " + this.x + ", " + this.y + "," + this.width + "," + this.height + "]" } + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* + * + * Constructor to manage your Sprites. + * + * Sprites (your bullets, aliens, enemies, players etc) will need to be + * updated, draw, deleted. Often in various orders and based on different conditions. + * + * This is where SpriteList() comes in. + * + * var enemies = new SpriteList() + * + * for(i=0; i < 100; i++) { // create 100 enemies + * enemies.push(new Sprite({image: "enemy.png", x: i, y: 200})) + * } + * enemies.draw() // calls draw() on all enemies + * enemies.deleteIf(isOutsideCanvas) // deletes each item in enemies that returns true when isOutsideCanvas(item) is called + * enemies.drawIf(isInsideViewport) // only call draw() on items that returns true when isInsideViewport is called with item as argument + * + */ + +jaws.SpriteList = function() {} +jaws.SpriteList.prototype = new Array + +jaws.SpriteList.prototype.remove = function(obj) { + var index = this.indexOf(obj) + if(index > -1) { this.splice(index, 1) } +} + +jaws.SpriteList.prototype.draw = function() { + for(i=0; this[i]; i++) { + this[i].draw() + } +} + +jaws.SpriteList.prototype.drawIf = function(condition) { + for(i=0; this[i]; i++) { + if( condition(this[i]) ) { this[i].draw() } + } +} + +jaws.SpriteList.prototype.update = function() { + for(i=0; this[i]; i++) { + this[i].update() + } +} + +jaws.SpriteList.prototype.updateIf = function(condition) { + for(i=0; this[i]; i++) { + if( condition(this[i]) ) { this[i].update() } + } +} + +jaws.SpriteList.prototype.deleteIf = function(condition) { + for(var i=0; this[i]; i++) { + if( condition(this[i]) ) { this.splice(i,1) } + } +} +jaws.SpriteList.prototype.toString = function() { return "[SpriteList " + this.length + " sprites]" } + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* Cut out a rectangular piece of a an image, returns as canvas-element */ +function cutImage(image, x, y, width, height) { + var cut = document.createElement("canvas") + cut.width = width + cut.height = height + + var ctx = cut.getContext("2d") + ctx.drawImage(image, x, y, width, height, 0, 0, cut.width, cut.height) + + return cut +}; + +/* Cut up into frame_size pieces and put them in frames[] */ +jaws.SpriteSheet = function(options) { + this.image = jaws.isDrawable(options.image) ? options.image : jaws.assets.data[options.image] + this.orientation = options.orientation || "right" + this.frame_size = options.frame_size || [32,32] + this.frames = [] + + var index = 0 + for(var x=0; x < this.image.width; x += this.frame_size[0]) { + for(var y=0; y < this.image.height; y += this.frame_size[1]) { + this.frames.push( cutImage(this.image, x, y, this.frame_size[0], this.frame_size[1]) ) + } + } +} + +jaws.SpriteSheet.prototype.toString = function() { return "[SpriteSheet " + this.frames.length + " frames]" } + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +jaws.Parallax = function(options) { + this.scale = options.scale || 1 + this.repeat_x = options.repeat_x + this.repeat_y = options.repeat_y + this.camera_x = options.camera_x || 0 + this.camera_y = options.camera_y || 0 + this.layers = [] +} + +jaws.Parallax.prototype.draw = function(options) { + var layer, save_x, save_y; + + for(var i=0; i < this.layers.length; i++) { + layer = this.layers[i] + + save_x = layer.x + save_y = layer.y + + layer.x = -(this.camera_x / layer.damping) + layer.y = -(this.camera_y / layer.damping) + + while(this.repeat_x && layer.x > 0) { layer.x -= layer.width } + while(this.repeat_y && layer.y > 0) { layer.y -= layer.width } + + while(this.repeat_x && layer.x < jaws.width) { + while(this.repeat_y && layer.y < jaws.height) { + layer.draw() + layer.y += layer.height + } + layer.y = save_y + layer.draw() + layer.x += (layer.width-1) // -1 to compensate for glitches in repeating tiles + } + while(layer.repeat_y && !layer.repeat_x && layer.y < jaws.height) { + layer.draw() + layer.y += layer.height + } + layer.x = save_x + } +} +jaws.Parallax.prototype.addLayer = function(options) { + var layer = new jaws.ParallaxLayer(options) + layer.scale(this.scale) + this.layers.push(layer) +} +jaws.Parallax.prototype.toString = function() { return "[Parallax " + this.x + ", " + this.y + ". " + this.layers.length + " layers]" } + +jaws.ParallaxLayer = function(options) { + this.damping = options.damping || 0 + jaws.Sprite.call(this, options) +} +jaws.ParallaxLayer.prototype = jaws.Sprite.prototype +jaws.Parallax.prototype.toString = function() { return "[ParallaxLayer " + this.x + ", " + this.y + "]" } + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* + * + * Animation() + * + * Manages animation with a given list of frames and durations + * Takes a object as argument: + * + * loop: true|false - restart animation when end is reached + * bounce: true|false - rewind the animation frame by frame when end is reached + * index: int - start on this frame + * frames array - array of image/canvas items + * frame_duration int - how long should each frame be displayed + * + */ +jaws.Animation = function(options) { + this.options = options + this.frames = options.frames || [] + this.frame_duration = options.frame_duration || 100 // default: 100ms between each frameswitch + this.index = options.index || 0 // default: start with the very first frame + this.loop = options.loop || 1 + this.bounce = options.bounce || 0 + this.frame_direction = 1 + + if(options.sprite_sheet) { + var image = (jaws.isDrawable(options.sprite_sheet) ? options.sprite_sheet : jaws.assets.get(options.sprite_sheet)) + var sprite_sheet = new jaws.SpriteSheet({image: image, frame_size: options.frame_size}) + this.frames = sprite_sheet.frames + } + + /* Initializing timer-stuff */ + this.current_tick = (new Date()).getTime(); + this.last_tick = (new Date()).getTime(); + this.sum_tick = 0 +} + +// Propells the animation forward by counting milliseconds and changing this.index accordingly +// Supports looping and bouncing animations. +jaws.Animation.prototype.update = function() { + this.current_tick = (new Date()).getTime(); + this.sum_tick += (this.current_tick - this.last_tick); + this.last_tick = this.current_tick; + + if(this.sum_tick > this.frame_duration) { + this.index += this.frame_direction + this.sum_tick = 0 + } + if( (this.index >= this.frames.length) || (this.index <= 0) ) { + if(this.bounce) { + this.frame_direction = -this.frame_direction + this.index += this.frame_direction*2 + } + else if(this.loop) { + this.index = 0 + } + } + return this +} + +// Like array.slice but returns a new Animation-object with a subset of the frames +jaws.Animation.prototype.slice = function(start, stop) { + var o = {} + o.frame_duration = this.frame_duration + o.loop = this.loop + o.bounce = this.bounce + o.frame_direction = this.frame_direction + o.frames = this.frames.slice().slice(start, stop) + return new jaws.Animation(o) +}; + +// Moves animation forward by calling update() and then return the current frame +jaws.Animation.prototype.next = function() { + this.update() + return this.frames[this.index] +}; + +// returns the current frame +jaws.Animation.prototype.currentFrame = function() { + return this.frames[this.index] +}; + +jaws.Animation.prototype.toString = function() { return "[Animation, " + this.frames.length + " frames]" } + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* + * + * Viewport() is a window (a Rect) into a bigger canvas/image + * + * It won't every go "outside" that image. + * It comes with convenience methods as: + * + * viewport.centerAround(player) which will do just what you think. (player needs to have properties x and y) + * + * + */ +jaws.Viewport = function(options) { + this.options = options + this.context = options.context || jaws.context + this.width = options.width || jaws.width + this.height = options.height || jaws.height + this.max_x = options.max_x || jaws.width + this.max_y = options.max_y || jaws.height + + this.verifyPosition = function() { + var max = this.max_x - this.width + if(this.x < 0) { this.x = 0 } + if(this.x > max) { this.x = max } + + var max = this.max_y - this.height + if(this.y < 0) { this.y = 0 } + if(this.y > max) { this.y = max } + }; + + this.move = function(x, y) { + x && (this.x += x) + y && (this.y += y) + this.verifyPosition() + }; + + this.moveTo = function(x, y) { + if(!(x==undefined)) { this.x = x } + if(!(y==undefined)) { this.y = y } + this.verifyPosition() + }; + + this.isOutside = function(item) { + return(!this.isInside(item)) + }; + + this.isInside = function(item) { + return( item.x >= this.x && item.x <= (this.x + this.width) && item.y >= this.y && item.y <= (this.y + this.height) ) + }; + + this.centerAround = function(item) { + this.x = (item.x - this.width / 2) + this.y = (item.y - this.height / 2) + this.verifyPosition() + }; + + this.apply = function(func) { + this.context.save() + this.context.translate(-this.x, -this.y) + func() + this.context.restore() + }; + + this.moveTo(options.x||0, options.y||0) +} + +jaws.Viewport.prototype.toString = function() { return "[Viewport " + this.x + ", " + this.y + "," + this.width + "," + this.height + "]" } + +return jaws; +})(jaws || {}); + +var jaws = (function(jaws) { + +/* + * TileMap - fast access to tiles + * + * var tile_map = new TileMap({size: [10, 10], cell_size: [16,16]}) + * var sprite = new jaws.Sprite({x: 40, y: 40}) + * var sprite2 = new jaws.Sprite({x: 41, y: 41}) + * tile_map.push(sprite) + * + * tile_map.at(10,10) // [] + * tile_map.at(40,40) // [sprite] + * tile_map.cell(0,0) // [] + * tile_map.cell(1,1) // [sprite] + * + */ +jaws.TileMap = function(options) { + this.cell_size = options.cell_size || [32,32] + this.size = options.size + this.cells = new Array(this.size[0]) + this.sortFunction = undefined + + for(var col=0; col < this.size[0]; col++) { + this.cells[col] = new Array(this.size[1]) + for(var row=0; row < this.size[1]; row++) { + this.cells[col][row] = [] // populate each cell with an empty array + } + } +} + +/* Clear all cells in tile map */ +jaws.TileMap.prototype.clear = function() { + for(var col=0; col < this.size[0]; col++) { + for(var row=0; row < this.size[1]; row++) { + this.cells[col][row] = [] + } + } +} + +/* Sort arrays in each cell in tile map according to sorter-function (see Array.sort) */ +jaws.TileMap.prototype.sortCells = function(sortFunction) { + for(var col=0; col < this.size[0]; col++) { + for(var row=0; row < this.size[1]; row++) { + this.cells[col][row].sort( sortFunction ) + } + } +} + +/* + * Push obj (or array of objs) into our cell-grid. + * + * Tries to read obj.x and obj.y to calculate what cell to occopy + */ +jaws.TileMap.prototype.push = function(obj) { + if(obj.length) { + for(var i=0; i < obj.length; i++) { this.push(obj[i]) } + return obj + } + if(obj.rect) { + return this.pushAsRect(obj, obj.rect()) + } + else { + var col = parseInt(obj.x / this.cell_size[0]) + var row = parseInt(obj.y / this.cell_size[1]) + return this.pushToCell(col, row, obj) + } + +} +jaws.TileMap.prototype.pushAsPoint = function(obj) { + if(Array.isArray(obj)) { + for(var i=0; i < obj.length; i++) { this.pushAsPoint(obj[i]) } + return obj + } + else { + var col = parseInt(obj.x / this.cell_size[0]) + var row = parseInt(obj.y / this.cell_size[1]) + return this.pushToCell(col, row, obj) + } +} + +/* save 'obj' in cells touched by 'rect' */ +jaws.TileMap.prototype.pushAsRect = function(obj, rect) { + var from_col = parseInt(rect.x / this.cell_size[0]) + var to_col = parseInt((rect.right-1) / this.cell_size[0]) + //jaws.log("rect.right: " + rect.right + " from/to col: " + from_col + " " + to_col, true) + + for(var col = from_col; col <= to_col; col++) { + var from_row = parseInt(rect.y / this.cell_size[1]) + var to_row = parseInt((rect.bottom-1) / this.cell_size[1]) + + //jaws.log("rect.bottom " + rect.bottom + " from/to row: " + from_row + " " + to_row, true) + for(var row = from_row; row <= to_row; row++) { + // console.log("pushAtRect() col/row: " + col + "/" + row + " - " + this.cells[col][row]) + this.pushToCell(col, row, obj) + } + } + return obj +} + +/* + * Push obj to a specific cell specified by col and row + * If cell is already occupied we create an array and push to that + */ +jaws.TileMap.prototype.pushToCell = function(col, row, obj) { + this.cells[col][row].push(obj) + if(this.sortFunction) this.cells[col][row].sort(this.sortFunction); + return this +} + +// +// READERS +// + +/* Get objects in cell that exists at coordinates x / y */ +jaws.TileMap.prototype.at = function(x, y) { + var col = parseInt(x / this.cell_size[0]) + var row = parseInt(y / this.cell_size[1]) + // console.log("at() col/row: " + col + "/" + row) + return this.cells[col][row] +} + +/* Returns occupants of all cells touched by 'rect' */ +jaws.TileMap.prototype.atRect = function(rect) { + var objects = [] + var items + var from_col = parseInt(rect.x / this.cell_size[0]) + var to_col = parseInt(rect.right / this.cell_size[0]) + for(var col = from_col; col <= to_col; col++) { + var from_row = parseInt(rect.y / this.cell_size[1]) + var to_row = parseInt(rect.bottom / this.cell_size[1]) + + for(var row = from_row; row <= to_row; row++) { + this.cells[col][row].forEach( function(item, total) { + if(objects.indexOf(item) == -1) { objects.push(item) } + }) + } + } + return objects +} + +/* Returns all objects in tile map */ +jaws.TileMap.prototype.all = function() { + var all = [] + for(var col=0; col < this.size[0]; col++) { + for(var row=0; row < this.size[1]; row++) { + this.cells[col][row].forEach( function(element, total) { + all.push(element) + }); + } + } + return all +} + +/* + * Get objects in cell at col / row + */ +jaws.TileMap.prototype.cell = function(col, row) { + return this.cells[col][row] +} + +jaws.TileMap.prototype.toString = function() { return "[TileMap " + this.size[0] + " cols, " + this.size[1] + " rows]" } + +return jaws; +})(jaws || {}); + +// Support CommonJS require() +if(typeof module !== "undefined" && ('exports' in module)) { module.exports = jaws.TileMap } \ No newline at end of file diff --git a/tetris/json-minified.js b/tetris/json-minified.js new file mode 100644 index 00000000..a92138c3 --- /dev/null +++ b/tetris/json-minified.js @@ -0,0 +1,4 @@ +window.jsonParse=function(){var r="(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)",k='(?:[^\\0-\\x08\\x0a-\\x1f"\\\\]|\\\\(?:["/\\\\bfnrt]|u[0-9A-Fa-f]{4}))';k='(?:"'+k+'*")';var s=new RegExp("(?:false|true|null|[\\{\\}\\[\\]]|"+r+"|"+k+")","g"),t=new RegExp("\\\\(?:([^u])|u(.{4}))","g"),u={'"':'"',"/":"/","\\":"\\",b:"\u0008",f:"\u000c",n:"\n",r:"\r",t:"\t"};function v(h,j,e){return j?u[j]:String.fromCharCode(parseInt(e,16))}var w=new String(""),x=Object.hasOwnProperty;return function(h, +j){h=h.match(s);var e,c=h[0],l=false;if("{"===c)e={};else if("["===c)e=[];else{e=[];l=true}for(var b,d=[e],m=1-l,y=h.length;m<y;++m){c=h[m];var a;switch(c.charCodeAt(0)){default:a=d[0];a[b||a.length]=+c;b=void 0;break;case 34:c=c.substring(1,c.length-1);if(c.indexOf("\\")!==-1)c=c.replace(t,v);a=d[0];if(!b)if(a instanceof Array)b=a.length;else{b=c||w;break}a[b]=c;b=void 0;break;case 91:a=d[0];d.unshift(a[b||a.length]=[]);b=void 0;break;case 93:d.shift();break;case 102:a=d[0];a[b||a.length]=false; +b=void 0;break;case 110:a=d[0];a[b||a.length]=null;b=void 0;break;case 116:a=d[0];a[b||a.length]=true;b=void 0;break;case 123:a=d[0];d.unshift(a[b||a.length]={});b=void 0;break;case 125:d.shift();break}}if(l){if(d.length!==1)throw new Error;e=e[0]}else if(d.length)throw new Error;if(j){var p=function(n,o){var f=n[o];if(f&&typeof f==="object"){var i=null;for(var g in f)if(x.call(f,g)&&f!==n){var q=p(f,g);if(q!==void 0)f[g]=q;else{i||(i=[]);i.push(g)}}if(i)for(g=i.length;--g>=0;)delete f[i[g]]}return j.call(n, +o,f)};e=p({"":e},"")}return e}}(); diff --git a/tetris/media/background/backdrop.png b/tetris/media/background/backdrop.png new file mode 100644 index 00000000..328e199e Binary files /dev/null and b/tetris/media/background/backdrop.png differ diff --git a/tetris/media/background/endconsole.png b/tetris/media/background/endconsole.png new file mode 100644 index 00000000..a971bd30 Binary files /dev/null and b/tetris/media/background/endconsole.png differ diff --git a/tetris/media/background/logo.png b/tetris/media/background/logo.png new file mode 100644 index 00000000..861a45ef Binary files /dev/null and b/tetris/media/background/logo.png differ diff --git a/tetris/media/background/topbar.png b/tetris/media/background/topbar.png new file mode 100644 index 00000000..1df90898 Binary files /dev/null and b/tetris/media/background/topbar.png differ diff --git a/tetris/media/blueblock.png b/tetris/media/blueblock.png new file mode 100644 index 00000000..adbd06a1 Binary files /dev/null and b/tetris/media/blueblock.png differ diff --git a/tetris/media/buttons/continue.png b/tetris/media/buttons/continue.png new file mode 100644 index 00000000..d27a6204 Binary files /dev/null and b/tetris/media/buttons/continue.png differ diff --git a/tetris/media/buttons/restart.png b/tetris/media/buttons/restart.png new file mode 100644 index 00000000..8f4c714c Binary files /dev/null and b/tetris/media/buttons/restart.png differ diff --git a/tetris/media/cyanblock.png b/tetris/media/cyanblock.png new file mode 100644 index 00000000..04dd5db0 Binary files /dev/null and b/tetris/media/cyanblock.png differ diff --git a/tetris/media/emptyblock.png b/tetris/media/emptyblock.png new file mode 100644 index 00000000..18353684 Binary files /dev/null and b/tetris/media/emptyblock.png differ diff --git a/tetris/media/greenblock.png b/tetris/media/greenblock.png new file mode 100644 index 00000000..7862bc40 Binary files /dev/null and b/tetris/media/greenblock.png differ diff --git a/tetris/media/greyblock.png b/tetris/media/greyblock.png new file mode 100644 index 00000000..1dcf8ffd Binary files /dev/null and b/tetris/media/greyblock.png differ diff --git a/tetris/media/orangeblock.png b/tetris/media/orangeblock.png new file mode 100644 index 00000000..815bd39b Binary files /dev/null and b/tetris/media/orangeblock.png differ diff --git a/tetris/media/purpleblock.png b/tetris/media/purpleblock.png new file mode 100644 index 00000000..5dede4c0 Binary files /dev/null and b/tetris/media/purpleblock.png differ diff --git a/tetris/media/redblock.png b/tetris/media/redblock.png new file mode 100644 index 00000000..4a702694 Binary files /dev/null and b/tetris/media/redblock.png differ diff --git a/tetris/media/yellowblock.png b/tetris/media/yellowblock.png new file mode 100644 index 00000000..c1c0ffab Binary files /dev/null and b/tetris/media/yellowblock.png differ diff --git a/tetris/scoreScreen.html b/tetris/scoreScreen.html new file mode 100644 index 00000000..20ffc8ef --- /dev/null +++ b/tetris/scoreScreen.html @@ -0,0 +1,69 @@ +<html> + <head> + <title>TwitchTetris + + + + + + + + + + + + + + +
+
+
+ Congratulations! You have made a top Score!
+ Enter your name and hit ENTER to submit your name!
+ + +
+

+ +
+ Play Again +
+
+ +
+ + \ No newline at end of file diff --git a/tetris/scoreScreen.js b/tetris/scoreScreen.js new file mode 100644 index 00000000..dc9a6daa --- /dev/null +++ b/tetris/scoreScreen.js @@ -0,0 +1,88 @@ +function $_GET(q) { + var s = window.location.search; + var re = new RegExp('&'+q+'(?:=([^&]*))?(?=&|$)','i'); + return (s=s.replace(/^\?/,'&').match(re)) ? (typeof s[1] == 'undefined' ? '' : decodeURIComponent(s[1])) : undefined; +} + +function getXmlHttp() { + if (window.XMLHttpRequest) + {// code for IE7+, Firefox, Chrome, Opera, Safari + return new XMLHttpRequest(); + } + else + {// code for IE6, IE5 + return new ActiveXObject("Microsoft.XMLHTTP"); + } +} + +function scoreScreenOnLoad() { + var sessionRef = $_GET('tempRef'); + + var xmlhttp = getXmlHttp(); + xmlhttp.onreadystatechange=function() + { + if (xmlhttp.readyState==4 && xmlhttp.status==200) + { + var response = jsonParse(xmlhttp.responseText), + ranked = false; + output = '

GOOD GAME!


'; + + output += ''; + + output += ''; + if (response.dailyRank > 0) { + output += ''; + ranked = true; + + } + if (response.totalRank > 0) { + output += ''; + ranked = true; + } + output += '
Score:' + response.userScore + '
Daily Rank:' + + response.dailyRank + '
Total Rank:' + + response.totalRank + '



'; + + document.getElementById("scoreDiv").innerHTML = output; + + // if ranked, prompt for a name + if (ranked) { + document.getElementById("applyNameDiv").setAttribute('class', 'applyNameVisible'); + } + } + } + + xmlhttp.open("POST", "/score/postGame?tempRef="+sessionRef, true); + xmlhttp.send(); +} + +function nameKeyDown(e) { + var keycode; + if (window.event) { //IE + keycode = e.keyCode; + } else { + keycode = e.which; + } + if (keycode === 13) { // if the enter key + applyName(); + } +} + +function applyName() { + var sessionRef = $_GET('tempRef'); + var name = document.getElementById("nameInput").value; + + if (name.length < 1 || sessionRef.length < 1) return; + + document.getElementById("applyNameDiv").setAttribute('class', 'applyNameHidden'); + + var xmlhttp = getXmlHttp(); + xmlhttp.open("POST", "/score/apply?tempRef="+sessionRef+"&name="+name, true); + xmlhttp.send(); + + return false; +} + +function trySubmitName() { + applyName(); +} \ No newline at end of file diff --git a/tetris/styles.css b/tetris/styles.css new file mode 100644 index 00000000..f4cdbdfc --- /dev/null +++ b/tetris/styles.css @@ -0,0 +1,289 @@ +/** PAGE ELEMENTS **/ + +.menuCell +{ + background-color: #001d00; + vertical-align: top; + height: 600px; + color: #000000; +} + + +.instructions +{ + margin: 0 5; + font-size: 15px; + font-family: VT323; + color: #008000; + background-color: #000d00; + border: 1px solid #008000; + border-radius: 5px; + padding: 5px; + width: 140; +} + +.menu +{ + width: 160; +} + +.menuLogo +{ + width: 150px; + margin: 5 5; + text-align: center; +} + +.menuItem +{ + font-family: VT323; + margin: 0 auto; + border: 1px solid #008000; + border-radius: 5px; + background-color: #000d00; + color: #008000; + padding: 5px; + width: 140px; + font-size: 18px; +} + +.scoreArea +{ + width: 500px; +} + +.playAgainButton +{ + font-family: VT323; + text-align: center; + margin: 0 auto; + border: 1px solid #008000; + border-radius: 5px; + background-color: #000d00; + color: #008000; + padding: 5px; + width: 140px; + font-size: 18px; +} + +a.bareLink:link +{ + text-decoration: none; +} +a.bareLink:active +{ + text-decoration: none; +} +a.bareLink:visited +{ + text-decoration: none; +} + +.selectedMenuItem +{ + color: #000d00; + background-color: #008000; +} + +.menuItem:hover +{ + color: #c0c0c0; +} + +body +{ + background-color: #000000; +} + +.aboutText +{ + margin: 5 5; + font-size: 16px; + font-family: VT323; + color: #008000; + background-color: #000d00; + border: 1px solid #008000; + border-radius: 5px; + padding: 5px; + width: 580; +} + +.contentCell +{ + background-color: #001d00; + color: #c0c0c0; + font-family: VT323; + vertical-align: top; +} + +.applyNameHidden +{ + visibility: hidden; + text-align: center; +} + +.applyNameVisible +{ + visibility: visible; + text-align: center; +} + +/* Page table */ +.mainTable +{ + margin: 0 auto; + border-collapse: collapse; + border: 1px solid #000000; + +} + +/* panel containing the game and controls */ +.gamePanel +{ + width: 601px; + position: relative; + top: 0; left: 0; +} + +/* controls table */ +.controlsTitle +{ + text-align: center; +} +.controlsTableHeader +{ + color: #c0c0c0; + font-weight: bold; +} +.controlsTable +{ + margin: 0 auto; + text-align: center; + border-collapse: collapse; + border: 1px solid #008000; + color: #008000; + background-color: #000d00; +} +.controlsTable td +{ + border-color: #008000; +} + + +.gameCanvas +{ + border: 1px solid #008000; +} + +.resTitle +{ + font-size: 26px; + text-align: center; +} +.resultsTable +{ + margin: 0 auto; + font-size: 26px; + border-collapse: collapse; + border: 1px solid #008000; + color: #008000; + background-color: #000d00; +} +.resultsTable td +{ + border: 1px solid #008000; + padding: 10px; + width: 200px; +} + +.highScoreTable +{ + margin: 0 auto; + font-size: 18px; + border-collapse: collapse; + border: 1px solid #008000; + color: #008000; + background-color: #000d00; +} +.highScoreTable td +{ + border: 1px solid #008000; + width: 200px; +} +.highScoreTableHeader +{ + color: #c0c0c0; + font-weight: bold; +} + +.resultsLeft +{ + text-align: right; +} +.resultsRight +{ + text-align: left; +} + +/** GAME ELEMENTS **/ +.gameElements +{ + position: relative; + height: 500px; + width: 600px; +} + +.gameCanvas +{ + position: absolute; + top: 0; left: 0; + z-index: 1; +} + +.ttyOutput +{ + font-family: VT323; + font-size:20px; + position: absolute; + height: 0px; width: 200px; + color: #008800; + line-height: 100%; + z-index: 2; +} +.scoreOutput +{ + top: 140px; left: 20px; +} +.linesOutput +{ + top: 235px; left: 20px; +} +.levelOutput +{ + top: 328; left: 20px; +} +.tickerOutput +{ + top: 372; left: 435; +} + +.gameEndOutputHidden +{ + top: 60px; left: 195px; + width: 210px; height: 216px; + background-image: url('media/background/endconsole.png'); + visibility: hidden; +} + +.gameEndOutputVisible +{ + font-family: VT323; + font-size:20px; + position: absolute; + color: #008800; + line-height: 100%; + z-index: 2; + top: 60px; left: 195px; + width: 210px; height: 216px; + background-image: url('media/background/endconsole.png'); +} diff --git a/tetris/tetris.js b/tetris/tetris.js new file mode 100644 index 00000000..7b2ad061 --- /dev/null +++ b/tetris/tetris.js @@ -0,0 +1,249 @@ + +FIELD_OFFSET_X = 180; +FIELD_OFFSET_Y = 12; + +function TetrisControl() { + var tetris = new Tetris(this); + + this.setup = function () { + tetris.setup(); + }; + this.update = function () { + tetris.update(); + }; + this.draw = function () { + tetris.draw(); + }; + + this.restart = function() { + // create a new Tetris object + tetris = new Tetris(this); + + // emulate an initial setup condition and the first loop + tetris.setup(); + tetris.update(); + }; +} + +function Tetris(controller) { + var background = null, + game = null, + timeOffset = 0, + + lastEscapeState = false, + startPauseTime = 0, + paused = false, + lastPaused = false, + + gameOver = false, + + mouseClick = null, + + self = this, + + continueButton = null, + restartButton = null, + + lastTime = null, + dTime = null, + + gameEndTty = new TtyBlock('gameEndDiv', 10, 20, 1); + + + this.setup = function () { + // find the keys to stop + var stoppedKeys = [], + curAction, i; + for (curAction in inputAssignments) { + stoppedKeys = stoppedKeys.concat(inputAssignments[curAction]); + } + jaws.preventDefaultKeys(stoppedKeys); + + + Tetris.currentInstance = self; + game = new Game(inputAssignments, autoRepeatConfig, thresholdConfig); + + continueButton = new Button({image: 'media/buttons/continue.png', x: 250, y: 150}); + restartButton = new Button({image: 'media/buttons/restart.png', x: 250, y: 200}); + + background = new Background(); + + timeOffset = (new Date()).getTime(); + }; + + this.update = function() { + var realTime = (new Date()).getTime(), + escapePressed = jaws.pressed('esc'), + scoreObject; + + if (lastTime === null) { + dTime = 0; + lastTime = realTime; + } else { + dTime = realTime - lastTime; + lastTime = realTime; + } + + if (!paused && !gameOver) { + // see if the game should be pased + if (escapePressed && (!lastEscapeState)) { + // go into pause mode + startPauseTime = realTime; + paused = true; + } else { + game.update(realTime - timeOffset); + // see if the game is over + scoreObject = game.getResults(); + if (scoreObject) { + gameOver = true; + + // make the game end visible + document.getElementById('gameEndContainer').setAttribute('class', 'gameEndOutputVisible'); + gameEndTty.addLine('GOOD GAME!!!'); + gameEndTty.addLine(''); + gameEndTty.addLine(''); + if (scoreObject.won) { + gameEndTty.addLine('You Win!'); + } else { + gameEndTty.addLine('Better Luck Next Time'); + } + gameEndTty.addLine(''); + gameEndTty.addLine(''); + + /* + gameEndTty.addLine('Re-directing you to'); + gameEndTty.addLine('the score screen...'); + */ + + gameEndTty.addLine('Your score was:'); + gameEndTty.addLine(scoreObject.score.toString()); + gameEndTty.addLine(''); + gameEndTty.addLine(''); + + //sendScoreRequest(scoreObject.score); + + window.setTimeout(function() { + document.getElementById('gameEndContainer').setAttribute('class', 'gameEndOutputHidden'); + controller.restart(); + }, 6000); + } + } + } else if (paused) { + // see if the escape key was hit + if (escapePressed && (!lastEscapeState)) { + // change the time offset + timeOffset += realTime - startPauseTime; + paused = false; + } + // see if any buttons were pressed + if (mouseClick) { + if (continueButton.isClicked(mouseClick.x, mouseClick.y)) { + // change the time offset + timeOffset += realTime - startPauseTime; + paused = false; + } + if (restartButton.isClicked(mouseClick.x, mouseClick.y)) { + // restart the game + controller.restart(); + return; + } + } + } else { + // TODO: nothing??? + } + + lastEscapeState = escapePressed; + mouseClick = null; + }; + + this.draw = function() { + + if (!paused && !gameOver) { + + // draw the game + background.draw(lastPaused); + if (lastPaused) { + lastPaused = false; + Block.invalidateAll(); + } + game.draw(dTime); + Block.invalidFlushed(); + + } else if (paused) { + // draw the game + background.draw(); + game.draw(dTime); + + //draw the pause menu + continueButton.draw(); + restartButton.draw(); + lastPaused = true; + } else { + // continue to draw the game for game over + // draw the game + background.draw(); + game.draw(dTime); + } + + gameEndTty.draw(dTime); + }; + + this.mouseClicked = function(x, y) { + mouseClick = {x: x, y: y}; + }; +} + +window.onload = function () { + loadGameControls(); + + jaws.assets.add('media/blueblock.png'); + jaws.assets.add('media/cyanblock.png'); + jaws.assets.add('media/greenblock.png'); + jaws.assets.add('media/orangeblock.png'); + jaws.assets.add('media/purpleblock.png'); + jaws.assets.add('media/redblock.png'); + jaws.assets.add('media/yellowblock.png'); + + jaws.assets.add('media/greyblock.png'); + jaws.assets.add('media/emptyblock.png'); + + jaws.assets.add('media/buttons/continue.png'); + jaws.assets.add('media/buttons/restart.png'); + + jaws.assets.add('media/background/backdrop.png'); + jaws.assets.add('media/background/topbar.png'); + + jaws.start(TetrisControl); +}; + +var redirCode; + +function redirectToScore() { + window.location.replace('/scoreScreen.html?tempRef=' + redirCode); +} + +function sendScoreRequest(score) { + var xmlhttp; + if (window.XMLHttpRequest) + {// code for IE7+, Firefox, Chrome, Opera, Safari + xmlhttp=new XMLHttpRequest(); + } + else + {// code for IE6, IE5 + xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); + } + xmlhttp.onreadystatechange=function() + { + if (xmlhttp.readyState==4 && xmlhttp.status==200) + { + redirCode = xmlhttp.responseText; + + setTimeout('redirectToScore();', 4000); + } + } + + // World's 3rd most piss-poor obfustication technique + // A serious real-time/replay game monitor is needed + xmlhttp.open("POST", "/score/reportScore?gthbyu="+(score*17), true); + xmlhttp.send(); +}