Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

31 changed files with 379 additions and 2493 deletions

View File

@ -1,2 +0,0 @@
projects
examples

View File

@ -1,33 +0,0 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};

View File

@ -1,48 +1,12 @@
![asdf-games](https://gitea.arnweb.nl/arne/asdf-games/raw/branch/master/res/asdf-logo.png "asdf-games logo")
# asdf-games # asdf-games
My attempt at making the framework featured in the book HTML5 Games: Novice to Ninja. Me making games using HTML5 Games: Novice to Ninja by Sitepoint.
[You can find the book here.](https://www.sitepoint.com/premium/books/html5-games-novice-to-ninja) [You can find the book here.](https://www.sitepoint.com/premium/books/html5-games-novice-to-ninja)
I turned the framework featured in the book into an npm package for use with browserify or Electron. The projects here are very similar to those featured in the book, and it will take some time before I get to make my own game.
If you are on Github or Gitea now, you can find the npm package [here](https://www.npmjs.com/package/asdf-games).
## Installation
To use asdf framework in your projects, you need to:
* Install npm on your device (if you don't have it already)
* Run ```npm install asdf-games```
* Use the snippet below to start off or check out one of the examples, keep in mind that the examples are not comepletely up to date.
* Thanks to my friend [Job](https://jobbel.nl/), this project supports TypeScript typings. Make sure your editor supports them to make your life easier.
## Example usage
```javascript
// Require asdf
const asdf = require("asdf-games");
// Add whatever classes you need here
const [ Game, Sprite, Texture, KeyControls ] = asdf;
// Game(width, height, disable pixel smoothening)
var game = new Game(720, 480, true);
// Any picture URL used in new Texture() must be relative to the location of the HTML file
const playerTexture = new Texture('player.png');
var player = new Sprite(texture);
player.pos = {
x: (game.w / 2) - (player.w / 2),
y: (game.h / 2) - (player.h / 2)
}
// Add your entities to the game's scene
game.scene.add(player);
game.run(() => {
// Game loop
});
```
To try out asdf library and the projects for yourself, you need to:
* TODO
Releases will be featured on
[ARNweb Games](https://arnweb.nl/games/)

View File

@ -1,65 +0,0 @@
class Anim {
constructor(frames, rate) {
this.frames = frames;
this.rate = rate;
this.reset();
}
reset() {
this.frame = this.frames[0];
this.curFrame = 0;
this.curTime = 0;
}
update(dt) {
const { rate, frames } = this;
if ((this.curTime += dt) > rate) {
this.curFrame++;
this.frame = frames[this.curFrame % frames.length];
this.curTime -= rate;
}
}
}
class AnimManager {
constructor(e = { x: 0, y: 0 }) {
this.anims = {};
this.running = false;
this.frameSource = e.frame || e;
this.current = null;
}
add(name, frames, speed) {
this.anims[name] = new Anim(frames, speed);
return this.anims[name];
}
update(dt) {
const { current, anims, frameSource } = this;
if (!current) {
return;
}
const anim = anims[current];
anim.update(dt);
// Sync the tileSprite frame
frameSource.x = anim.frame.x;
frameSource.y = anim.frame.y;
}
play(anim) {
const { current, anims } = this;
if (anim === current) {
return;
}
this.current = anim;
anims[anim].reset();
}
stop() {
this.current = null;
}
}
module.exports = AnimManager;

View File

@ -1,54 +0,0 @@
const Container = require("./Container");
const math = require("./utilities/math");
class Camera extends Container {
constructor(subject, viewport, worldSize = viewport) {
super();
this.w = viewport.w;
this.h = viewport.h;
this.worldSize = worldSize;
this.setSubject(subject);
}
setSubject(e) {
this.subject = e ? e.pos || e : this.pos;
this.offset = { x: 0, y: 0 };
// Center on the entity
if (e && e.w) {
this.offset.x += e.w / 2;
this.offset.y += e.h / 2;
}
if (e && e.anchor) {
this.offset.x -= e.anchor.x;
this.offset.y -= e.anchor.y;
}
this.focus();
}
focus() {
const { pos, w, h, worldSize, subject, offset } = this;
const centeredX = subject.x + offset.x - w / 2;
const maxX = worldSize.w - w;
const x = -math.clamp(centeredX, 0, maxX);
const centeredY = subject.y + offset.y - h / 2;
const maxY = worldSize.h - h;
const y = -math.clamp(centeredY, 0, maxY);
pos.x = x;
pos.y = y;
}
update(dt, t) {
super.update(dt, t);
if (this.subject) {
this.focus();
}
}
}
module.exports = Camera;

View File

@ -1,30 +1,30 @@
class Container { class Container {
constructor() { constructor() {
this.pos = { x: 0, y: 0 }; this.pos = { x: 0, y: 0 };
this.children = []; this.children = [];
} }
add(child) { add(child) {
this.children.push(child); this.children.push(child);
return child; return child;
} }
remove(child) { remove(child) {
this.children = this.children.filter(c => c !== child); this.children = this.children.filter(c => c !== child);
return child; return child;
} }
map(f) { map(f) {
return this.children.map(f); return this.children.map(f);
} }
update(dt, t) { update(dt, t) {
this.children = this.children.filter(child => { this.children = this.children.filter(child => {
if (child.update) { if (child.update) {
child.update(dt, t, this); child.update(dt, t, this);
} }
return child.dead ? false : true; return child.dead ? false : true;
}); });
} }
} }
module.exports = Container; module.exports = Container;

View File

@ -1,43 +1,40 @@
var Container = require("./Container"), var Container = require("./Container"),
CanvasRenderer = require("./renderer/CanvasRenderer") CanvasRenderer = require('./renderer/CanvasRenderer')
; ;
const STEP = 1 / 60; const STEP = 1 / 60;
const FRAME_MAX = 5 * STEP; const FRAME_MAX = 5 * STEP;
class Game { class Game {
constructor(w, h, pixelated, parent = "#board") { constructor(w, h, pixelated, parent = "#board") {
this.w = w; this.w = w;
this.h = h; this.h = h;
this.renderer = new CanvasRenderer(w, h); this.renderer = new CanvasRenderer(w, h);
document.querySelector(parent).appendChild(this.renderer.view); document.querySelector(parent).appendChild(this.renderer.view);
if (pixelated) { if (pixelated) {
this.renderer.setPixelated(); this.renderer.setPixelated();
} }
this.scene = new Container(); this.scene = new Container();
this.paused = false; }
}
run(gameUpdate = () => { }) { run(gameUpdate = () => { }) {
let dt = 0; let dt = 0;
let last = 0; let last = 0;
const loop = ms => { const loop = ms => {
requestAnimationFrame(loop); requestAnimationFrame(loop);
const t = ms / 1000; const t = ms / 1000;
dt = Math.min(t - last, FRAME_MAX); dt = Math.min(t - last, FRAME_MAX);
last = t; last = t;
if (!this.paused) { this.scene.update(dt, t);
this.scene.update(dt, t); gameUpdate(dt, t);
gameUpdate(dt, t); this.renderer.render(this.scene);
this.renderer.render(this.scene); };
} requestAnimationFrame(loop);
}; }
requestAnimationFrame(loop);
}
} }
module.exports = Game; module.exports = Game;

View File

@ -1,9 +0,0 @@
class Line {
constructor(x1, y1, x2, y2, style = "#000000") {
this.pos = { x: x1, y: y1 };
this.target = { x: x2, y: y2 };
this.style = style;
}
}
module.exports = Line;

View File

@ -1,10 +0,0 @@
class Rect {
constructor(w, h, style = { fill: "#333" }) {
this.pos = { x: 0, y: 0 };
this.w = w;
this.h = h;
this.style = style;
}
}
module.exports = Rect;

View File

@ -1,11 +1,10 @@
class Sprite { class Sprite {
constructor(texture) { constructor(texture) {
this.texture = texture; this.texture = texture;
this.pos = { x: 0, y: 0 }; this.pos = { x: 0, y: 0 };
this.anchor = { x: 0, y: 0 }; this.anchor = { x: 0, y: 0 };
this.scale = { x: 1, y: 1 }; this.scale = { x: 1, y: 1 };
this.pivot = { x: 0, y: 0 }; this.rotation = 0;
this.rotation = 0; }
}
} }
module.exports = Sprite; module.exports = Sprite;

View File

@ -1,39 +1,39 @@
class SpriteSheetXML { class SpriteSheetXML {
constructor(url) { constructor(url) {
this.array = []; this.array = [];
this.fetchXMLtoArray(url); this.fetchXMLtoArray(url);
} }
fetchXMLtoArray(url) { fetchXMLtoArray(url) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", url, false); xhr.open('GET', url, false);
xhr.send(null); xhr.send(null);
if (xhr.status === 200) { if (xhr.status === 200) {
var children = xhr.responseXML.children[0].children; var children = xhr.responseXML.children[0].children;
for (let index = 0; index < children.length; index++) { for (let index = 0; index < children.length; index++) {
const element = children[index]; const element = children[index];
this.array.push({ this.array.push({
name: element.attributes.name.nodeValue, name: element.attributes.name.nodeValue,
x: element.attributes.x.nodeValue, x: element.attributes.x.nodeValue,
y: element.attributes.y.nodeValue, y: element.attributes.y.nodeValue,
width: element.attributes.width.nodeValue, width: element.attributes.width.nodeValue,
height: element.attributes.height.nodeValue height: element.attributes.height.nodeValue
}); });
} }
} else { } else {
console.error("XML file cannot be loaded!"); console.error('XML file cannot be loaded!')
} }
} }
findIndex(attribute, value) { findIndex(attribute, value) {
for (let index = 0; index < this.array.length; index++) { for (let index = 0; index < this.array.length; index++) {
const element = this.array[index]; const element = this.array[index];
if (element[attribute] == value) { if (element[attribute] == value) {
return index; return index;
} }
} }
} }
} }
module.exports = SpriteSheetXML; module.exports = SpriteSheetXML;

View File

@ -1,29 +0,0 @@
class State {
constructor(state) {
this.set(state);
}
set(state) {
this.last = this.state;
this.state = state;
this.time = 0;
this.justSetState = true;
}
update(dt) {
this.first = this.justSetState;
this.justSetStrate = false;
this.time += this.first ? 0 : dt;
}
is(state) {
return this.state === state;
}
isIn(...states) {
return states.some(s => this.is(s));
}
}
module.exports = State;

View File

@ -1,8 +1,8 @@
class Text { class Text {
constructor(text = "", style = {}) { constructor(text = "", style = {}) {
this.pos = { x: 0, y: 0 }; this.pos = { x: 0, y: 0 };
this.text = text; this.text = text;
this.style = style; this.style = style;
} }
} }
module.exports = Text; module.exports = Text;

View File

@ -1,7 +1,7 @@
class Texture { class Texture {
constructor(url) { constructor(url) {
this.img = new Image(); this.img = new Image();
this.img.src = url; this.img.src = url;
} }
} }
module.exports = Texture; module.exports = Texture;

View File

@ -1,73 +1,25 @@
var Container = require("./Container"), var Container = require("./Container"),
TileSprite = require("./TileSprite") TileSprite = require("./TileSprite")
; ;
class TileMap extends Container { class TileMap extends Container {
constructor(tiles, mapW, mapH, tileW, tileH, texture) { constructor(tiles, mapW, mapH, tileW, tileH, texture) {
super(); super();
this.mapW = mapW; this.mapW = mapW;
this.mapH = mapH; this.mapH = mapH;
this.tileW = tileW; this.tileW = tileW;
this.tileH = tileH; this.tileH = tileH;
this.w = mapW * tileW; this.w = mapW * tileW;
this.h = mapH * tileH; this.h = mapH * tileH;
this.children = tiles.map((frame, i) => { this.children = tiles.map((frame, i) => {
const s = new TileSprite(texture, tileW, tileH); const s = new TileSprite(texture, tileW, tileH);
s.frame = frame; s.frame = frame;
s.pos.x = i % mapW * tileW; s.pos.x = i % mapW * tileW;
s.pos.y = Math.floor(i / mapW) * tileH; s.pos.y = Math.floor(i / mapW) * tileH;
return s; return s;
}); });
} }
pixelToMapPos(pos) {
const { tileW, tileH } = this;
return {
x: Math.floor(pos.x / tileW),
y: Math.floor(pos.y / tileH)
};
}
mapToPixelPos(mapPos) {
const { tileW, tileH } = this;
return {
x: mapPos.x * tileW,
y: mapPos.y * tileH
};
}
tileAtMapPos(mapPos) {
return this.children[mapPos.y * this.mapW + mapPos.x];
}
tileAtPixelPos(pos) {
return this.tileAtMapPos(this.pixelToMapPos(pos));
}
setFrameAtMapPos(mapPos, frame) {
const tile = this.tileAtMapPos(mapPos);
tile.frame = frame;
return tile;
}
setFrameAtPixelPos(pos, frame) {
return this.setFrameAtMapPos(this.pixelToMapPos(pos), frame);
}
tilesAtCorners(bounds, xo = 0, yo = 0) {
return [
[bounds.x, bounds.y], // Top-left
[bounds.x + bounds.w, bounds.y], // Top-right
[bounds.x, bounds.y + bounds.h], // Bottom-left
[bounds.x + bounds.w, bounds.y + bounds.h] // Bottom-right
].map(([x, y]) =>
this.tileAtPixelPos({
x: x + xo,
y: y + yo
})
);
}
} }
module.exports = TileMap; module.exports = TileMap;

View File

@ -1,73 +1,25 @@
var Container = require("./Container"), var Container = require("./Container"),
TileSpriteXML = require("./TileSpriteXML") TileSpriteXML = require("./TileSpriteXML")
; ;
class TileMapXML extends Container { class TileMapXML extends Container {
constructor(tiles, mapW, mapH, texture, xml) { constructor(tiles, mapW, mapH, texture, xml) {
super(texture); super(texture);
this.mapW = mapW; this.mapW = mapW;
this.mapH = mapH; this.mapH = mapH;
this.tileW = xml.array[tiles[0]].width; this.tileW = xml.array[tiles[0]].width;
this.tileH = xml.array[tiles[0]].height; this.tileH = xml.array[tiles[0]].height;
this.w = mapW * this.tileW; this.w = mapW * this.tileW;
this.h = mapH * this.tileH; this.h = mapH * this.tileH;
this.children = tiles.map((frame, i) => { this.children = tiles.map((frame, i) => {
const s = new TileSpriteXML(texture, xml, frame); const s = new TileSpriteXML(texture, xml, frame);
s.frame = frame; s.frame = frame;
s.pos.x = i % mapW * this.tileW; s.pos.x = i % mapW * this.tileW;
s.pos.y = Math.floor(i / mapW) * this.tileH; s.pos.y = Math.floor(i / mapW) * this.tileH;
return s; return s;
}); });
} }
pixelToMapPos(pos) {
const { tileW, tileH } = this;
return {
x: Math.floor(pos.x / tileW),
y: Math.floor(pos.y / tileH)
};
}
mapToPixelPos(mapPos) {
const { tileW, tileH } = this;
return {
x: mapPos.x * tileW,
y: mapPos.y * tileH
};
}
tileAtMapPos(mapPos) {
return this.children[mapPos.y * this.mapW + mapPos.x];
}
tileAtPixelPos(pos) {
return this.tileAtMapPos(this.pixelToMapPos(pos));
}
setFrameAtMapPos(mapPos, frame) {
const tile = this.tileAtMapPos(mapPos);
tile.frame = frame;
return tile;
}
setFrameAtPixelPos(pos, frame) {
return this.setFrameAtMapPos(this.pixelToMapPos(pos), frame);
}
tilesAtCorners(bounds, xo = 0, yo = 0) {
return [
[bounds.x, bounds.y], // Top-left
[bounds.x + bounds.w, bounds.y], // Top-right
[bounds.x, bounds.y + bounds.h], // Bottom-left
[bounds.x + bounds.w, bounds.y + bounds.h] // Bottom-right
].map(([x, y]) =>
this.tileAtPixelPos({
x: x + xo,
y: y + yo
})
);
}
} }
module.exports = TileMapXML; module.exports = TileMapXML;

View File

@ -1,26 +1,12 @@
var Sprite = require("./Sprite"), var Sprite = require("./Sprite");
AnimManager = require("./AnimManager");
class TileSprite extends Sprite { class TileSprite extends Sprite {
constructor(texture, w, h) { constructor(texture, w, h) {
super(texture); super(texture);
this.tileW = w; this.tileW = w;
this.tileH = h; this.tileH = h;
this.frame = { x: 0, y: 0 }; this.frame = { x: 0, y: 0 };
this.anims = new AnimManager(this); }
}
update(dt) {
this.anims.update(dt);
}
get w() {
return this.tileW * Math.abs(this.scale.x);
}
get h() {
return this.tileH * Math.abs(this.scale.y);
}
} }
module.exports = TileSprite; module.exports = TileSprite;

View File

@ -1,27 +1,13 @@
var Sprite = require("./Sprite"), var Sprite = require("./Sprite");
AnimManager = require("./AnimManager");
class TileSpriteXML extends Sprite { class TileSpriteXML extends Sprite {
constructor(texture, xml, index) { constructor(texture, xml, index) {
super(texture); super(texture);
var src = xml.array[index]; var src = xml.array[index];
this.imgPos = { x: src["x"], y: src["y"] }; this.imgPos = { x: src['x'], y: src['y'] };
this.width = src["width"]; this.width = src['width'];
this.height = src["height"]; this.height = src['height'];
this.anims = new AnimManager(this); }
}
update(dt) {
this.anims.update(dt);
}
get w() {
return this.width * Math.abs(this.scale.x);
}
get h() {
return this.height * Math.abs(this.scale.y);
}
} }
module.exports = TileSpriteXML; module.exports = TileSpriteXML;

View File

@ -1,78 +1,61 @@
class KeyControls { class KeyControls {
constructor(listen = true) { constructor() {
this.keys = {}; this.keys = {};
if (listen) { // Bind event handlers
// Bind event handlers document.addEventListener("keydown", e => {
document.addEventListener("keydown", e => { if ([37, 38, 39, 40].indexOf(e.which) >= 0) {
if ([37, 38, 39, 40].indexOf(e.which) >= 0) { e.preventDefault();
e.preventDefault(); }
} this.keys[e.which] = true;
this.keys[e.which] = true; }, false);
}, false); document.addEventListener('keyup', e => {
document.addEventListener("keyup", e => { this.keys[e.which] = false;
this.keys[e.which] = false; }, false);
}, false); }
}
}
get action() { get action() {
// Spacebar // Spacebar
return this.keys[32]; return this.keys[32];
} }
get ctrl() { get x() {
// Control // Arrow Left or A (WASD)
return this.keys[17]; if (this.keys[37] || this.keys[65]) {
} return -1;
}
// Arrow Right or D (WASD)
if (this.keys[39] || this.keys[68]) {
return 1;
}
return 0;
}
get shift() { get y() {
// Shift // Arrow Up or W (WASD)
return this.keys[16]; if (this.keys[38] || this.keys[87]) {
} return -1;
}
get escape() { // Arrow Down or S (WASD)
// Escape if (this.keys[40] || this.keys[83]) {
return this.keys[27]; return 1;
} }
return 0;
get x() { }
// Arrow Left or A (WASD)
if (this.keys[37] || this.keys[65]) {
return -1;
}
// Arrow Right or D (WASD)
if (this.keys[39] || this.keys[68]) {
return 1;
}
return 0;
}
get y() {
// Arrow Up or W (WASD)
if (this.keys[38] || this.keys[87]) {
return -1;
}
// Arrow Down or S (WASD)
if (this.keys[40] || this.keys[83]) {
return 1;
}
return 0;
}
key(key, value) { key(key, value) {
if (value !== undefined) { if (value !== undefined) {
this.keys[key] = value; this.keys[key] = value;
} }
return this.keys[key]; return this.keys[key];
} }
reset() { reset() {
for (let key in this.keys) { for (let key in this.keys) {
this.keys[key] = false; this.keys[key] = false;
} }
} }
} }

View File

@ -1,49 +1,45 @@
class MouseControls { class MouseControls {
constructor(container, listen = true) { constructor(container) {
this.el = container || document.body; this.el = container || document.body;
// State // State
this.pos = { x: 0, y: 0 }; this.pos = { x: 0, y: 0 };
this.isDown = false; this.isDown = false;
this.pressed = false; this.pressed = false;
this.released = false; this.released = false;
// Handlers
document.addEventListener('mousemove', this.move.bind(this), false);
document.addEventListener('mousedown', this.down.bind(this), false);
document.addEventListener('mouseup', this.up.bind(this), false);
}
if (listen) { mousePosFromEvent({ clientX, clientY }) {
// Handlers const { el, pos } = this;
document.addEventListener("mousemove", this.move.bind(this), false); const rect = el.getBoundingClientRect();
document.addEventListener("mousedown", this.down.bind(this), false); const xr = el.width / el.clientWidth;
document.addEventListener("mouseup", this.up.bind(this), false); const yr = el.height / el.clientHeight;
} pos.x = (clientX - rect.left) * xr;
pos.y = (clientY - rect.top) * yr;
}
} move(e) {
this.mousePosFromEvent(e);
}
mousePosFromEvent({ clientX, clientY }) { down(e) {
const { el, pos } = this; this.isDown = true;
const rect = el.getBoundingClientRect(); this.pressed = true;
const xr = el.width / el.clientWidth; this.mousePosFromEvent(e);
const yr = el.height / el.clientHeight; }
pos.x = (clientX - rect.left) * xr;
pos.y = (clientY - rect.top) * yr;
}
move(e) { up() {
this.mousePosFromEvent(e); this.isDown = false;
} this.released = true;
}
down(e) { update() {
this.isDown = true; this.released = false;
this.pressed = true; this.pressed = false;
this.mousePosFromEvent(e); }
}
up() {
this.isDown = false;
this.released = true;
}
update() {
this.released = false;
this.pressed = false;
}
} }
module.exports = MouseControls; module.exports = MouseControls;

372
lib/index.d.ts vendored
View File

@ -2,11 +2,6 @@ export as namespace asdf;
type Coordinates = {x: number, y: number}; type Coordinates = {x: number, y: number};
class Renderable {
visible?: boolean;
dead?: boolean
}
/** /**
* TileSpriteXML class * TileSpriteXML class
*/ */
@ -92,52 +87,6 @@ export class TileMap extends Container<TileSprite> {
* @param texture Texture instance of source image file * @param texture Texture instance of source image file
*/ */
constructor(tiles: Coordinates[], mapW: number, mapH: number, tileW: number, tileH: number, texture: Texture); constructor(tiles: Coordinates[], mapW: number, mapH: number, tileW: number, tileH: number, texture: Texture);
/**
* Calculates in which tile a pixel is located.
* @param pos The position of the pixel.
*/
pixelToMapPos(pos: Coordinates): Coordinates
/**
* Calculates the pixel position of a tile in a map.
* @param pos The position of the tile in the map.
*/
mapToPixelPos(pos: Coordinates): Coordinates
/**
* Returns the TileSprite at a given map position.
* @param mapPos the tile of a given position.
*/
tileAtMapPos(mapPos: Coordinates): TileSprite
/**
* Returns the TileSprite at a given pixel position.
* @param pos The pixel position of the tile
*/
tileAtPixelPos(pos: Coordinates): TileSprite
/**
* Changes the frame attribute of the TileSprite at the given mapPos.
* @param mapPos the map position of the tile.
* @param frame the new value for the frame attribute.
*/
setFrameAtMapPos(mapPos: Coordinates, frame: Coordinates): TileSprite
/**
* Changes the frame attribute of the TileSprite at the given pixel position.
* @param mapPos the pixel position of the tile.
* @param frame the new value for the frame attribute.
*/
setFrameAtPixelPos(pos: Coordinates, frame: Coordinates): TileSprite
/**
* Returns the tiles at the corner of the bounds.
* @param bounds a rectangle which defines what the corner are.
* @param xo offset to the x values.
* @param yo offset to the y values.
*/
tilesAtCorners(bounds: {x: number, y: number, w: number, h: number}, xo?: number, yo?: number): TileSprite[]
} }
/** /**
@ -154,29 +103,27 @@ export class Texture {
constructor(url: string); constructor(url: string);
} }
interface TextStyleOptions {
font?: string,
fill?: string | CanvasGradient | CanvasPattern,
align?: CanvasTextAlign
}
/** /**
* Text class * Text class
*/ */
export class Text extends Renderable { export class Text {
pos: Coordinates; pos: Coordinates;
text: string; text: string;
visible: boolean; visible: boolean;
update?: (dt?: number, t?: number) => void; update?: (dt?: number, t?: number) => void;
style: TextStyleOptions; style: {
font?: string,
fill?: string | CanvasGradient | CanvasPattern,
align?: CanvasTextAlign
};
/** /**
* Prints styled text on canvas * Prints styled text on canvas
* @param text Text to print * @param text Text to print
* @param style Styles to apply to text * @param style Styles to apply to text
*/ */
constructor(text: string, style: TextStyleOptions); constructor(text: string, style: {});
} }
/** /**
@ -224,13 +171,15 @@ export class SpriteSheetXML {
/** /**
* Sprite class * Sprite class
*/ */
export class Sprite extends Renderable { export class Sprite {
texture: Texture; texture: Texture;
pos: Coordinates; pos: Coordinates;
anchor: Coordinates; anchor: Coordinates;
scale: Coordinates; scale: Coordinates;
pivot: Coordinates; pivot: Coordinates;
visible: boolean;
rotation: number; rotation: number;
dead: boolean;
update?: (dt?: number, t?: number) => void; update?: (dt?: number, t?: number) => void;
@ -249,8 +198,7 @@ export class Game {
h: number; h: number;
renderer: CanvasRenderer; renderer: CanvasRenderer;
scene: Container<unknown>; scene: Container<unknown>;
paused: boolean;
/** /**
* Set the games parameters * Set the games parameters
* @param w Width of canvas * @param w Width of canvas
@ -264,14 +212,14 @@ export class Game {
* Start game loop * Start game loop
* @param gameUpdate Function to run next to scene updates such as debug logging, etc. * @param gameUpdate Function to run next to scene updates such as debug logging, etc.
*/ */
run(gameUpdate?: (dt?: number, t?: number) => void): void; run(gameUpdate: (dt?: number, t?: number) => void): void;
} }
/** /**
* Container class * Container class
*/ */
export class Container<T> { export class Container<T> {
pos: Coordinates; pos: {x: number, y: number};
children: T[]; children: T[];
constructor(); constructor();
@ -301,9 +249,9 @@ export class Container<T> {
* Updates all children when called * Updates all children when called
* @param dt Delta time * @param dt Delta time
* @param t Total time * @param t Total time
* @returns Returns anything * @returns Returns if the child is dead or not
*/ */
update(dt: number, t: number): any; update(dt: number, t: number): boolean;
} }
/** /**
@ -373,24 +321,6 @@ export class KeyControls {
*/ */
get action(): boolean; get action(): boolean;
/**
* Returns value of ctrl key.
* @returns Key value
*/
get ctrl(): boolean;
/**
* Returns value of shift key.
* @returns Key value
*/
get shift(): boolean;
/**
* Returns value of escape key.
* @returns Key value
*/
get escape(): boolean;
/** /**
* Returns -1 on Arrow Left or A * Returns -1 on Arrow Left or A
* *
@ -413,10 +343,10 @@ export class KeyControls {
* @param value Value to set to key * @param value Value to set to key
* @return Value of key * @return Value of key
*/ */
key(key: number, value?: boolean): boolean; key(key: number, value: boolean): boolean;
/** /**
* Resets default value (false) to all keys * Resets default value to all keys
*/ */
reset(): void; reset(): void;
} }
@ -451,275 +381,7 @@ export class CanvasRenderer {
render(container: Container<unknown>, clear?: boolean): void render(container: Container<unknown>, clear?: boolean): void
} }
/**
* Anim represents a single animation.
*/
declare class Anim {
curTime: number;
frames: Coordinates[];
rate: number;
curFrame: number;
frame: Coordinates;
/**
* Constructor for an animation.
* @param frames A collection of coordinates for each frame in the texture file.
* @param rate The rate at which the animation plays.
*/
constructor(frames: Coordinates[], rate: number);
/**
* Resets the animation to the first frame.
*/
reset(): void;
/**
* Causes the animation to update based on dt.
* @param dt Delta time
*/
update(dt: number): void;
}
/**
* The AnimManager class
*/
export class AnimManager {
anims: {
[name: string]: Anim
}
running: boolean;
frameSource: Coordinates;
current: string | null;
constructor(e: Coordinates);
/**
* Adds an animation to the AnimManager
* @param name The name for the animation
* @param frames Where each frame is located in the texture
* @param speed The speed at which the animation plays.
*/
add(name: string, frames: Coordinates[], speed: number): Anim;
/**
* Updates the current animation.
* @param dt delta time
*/
update(dt: number): void;
/**
* Starts playing an animation.
* @param anim The name of the animation
*/
play(anim: string): void;
/**
* Stops playing any animation
*/
stop(): void;
}
/**
* The Rect class
*/
export class Rect {
pos: Coordinates
w: number;
h: number;
style: {
fill: string
}
/**
* Constructs a rectangle.
* @param w The width of the rectangle
* @param h The height of the rectangle
* @param style The styles of the rectangle
*/
constructor(w: number, h: number, style: { fill: string })
}
export class Camera extends Container<unknown> {
w: number;
h: number;
worldSize: {w: number, h: number};
offset: Coordinates;
subject: Coordinates;
/**
* Constructs a camera object. This can be fed into the main scene: `scene.add(camera);`
* @param subject The entity on which should be focused
* @param viewport the size of the viewport - how much should be visible.
* @param worldSize the size of the whole map.
*/
constructor(subject: NumericalEntity | Coordinates, viewport: {w: number, h: number}, worldSize: {w: number, h: number})
/**
* Sets the subject of the camera.
* @param e The entity that needs to be set as subject.
*/
setSubject(e: NumericalEntity | Coordinates): void;
/**
* Moves the camera to the subject
*/
focus(): void;
/**
* update function for the container.
* @param t the elapsed time
* @param dt delta time
*/
update(t: number, dt: number): void;
}
interface SoundOptions {
loop?: boolean,
volume?: number
}
export class Sound {
src: string;
options: SoundOptions;
/**
* Initiates HTML5 audio element for source audio file with control methods
* @param src Source audio file
* @param options Play settings
*/
constructor(src: string, options?: SoundOptions);
/**
* Starts playing the audio file
* @param overrides sets options for playing the sound using different setting as defined in `constructor()`
*/
play(overrides?: SoundOptions): void;
/**
* Stops playing the audio file
*/
stop(): void;
}
interface NumericalEntityBase {pos: Coordinates}
interface NumericalSprite {w: number, h: number}
interface NumericalTileSprite {tileW: number, tileH: number}
type NumericalEntity = NumericalSprite | NumericalTileSprite
type NumericalEntityWithHitbox = {hitBox: NumericalEntity} & NumericalEntity
export namespace deadInTracks {
/**
* This functions checks whether ent walks against a non-walkable object and whether it should move in the x and y position and how much.
* The difference with wallslide is that deadInTracks stops the entity entirely when it touches a non-walkable surface.
* wallslide will move the entity in x or y if possible.
* @param ent The entity that is moving.
* @param map The TileMap the entity moves on.
* @param x The maximal movement on the x. default is 0
* @param y The maximal movement on the y. default is 0
* @returns Coordinates of how much the entity walks in x and y.
*/
export function deadInTracks(ent: NumericalEntity | NumericalEntityWithHitbox, map: TileMap, x?: number, y?: number): Coordinates;
}
export namespace wallslide {
/**
* This functions checks whether ent walks against a non-walkable object and whether it should move in the x and y position and how much.
* The difference with wallslide is that deadInTracks stops the entity entirely when it touches a non-walkable surface.
* wallslide will move the entity in x or y if possible.
* @param ent The entity that is moving.
* @param map The TileMap the entity moves on.
* @param x The maximal movement on the x. default is 0
* @param y The maximal movement on the y. default is 0
* @returns Coordinates of how much the entity walks in x and y.
*/
export function wallslide(ent: NumericalEntity | NumericalEntityWithHitbox, map: TileMap, x?: number, y?: number): Coordinates;
}
export namespace entity {
/**
* addDebug adds a red border around the hitboxes of an entity.
* @param e The entity.
*/
export function addDebug<T extends Container<unknown> | NumericalEntityWithHitbox>(e: T): T;
/**
* This function checks if an entity hits anything in a container.
* @param entity The entity.
* @param container The container.
* @param hitCallback The callback that is executed when an entity hits something in the container.
*/
export function hits<T extends NumericalEntityWithHitbox | NumericalEntity>(entity: NumericalEntityWithHitbox | NumericalEntity, container: Container<T>, hitCallback: (e2: T) => any): void;
/**
* This functions calculates whether two entities hit each other.
* @param e1 The first entity.
* @param e2 The second entity.
*/
export function hit(e1: NumericalEntityWithHitbox | NumericalEntity, e2: NumericalEntityWithHitbox | NumericalEntity): boolean;
/**
* This function calculates the angle relative to the x-axis between the centers of two entities.
* @param a The first entity.
* @param b The second entity.
* @returns the angle in radians.
*/
export function angle(a: NumericalEntity, b: NumericalEntity): number;
/**
* This function calculates the full hitbox of an entity.
* @param entity The enitity
*/
export function bounds(entity: NumericalEntityWithHitbox | NumericalEntity): {x: number, y: number, w: number, h: number};
/**
* This function calculates the distance between the centers of two entities.
* @param a The first entity.
* @param b The second entity.
*/
export function distance(a: NumericalEntity, b: NumericalEntity): number;
/**
* This function calculates the center of an entity.
* @param entity The entity to calculate the center of.
*/
export function center(entity: NumericalEntity): number;
}
export namespace math { export namespace math {
/**
* This function calculates the angle relative from the x-axis between two points.
* @param a The first point.
* @param b The second point.
* @returns The angle in radians.
*/
export function angle(a: Coordinates, b: Coordinates): number;
/**
* This function calculates if x is between min and max.
* @param x A numerical value
* @param min A numerical value
* @param max A numerical value
* @returns x if x is between min and max.
*/
export function clamp(x: number, min: number, max: number): number;
/**
* Calculates the distance between two points
* @param a The first point.
* @param b The second point.
*/
export function distance(a: Coordinates, b: Coordinates): number;
/** /**
* Returns random integer between min and max * Returns random integer between min and max
* @param min Minimum value * @param min Minimum value

View File

@ -1,50 +1,32 @@
var AnimManager = require("./AnimManager.js"), var Container = require("./Container.js"),
Camera = require("./Camera.js"), CanvasRenderer = require("./renderer/CanvasRenderer.js"),
Container = require("./Container.js"), Game = require("./Game.js"),
CanvasRenderer = require("./renderer/CanvasRenderer.js"), math = require("./utilities/math.js"),
Game = require("./Game.js"), KeyControls = require("./controls/KeyControls.js"),
math = require("./utilities/math.js"), MouseControls = require("./controls/MouseControls.js"),
entity = require("./utilities/entity.js"), Sprite = require("./Sprite.js"),
wallslide = require("./movement/wallslide.js"), TileMap = require("./TileMap.js"),
deadInTracks = require("./movement/deadInTracks.js"), TileMapXML = require("./TileMapXML.js"),
State = require("./State.js"), TileSprite = require("./TileSprite.js"),
Sound = require("./sound/Sound.js"), TileSpriteXML = require("./TileSpriteXML.js"),
Rect = require("./Rect.js"), Text = require("./Text.js"),
Line = require("./Line.js"), Texture = require("./Texture.js"),
KeyControls = require("./controls/KeyControls.js"), SpriteSheetXML = require("./SpriteSheetXML.js")
MouseControls = require("./controls/MouseControls.js"),
Sprite = require("./Sprite.js"),
TileMap = require("./TileMap.js"),
TileMapXML = require("./TileMapXML.js"),
TileSprite = require("./TileSprite.js"),
TileSpriteXML = require("./TileSpriteXML.js"),
Text = require("./Text.js"),
Texture = require("./Texture.js"),
SpriteSheetXML = require("./SpriteSheetXML.js")
; ;
module.exports = { module.exports = {
AnimManager, CanvasRenderer,
CanvasRenderer, Container,
Camera, Game,
Container, math,
Game, KeyControls,
math, MouseControls,
entity, Sprite,
wallslide, TileMap,
deadInTracks, TileMapXML,
State, TileSprite,
Sound, SpriteSheetXML,
Rect, TileSpriteXML,
Line, Text,
KeyControls, Texture
MouseControls,
Sprite,
TileMap,
TileMapXML,
TileSprite,
SpriteSheetXML,
TileSpriteXML,
Text,
Texture
}; };

View File

@ -1,16 +0,0 @@
const entity = require("../utilities/entity");
function deadInTracks(ent, map, x = 0, y = 0) {
const bounds = entity.bounds(ent);
const tiles = map.tilesAtCorners(bounds, x, y);
const walks = tiles.map(t => t && t.frame.walkable);
const blocked = walks.some(w => !w);
if (blocked) {
x = 0,
y = 0;
}
return { x, y };
}
module.exports = {
deadInTracks
};

View File

@ -1,51 +0,0 @@
const entity = require("../utilities/entity.js");
function wallslide(ent, map, x = 0, y = 0) {
let tiles;
let tileEdge;
const bounds = entity.bounds(ent);
// Final amounts of movement to allow
let xo = x;
let yo = y;
// Check vertical movement
if (y !== 0) {
tiles = map.tilesAtCorners(bounds, 0, yo);
const [tl, tr, bl, br] = tiles.map(t => t && t.frame.walkable);
// Hit your head
if (y < 0 && !(tl && tr)) {
tileEdge = tiles[0].pos.y + tiles[0].h;
yo = tileEdge - bounds.y;
}
// Hit your feet
if (y > 0 && !(bl && br)) {
tileEdge = tiles[2].pos.y - 1;
yo = tileEdge - (bounds.y + bounds.h);
}
}
// Check horizontal movement
if (x !== 0) {
tiles = map.tilesAtCorners(bounds, xo, yo);
const [tl, tr, bl, br] = tiles.map(t => t && t.frame.walkable);
// Hit left edge
if (x < 0 && !(tl && bl)) {
tileEdge = tiles[0].pos.x + tiles[0].w;
xo = tileEdge - bounds.x;
}
// Hit right edge
if (x > 0 && !(tr && br)) {
tileEdge = tiles[1].pos.x - 1;
xo = tileEdge - (bounds.x + bounds.w);
}
}
// xo & yo contain the amount we're allowed to move by.
return { x: xo, y: yo };
}
module.exports = {
wallslide
};

View File

@ -1,115 +1,97 @@
class CanvasRenderer { class CanvasRenderer {
constructor(w, h) { constructor(w, h) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
this.w = canvas.width = w; this.w = canvas.width = w;
this.h = canvas.height = h; this.h = canvas.height = h;
this.view = canvas; this.view = canvas;
this.ctx = canvas.getContext("2d"); this.ctx = canvas.getContext("2d");
this.ctx.textBaseLine = "top"; this.ctx.textBaseLine = "top";
} }
setPixelated() { setPixelated() {
this.ctx["imageSmoothingEnabled"] = false; /* standard */ this.ctx['imageSmoothingEnabled'] = false; /* standard */
this.ctx["mozImageSmoothingEnabled"] = false; /* Firefox */ this.ctx['mozImageSmoothingEnabled'] = false; /* Firefox */
this.ctx["oImageSmoothingEnabled"] = false; /* Opera */ this.ctx['oImageSmoothingEnabled'] = false; /* Opera */
this.ctx["webkitImageSmoothingEnabled"] = false; /* Safari */ this.ctx['webkitImageSmoothingEnabled'] = false; /* Safari */
this.ctx["msImageSmoothingEnabled"] = false; /* IE */ this.ctx['msImageSmoothingEnabled'] = false; /* IE */
} }
render(container, clear = true) { render(container, clear = true) {
const { ctx } = this; const { ctx } = this;
function renderRec(container) { function renderRec(container) {
// Render container children // Render container children
container.children.forEach(child => { container.children.forEach(child => {
if (child.visible == false) { if (child.visible == false) {
return; return;
} }
ctx.save(); ctx.save();
if (child.pos) { if (child.pos) {
ctx.translate(Math.round(child.pos.x), Math.round(child.pos.y)); ctx.translate(Math.round(child.pos.x), Math.round(child.pos.y));
} }
if (child.anchor) { if (child.anchor) {
ctx.translate(child.anchor.x, child.anchor.y); ctx.translate(child.anchor.x, child.anchor.y);
} }
if (child.scale) { if (child.scale) {
ctx.scale(child.scale.x, child.scale.y); ctx.scale(child.scale.x, child.scale.y);
} }
if (child.rotation) { if (child.rotation) {
const px = child.pivot ? child.pivot.x : 0; const px = child.pivot ? child.pivot.x : 0;
const py = child.pivot ? child.pivot.y : 0; const py = child.pivot ? child.pivot.y : 0;
ctx.translate(px, py); ctx.translate(px, py);
ctx.rotate(child.rotation); ctx.rotate(child.rotation);
ctx.translate(-px, -py); ctx.translate(-px, -py);
} }
if (child.text) { if (child.text) {
const { font, fill, align } = child.style; const { font, fill, align } = child.style;
if (font) ctx.font = font; if (font) ctx.font = font;
if (fill) ctx.fillStyle = fill; if (fill) ctx.fillStyle = fill;
if (align) ctx.textAlign = align; if (align) ctx.textAlign = align;
ctx.fillText(child.text, 0, 0); ctx.fillText(child.text, 0, 0);
} }
else if (child.texture) { else if (child.texture) {
const img = child.texture.img; const img = child.texture.img;
if (child.tileW && child.tileH) { if (child.tileW && child.tileH) {
ctx.drawImage( ctx.drawImage(
img, img,
child.frame.x * child.tileW, child.frame.x * child.tileW,
child.frame.y * child.tileH, child.frame.y * child.tileH,
child.tileW, child.tileH, child.tileW, child.tileH,
0, 0, 0, 0,
child.tileW, child.tileH child.tileW, child.tileH
); );
} else if (child.imgPos && child.width && child.height) { } else if (child.imgPos && child.width && child.height) {
ctx.drawImage( ctx.drawImage(
img, img,
child.imgPos.x, child.imgPos.x,
child.imgPos.y, child.imgPos.y,
child.width, child.height, child.width, child.height,
0, 0, 0, 0,
child.width, child.height child.width, child.height
); );
} else { } else {
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
} }
} else if (child.style && child.w && child.h) { }
ctx.fillStyle = child.style.fill;
ctx.fillRect(0, 0, child.w, child.h);
} else if (child.style && child.radius) {
var gradient = ctx.createRadialGradient(0, 0, 50, 0, 0, 50);
gradient.addColorStop(0, child.style.start);
gradient.addColorStop(1, child.style.stop);
ctx.arc(0, 0, child.radius, 0, 2 * Math.PI); // Handle children with children
ctx.fillStyle = gradient; if (child.children) {
renderRec(child);
ctx.fill(); }
} else if (child.style && child.target) { ctx.restore();
ctx.beginPath(); })
ctx.moveTo(0,0); }
ctx.lineTo(child.target.x, child.target.y); if (clear) {
ctx.strokeStyle = child.style; ctx.clearRect(0, 0, this.w, this.h);
ctx.stroke(); }
} renderRec(container);
}
// Handle children with children
if (child.children) {
renderRec(child);
}
ctx.restore();
});
}
if (clear) {
ctx.clearRect(0, 0, this.w, this.h);
}
renderRec(container);
}
} }
module.exports = CanvasRenderer; module.exports = CanvasRenderer;

View File

@ -1,53 +0,0 @@
class Sound {
constructor(src, options = {}) {
this.playing = false;
this.src = src;
this.options = Object.assign({ volume: 1 }, options);
// Configure audio element
const audio = new Audio();
audio.src = src;
if (options.loop) {
audio.loop = true;
}
audio.addEventListener(
"error",
() => {
throw Error(`Error loading audio: ${src}`);
},
false
);
audio.addEventListener(
"ended",
() => {
this.playing = false;
},
false
);
this.audio = audio;
}
play(overrides) {
const { audio, options } = this;
const opts = Object.assign({ time: 0 }, options, overrides);
audio.volume = opts.volume;
audio.currentTime = opts.time;
audio.play();
this.playing = true;
}
stop() {
this.audio.pause();
this.playing = false;
}
get volume() {
return this.audio.volume;
}
set volume(volume) {
this.options.volume = this.audio.volume = volume;
}
}
module.exports = Sound;

View File

@ -1,96 +0,0 @@
const math = require("./math");
const Rect = require("../Rect");
function addDebug(e) {
e.children = e.children || [];
const bb = new Rect(e.w, e.h, { fill: "rgba(255, 0, 0, 0.3)" });
e.children.push(bb);
if (e.hitBox) {
const { x, y, w, h } = e.hitBox;
const hb = new Rect(w, h, { fill: "rgba(255, 0, 0, 0.5)" });
hb.pos.x = x;
hb.pos.y = y;
e.children.push(hb);
}
return e;
}
function angle(a, b) {
return math.angle(center(a), center(b));
}
function bounds(entity) {
let pos, w, h, hitBox;
// Object.prototype.hasOwnProperty.call is needed because of eslint
if (Object.prototype.hasOwnProperty.call(entity, "tileW") && Object.prototype.hasOwnProperty.call(entity, "tileH")) {
({pos, w, h} = {pos: entity.pos, w: entity.tileW, h: entity.tileH});
} else {
({pos, w, h} = entity);
}
if (entity.hitBox) hitBox = entity.hitBox;
const hit = hitBox || { x: 0, y: 0, w, h };
return {
x: hit.x + pos.x,
y: hit.y + pos.y,
w: hit.w - 1,
h: hit.h - 1
};
}
function center(entity) {
let pos, w, h;
// Object.prototype.hasOwnProperty.call is needed because of eslint
if (Object.prototype.hasOwnProperty.call(entity, "tileW") && Object.prototype.hasOwnProperty.call(entity, "tileH")) {
({pos, w, h} = {pos: entity.pos, w: entity.tileW, h: entity.tileH});
} else {
({pos, w, h} = entity);
}
return {
x: pos.x + w / 2,
y: pos.y + h / 2
};
}
function distance(a, b) {
return math.distance(center(a), center(b));
}
function hit(e1, e2) {
const a = bounds(e1);
const b = bounds(e2);
return (
a.x + a.w >= b.x &&
a.x <= b.x + b.w &&
a.y + a.h >= b.y &&
a.y <= b.y + b.h
);
}
function hits(entity, container, hitCallback) {
const a = bounds(entity);
container.map(e2 => {
const b = bounds(e2);
if (
a.x + a.w >= b.x &&
a.x <= b.x + b.w &&
a.y + a.h >= b.y &&
a.y <= b.y + b.h
) {
hitCallback(e2);
}
});
}
module.exports = {
addDebug,
angle,
bounds,
center,
distance,
hit,
hits
};

View File

@ -1,48 +1,26 @@
function angle(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
const angle = Math.atan2(dy, dx);
return angle;
}
function clamp(x, min, max) {
return Math.max(min, Math.min(x, max));
}
function distance (a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function rand(min, max) { function rand(min, max) {
return Math.floor(randf(min, max)); return Math.floor(randf(min, max));
} }
function randf(min, max) { function randf(min, max) {
if (max == null) { if (max == null) {
max = min || 1; max = min || 1;
min = 0; min = 0;
} }
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
} }
function randOneFrom(items) { function randOneFrom(items) {
return items[rand(items.length)]; return items[rand(items.length)];
} }
function randOneIn(max = 2) { function randOneIn(max = 2) {
return rand(0, max) === 0; return rand(0, max) === 0;
} }
module.exports = { module.exports = {
angle, rand,
clamp, randf,
distance, randOneFrom,
rand, randOneIn
randf,
randOneFrom,
randOneIn
}; };

1109
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "asdf-games", "name": "asdf-games",
"version": "1.1.6", "version": "1.0.0",
"description": "Javascript gaming framework, based upon the framework made in the book HTML5 Games: Novice to Ninja by Sitepoint.", "description": "Javascript gaming framework, based upon the framework made in the book HTML5 Games: Novice to Ninja by Sitepoint.",
"main": "lib/index.js", "main": "lib/index.js",
"repository": { "repository": {
@ -17,11 +17,5 @@
"name": "Arne van Iterson", "name": "Arne van Iterson",
"url": "https://gitea.arnweb.nl/arne/" "url": "https://gitea.arnweb.nl/arne/"
}, },
"license": "ISC", "license": "ISC"
"devDependencies": {
"eslint": "^6.8.0"
},
"dependencies": {
"easystarjs": "^0.4.3"
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB