| body { | |||||
| margin: 0px; | |||||
| } | |||||
| canvas { | |||||
| position: absolute; | |||||
| } |
| <!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> |
| 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); | |||||
| } | |||||
| } |
| /* | |||||
| * 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; | |||||
| } |
| 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(); | |||||
| } |
| 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(); |
| 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; | |||||
| } |
| 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); | |||||
| } |
| 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]); | |||||
| } | |||||
| } |