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