@@ -1,5 +1,5 @@ | |||
import Rect from './Rect'; | |||
import Vec2 from './Vec2'; | |||
import Rect from './Rect.js'; | |||
import Vec2 from './Vec2.js'; | |||
export class Trait { | |||
constructor(entity, name) { |
@@ -1,8 +1,10 @@ | |||
import Player from './entities/Player'; | |||
import Player from './entities/Player.js'; | |||
import structures from './structures.js'; | |||
export default class Level { | |||
constructor(canvas) { | |||
this.step = 1 / 120; | |||
this.stepLimit = 2; | |||
this.canvas = canvas; | |||
this.ctx = canvas.getContext("2d"); | |||
@@ -10,6 +12,10 @@ export default class Level { | |||
this.raf = null; | |||
this.timeAcc = 0; | |||
this.entities = []; | |||
this.evts = []; | |||
this.structure = structures.floor(2, 2, 1); | |||
} | |||
spawn(ent, x, y) { | |||
@@ -28,9 +34,12 @@ export default class Level { | |||
} | |||
draw() { | |||
this.canvas.width = window.innerWidth; | |||
this.canvas.height = window.innerHeight; | |||
this.ctx.resetTransform(); | |||
this.ctx.beginPath(); | |||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |||
this.entities.forEach(ent => ent.draw(this.ctx)); | |||
this.structure.draw(this.ctx); | |||
} | |||
update(time) { | |||
@@ -38,7 +47,17 @@ export default class Level { | |||
let dt = (time - this.lastTime) / 1000; | |||
this.timeAcc += dt; | |||
while (this.timeAcc > this.step) { | |||
if (this.timeAcc > this.stepLimit) { | |||
console.warn( | |||
"Attempt to simulate "+this.timeAcc.toFixed(2)+" "+ | |||
"seconds, which is over the limit ("+this.stepLimit+"s). "+ | |||
"Resetting accumulator."); | |||
this.timeAcc = dt; | |||
} | |||
let nticks = 0; | |||
while (this.timeAcc >= this.step) { | |||
nticks += 1; | |||
this.physics(this.step); | |||
this.timeAcc -= this.step; | |||
} | |||
@@ -47,19 +66,22 @@ export default class Level { | |||
} | |||
this.lastTime = time; | |||
this.raf = requestAnimationFrame(time => this.update(time)); | |||
this.raf = requestAnimationFrame(this.update.bind(this)); | |||
} | |||
start() { | |||
this.stop(); | |||
this.lastTime = null; | |||
this.update(); | |||
if (this.raf == null) { | |||
this.lastTime = null; | |||
this.update(); | |||
console.log("Started."); | |||
} | |||
} | |||
stop() { | |||
if (this.raf != null) { | |||
cancelAnimationFrame(this.raf); | |||
this.raf = null; | |||
console.log("Stopped."); | |||
} | |||
} | |||
} |
@@ -1,4 +1,4 @@ | |||
import Vec2 from './Vec2'; | |||
import Vec2 from './Vec2.js'; | |||
export default class Rect { | |||
constructor(pos = new Vec2(), size = new Vec2()) { |
@@ -0,0 +1,81 @@ | |||
export default class SpriteSheet { | |||
constructor(url, tilew = 1, tileh = 1, scale = 1) { | |||
this.url = url; | |||
this.tilew = tilew; | |||
this.tileh = tileh; | |||
this.scale = scale; | |||
this.definitions = {}; | |||
this.waiting = []; | |||
this.ready = false; | |||
this.img = new Image(); | |||
this.img.src = url; | |||
this.img.onload = () => { | |||
this.ready = true; | |||
this.waiting.forEach(f => f()); | |||
}; | |||
} | |||
whenReady(fn) { | |||
if (this.ready) return fn(); | |||
this.waiting.push(fn); | |||
return this; | |||
} | |||
define(name, x, y, w = this.tilew, h = this.tileh) { | |||
this.definitions[name] = null; | |||
this.whenReady(() => { | |||
let can = document.createElement("canvas"); | |||
can.width = w * this.scale; | |||
can.height = h * this.scale; | |||
let ctx = can.getContext("2d"); | |||
ctx.mozImageSmoothingEnabled = false; | |||
ctx.webkitImageSmoothingEnabled = false; | |||
ctx.msImageSmoothingEnabled = false; | |||
ctx.imageSmoothingEnabled = false; | |||
ctx.drawImage( | |||
this.img, x, y, | |||
w, h, 0, 0, | |||
w * this.scale, h * this.scale); | |||
this.definitions[name] = can; | |||
}); | |||
return this; | |||
} | |||
defineTile(name, tx, ty) { | |||
return this.define(name, tx * this.tilew, ty * this.tileh); | |||
} | |||
draw(ctx, name, x, y, sx = 1, sy = 1) { | |||
let def = this.definitions[name]; | |||
if (def === null) return; | |||
if (def === undefined) throw new Error("Undefined sprite: "+name); | |||
ctx.drawImage( | |||
def, x, y, | |||
def.width * sx, | |||
def.height * sy); | |||
return this; | |||
} | |||
drawTile(ctx, name, tx, ty, sx = 1, sy = 1) { | |||
this.draw( | |||
ctx, name, | |||
tx * this.tilew * this.scale, ty * this.tileh * this.scale, | |||
sx, sy); | |||
return this; | |||
} | |||
get tileWidth() { | |||
return this.tilew * this.scale; | |||
} | |||
get tileHeight() { | |||
return this.tileh * this.scale; | |||
} | |||
} |
@@ -0,0 +1,52 @@ | |||
import assets from './assets.js'; | |||
import Rect from './Rect.js'; | |||
import Vec2 from './Vec2.js'; | |||
function findBounds(ctx, arr, bounds) { | |||
arr.forEach(e => { | |||
if (e instanceof Array) { | |||
drawArr(ctx, e); | |||
} else { | |||
let right = (e.x + 1) * assets.tiles.tileWidth; | |||
let bottom = (e.y + 1) * assets.tiles.tileHeight; | |||
if (bounds.size.x < right) | |||
bounds.size.x = right; | |||
if (bounds.size.y < bottom) | |||
bounds.size.y = bottom; | |||
} | |||
}); | |||
} | |||
function drawArr(ctx, arr) { | |||
arr.forEach(e => { | |||
if (e instanceof Array) { | |||
drawArr(ctx, e); | |||
} else { | |||
assets.tiles.drawTile(ctx, e.tile, e.x, e.y); | |||
} | |||
}); | |||
} | |||
export default class Structure { | |||
constructor(x, y, arr) { | |||
console.log(arr); | |||
this.can = document.createElement("canvas"); | |||
this.bounds = new Rect(new Vec2( | |||
x * assets.tiles.tileWidth, y * assets.tiles.tileHeight)); | |||
let ctx = this.can.getContext("2d"); | |||
findBounds(ctx, arr, this.bounds); | |||
this.can.width = this.bounds.size.x; | |||
this.can.height = this.bounds.size.y; | |||
assets.tiles.whenReady(() => drawArr(ctx, arr)); | |||
console.log(this.bounds.pos, this.bounds.size); | |||
} | |||
draw(ctx) { | |||
ctx.drawImage( | |||
this.can, this.bounds.pos.x, this.bounds.pos.y, | |||
this.bounds.size.x, this.bounds.size.y); | |||
} | |||
} |
@@ -0,0 +1,10 @@ | |||
import SpriteSheet from './SpriteSheet.js'; | |||
export default { | |||
tiles: new SpriteSheet("assets/tiles.png", 32, 32, 2) | |||
.defineTile("ground", 0, 0) | |||
.defineTile("grass", 1, 0) | |||
.defineTile("grass-l", 2, 0) | |||
.defineTile("grass-r", 3, 0) | |||
.defineTile("grass-lr", 4, 0), | |||
} |
@@ -1,7 +1,8 @@ | |||
import Entity from '../Entity'; | |||
import Vec2 from '../Vec2'; | |||
import Entity from '../Entity.js'; | |||
import Vec2 from '../Vec2.js'; | |||
import KeyboardControls from '../traits/KeyboardControls'; | |||
import KeyboardControls from '../traits/KeyboardControls.js'; | |||
import Physics from '../traits/Physics.js'; | |||
export default class Player extends Entity { | |||
constructor(level) { | |||
@@ -9,6 +10,7 @@ export default class Player extends Entity { | |||
this.bounds.size.set(20, 20); | |||
this.addTrait(new KeyboardControls(this)); | |||
this.addTrait(new Physics(this)); | |||
} | |||
draw(ctx) { |
@@ -2,8 +2,33 @@ import Level from './Level'; | |||
import Player from './entities/Player'; | |||
import Vec2 from './Vec2'; | |||
let level = new Level(document.getElementById("canvas")); | |||
let canvas = document.getElementById("canvas"); | |||
let level = new Level(canvas); | |||
level.spawn(new Player(level), 20, 20); | |||
level.start(); | |||
// Pause the game when the tab has been out of focus for more than half a second | |||
let blurTimeout = null; | |||
window.addEventListener("focus", () => { | |||
if (blurTimeout != null) { | |||
clearTimeout(blurTimeout); | |||
blurTimeout = null; | |||
} | |||
level.start(); | |||
}); | |||
window.addEventListener("blur", () => { | |||
if (blurTimeout == null) { | |||
blurTimeout = setTimeout(() => level.stop(), 500); | |||
} | |||
}); | |||
// Resize canvas | |||
function resize() { | |||
canvas.width = window.innerWidth; | |||
canvas.height = window.innerHeight; | |||
} | |||
window.addEventListener("resize", resize); | |||
resize(); |
@@ -0,0 +1,16 @@ | |||
import Structure from './Structure.js'; | |||
export default { | |||
floor: (x, y, width) => { | |||
if (width <= 1) { | |||
return new Structure(x, y, [ { x: 0, y: 0, tile: "grass-lr" }]); | |||
} else { | |||
return new Structure(x, y, [ | |||
{ x: 0, y: 0, tile: "grass-l", }, | |||
Array.from({ length: width - 2 }, (x, i) => | |||
({ x: i + 1, y: 0, tile: "grass" })), | |||
{ x: width - 1, y: 0, tile: "grass-r" }, | |||
]); | |||
} | |||
}, | |||
} |
@@ -1,4 +1,4 @@ | |||
import {Trait} from '../Entity'; | |||
import {Trait} from '../Entity.js'; | |||
export default class KeyboardControls extends Trait { | |||
constructor(entity) { |
@@ -0,0 +1,13 @@ | |||
import {Trait} from '../Entity.js'; | |||
export default class Physics extends Trait { | |||
constructor(entity) { | |||
super(entity, "physics"); | |||
this.gravity = 9.81; | |||
} | |||
update(dt) { | |||
this.entity.velocity.y += this.gravity * dt; | |||
} | |||
} |
@@ -115,14 +115,6 @@ | |||
"private": "0.1.8", | |||
"slash": "1.0.0", | |||
"source-map": "0.5.7" | |||
}, | |||
"dependencies": { | |||
"convert-source-map": { | |||
"version": "1.5.1", | |||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", | |||
"integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", | |||
"dev": true | |||
} | |||
} | |||
}, | |||
"babel-generator": { | |||
@@ -622,7 +614,7 @@ | |||
"babel-plugin-transform-es2015-unicode-regex": "6.24.1", | |||
"babel-plugin-transform-exponentiation-operator": "6.24.1", | |||
"babel-plugin-transform-regenerator": "6.26.0", | |||
"browserslist": "2.9.1", | |||
"browserslist": "2.10.0", | |||
"invariant": "2.2.2", | |||
"semver": "5.4.1" | |||
} | |||
@@ -896,9 +888,9 @@ | |||
} | |||
}, | |||
"browserslist": { | |||
"version": "2.9.1", | |||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.9.1.tgz", | |||
"integrity": "sha512-3n3nPdbUqn3nWmsy4PeSQthz2ja1ndpoXta+dwFFNhveGjMg6FXpWYe12vsTpNoXJbzx3j7GZXdtoVIdvh3JbA==", | |||
"version": "2.10.0", | |||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.10.0.tgz", | |||
"integrity": "sha512-WyvzSLsuAVPOjbljXnyeWl14Ae+ukAT8MUuagKVzIDvwBxl4UAwD1xqtyQs2eWYPGUKMeC3Ol62goqYuKqTTcw==", | |||
"dev": true, | |||
"requires": { | |||
"caniuse-lite": "1.0.30000780", | |||
@@ -972,6 +964,14 @@ | |||
"inline-source-map": "0.6.2", | |||
"lodash.memoize": "3.0.4", | |||
"source-map": "0.5.7" | |||
}, | |||
"dependencies": { | |||
"convert-source-map": { | |||
"version": "1.1.3", | |||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", | |||
"integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", | |||
"dev": true | |||
} | |||
} | |||
}, | |||
"concat-map": { | |||
@@ -1029,9 +1029,9 @@ | |||
"dev": true | |||
}, | |||
"convert-source-map": { | |||
"version": "1.1.3", | |||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", | |||
"integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", | |||
"version": "1.5.1", | |||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", | |||
"integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", | |||
"dev": true | |||
}, | |||
"core-js": { | |||
@@ -1171,6 +1171,18 @@ | |||
} | |||
} | |||
}, | |||
"dev-refresh": { | |||
"version": "0.3.0", | |||
"resolved": "https://registry.npmjs.org/dev-refresh/-/dev-refresh-0.3.0.tgz", | |||
"integrity": "sha512-aSbk/oqQ3MokCkNscC0Evu20ht82YgTIPnV2essHFb1bDHzftM88U/ovETFBAzs/9F45uf3t5qNAgiqwMSD6Og==", | |||
"dev": true, | |||
"requires": { | |||
"minimist": "1.2.0", | |||
"node-watch": "0.5.5", | |||
"open": "0.0.5", | |||
"webframe": "0.8.2" | |||
} | |||
}, | |||
"diffie-hellman": { | |||
"version": "5.0.2", | |||
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", | |||
@@ -1624,6 +1636,12 @@ | |||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", | |||
"dev": true | |||
}, | |||
"node-watch": { | |||
"version": "0.5.5", | |||
"resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.5.5.tgz", | |||
"integrity": "sha512-z9xN2ibI6P0UylFadN7oMcIMsoTeCENC0rZyRM5MVK9AqzSPx+uGqKG6KMPeC/laOV4wOGZq/GH0PTstRNSqOA==", | |||
"dev": true | |||
}, | |||
"number-is-nan": { | |||
"version": "1.0.1", | |||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", | |||
@@ -1639,6 +1657,12 @@ | |||
"wrappy": "1.0.2" | |||
} | |||
}, | |||
"open": { | |||
"version": "0.0.5", | |||
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", | |||
"integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=", | |||
"dev": true | |||
}, | |||
"os-browserify": { | |||
"version": "0.3.0", | |||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", | |||
@@ -2160,6 +2184,12 @@ | |||
"indexof": "0.0.1" | |||
} | |||
}, | |||
"webframe": { | |||
"version": "0.8.2", | |||
"resolved": "https://registry.npmjs.org/webframe/-/webframe-0.8.2.tgz", | |||
"integrity": "sha512-ohoXTI8ULn/gGJ6lfhXAYn/2qsfApdd7wTJSts0jYcCJyWOW3+VSKLMEddRn9JLOu88/OeuPIiuHlxZ92IHPBA==", | |||
"dev": true | |||
}, | |||
"wrappy": { | |||
"version": "1.0.2", | |||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", |
@@ -5,8 +5,9 @@ | |||
"main": "index.js", | |||
"scripts": { | |||
"test": "echo \"Error: no test specified\" && exit 1", | |||
"build-dbg": "browserify js/main.js -t [ babelify --sourceMap ] --debug --outfile public/bundle.js", | |||
"build-prod": "browserify js/main.js -t [ babelify --sourceMap ] --outfile public/bundle.js" | |||
"build-dev": "browserify js/main.js -t [ babelify --sourceMap ] --debug --outfile public/bundle.js", | |||
"build-prod": "browserify js/main.js -t [ babelify ] --outfile public/bundle.js", | |||
"watch": "dev-refresh --serve public --cmd 'npm run build-dev' js" | |||
}, | |||
"author": "", | |||
"license": "ISC", | |||
@@ -14,6 +15,7 @@ | |||
"babel-core": "^6.26.0", | |||
"babel-preset-env": "^1.6.1", | |||
"babelify": "^8.0.0", | |||
"browserify": "^14.5.0" | |||
"browserify": "^14.5.0", | |||
"dev-refresh": "^0.3.0" | |||
} | |||
} |
@@ -14,7 +14,7 @@ canvas { | |||
</style> | |||
</head> | |||
<body> | |||
<canvas id="canvas"></canvas> | |||
<canvas id="canvas" width="640" height="480"></canvas> | |||
<script src="bundle.js"></script> | |||
</body> | |||
</html> |