| import Rect from './Rect'; | |||||
| import Vec2 from './Vec2'; | |||||
| import Rect from './Rect.js'; | |||||
| import Vec2 from './Vec2.js'; | |||||
| export class Trait { | export class Trait { | ||||
| constructor(entity, name) { | constructor(entity, name) { |
| import Player from './entities/Player'; | |||||
| import Player from './entities/Player.js'; | |||||
| import structures from './structures.js'; | |||||
| export default class Level { | export default class Level { | ||||
| constructor(canvas) { | constructor(canvas) { | ||||
| this.step = 1 / 120; | this.step = 1 / 120; | ||||
| this.stepLimit = 2; | |||||
| this.canvas = canvas; | this.canvas = canvas; | ||||
| this.ctx = canvas.getContext("2d"); | this.ctx = canvas.getContext("2d"); | ||||
| this.raf = null; | this.raf = null; | ||||
| this.timeAcc = 0; | this.timeAcc = 0; | ||||
| this.entities = []; | this.entities = []; | ||||
| this.evts = []; | |||||
| this.structure = structures.floor(2, 2, 1); | |||||
| } | } | ||||
| spawn(ent, x, y) { | spawn(ent, x, y) { | ||||
| } | } | ||||
| draw() { | 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.entities.forEach(ent => ent.draw(this.ctx)); | ||||
| this.structure.draw(this.ctx); | |||||
| } | } | ||||
| update(time) { | update(time) { | ||||
| let dt = (time - this.lastTime) / 1000; | let dt = (time - this.lastTime) / 1000; | ||||
| this.timeAcc += dt; | 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.physics(this.step); | ||||
| this.timeAcc -= this.step; | this.timeAcc -= this.step; | ||||
| } | } | ||||
| } | } | ||||
| this.lastTime = time; | this.lastTime = time; | ||||
| this.raf = requestAnimationFrame(time => this.update(time)); | |||||
| this.raf = requestAnimationFrame(this.update.bind(this)); | |||||
| } | } | ||||
| start() { | start() { | ||||
| this.stop(); | |||||
| this.lastTime = null; | |||||
| this.update(); | |||||
| if (this.raf == null) { | |||||
| this.lastTime = null; | |||||
| this.update(); | |||||
| console.log("Started."); | |||||
| } | |||||
| } | } | ||||
| stop() { | stop() { | ||||
| if (this.raf != null) { | if (this.raf != null) { | ||||
| cancelAnimationFrame(this.raf); | cancelAnimationFrame(this.raf); | ||||
| this.raf = null; | this.raf = null; | ||||
| console.log("Stopped."); | |||||
| } | } | ||||
| } | } | ||||
| } | } |
| import Vec2 from './Vec2'; | |||||
| import Vec2 from './Vec2.js'; | |||||
| export default class Rect { | export default class Rect { | ||||
| constructor(pos = new Vec2(), size = new Vec2()) { | constructor(pos = new Vec2(), size = new Vec2()) { |
| 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; | |||||
| } | |||||
| } |
| 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); | |||||
| } | |||||
| } |
| 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), | |||||
| } |
| 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 { | export default class Player extends Entity { | ||||
| constructor(level) { | constructor(level) { | ||||
| this.bounds.size.set(20, 20); | this.bounds.size.set(20, 20); | ||||
| this.addTrait(new KeyboardControls(this)); | this.addTrait(new KeyboardControls(this)); | ||||
| this.addTrait(new Physics(this)); | |||||
| } | } | ||||
| draw(ctx) { | draw(ctx) { |
| import Player from './entities/Player'; | import Player from './entities/Player'; | ||||
| import Vec2 from './Vec2'; | 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.spawn(new Player(level), 20, 20); | ||||
| level.start(); | 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(); |
| 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" }, | |||||
| ]); | |||||
| } | |||||
| }, | |||||
| } |
| import {Trait} from '../Entity'; | |||||
| import {Trait} from '../Entity.js'; | |||||
| export default class KeyboardControls extends Trait { | export default class KeyboardControls extends Trait { | ||||
| constructor(entity) { | constructor(entity) { |
| 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; | |||||
| } | |||||
| } |
| "private": "0.1.8", | "private": "0.1.8", | ||||
| "slash": "1.0.0", | "slash": "1.0.0", | ||||
| "source-map": "0.5.7" | "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": { | "babel-generator": { | ||||
| "babel-plugin-transform-es2015-unicode-regex": "6.24.1", | "babel-plugin-transform-es2015-unicode-regex": "6.24.1", | ||||
| "babel-plugin-transform-exponentiation-operator": "6.24.1", | "babel-plugin-transform-exponentiation-operator": "6.24.1", | ||||
| "babel-plugin-transform-regenerator": "6.26.0", | "babel-plugin-transform-regenerator": "6.26.0", | ||||
| "browserslist": "2.9.1", | |||||
| "browserslist": "2.10.0", | |||||
| "invariant": "2.2.2", | "invariant": "2.2.2", | ||||
| "semver": "5.4.1" | "semver": "5.4.1" | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "browserslist": { | "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, | "dev": true, | ||||
| "requires": { | "requires": { | ||||
| "caniuse-lite": "1.0.30000780", | "caniuse-lite": "1.0.30000780", | ||||
| "inline-source-map": "0.6.2", | "inline-source-map": "0.6.2", | ||||
| "lodash.memoize": "3.0.4", | "lodash.memoize": "3.0.4", | ||||
| "source-map": "0.5.7" | "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": { | "concat-map": { | ||||
| "dev": true | "dev": true | ||||
| }, | }, | ||||
| "convert-source-map": { | "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 | "dev": true | ||||
| }, | }, | ||||
| "core-js": { | "core-js": { | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "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": { | "diffie-hellman": { | ||||
| "version": "5.0.2", | "version": "5.0.2", | ||||
| "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", | "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", | ||||
| "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", | ||||
| "dev": true | "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": { | "number-is-nan": { | ||||
| "version": "1.0.1", | "version": "1.0.1", | ||||
| "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", | ||||
| "wrappy": "1.0.2" | "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": { | "os-browserify": { | ||||
| "version": "0.3.0", | "version": "0.3.0", | ||||
| "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", | "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", | ||||
| "indexof": "0.0.1" | "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": { | "wrappy": { | ||||
| "version": "1.0.2", | "version": "1.0.2", | ||||
| "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", |
| "main": "index.js", | "main": "index.js", | ||||
| "scripts": { | "scripts": { | ||||
| "test": "echo \"Error: no test specified\" && exit 1", | "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": "", | "author": "", | ||||
| "license": "ISC", | "license": "ISC", | ||||
| "babel-core": "^6.26.0", | "babel-core": "^6.26.0", | ||||
| "babel-preset-env": "^1.6.1", | "babel-preset-env": "^1.6.1", | ||||
| "babelify": "^8.0.0", | "babelify": "^8.0.0", | ||||
| "browserify": "^14.5.0" | |||||
| "browserify": "^14.5.0", | |||||
| "dev-refresh": "^0.3.0" | |||||
| } | } | ||||
| } | } |
| </style> | </style> | ||||
| </head> | </head> | ||||
| <body> | <body> | ||||
| <canvas id="canvas"></canvas> | |||||
| <canvas id="canvas" width="640" height="480"></canvas> | |||||
| <script src="bundle.js"></script> | <script src="bundle.js"></script> | ||||
| </body> | </body> | ||||
| </html> | </html> |