diff --git a/.gitignore b/.gitignore index 5148e52..6d2145b 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ jspm_packages # Optional REPL history .node_repl_history + +# DS_Store +.DS_Store diff --git a/README.md b/README.md index 09a972f..e96f235 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ +# Platform Game + +## Specifications + +### General + +- [x] Artifact produced is a fork of the browser-games repo. +- [x] Variables, functions, files, etc. have appropriate and meaningful names. +- [x] HTML, CSS, and JS files are well formatted with proper spacing and indentation. +- [x] There is a clear separation of game logic code from view/rendering code. +- [x] All major features are added via pull requests with a clear description and concise commit messages. +- [x] The artifact produced is properly licensed, preferably with the MIT license. + +### Generic Platform Game + +- [x] Game can be found at ```public/platform.html``` +- [x] Game is playable by one player +- [x] Game follows rules established in tutorial +- [x] Game page is linked from ```public/index.html``` + # Browser Games A collection of games to play in a web browser. See the full list of games in the [games.md](games.md) file. diff --git a/public/css/platform.css b/public/css/platform.css new file mode 100644 index 0000000..161d919 --- /dev/null +++ b/public/css/platform.css @@ -0,0 +1,21 @@ +.background { background: rgb(52, 166, 251); + table-layout: fixed; + border-spacing: 0; } +.background td { padding: 0; } +.lava { background: rgb(255, 100, 100); } +.wall { background: white; } +.actor { position: absolute; } +.coin { background: rgb(241, 229, 89); } +.player { background: rgb(64, 64, 64); } +.lost .player { + background: rgb(160, 64, 64); +} +.won .player { + box-shadow: -4px -7px 8px white, 4px -7px 8px white; +} +.game { + overflow: hidden; + max-width: 600px; + max-height: 450px; + position: relative; +} diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 278d291..0000000 --- a/public/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Browser Games - - -

Browser Games

- -

A collection of games to play in a web browser.

- -
- - - - diff --git a/public/js/platform.js b/public/js/platform.js new file mode 100644 index 0000000..2ff8fb0 --- /dev/null +++ b/public/js/platform.js @@ -0,0 +1,533 @@ +const GAME_LEVELS = [ + [" ", + " ", + " ", + " ", + " ", + " ", + " xxx ", + " xx xx xx!xx ", + " o o xx x!!!x ", + " xx!xx ", + " xxxxx xvx ", + " xx ", + " xx o o x ", + " x o x ", + " x xxxxx o x ", + " x xxxx o x ", + " x @ x x xxxxx x ", + " xxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxxxx ", + " x x x x ", + " x!!!x x!!!!!x ", + " x!!!x x!!!!!x ", + " xxxxx xxxxxxx ", + " ", + " "], + [" x!!x xxxxxxx x!x ", + " x!!x xxxx xxxx x!x ", + " x!!xxxxxxxxxx xx xx x!x ", + " xx!!!!!!!!!!xx xx xx x!x ", + " xxxxxxxxxx!!x x o o o x!x ", + " xx!x x o o xx!x ", + " x!x x xxxxxxxxxxxxxxx!!x ", + " xvx x x x !!!!!!!!!!!!!!xx ", + " xx | | | xx xxxxxxxxxxxxxxxxxxxxx ", + " xx!!!!!!!!!!!xx v ", + " xxxx!!!!!xxxx ", + " x x xxxxxxx xxx xxx ", + " x x x x x x ", + " x x x x ", + " x x xx x ", + " xx x x x ", + " x x o o x x x x ", + " xxxxxxx xxx xxx x x x x x x ", + " xx xx x x x x xxxxxx x x xxxxxxxxx x ", + " xx xx x o x x xx x x x x ", + " @ x x x x x x x x x x ", + " xxx x x x x x x x xxxxx xxxxxx x ", + " x x x x xx o xx x x x o x x x ", + "!!!!x x!!!!!!x x!!!!!!xx xx!!!!!!!!xx x!!!!!!!!!! x = x x x ", + "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxx x!!!!!!!xx! xxxxxxxxxxxxx xx o o xx ", + "!!!!x x!!!!!!x x!!!!!x o xx!!!!!!xx ! xx xx ", + "!!!!x x!!!!!!x x!!!!!x xx!!!!!!xx ! xxxxxxx ", + "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxxxxxx!!!!!!xx ! ", + "!!!!x x!!!!!!x x!!!!!!xxxxxxxxx!!!!!!!!!!!!!!!!!!xx ! ", + "!!!!x x!!!!!!x x!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!xx ! "], + [" ", + " ", + " ", + " ", + " ", + " o ", + " ", + " x ", + " x ", + " x ", + " x ", + " xxx ", + " x x !!! !!! xxx ", + " x x !x! !x! ", + " xxx xxx x x ", + " x x x oooo x xxx ", + " x x x x x!!!x ", + " x x xxxxxxxxxxxx xxx ", + " xx xx x x x ", + " x xxxxxxxxx xxxxxxxx x x ", + " x x x x!!!x ", + " x x x xxx ", + " xx xx x ", + " x x= = = = x xxx ", + " x x x x!!!x ", + " x x = = = =x o xxx xxx ", + " xx xx x x!!!x ", + " o o x x x x xxv xxx ", + " x x x x x!!!x ", + " xxx xxx xxx xxx o o x!!!!!!!!!!!!!!x vx ", + " x xxx x x xxx x x!!!!!!!!!!!!!!x ", + " x x xxxxxxxxxxxxxxxxxxxxxxx ", + " xx xx xxx ", + " xxx x x x x!!!x xxx ", + " x x x xxx x xxx x x ", + " x x xxx xxxxxxx xxxxx x ", + " x x x x x x ", + " x xx x x x x x ", + " x x |xxxx| |xxxx| xxx xxx x ", + " x xxx o o x x xxx x ", + " x xxxxx xx x xxx x!!!x x x ", + " x oxxxo x xxx x x x xxx xxx x ", + " x xxx xxxxxxxxxxxxx x oo x x oo x x oo xx xx xxx x ", + " x @ x x x!!x x!!!!x x!!!!x xx xx x x ", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", + " ", + " "], + [" xxx x ", + " x ", + " xxxxx ", + " x ", + " x xxx ", + " o x x x ", + " o o oxxx x ", + " xxx x ", + " ! o ! xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx ", + " x x x x x x x x x x x x x x x ", + " x= o x x xxx x xxx x xxx x xxx x xxx x xxx x xxxxx ", + " x x x x x x x x x x x x x x x ", + " ! o ! o xxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxxxx ", + " ", + " o xxx xx ", + " ", + " ", + " xx ", + " xxx xxx ", + " ", + " o x x ", + " xx xx ", + " xxx xxx xxx x x ", + " ", + " || ", + " xxxxxxxxxxx ", + " x x o xxxxxxxxx o xxxxxxxxx o xx x ", + " x x x x x x x || x x ", + " x @ xxxxx o xxxxx o xxxxx ", + " xxxxxxx xxxxx xx xx xxx ", + " x= = =x x xxx ", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x!!!!!!!!!!!!!!!!!!!!!xxx!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + " "] +] + +// Level constructor +function Level(plan) { + this.width = plan[0].length + this.height = plan.length + this.grid = [] + this.actors = [] + + for (let y = 0; y < this.height; y++) { + let line = plan[y], + gridLine = [] + for (let x = 0; x < this.width; x++) { + let ch = line[x], fieldType = null + let Actor = actorChars[ch] + if (Actor) + this.actors.push(new Actor(new Vector(x, y), ch)) + else if (ch == "x") + fieldType = "wall" + else if (ch == "!") + fieldType = "lava" + gridLine.push(fieldType) + } + this.grid.push(gridLine) + } + + this.player = this.actors.filter(function(actor) { + return actor.type == "player" + })[0] + this.status = this.finishDelay = null +} + +Level.prototype.isFinished = function() { + return this.status != null && this.finishDelay < 0 +} + +// Vector constructor +function Vector(x, y) { + this.x = x + this.y = y +} + +Vector.prototype.plus = function(other) { + return new Vector(this.x + other.x, this.y + other.y) +} + +Vector.prototype.times = function(factor) { + return new Vector(this.x * factor, this.y * factor) +} + +// Defining characters used to represent the game componensts +const actorChars = { + "@": Player, + "o": Coin, + "=": Lava, "|": Lava, "v": Lava +} + +function Player(pos) { + this.pos = pos.plus(new Vector(0, -0.5)) + this.size = new Vector(0.8, 1.5) + this.speed = new Vector(0, 0) +} + +Player.prototype.type = "player" + +// Lava constructor +function Lava(pos, ch) { + this.pos = pos + this.size = new Vector(1, 1) + if (ch == "=") { + this.speed = new Vector(2, 0) + } else if (ch == "|") { + this.speed = new Vector(0, 2) + } else if (ch == "v") { + this.speed = new Vector(0, 3) + this.repeatPos = pos + } +} + +Lava.prototype.type = "lava" + +// Coin constructor +function Coin(pos) { + this.basePos = this.pos = pos.plus(new Vector(0.2, 0.1)) + this.size = new Vector(0.6, 0.6) + this.wobble = Math.random() * Math.PI * 2 +} +Coin.prototype.type = "coin" + +function elt(name, className) { + let elt = document.createElement(name) + if (className) elt.className = className + return elt +} + +// DOMDisplay constructor +function DOMDisplay(parent, level) { + this.wrap = parent.appendChild(elt("div", "game")) + this.level = level + + this.wrap.appendChild(this.drawBackground()) + this.actorLayer = null + this.drawFrame() +} + +let scale = 20 + +DOMDisplay.prototype.drawBackground = function() { + let table = elt("table", "background") + table.style.width = this.level.width * scale + "px" + this.level.grid.forEach(function(row) { + let rowElt = table.appendChild(elt("tr")) + rowElt.style.height = scale + "px" + row.forEach(function(type) { + rowElt.appendChild(elt("td", type)) + }) + }) + return table +} + +DOMDisplay.prototype.drawActors = function() { + let wrap = elt("div") + this.level.actors.forEach(function(actor) { + let rect = wrap.appendChild(elt("div", + "actor " + actor.type)) + rect.style.width = actor.size.x * scale + "px" + rect.style.height = actor.size.y * scale + "px" + rect.style.left = actor.pos.x * scale + "px" + rect.style.top = actor.pos.y * scale + "px" + }) + return wrap +} + +DOMDisplay.prototype.drawFrame = function() { + if (this.actorLayer) + this.wrap.removeChild(this.actorLayer) + this.actorLayer = this.wrap.appendChild(this.drawActors()) + this.wrap.className = "game " + (this.level.status || "") + this.scrollPlayerIntoView() +} + +DOMDisplay.prototype.scrollPlayerIntoView = function() { + let width = this.wrap.clientWidth, + height = this.wrap.clientHeight, + margin = width / 3 + + // The viewport + let left = this.wrap.scrollLeft, right = left + width, + top = this.wrap.scrollTop, bottom = top + height, + player = this.level.player, + center = player.pos.plus(player.size.times(0.5)).times(scale) + + if (center.x < left + margin) + this.wrap.scrollLeft = center.x - margin + else if (center.x > right - margin) + this.wrap.scrollLeft = center.x + margin - width + if (center.y < top + margin) + this.wrap.scrollTop = center.y - margin + else if (center.y > bottom - margin) + this.wrap.scrollTop = center.y + margin - height +} + +DOMDisplay.prototype.clear = function() { + this.wrap.parentNode.removeChild(this.wrap) +} + +Level.prototype.obstableAt = function(pos, size) { + let xStart = Math.floor(pos.x), + xEnd = Math.ceil(pos.x + size.x), + yStart = Math.floor(pos.y), + yEnd = Math.ceil(pos.y + size.y) + + if (xStart < 0 || xEnd > this.width || yStart < 0) { + return 'wall' + } + if (yEnd > this.height) { + return 'lava' + } + for (let y = yStart; y < yEnd; y++) { + for (let x = xStart; x < xEnd; x++) { + let fieldType = this.grid[y][x] + if (fieldType) { + return fieldType + } + } + } +} + +Level.prototype.actorAt = function(actor) { + for (let i = 0; i < this.actors.length; i++) { + let other = this.actors[i] + if (other != actor && + actor.pos.x + actor.size.x > other.pos.x && + actor.pos.x < other.pos.x + other.size.x && + actor.pos.y + actor.size.y > other.pos.y && + actor.pos.y < other.pos.y + other.size.y) { + return other + } + } +} + +let maxStep = 0.05 + +Level.prototype.animate = function(step, keys) { + if (this.status != null) { + this.finishDelay -= step + } + + while (step > 0) { + let thisStep = Math.min(step, maxStep) + this.actors.forEach(function(actor) { + actor.act(thisStep, this, keys) + }, this) + step -= thisStep + } +} + +Lava.prototype.act = function(step, level) { + let newPos = this.pos.plus(this.speed.times(step)) + if (!level.obstableAt(newPos, this.size)) { + this.pos = newPos + } else if (this.repeatPos) { + this.pos = this.repeatPos + } else { + this.speed = this.speed.times(-1) + } +} + +let wobbleSpeed = 8, + wobbleDist = 0.07 + +Coin.prototype.act = function(step) { + this.wobble += step * wobbleSpeed + + let wobblePos = Math.sin(this.wobble) * wobbleDist + this.pos = this.basePos.plus(new Vector(0, wobblePos)) +} + +// horizontal motion + +let playerXSpeed = 7 + +Player.prototype.moveX = function(step, level, keys) { + this.speed.x = 0 + if (keys.left) { + this.speed.x -= playerXSpeed + } + if (keys.right) { + this.speed.x += playerXSpeed + } + let motion = new Vector(this.speed.x * step, 0), + newPos = this.pos.plus(motion), + obstacle = level.obstableAt(newPos, this.size) + if (obstacle) { + level.playerTouched(obstacle) + } else { + this.pos = newPos + } +} + +// vertical motion - jumping + +let gravity = 30, + jumpSpeed = 17 + +Player.prototype.moveY = function(step, level, keys) { + this.speed.y += step * gravity + + let motion = new Vector(0, this.speed.y * step), + newPos = this.pos.plus(motion), + obstacle = level.obstableAt(newPos, this.size) + + if (obstacle) { + level.playerTouched(obstacle) + if (keys.up && this.speed.y > 0) { + this.speed.y = -jumpSpeed + } else { + this.speed.y = 0 + } + } else { + this.pos = newPos + } +} + +Player.prototype.act = function(step, level, keys) { + this.moveX(step, level, keys) + this.moveY(step, level, keys) + + let otherActor = level.actorAt(this) + + if (otherActor) { + level.playerTouched(otherActor.type, otherActor) + } + + // Losing animation + if (level.status == 'lost') { + this.pos.y += step + this.pos.x -= step + } +} + +Level.prototype.playerTouched = function(type, actor) { + if (type == 'lava' && this.status == null) { + this.status = 'lost' + this.finishDelay = 1 + } else if (type == 'coin') { + this.actors = this.actors.filter(function(other) { + return other != actor + }) + } + + if (!this.actors.some(function(actor) { // Need some clarity on this syntax + return actor.type == "coin" + })) { + this.status = "won" + this.finishDelay = 1 + } +} + +// Tracking keys + +let arrowCodes = { + 37: 'left', + 38: 'up', + 39: 'right' +} + +function trackKeys(codes) { + let pressed = Object.create(null) + function handler(event) { + if (codes.hasOwnProperty(event.keyCode)) { + let down = event.type == 'keydown' + pressed[codes[event.keyCode]] = down + event.preventDefault() + } + } + addEventListener('keydown', handler) + addEventListener('keyup', handler) + return pressed +} + +// Running the game + +// Helper function to wrap requestAnimationFrame function + +function runAnimation(frameFunc) { + let lastTime = null + function frame(time) { + let stop = false + if (lastTime != null) { + let timeStep = Math.min(time - lastTime, 100) / 1000 + stop = frameFunc(timeStep) === false + } + lastTime = time + if (!stop) { + requestAnimationFrame(frame) + } + } + requestAnimationFrame(frame) +} + +// takes level object, display constructor. displays level in document.body & let's user play through it. + +let arrows = trackKeys(arrowCodes) + +function runLevel(level, Display, andThen) { + let display = new Display(document.body, level) + + runAnimation(function(step) { + level.animate(step, arrows) + display.drawFrame(step) + if (level.isFinished()) { + display.clear() + if (andThen) { + andThen(level.status) + return false + } + } + }) +} + +// reset level if player loses, keep going if player passes level + +function runGame(plans, Display) { + function startLevel(n) { + runLevel(new Level(plans[n]), Display, function(status) { + if (status == 'lost') { + startLevel(n) + } else if (n < plans.length - 1) { + startLevel(n + 1) + } else { + console.log('You win!') + } + }) + } + startLevel(0) +} diff --git a/public/platform.html b/public/platform.html new file mode 100644 index 0000000..2f723c4 --- /dev/null +++ b/public/platform.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +