| @@ -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> | |||