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]); | |||||
} | |||||
} |