| @@ -0,0 +1,7 @@ | |||
| body { | |||
| margin: 0px; | |||
| } | |||
| canvas { | |||
| position: absolute; | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <title>Flappy Dick</title> | |||
| <link rel="stylesheet" href="css/style.css"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"> | |||
| </head> | |||
| <body> | |||
| <canvas id="canvas"></canvas> | |||
| <script src="js/util.js"></script> | |||
| <script src="js/vec2.js"></script> | |||
| <script src="js/assets.js"></script> | |||
| <script src="js/game.js"></script> | |||
| <script src="js/entities.js"></script> | |||
| <script src="js/worldgen.js"></script> | |||
| <script src="js/script.js"></script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,106 @@ | |||
| var assets = { | |||
| }; | |||
| /* | |||
| * Image source. | |||
| * If animation, the images must be stacked on top of each other. | |||
| */ | |||
| function ImageSource(src, frameh) { | |||
| var ext = ".png"; | |||
| this.img = document.createElement("img"); | |||
| this.ready = false; | |||
| this.width = 0; | |||
| this.height = 0; | |||
| this.frameh = frameh == null ? -1 : frameh; | |||
| this.steps = 1; | |||
| this.img.onload = function() { | |||
| this.ready = true; | |||
| this.width = this.img.width; | |||
| this.height = this.img.height; | |||
| if (this.frameh === -1) | |||
| this.frameh = this.height; | |||
| this.steps = this.height / this.frameh; | |||
| if (this.steps !== Math.round(this.steps)) { | |||
| this.steps = Math.round(this.steps); | |||
| console.log( | |||
| "Warning: '"+src+ext+"': "+ | |||
| "Height isn't evenly divisible by frame height. "+ | |||
| "Height: "+this.height+", frame height: "+this.frameh); | |||
| } | |||
| }.bind(this); | |||
| this.img.src = "assets/"+src+ext; | |||
| } | |||
| ImageSource.prototype.draw = function(ctx, step) { | |||
| if (!step) step = 0; | |||
| ctx.drawImage(this.img, 0, 0, | |||
| this.width, this.frameh, | |||
| 0, this.frameh * step, | |||
| this.width, this.frameh); | |||
| } | |||
| function Animation(imgSrc, type, loop, fps) { | |||
| this.type = type || "forward"; | |||
| this.loop = loop || false; | |||
| this.imgSrc = imgSrc; | |||
| this.fps = fps || 12; | |||
| this.waitTime = 1000 / this.fps; | |||
| this.done = false; | |||
| this.doStep = true; | |||
| if (type === "forward" || type === "bounce") { | |||
| this.step = 0; | |||
| this.direction = 1; | |||
| } else if (type === "reverse" || type === "bounce-reverse") { | |||
| this.step = -1; | |||
| this.direction = -1; | |||
| } | |||
| // We don't care about the direction after setting this.direction | |||
| if (this.type === "bounce-reverse") | |||
| this.type = "bounce"; | |||
| else if (this.type === "reverse") | |||
| this.type = "forward"; | |||
| } | |||
| Animation.prototype.nextFrame = function() { | |||
| if (!this.imgSrc.ready) { | |||
| return; | |||
| } else if (this.step === -1) { | |||
| this.step = this.imgSrc.steps - 1; | |||
| return; | |||
| } else if (this.imgSrc.steps === 1) { | |||
| return; | |||
| } | |||
| var next = this.step + this.direction; | |||
| if (next < 0 || next > this.imgSrc.steps - 1) { | |||
| if (this.loop) { | |||
| this.direction = -this.direction; | |||
| next = this.step + this.direction; | |||
| } else { | |||
| this.done = true; | |||
| return; | |||
| } | |||
| } | |||
| this.step = next; | |||
| } | |||
| Animation.prototype.draw = function(ctx) { | |||
| if (!this.imgSrc.ready) | |||
| return; | |||
| this.imgSrc.draw(ctx, this.step); | |||
| if (this.doStep) { | |||
| this.nextFrame(); | |||
| this.doStep = false; | |||
| this.setTimeout(function() { | |||
| this.doStep = true; | |||
| }.bind(this), this.waitTime); | |||
| } | |||
| } | |||
| @@ -0,0 +1,151 @@ | |||
| /* | |||
| * Player | |||
| */ | |||
| function Player(game) { | |||
| makeEnt(this, game, 100); | |||
| this.moves = true; | |||
| this.invincible = false; | |||
| this.invincibleTimeout = null; | |||
| this.started = false; | |||
| this.rotation = 0; | |||
| this.erectLevel = 0; | |||
| this.rise(); | |||
| this.pos.set({ x: 0, y: game.canvas.height / 2 }); | |||
| } | |||
| Player.prototype.rise = function() { | |||
| this.erectLevel += 1; | |||
| var w = 30 + ((this.erectLevel - 1) * 15); | |||
| var h = 15 + ((this.erectLevel - 1) * 6); | |||
| this.shape.push(new Box(w, h, { x: -(w/2), y: -(h/2) })); | |||
| this.setInvincible(200); | |||
| } | |||
| Player.prototype.lower = function() { | |||
| if (this.invincible) | |||
| return; | |||
| this.shape.pop(); | |||
| this.erectLevel -= 1; | |||
| if (this.erectLevel === 0) | |||
| this.lose(); | |||
| else | |||
| this.setInvincible(500); | |||
| } | |||
| Player.prototype.setInvincible = function(time) { | |||
| clearTimeout(this.invincibleTimeout); | |||
| this.invincible = true; | |||
| this.invincibleTimeout = setTimeout(function() { | |||
| this.invincible = false; | |||
| }.bind(this), time); | |||
| } | |||
| Player.prototype.update = function() { | |||
| // Jump | |||
| if (this.game.presses.jump) { | |||
| this.started = true; | |||
| this.vel.y = -1.3; | |||
| } | |||
| // Gravity and movement | |||
| if (this.started) { | |||
| this.force.y = 0.4; | |||
| this.force.x = 0.13 + ((this.erectLevel - 1) * 0.02); | |||
| } | |||
| // Lose if we hit the edge | |||
| if ( | |||
| this.pos.y < 0 || | |||
| this.pos.y > this.game.canvas.height - this.shape.height()) { | |||
| this.lose(); | |||
| return; | |||
| } | |||
| // Collide | |||
| for (var i in this.game.entities) { | |||
| var ent = this.game.entities[i]; | |||
| if (ent === this) | |||
| continue; | |||
| if (!this.shape.collidesWith(ent.shape)) | |||
| continue; | |||
| if (ent instanceof Obstacle) { | |||
| this.lower(); | |||
| return; | |||
| } else if (ent instanceof PowerUp) { | |||
| this.rise(); | |||
| ent.dead = true; | |||
| } | |||
| } | |||
| } | |||
| Player.prototype.move = function() { | |||
| // Move camera | |||
| this.game.camera.x = this.pos.x - (this.game.canvas.width / 7); | |||
| // Rotate | |||
| this.rotation = this.vel.rotation(); | |||
| } | |||
| Player.prototype.lose = function() { | |||
| alert("You died!"); | |||
| this.game.stop(); | |||
| } | |||
| Player.prototype.draw = function(ctx) { | |||
| if (this.invincible) | |||
| ctx.fillStyle = "#d5d5bf"; | |||
| else | |||
| ctx.fillStyle = "#f5f5dc"; | |||
| ctx.rotate(this.rotation); | |||
| this.shape.draw(ctx); | |||
| } | |||
| /* | |||
| * Obstacle | |||
| */ | |||
| function Obstacle(game, x, y) { | |||
| makeEnt(this, game, 100); | |||
| this.pos.set({ x: x, y: y }); | |||
| var w = 40; | |||
| var h = game.canvas.height; | |||
| var gap = 200; | |||
| this.shape.push(new Box(w, h, { x: 0, y: -(h / 2) - (gap / 2) })); | |||
| this.shape.push(new Box(w, h, { x: 0, y: (h / 2) + (gap / 2) })); | |||
| } | |||
| Obstacle.prototype.draw = function(ctx) { | |||
| this.shape.draw(ctx); | |||
| } | |||
| Obstacle.prototype.update = function() { | |||
| if (this.game.camera.x > this.pos.x + this.shape.width()) | |||
| this.dead = true; | |||
| } | |||
| /* | |||
| * PowerUp | |||
| */ | |||
| function PowerUp(game, x, y, type) { | |||
| makeEnt(this, game, 100); | |||
| this.pos.set({ x: x, y: y }); | |||
| this.type = type; | |||
| this.shape.push(new Box(30, 30)); | |||
| } | |||
| PowerUp.prototype.draw = function(ctx) { | |||
| ctx.beginPath(); | |||
| ctx.arc(15, 15, 15, 0, 2 * Math.PI); | |||
| ctx.stroke(); | |||
| } | |||
| PowerUp.prototype.update = function() { | |||
| if (this.game.camera.x > this.pos.x + this.shape.width()) | |||
| this.dead = true; | |||
| } | |||
| @@ -0,0 +1,250 @@ | |||
| var keymap = { | |||
| 32: "jump", | |||
| touch: "jump" | |||
| }; | |||
| /* | |||
| * Collision box | |||
| */ | |||
| function Box(width, height, pos) { | |||
| pos = pos || {}; | |||
| this.width = width; | |||
| this.height = height; | |||
| this.pos = new Vec2(pos.x, pos.y); | |||
| } | |||
| Box.prototype.collidesWith = function(box, ent1pos, ent2pos) { | |||
| var x1 = ent1pos.x + this.pos.x; | |||
| var x2 = ent2pos.x + box.pos.x; | |||
| var y1 = ent1pos.y + this.pos.y; | |||
| var y2 = ent2pos.y + box.pos.y; | |||
| var w1 = this.width; | |||
| var w2 = box.width; | |||
| var h1 = this.height; | |||
| var h2 = box.height; | |||
| return ((x1 + w1 >= x2 && x1 <= x2 + w2) && | |||
| (y1 + h1 >= y2 && y1 <= y2 + h2)); | |||
| } | |||
| Box.prototype.trace = function(ctx) { | |||
| ctx.beginPath(); | |||
| ctx.moveTo(this.pos.x, this.pos.y); | |||
| ctx.lineTo(this.pos.x, this.pos.y + this.height); | |||
| ctx.lineTo(this.pos.x + this.width, this.pos.y + this.height); | |||
| ctx.lineTo(this.pos.x + this.width, this.pos.y); | |||
| ctx.closePath(); | |||
| } | |||
| /* | |||
| * Collision shape | |||
| */ | |||
| function Shape(ent) { | |||
| this.ent = ent; | |||
| this.boxes = []; | |||
| this._height = -1; | |||
| this._width = -1; | |||
| } | |||
| Shape.prototype.push = function(box) { | |||
| this._height = -1; | |||
| this._width = -1; | |||
| this.boxes.push(box); | |||
| } | |||
| Shape.prototype.pop = function() { | |||
| this._height = -1; | |||
| this._width = -1; | |||
| return this.boxes.pop(); | |||
| } | |||
| Shape.prototype.draw = function(ctx) { | |||
| for (var i = 0; i < this.boxes.length; ++i) { | |||
| var box = this.boxes[i]; | |||
| box.trace(ctx); | |||
| ctx.stroke(); | |||
| ctx.fill(); | |||
| } | |||
| } | |||
| Shape.prototype.collidesWith = function(shape) { | |||
| for (var i = 0; i < this.boxes.length; ++i) { | |||
| var box = this.boxes[i]; | |||
| for (var j = 0; j < shape.boxes.length; ++j) { | |||
| var otherBox = shape.boxes[j]; | |||
| if (box.collidesWith(otherBox, this.ent.pos, shape.ent.pos)) | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| Shape.prototype.width = function() { | |||
| if (this._width !== -1) | |||
| return this._width; | |||
| var minX = 0; | |||
| var maxX = 0; | |||
| this.boxes.forEach(function(box) { | |||
| if (box.pos.x < minX) | |||
| minX = box.pos.x; | |||
| if (box.pos.x + box.width > maxX) | |||
| maxX = box.pos.x + box.width; | |||
| }); | |||
| this._width = Math.abs(maxX - minX); | |||
| return this._width; | |||
| } | |||
| Shape.prototype.height = function() { | |||
| if (this._height !== -1) | |||
| return this._height; | |||
| var minY = 0; | |||
| var maxY = 0; | |||
| this.boxes.forEach(function(box) { | |||
| if (box.pos.y < minY) | |||
| minY = box.pos.y; | |||
| if (box.pos.y + box.height > maxY) | |||
| maxY = box.pos.y + box.height; | |||
| }); | |||
| this._height = Math.abs(maxY - minY); | |||
| return this._height; | |||
| } | |||
| // Make entity from object | |||
| function makeEnt(obj, game, mass) { | |||
| obj.pos = new Vec2(); | |||
| obj.vel = new Vec2(); | |||
| obj.force = new Vec2(); | |||
| obj.game = game; | |||
| obj.shape = new Shape(obj); | |||
| obj.moves = false; | |||
| this.dead = false; | |||
| obj.mass = mass || 0; | |||
| obj.forceScalar = 1 / obj.mass; | |||
| } | |||
| /* | |||
| * Game | |||
| */ | |||
| function Game(canvas) { | |||
| this.canvas = canvas; | |||
| this.ctx = canvas.getContext("2d"); | |||
| this.raf = null; | |||
| this.prevTime = null; | |||
| this.stopped = true; | |||
| this.camera = new Vec2(); | |||
| this.worldgen = null; | |||
| this.entities = []; | |||
| this.keys = {}; | |||
| this.presses = {}; | |||
| this.onkey = function onkey(evt) { | |||
| var down = (evt.type === "keydown" || evt.type === "touchstart"); | |||
| var code = evt.keyCode || "touch"; | |||
| var name = keymap[code]; | |||
| if (name) { | |||
| this.keys[name] = down; | |||
| if (down) | |||
| this.presses[name] = true; | |||
| } | |||
| }.bind(this); | |||
| } | |||
| Game.prototype.start = function(worldgen) { | |||
| window.addEventListener("keydown", this.onkey); | |||
| window.addEventListener("keyup", this.onkey); | |||
| window.addEventListener("touchstart", this.onkey); | |||
| window.addEventListener("touchend", this.onkey); | |||
| this.prevTime = new Date().getTime(); | |||
| this.stopped = false; | |||
| this.worldgen = worldgen; | |||
| this.update(); | |||
| } | |||
| Game.prototype.update = function() { | |||
| var time = new Date().getTime(); | |||
| var dt = time - this.prevTime; | |||
| if (this.stopped) | |||
| return; | |||
| // Go through and update | |||
| var xRatio = 1 / (1 + (dt * 0.005)); | |||
| for (var i = 0; i < this.entities.length; ++i) { | |||
| var ent = this.entities[i]; | |||
| // Remove dead entities, replace them with the last entity | |||
| if (ent.dead) { | |||
| if (i + 1 === this.entities.length) { | |||
| this.entities.pop(); | |||
| } else { | |||
| this.entities[i] = this.entities.pop(); | |||
| var ent = this.entities[i]; | |||
| } | |||
| } | |||
| if (ent.update) | |||
| ent.update(); | |||
| if (ent.moves) { | |||
| ent.force.scale(ent.forceScalar * dt); | |||
| ent.vel.add(ent.force); | |||
| ent.force.set({ x: 0, y: 0 }); | |||
| ent.vel.scale(xRatio); | |||
| ent.pos.add(ent.vel.clone().scale(dt)); | |||
| } | |||
| if (ent.move) | |||
| ent.move(); | |||
| } | |||
| // Exit if stopped | |||
| if (this.stopped) | |||
| return; | |||
| // Tick worldgen | |||
| this.worldgen.update(); | |||
| // Go through and draw | |||
| this.canvas.width = this.canvas.width; | |||
| for (var i = 0; i < this.entities.length; ++i) { | |||
| var ent = this.entities[i]; | |||
| if (ent.dead) | |||
| continue; | |||
| if (ent.pos.x + ent.shape.width() < this.camera.x) | |||
| continue; | |||
| if (ent.pos.x > this.camera.x + this.canvas.width) | |||
| continue; | |||
| this.ctx.save(); | |||
| this.ctx.translate( | |||
| ent.pos.x - this.camera.x, | |||
| ent.pos.y - this.camera.y); | |||
| ent.draw(this.ctx); | |||
| this.ctx.restore(); | |||
| } | |||
| // Clear presses | |||
| for (var i in this.presses) { | |||
| this.presses[i] = false; | |||
| } | |||
| this.prevTime = time; | |||
| if (!this.stopped) | |||
| this.raf = reqAnimFrame(this.update.bind(this)); | |||
| } | |||
| Game.prototype.stop = function() { | |||
| this.stopped = true; | |||
| cancelAnimFrame(this.raf); | |||
| window.removeEventListener("keyup", this.onkey); | |||
| window.removeEventListener("keydown", this.onkey); | |||
| if (this.onstop) | |||
| this.onstop(); | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| window.ontouchmove = function(evt) { | |||
| evt.preventDefault(); | |||
| } | |||
| function run() { | |||
| var game = new Game(document.getElementById("canvas")); | |||
| game.canvas.width = window.innerWidth; | |||
| game.canvas.height = window.innerHeight; | |||
| var worldgen = new WorldGen(game); | |||
| game.start(worldgen); | |||
| game.onstop = run; | |||
| } | |||
| run(); | |||
| @@ -0,0 +1,8 @@ | |||
| window.reqAnimFrame = window.requestAnimationFrame || (function(fn) { | |||
| return setTimeout(fn, 1000 / 60) }); | |||
| window.cancelAnimFrame = window.cancelAnimationFrame || window.clearTimeout; | |||
| function randInt(min, max) { | |||
| return Math.floor(Math.random() * (max - min)) + min; | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| function Vec2(x, y) { | |||
| this.x = x || 0; | |||
| this.y = y || 0; | |||
| return this; | |||
| } | |||
| Vec2.prototype.clone = function() { | |||
| return new Vec2(this.x, this.y); | |||
| } | |||
| Vec2.prototype.length = function() { | |||
| return Math.sqrt((this.x * this.x) + (this.y * this.y)); | |||
| } | |||
| Vec2.prototype.set = function(vec) { | |||
| this.x = vec.x; | |||
| this.y = vec.y; | |||
| return this; | |||
| } | |||
| Vec2.prototype.add = function(vec) { | |||
| this.x += vec.x; | |||
| this.y += vec.y; | |||
| return this; | |||
| } | |||
| Vec2.prototype.sub = function(vec) { | |||
| this.x -= vec.x; | |||
| this.y -= vec.y; | |||
| return this; | |||
| } | |||
| Vec2.prototype.scale = function(num) { | |||
| this.x *= num; | |||
| this.y *= num; | |||
| return this; | |||
| } | |||
| Vec2.prototype.normalize = function() { | |||
| var len = this.length(); | |||
| if (len === 0) { | |||
| this.x = 1; | |||
| this.y = 0; | |||
| } else { | |||
| this.scale(1 / len); | |||
| } | |||
| return this; | |||
| } | |||
| Vec2.prototype.rotate = function(rad) { | |||
| var x = this.x; | |||
| var y = this.y; | |||
| this.x = x * Math.cos(rad) - y * Math.sin(rad); | |||
| this.y = y * Math.cos(rad) + x * Math.sin(rad); | |||
| return this; | |||
| } | |||
| Vec2.prototype.rotation = function() { | |||
| return Math.atan2(this.y, this.x); | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| function WorldGen(game) { | |||
| this.game = game; | |||
| this.prevX = 0; | |||
| this.minY = -400; | |||
| this.maxY = 400; | |||
| this.powerupCounter = randInt(WorldGen.range[0], WorldGen.range[1]); | |||
| // Spawn player | |||
| game.entities.push(new Player(game)); | |||
| } | |||
| WorldGen.range = [5, 12]; | |||
| WorldGen.prototype.update = function() { | |||
| while (this.game.camera.x + this.game.canvas.width > this.prevX) | |||
| this.genNext(); | |||
| } | |||
| WorldGen.prototype.genNext = function() { | |||
| var x = this.prevX + 300; | |||
| var y = Math.round((Math.random() - 0.5) * 300); | |||
| if (y < this.minY) y = this.minY; | |||
| if (y > this.maxY) y = this.maxY; | |||
| var obstacle = new Obstacle(this.game, x, y); | |||
| this.game.entities.push(obstacle); | |||
| this.prevX = x; | |||
| if (--this.powerupCounter === 0) { | |||
| var powerup = new PowerUp( | |||
| this.game, | |||
| x - 50, | |||
| y + 80 + (this.game.canvas.height / 2)); | |||
| this.game.entities.push(powerup); | |||
| this.powerupCounter = randInt(WorldGen.range[0], WorldGen.range[1]); | |||
| } | |||
| } | |||