From 2c6cd362c32a2e9460e5ddebd85f050b9b5ab4e4 Mon Sep 17 00:00:00 2001
From: Matthias Schiffer <mschiffer@universe-factory.net>
Date: Wed, 21 Nov 2018 21:44:11 +0100
Subject: [PATCH] Allow using animated entities as map tiles

A more optimized implementation for animated tiles is planned.
---
 dist/resources/entity/water.json       |  12 +++
 dist/resources/map/test.json           |  34 +++----
 dist/resources/sprite/entity/water.png | Bin 0 -> 1718 bytes
 dist/resources/sprite/tile/water.png   | Bin 778 -> 0 bytes
 src/view/map.ts                        | 129 ++++++++++++++++++++-----
 5 files changed, 133 insertions(+), 42 deletions(-)
 create mode 100644 dist/resources/entity/water.json
 create mode 100644 dist/resources/sprite/entity/water.png
 delete mode 100644 dist/resources/sprite/tile/water.png

diff --git a/dist/resources/entity/water.json b/dist/resources/entity/water.json
new file mode 100644
index 0000000..79c7780
--- /dev/null
+++ b/dist/resources/entity/water.json
@@ -0,0 +1,12 @@
+{
+	"sprite": "water",
+	"frames": 4,
+	"animation": {
+		"sequence": [
+			[500, 0],
+			[500, 1],
+			[500, 2],
+			[500, 3]
+		]
+	}
+}
diff --git a/dist/resources/map/test.json b/dist/resources/map/test.json
index ec3b3fb..91efc19 100644
--- a/dist/resources/map/test.json
+++ b/dist/resources/map/test.json
@@ -1,22 +1,22 @@
 {
 	"tiles": [
-		"stone/floor",
-		"stone/plate",
-		"stone/wall/top",
-		"stone/wall/right",
-		"stone/wall/bottom",
-		"stone/wall/left",
-		"stone/wall/top_left",
-		"stone/wall/top_right",
-		"stone/wall/bottom_right",
-		"stone/wall/bottom_left",
-		"stone/wall/top_left_inner",
-		"stone/wall/top_right_inner",
-		"stone/wall/bottom_right_inner",
-		"stone/wall/bottom_left_inner",
-		"water",
-		"stone/border/bottom_right",
-		"stone/border/bottom_left"
+		"-stone/floor",
+		"-stone/plate",
+		"-stone/wall/top",
+		"-stone/wall/right",
+		"-stone/wall/bottom",
+		"-stone/wall/left",
+		"-stone/wall/top_left",
+		"-stone/wall/top_right",
+		"-stone/wall/bottom_right",
+		"-stone/wall/bottom_left",
+		"-stone/wall/top_left_inner",
+		"-stone/wall/top_right_inner",
+		"-stone/wall/bottom_right_inner",
+		"-stone/wall/bottom_left_inner",
+		"@water",
+		"-stone/border/bottom_right",
+		"-stone/border/bottom_left"
 	],
 	"layers": [
 		{
diff --git a/dist/resources/sprite/entity/water.png b/dist/resources/sprite/entity/water.png
new file mode 100644
index 0000000000000000000000000000000000000000..61c2d4f74eba120340aaf78c2e4e7ce7a3a6b3d4
GIT binary patch
literal 1718
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
M07*qoM6N<$f)QOi0{{R3

literal 0
HcmV?d00001

diff --git a/dist/resources/sprite/tile/water.png b/dist/resources/sprite/tile/water.png
deleted file mode 100644
index 4a965436f949a880bbdbe6872afad4b1b817c460..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 778
zcmV+l1NHogP)<h;3K|Lk000e1NJLTq001BW001Be0ssI2{21+{0007odQ@0+Qek%>
zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=HGj@=*(hTmC5mw;~=mP5Em-9eY%pCl(|Iz8%4
zHI@1jti%y9w()18gz@A14Bs%wGY2)#siYJ!v}oaqha8_{ocfrfsQdjf=)6;S`GTPZ
zT8>HA^R1lo5Bqd!IC}Ad?A-#n!tmR`Ue9@s{7Xh|SB&R&&TGz|=N!lGswR{n9h*8r
zgohg#Zm5#%%M!$K8J>NY0?Dz)MjCr;af37}#3XT&QV&#V(x49As3Aw?!4A#bSoXOr
z&V`#qA<-o?*`-LAEG&(Oh-eVLa-ltL+2b|Jh};RLOmIe6@<$0D8GJ_OY!Njg!tUv;
z74l+U9B7qM)*WU5Li6Nidx0NvjqnDnfS~PV&e&j$+u>p}diIv=4Ir>H=%c^L`=Z8s
zmndQzk|6<tm`IEgMQ3&*70v)ukvWU<1_ETp9Z9l^B(T8|S+TLkX}4wLV;)BZd`yx!
z*c7RNrE2IO*Nh&jCZ=YV%q?578j>VUmQwPx&RlhIb@Sxz*^AdKS+nJoJ?C6<EnEag
z(n=~`aw(-&#H<>sRjjVCsCDB_Hr;GXn{Rn5TWx5crp;Pv-f}CgcJ9)(TTk74?xoj3
z1FaN>4jXCs$fJxp(Q8wtPMc}^%(KjTQ9G&rCVxSVPHMc9T6TU>!>s#qw7a17ow3CX
z#8?T$eGx!I^I{e=sq!MXn8m_46~;)?7Mo7721-DvgV-ir?4IO4%`KqTA93T)kP8dl
zKOo0p$UAN?sP*+FY&)^z6t0>^!RZ@>@SN+cL8Z00o*AFPFX-@92e+f!(e3DVbUV5o
z-H!gdBboStDEJ%u3Ft>w9G?X!k^lez32;bRa{vGf6951U69E94oEQKA04qsEK~zY`
z?ae^}06+u)FfNWH(#d=!_-zH2)f1Oq91{ox0)apv5C{ZYKCXWOK*AzUJOBUy07*qo
IM6N<$f*2iRx&QzG

diff --git a/src/view/map.ts b/src/view/map.ts
index c796c6f..5eedaca 100644
--- a/src/view/map.ts
+++ b/src/view/map.ts
@@ -1,3 +1,4 @@
+import { EntityView } from './entity';
 import { Renderer } from './renderer/renderer';
 import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
 import { loadImage, mkTexture } from './util/image';
@@ -6,39 +7,91 @@ import { MapData } from '../model/data/map';
 
 import { nextPowerOf2 } from '../util';
 
-interface Tileset {
-	texture: WebGLTexture;
-	tiles: SpriteCoords[];
+import { vec2 } from 'gl-matrix';
+
+interface StaticMapTile {
+	type: 'static';
+	image: HTMLImageElement;
 }
 
-function loadTiles(tiles: string[]): Promise<HTMLImageElement[]> {
-	return Promise.all(tiles.map((t) => loadImage(`resources/sprite/tile/${t}.png`)));
+interface EntityTile {
+	type: 'entity';
+	entity: EntityView;
+}
+
+type MapTile = StaticMapTile | EntityTile;
+
+interface StaticTilesetTile {
+	type: 'static';
+	coords: SpriteCoords;
+}
+
+type TilesetTile = StaticTilesetTile | EntityTile;
+
+interface Tileset {
+	texture: WebGLTexture;
+	tiles: TilesetTile[];
+}
+
+async function loadTile(r: Renderer, tile: string): Promise<MapTile> {
+	const name = tile.substr(1);
+	switch (tile[0]) {
+	case '-':
+		return {
+			type: 'static',
+			image: await loadImage(`resources/sprite/tile/${name}.png`),
+		};
+
+	case '@':
+		return {
+			type: 'entity',
+			entity: await EntityView.load(r, name),
+		};
+
+	default:
+		throw new Error('invalid tile specifier');
+	}
+}
+
+function loadTiles(r: Renderer, tiles: string[]): Promise<MapTile[]> {
+	return Promise.all(tiles.map((tile) => loadTile(r, tile)));
 }
 
 function mkTileset(
 	r: Renderer,
-	tiles: HTMLImageElement[],
+	mapTiles: MapTile[],
 ): Tileset {
 	const tileSize = 32;
 
-	const canvasDim = nextPowerOf2(Math.sqrt(tiles.length));
+	const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
 	const canvasSize = canvasDim * tileSize;
 
 	const canvas = document.createElement('canvas');
 	canvas.width = canvas.height = canvasSize;
 
 	let x = 0, y = 0;
-	const tileCoords: SpriteCoords[] = [];
+	const tiles: TilesetTile[] = [];
 	const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
 
-	for (const tile of tiles) {
-		ctx.drawImage(tile, x * tileSize, y * tileSize);
-		tileCoords.push([x / canvasDim, y / canvasDim, (x + 1) / canvasDim, (y + 1) / canvasDim]);
+	for (const tile of mapTiles) {
+		switch (tile.type) {
+		case 'static':
+			ctx.drawImage(tile.image, x * tileSize, y * tileSize);
+			tiles.push({
+				type: 'static',
+				coords: [x / canvasDim, y / canvasDim, (x + 1) / canvasDim, (y + 1) / canvasDim],
+			});
 
-		x++;
-		if (x === canvasDim) {
-			x = 0;
-			y++;
+			x++;
+			if (x === canvasDim) {
+				x = 0;
+				y++;
+			}
+			break;
+
+		case 'entity':
+			tiles.push(tile);
+			break;
 		}
 	}
 
@@ -46,40 +99,66 @@ function mkTileset(
 
 	return {
 		texture,
-		tiles: tileCoords,
+		tiles,
 	};
 }
 
-function addSprite(builder: SpriteViewBuilder, tileset: Tileset, x: number, y: number, tile: number) {
-	if (tile === 0)
+function addSprite(
+	builder: SpriteViewBuilder,
+	entityTiles: Array<[[number, number], EntityView]>,
+	tileset: Tileset,
+	x: number,
+	y: number,
+	tile: number) {
+	if (!tile)
 		return;
 
-	const tilePos = tileset.tiles[tile - 1];
-	builder.addSprite([x, y, x + 1, y + 1], tilePos);
+	const tilesetTile = tileset.tiles[tile - 1];
+
+	switch (tilesetTile.type) {
+		case 'static':
+			builder.addSprite([x, y, x + 1, y + 1], tilesetTile.coords);
+			break;
+
+		case 'entity':
+			entityTiles.push([[x + 0.5, y + 0.5], tilesetTile.entity]);
+			break;
+	}
 }
 
 function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
 	const builder = new SpriteViewBuilder(r, tileset.texture);
+	const entityTiles: Array<[[number, number], EntityView]> = [];
 
 	for (let x = 0; x < layer[0].length; x++)
 		for (let y = 0; y < layer.length; y++)
-			addSprite(builder, tileset, x, y, layer[y][x]);
+			addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
 
-	return new MapLayerView(builder.build());
+	return new MapLayerView(r, builder.build(), entityTiles);
 }
 
 class MapLayerView {
-	public constructor(private sprites: SpriteView) {
+	public constructor(
+		private r: Renderer,
+		private staticTiles: SpriteView,
+		private entityTiles: Array<[[number, number], EntityView]>,
+	) {
 	}
 
 	public render(time: number): void {
-		this.sprites.render();
+		this.r.setTranslation([0, 0]);
+		this.staticTiles.render();
+
+		for (const [coords, entity] of this.entityTiles) {
+			this.r.setTranslation(coords);
+			entity.renderByTime(time);
+		}
 	}
 }
 
 export class MapView {
 	public static async load(r: Renderer, map: MapData): Promise<MapView> {
-		const tiles = await loadTiles(map.tiles);
+		const tiles = await loadTiles(r, map.tiles);
 		const tileset = mkTileset(r, tiles);
 
 		const layers = map.layers.map((layer) => buildMapLayer(r, tileset, layer.tiles));