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