tin-engine
Version:
Simple game engine to make small canvas based games using es6
876 lines (756 loc) • 21.3 kB
JavaScript
import Entity from '../basic/entity.js';
import graphics from '../core/graphic.js';
import Elli from '../geo/ellipse.js';
import Poly from '../geo/poly.js';
import Rect from '../geo/rect.js';
import V2 from '../geo/v2.js';
import {Zero} from '../geo/v2.js';
import {checkBit,clearBit} from '../util.js';
export class TiledDataLoader {
constructor() {
this.data = [];
}
// Load exported .js map files
loadJS() {
if (typeof(window.TileMaps) == 'undefined') {
window.onTileMapLoaded = this.onTileMapLoaded.bind(this);
} else {
for (const map in window.TileMaps)
this.data[map] = window.TileMaps[map];
}
}
onTileMapLoaded(map, data) {
this.data[map] = data;
}
// Load exported .json map files
preloadJSON(paths, callback) {
if (!Array.isArray(paths)) paths = [paths];
Promise.all(paths.map(url => {
return fetch(url)
.then(response => {
return response.json();
});
}))
.then(data => {
for (let i = 0; i < data.length; i++) {
this.data[paths[i]] = data[i];
}
callback();
});
}
async loadJSON(paths) {
if (!Array.isArray(paths)) paths = [paths];
const promises = paths.map(url => fetch(url));
const responses = await Promise.all(promises);
const maps = [];
for (const response of responses) {
maps.push(await response.json());
}
for (let i = 0; i < maps.length; i++) {
this.data[paths[i]] = maps[i];
}
}
// Loading of Tilesets
preloadTilesets(callback) {
const tilesets = { paths: [], count: 0 };
this.getTilesetPaths(tilesets);
if (tilesets.count == 0) return callback();
let loaded = 0;
function complete() {
if (++loaded >= tilesets.count) callback();
}
for (const map in tilesets.paths) {
Promise.all(tilesets.paths[map].map(url => {
return fetch(url)
.then(response => {
return response.json();
});
}))
.then(data => {
let offset = 0;
for (let i = 0; i < data.length; i++) {
while(!this.data[map].tilesets[i+offset].source)
offset++;
data[i].firstgid = this.data[map].tilesets[i+offset].firstgid;
this.data[map].tilesets[i+offset] = data[i];
complete();
}
});
}
}
async loadTilesets() {
const tilesets = { paths: [], count: 0 };
this.getTilesetPaths(tilesets);
for (const map in tilesets.paths) {
const promises = tilesets.paths[map].map(url => fetch(url));
const responses = await Promise.all(promises);
let i = 0;
for (const response of responses) {
while(!this.data[map].tilesets[i].source)
i++;
const firstgid = this.data[map].tilesets[i].firstgid;
this.data[map].tilesets[i] = await response.json();
this.data[map].tilesets[i].firstgid = firstgid;
i++;
}
}
}
getTilesetPaths(tilesets) {
for (const map in this.data) {
const path = map.substring(0, map.lastIndexOf('/'));
const mapsets = [];
for (let i = 0; i < this.data[map].tilesets.length; i++) {
if (!this.data[map].tilesets[i].source) continue;
let tileset = this.data[map].tilesets[i].source;
if (path.length) tileset = path + '/' + tileset;
mapsets.push(tileset);
tilesets.count++;
}
tilesets.paths[map] = mapsets;
}
}
// Loading of images associated with tilesets
preloadImages(doload, callback) {
const paths = this.getImagePaths();
for (let i = 0; i < paths.length; i++) {
graphics.add(paths[i]);
}
if (doload) {
graphics.load(callback);
}
}
getImagePaths() {
const images = [];
for (const map in this.data) {
const mappath = map.substring(0, map.lastIndexOf('/'));
this.getTilesetImagePaths(this.data[map].tilesets, mappath, images);
this.getLayerImagePaths(this.data[map].layers, mappath, images);
}
return images;
}
getTilesetImagePaths(tilesets, mappath, images) {
for (let i = 0; i < tilesets.length; i++) {
let imagepath = tilesets[i].image;
if (mappath.length) imagepath = mappath + '/' + imagepath;
images.push(imagepath);
tilesets[i].image = imagepath;
}
}
getLayerImagePaths(layers, mappath, images) {
for (let i = 0; i < layers.length; i++) {
if (layers[i].type == "imagelayer") {
let imagepath = layers[i].image;
if (mappath.length) imagepath = mappath + '/' + imagepath;
images.push(imagepath);
layers[i].image = imagepath;
}
if (layers[i].type == "group") {
this.getLayerImagePaths(layers[i].layers, mappath, images);
}
}
}
// Combine everything
preloadCompleteJSON(paths, callback) {
this.preloadJSON(paths, () => {
this.preloadTilesets(() => {
this.preloadImages(true, callback);
});
});
}
async loadCompleteJSON(paths, doload) {
await this.loadJSON(paths);
await this.loadTilesets();
this.preloadImages(doload, () => {});
}
preloadCompleteJS(callback) {
this.loadJS();
this.preloadTilesets(() => {
this.preloadImages(true, callback);
});
}
async loadCompleteJS(doload) {
this.loadJS();
await this.loadTilesets();
this.preloadImages(doload, () => {});
}
}
class TiledTileset {
constructor(data, palette) {
this.img = graphics[data.image];
this.start = data.firstgid;
this.end = this.start + data.tilecount;
this.data = data;
this.palette = palette;
}
draw(ctx, gid, x, y, transforms) {
let width = this.data.imagewidth;
let tilex = 0;
let tiley = 0;
if (this.data.margin) {
width -= this.data.margin * 2;
tilex = this.data.margin;
tiley = this.data.margin;
}
const tilewidth = this.data.tilewidth;
const tileheight = this.data.tileheight;
let drawwidth = this.data.tilewidth;
let drawheight = this.data.tileheight;
const spacing = this.data.spacing || 0;
const columns = Math.ceil(width / (tilewidth + spacing));
tilex += ((gid - this.start) % columns) * (tilewidth + spacing);
tiley += Math.floor((gid - this.start) / columns) * (tileheight + spacing);
let scalex = transforms.hflip ? -1 : 1;
let scaley = transforms.vflip ? -1 : 1;
if (this.data.tilerendersize == "grid") {
if (tilewidth != this.palette.map.tile.x ||
tileheight != this.palette.map.tile.y) {
if (this.data.fillmode == "preserve-aspect-fit") {
const mylongest = tilewidth > tileheight ? tilewidth : tileheight;
const theirlongest = this.palette.map.tile.x > this.palette.map.tile.y ? this.palette.map.tile.x : this.palette.map.tile.y;
const ratio = theirlongest / mylongest;
drawwidth *= ratio;
drawheight *= ratio;
x += this.palette.map.tile.x - drawwidth;
y += this.palette.map.tile.y - drawheight;
} else {
// fill mode "stretch"
drawwidth = this.palette.map.tile.x;
drawheight = this.palette.map.tile.y;
}
}
}
if (this.data.tileoffset) {
x += this.data.tileoffset.x;
y += this.data.tileoffset.y;
}
ctx.save();
if (transforms.diaflip || transforms.hexflip) {
ctx.translate(x + drawwidth/2, y + drawheight/2);
x = -drawwidth / 2;
y = -drawheight / 2;
let r = 90;
if (transforms.hex) {
if (transforms.hexflip) {
r = 120;
} else {
r = 60;
}
} else {
// The diagonal flip required by Tiled is equivalent to a 90° turn and a horizontal flip
scalex *= -1;
// hardcoded fix
if (transforms.hflip == transforms.vflip) {
scalex *= -1;
scaley *= -1;
}
}
ctx.rotate(r * Math.PI / 180);
}
ctx.scale(scalex, scaley);
ctx.drawImage(this.img,
tilex,
tiley,
tilewidth,
tileheight,
x * scalex,
y * scaley,
drawwidth * scalex,
drawheight * scaley);
ctx.restore();
}
contains(gid) {
return gid >= this.start && gid < this.end;
}
}
class TiledPalette {
constructor(data, map) {
this.sets = [];
this.map = map;
for (const i in data)
this.sets.push(new TiledTileset(data[i], this));
}
draw(ctx, gid, x, y) {
if (gid == 0) return;
const transforms = {
hflip: false,
vflip: false,
diaflip: false,
hexflip: false,
hex: this.map.data.orientation == "hexagonal" ? true : false
};
gid = this.checkTransformations(gid, transforms);
for (const tileset of this.sets) {
if (tileset.contains(gid))
tileset.draw(ctx, gid, x, y, transforms);
}
}
checkTransformations(gid, transforms) {
if (checkBit(gid, 31)) {
gid = clearBit(gid, 31);
transforms.hflip = true;
}
if (checkBit(gid, 30)) {
gid = clearBit(gid, 30);
transforms.vflip = true;
}
if (checkBit(gid, 29)) {
gid = clearBit(gid, 29);
transforms.diaflip = true;
}
if (transforms.hex && checkBit(gid, 28)) {
transforms.hexflip = true;
}
// Always remove bit 29 as per the Tiled instructions
gid = clearBit(gid, 28);
return gid;
}
}
class TiledLayer extends Entity {
constructor(data, map) {
super(new V2(data.offsetx || 0, data.offsety || 0), map.size.clone());
this.data = data;
this.visible = data.visible;
this.scale = 1;
if (data.encoding == "base64")
this.decodeBase64();
}
decodeBase64() {
const decoded = [];
const binary = window.atob(this.data.data);
let i = 0, b = [];
while(!isNaN(binary.charCodeAt(i))) {
b.push(binary.charCodeAt(i));
if (b.length == 4) {
const bytes = new Uint8Array(b);
const dv = new DataView(bytes.buffer);
const uint = dv.getUint32(0, true);
decoded.push(uint);
b = [];
}
i++;
}
this.data.data = decoded;
this.data.encoding = "CSV";
}
staticRender(canvas) {
let ctx;
if (!canvas) {
canvas = document.createElement("canvas");
canvas.width = this.size.x;
canvas.height = this.size.y;
this.img = canvas;
ctx = canvas.getContext("2d");
ctx.save();
} else {
this.visible = false;
ctx = canvas.getContext("2d");
ctx.save();
ctx.translate(this.position.x, this.position.y);
}
this.render(ctx);
ctx.restore();
}
render(ctx) {
ctx.globalAlpha = this.data.opacity;
const palette = this.parent.palette;
const brush = Zero();
const step = new V2(this.parent.tile.x, this.parent.tile.y);
switch (this.parent.data.orientation) {
case "orthogonal":
this.renderOrthogonal(ctx, palette, brush, step);
break;
case "hexagonal":
this.renderHexagonal(ctx, palette, brush, step);
break;
case "isometric":
this.renderIsometric(ctx, palette, brush, step);
break;
case "staggered":
this.renderStaggered(ctx, palette, brush, step);
break;
}
}
renderOrthogonal(ctx, palette, brush, step) {
// Fix starting point for renderorder
if (this.parent.data.renderorder.indexOf("left") > -1) {
step.x *= -1;
brush.x = this.size.x + step.x;
}
if (this.parent.data.renderorder.indexOf("up") > -1) {
step.y *= -1;
brush.y = this.size.y + step.y;
}
for (let i = 0; i < this.data.data.length; i++) {
palette.draw(ctx, this.data.data[i], brush.x, brush.y);
brush.x += step.x;
// renderorder "right"
if (brush.x >= this.size.x) {
brush.x = 0;
brush.y += step.y;
}
// renderorder "left"
if (brush.x < 0) {
brush.x = this.size.x + step.x;
brush.y += step.y;
}
}
}
renderHexagonal(ctx, palette, brush, step) {
let mapwidth = this.parent.data.width * this.parent.data.tilewidth;
const offset = Zero();
let even = true;
// On the staggeraxis, a draw step doesn't proceed a full tile size
if (this.parent.data.staggeraxis == "y") {
step.y -= (step.y - this.parent.data.hexsidelength) / 2;
if (this.parent.data.staggerindex == "even")
offset.x += step.x/2;
} else {
mapwidth = this.size.x - (this.parent.data.tilewidth - this.parent.data.hexsidelength)/2;
step.x -= (step.x - this.parent.data.hexsidelength) / 2;
if (this.parent.data.staggerindex == "even")
offset.y += step.x/2;
}
for (let i = 0; i < this.data.data.length; i++) {
palette.draw(ctx, this.data.data[i], brush.x + offset.x, brush.y + offset.y);
brush.x += step.x;
if (brush.x >= mapwidth) {
brush.x = 0;
brush.y += step.y;
if (this.parent.data.staggeraxis == "y") {
even = !even;
offset.x = 0;
if (this.parent.data.staggerindex == "even" && even)
offset.x = step.x/2;
if (this.parent.data.staggerindex == "odd" && !even)
offset.x = step.x/2;
} else {
// staggeraxis "x"
// Will be set to true down below
even = false;
}
}
if(this.parent.data.staggeraxis == "x") {
even = !even;
offset.y = 0;
if (this.parent.data.staggerindex == "even" && even)
offset.y = step.y/2;
if (this.parent.data.staggerindex == "odd" && !even)
offset.y = step.y/2;
}
}
}
renderIsometric(ctx, palette, brush, step) {
step.mul(.5);
brush.x = this.size.x/2 - this.parent.data.tilewidth/2;
for (let i = 0; i < this.data.data.length; i++) {
if (i > 0 && i % this.data.width == 0) {
const row = i / this.data.width;
brush.x = this.size.x/2 - this.parent.data.tilewidth/2 * (row+1);
brush.y = this.parent.data.tileheight/2 * row;
}
palette.draw(ctx, this.data.data[i], brush.x, brush.y);
brush.add(step);
}
}
renderStaggered(ctx, palette, brush, step) {
const offset = Zero();
let even = true;
// On the staggeraxis, a draw step doesn't proceed a full tile size
if (this.parent.data.staggeraxis == "y") {
step.y /= 2;
if (this.parent.data.staggerindex == "even")
offset.x += step.x/2;
} else {
step.x /= 2;
if (this.parent.data.staggerindex == "even")
offset.y += step.x/2;
}
for (let i = 0; i < this.data.data.length; i++) {
palette.draw(ctx, this.data.data[i], brush.x + offset.x, brush.y + offset.y);
brush.x += step.x;
if (i > 0 && i % this.data.width == this.data.width-1) {
brush.x = 0;
brush.y += step.y;
if (this.parent.data.staggeraxis == "y") {
even = !even;
offset.x = 0;
if (this.parent.data.staggerindex == "even" && even)
offset.x = step.x/2;
if (this.parent.data.staggerindex == "odd" && !even)
offset.x = step.x/2;
} else {
// staggeraxis "x"
// Will be set to true down below
even = false;
}
}
if(this.parent.data.staggeraxis == "x") {
even = !even;
offset.y = 0;
if (this.parent.data.staggerindex == "even" && even)
offset.y = step.y/2;
if (this.parent.data.staggerindex == "odd" && !even)
offset.y = step.y/2;
}
}
}
onDraw(ctx) {
if (this.img) {
ctx.drawImage(this.img, 0, 0, this.size.x | 0, this.size.y | 0, 0, 0, (this.size.x * this.scale) | 0, (this.size.y * this.scale) | 0);
} else {
this.render(ctx);
}
}
}
class TiledImageLayer extends Entity {
constructor(data, map) {
super();
this.data = data;
this.position.x = data.offsetx || 0;
this.position.y = data.offsety || 0;
this.img = graphics[data.image];
this.size.x = this.img.width;
this.size.y = this.img.height;
this.visible = data.visible;
}
staticRender(canvas) {
if (canvas) {
this.visible = false;
ctx = canvas.getContext("2d");
ctx.save();
ctx.translate(this.position.x, this.position.y);
this.onDraw(ctx);
ctx.restore();
}
}
onDraw(ctx) {
ctx.globalAlpha = this.data.opacity;
ctx.drawImage(this.img, 0, 0, this.size.x | 0, this.size.y | 0, 0, 0, this.size.x | 0, this.size.y | 0);
}
}
class TiledObjectLayer extends Entity {
constructor(data, map) {
super();
this.data = data;
this.position.x = data.offsetx || 0;
this.position.y = data.offsety || 0;
this.size = map.size.clone();
this.objects = [];
this.tiles = [];
this.createObjects();
this.visible = data.visible;
this.scale = 1;
}
createObjects() {
for (const object of this.data.objects) {
const position = new V2(object.x || 0, object.y || 0);
if (object.point) {
this.objects.push({
type: "point",
data: position });
} else if (object.ellipse) {
const radii = new V2(this.objects.width / 2, this.objects.height / 2);
const center = new V2(this.objects.x, this.objects.y);
center.add(radii);
this.objects.push({
type: "ellipse",
data: new Elli(center, radii) });
} else if (object.polygon) {
const points = [];
for (let i = 0; i < object.polygon.length; i++) {
points.push(new V2(object.polygon[i].x, object.polygon[i].y));
}
this.objects.push({
type: "polygon",
data: new Poly(points) });
} else if (object.text) {
this.objects.push({
type: "text",
data: { text: object.text.text,
position: position }
});
} else if (object.gid) {
const tile = {
type: "tile",
data: { gid: object.gid,
x: object.x,
y: object.y,
visible: object.visible }
};
this.objects.push(tile);
this.tiles.push(tile);
} else {
this.objects.push({
type: "rectangle",
data: new Rect(
position,
new V2(object.width || 0, object.height || 0)
)
});
}
}
}
staticRender(canvas) {
if (!this.tiles.length) return;
let ctx;
if (!canvas) {
canvas = document.createElement("canvas");
canvas.width = this.size.x;
canvas.height = this.size.y;
this.img = canvas;
ctx = canvas.getContext("2d");
ctx.save();
} else {
this.visible = false;
ctx = canvas.getContext("2d");
ctx.save();
ctx.translate(this.position.x, this.position.y);
}
this.render(ctx);
ctx.restore();
}
render(ctx) {
ctx.globalAlpha = this.data.opacity;
for (const tile of this.tiles) {
if (tile.data.visible) {
this.parent.palette.draw(ctx, tile.data.gid, tile.data.x, tile.data.y);
}
}
}
onDraw(ctx) {
if (this.img) {
ctx.drawImage(this.img, 0, 0, this.size.x | 0, this.size.y | 0, 0, 0, (this.size.x * this.scale) | 0, (this.size.y * this.scale) | 0);
} else {
this.render(ctx);
}
}
}
export class TiledMap extends Entity {
constructor(data, pos) {
let w, h;
switch (data.orientation) {
case "orthogonal":
case "isometric":
w = data.width * data.tilewidth;
h = data.height * data.tileheight;
break;
case "hexagonal":
if (data.staggeraxis == "y") {
w = data.width * data.tilewidth + data.tilewidth / 2;
h = data.height * (data.tileheight - data.hexsidelength/2) + (data.tileheight - data.hexsidelength)/2;
} else {
w = data.width * (data.tilewidth - data.hexsidelength/2) + (data.tilewidth - data.hexsidelength)/2;
h = data.height * data.tileheight + data.tileheight / 2;
}
break;
case "staggered":
if (data.staggeraxis == "y") {
w = data.width * data.tilewidth + data.tilewidth / 2;
h = data.height * data.tileheight / 2 + data.tileheight / 2;
} else {
w = data.width * data.tilewidth / 2 + data.tilewidth / 2;
h = data.height * data.tileheight + data.tileheight / 2;
}
break;
}
super(pos, new V2(w, h));
this.data = data;
this.tile = new V2(data.tilewidth, data.tileheight);
this.height = data.height;
this.width = data.width;
this.palette = new TiledPalette(data.tilesets, this);
this.layers = [];
this.fillLayers(this.data.layers, {
offsetx: 0,
offsety: 0,
opacity: 1,
visible: true,
});
this.scale = 1;
}
fillLayers(layers, groupstats) {
for (let i = 0; i < layers.length; i++) {
let layer;
switch (layers[i].type) {
case "tilelayer":
layer = new TiledLayer(layers[i], this);
break;
case "imagelayer":
layer = new TiledImageLayer(layers[i], this);
break;
case "objectgroup":
layer = new TiledObjectLayer(layers[i], this);
break;
case "group":
const combined = {
offsetx: groupstats.offsetx + layers[i].offsetx,
offsety: groupstats.offsety + layers[i].offsety,
opacity: groupstats.opacity * layers[i].opacity,
visible: groupstats.visible && layers[i].visible,
};
this.fillLayers(layers[i].layers, combined);
break;
}
if (layer) {
this.add(layer);
this.layers.push(layer);
}
}
}
staticRender(merge, filter) {
let canvas;
if (merge) {
canvas = document.createElement("canvas");
canvas.width = this.size.x;
canvas.height = this.size.y;
this.img = canvas;
}
for (const layer of this.layers) {
if (layer.visible && (!filter || filter.indexOf(layer.data.name) >= 0)) {
layer.staticRender(canvas);
} else {
layer.visible = false;
}
}
}
onDraw(ctx) {
if (this.data.backgroundcolor) {
ctx.fillStyle = this.data.backgroundcolor;
ctx.fillRect(0,0, this.size.x, this.size.y);
}
if (this.img)
ctx.drawImage(this.img, 0, 0, this.size.x | 0, this.size.y | 0, 0, 0, (this.size.x * this.scale) | 0, (this.size.y * this.scale) | 0);
}
toTile(pos) {
const result = pos.clone();
result.grid(this.tile.x, this.tile.y);
return result;
}
getLayer(name) {
for (const i in this.layers) {
const l = this.layers[i];
if (l.data.name == name) return l;
}
}
blocked(pos) {
return this.has(pos, 'collision', true);
}
has(pos, property, def) {
if (pos.x < 0 || pos.y < 0 || pos.x >= this.width || pos.y >= this.height)
return def;
const flags = this.flags(pos);
return flags[property];
}
flags(pos) {
if (pos.x < 0 || pos.y < 0 || pos.x >= this.width || pos.y >= this.height)
return {};
const result = {};
for (const i in this.layers) {
const l = this.layers[i];
if (l.data && l.data[pos.x + (pos.y * l.width)])
for (const p in l.properties)
if (!result[p]) result[p] = l.properties[p];
}
return result;
}
}