-
-
Notifications
You must be signed in to change notification settings - Fork 189
/
TileMap.ts
424 lines (394 loc) · 13.5 KB
/
TileMap.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
import { BoundingBox } from './Collision/BoundingBox';
import { Color } from './Drawing/Color';
import { Class } from './Class';
import { Engine } from './Engine';
import { Vector } from './Algebra';
import { Actor } from './Actor';
import { Logger } from './Util/Log';
import { SpriteSheet } from './Drawing/SpriteSheet';
import * as Events from './Events';
import { Configurable } from './Configurable';
/**
* @hidden
*/
export class TileMapImpl extends Class {
private _collidingX: number = -1;
private _collidingY: number = -1;
private _onScreenXStart: number = 0;
private _onScreenXEnd: number = 9999;
private _onScreenYStart: number = 0;
private _onScreenYEnd: number = 9999;
private _spriteSheets: { [key: string]: SpriteSheet } = {};
public logger: Logger = Logger.getInstance();
public data: Cell[] = [];
public x: number;
public y: number;
public cellWidth: number;
public cellHeight: number;
public rows: number;
public cols: number;
public on(eventName: Events.preupdate, handler: (event: Events.PreUpdateEvent<TileMap>) => void): void;
public on(eventName: Events.postupdate, handler: (event: Events.PostUpdateEvent<TileMap>) => void): void;
public on(eventName: Events.predraw, handler: (event: Events.PreDrawEvent) => void): void;
public on(eventName: Events.postdraw, handler: (event: Events.PostDrawEvent) => void): void;
public on(eventName: string, handler: (event: Events.GameEvent<any>) => void): void;
public on(eventName: string, handler: (event: any) => void): void {
super.on(eventName, handler);
}
/**
* @param x The x coordinate to anchor the TileMap's upper left corner (should not be changed once set)
* @param y The y coordinate to anchor the TileMap's upper left corner (should not be changed once set)
* @param cellWidth The individual width of each cell (in pixels) (should not be changed once set)
* @param cellHeight The individual height of each cell (in pixels) (should not be changed once set)
* @param rows The number of rows in the TileMap (should not be changed once set)
* @param cols The number of cols in the TileMap (should not be changed once set)
*/
constructor(xOrConfig: number | TileMapArgs, y: number, cellWidth: number, cellHeight: number, rows: number, cols: number) {
super();
if (xOrConfig && typeof xOrConfig === 'object') {
const config = xOrConfig;
xOrConfig = config.x;
y = config.y;
cellWidth = config.cellWidth;
cellHeight = config.cellHeight;
rows = config.rows;
cols = config.cols;
}
this.x = <number>xOrConfig;
this.y = y;
this.cellWidth = cellWidth;
this.cellHeight = cellHeight;
this.rows = rows;
this.cols = cols;
this.data = new Array<Cell>(rows * cols);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
(() => {
const cd = new Cell(i * cellWidth + <number>xOrConfig, j * cellHeight + y, cellWidth, cellHeight, i + j * cols);
this.data[i + j * cols] = cd;
})();
}
}
}
public registerSpriteSheet(key: string, spriteSheet: SpriteSheet) {
this._spriteSheets[key] = spriteSheet;
}
/**
* Returns the intersection vector that can be used to resolve collisions with actors. If there
* is no collision null is returned.
*/
public collides(actor: Actor): Vector {
const width = actor.pos.x + actor.width;
const height = actor.pos.y + actor.height;
const actorBounds = actor.body.collider.bounds;
const overlaps: Vector[] = [];
if (actor.width <= 0 || actor.height <= 0) {
return null;
}
// trace points for overlap
for (let x = actorBounds.left; x <= width; x += Math.min(actor.width / 2, this.cellWidth / 2)) {
for (let y = actorBounds.top; y <= height; y += Math.min(actor.height / 2, this.cellHeight / 2)) {
const cell = this.getCellByPoint(x, y);
if (cell && cell.solid) {
const overlap = actorBounds.intersect(cell.bounds);
const dir = actor.center.sub(cell.center);
if (overlap && overlap.dot(dir) > 0) {
overlaps.push(overlap);
}
}
}
}
if (overlaps.length === 0) {
return null;
}
// Return the smallest change other than zero
const result = overlaps.reduce((accum, next) => {
let x = accum.x;
let y = accum.y;
if (Math.abs(accum.x) < Math.abs(next.x)) {
x = next.x;
}
if (Math.abs(accum.y) < Math.abs(next.y)) {
y = next.y;
}
return new Vector(x, y);
});
return result;
}
/**
* Returns the [[Cell]] by index (row major order)
*/
public getCellByIndex(index: number): Cell {
return this.data[index];
}
/**
* Returns the [[Cell]] by its x and y coordinates
*/
public getCell(x: number, y: number): Cell {
if (x < 0 || y < 0 || x >= this.cols || y >= this.rows) {
return null;
}
return this.data[x + y * this.cols];
}
/**
* Returns the [[Cell]] by testing a point in global coordinates,
* returns `null` if no cell was found.
*/
public getCellByPoint(x: number, y: number): Cell {
x = Math.floor((x - this.x) / this.cellWidth);
y = Math.floor((y - this.y) / this.cellHeight);
const cell = this.getCell(x, y);
if (x >= 0 && y >= 0 && x < this.cols && y < this.rows && cell) {
return cell;
}
return null;
}
public onPreUpdate(_engine: Engine, _delta: number) {
// Override me
}
public onPostUpdate(_engine: Engine, _delta: number) {
// Override me
}
public update(engine: Engine, delta: number) {
this.onPreUpdate(engine, delta);
this.emit('preupdate', new Events.PreUpdateEvent(engine, delta, this));
const worldCoordsUpperLeft = engine.screenToWorldCoordinates(new Vector(0, 0));
const worldCoordsLowerRight = engine.screenToWorldCoordinates(new Vector(engine.canvas.clientWidth, engine.canvas.clientHeight));
this._onScreenXStart = Math.max(Math.floor((worldCoordsUpperLeft.x - this.x) / this.cellWidth) - 2, 0);
this._onScreenYStart = Math.max(Math.floor((worldCoordsUpperLeft.y - this.y) / this.cellHeight) - 2, 0);
this._onScreenXEnd = Math.max(Math.floor((worldCoordsLowerRight.x - this.x) / this.cellWidth) + 2, 0);
this._onScreenYEnd = Math.max(Math.floor((worldCoordsLowerRight.y - this.y) / this.cellHeight) + 2, 0);
this.onPostUpdate(engine, delta);
this.emit('postupdate', new Events.PostUpdateEvent(engine, delta, this));
}
/**
* Draws the tile map to the screen. Called by the [[Scene]].
* @param ctx The current rendering context
* @param delta The number of milliseconds since the last draw
*/
public draw(ctx: CanvasRenderingContext2D, delta: number) {
this.emit('predraw', new Events.PreDrawEvent(ctx, delta, this));
ctx.save();
ctx.translate(this.x, this.y);
let x = this._onScreenXStart;
const xEnd = Math.min(this._onScreenXEnd, this.cols);
let y = this._onScreenYStart;
const yEnd = Math.min(this._onScreenYEnd, this.rows);
let cs: TileSprite[], csi: number, cslen: number;
for (x; x < xEnd; x++) {
for (y; y < yEnd; y++) {
// get non-negative tile sprites
cs = this.getCell(x, y).sprites.filter((s) => {
return s.spriteId > -1;
});
for (csi = 0, cslen = cs.length; csi < cslen; csi++) {
const ss = this._spriteSheets[cs[csi].spriteSheetKey];
// draw sprite, warning if sprite doesn't exist
if (ss) {
const sprite = ss.getSprite(cs[csi].spriteId);
if (sprite) {
sprite.draw(ctx, x * this.cellWidth, y * this.cellHeight);
} else {
this.logger.warn('Sprite does not exist for id', cs[csi].spriteId, 'in sprite sheet', cs[csi].spriteSheetKey, sprite, ss);
}
} else {
this.logger.warn('Sprite sheet', cs[csi].spriteSheetKey, 'does not exist', ss);
}
}
}
y = this._onScreenYStart;
}
ctx.restore();
this.emit('postdraw', new Events.PostDrawEvent(ctx, delta, this));
}
/**
* Draws all the tile map's debug info. Called by the [[Scene]].
* @param ctx The current rendering context
*/
public debugDraw(ctx: CanvasRenderingContext2D) {
const width = this.cols * this.cellWidth;
const height = this.rows * this.cellHeight;
ctx.save();
ctx.strokeStyle = Color.Red.toString();
for (let x = 0; x < this.cols + 1; x++) {
ctx.beginPath();
ctx.moveTo(this.x + x * this.cellWidth, this.y);
ctx.lineTo(this.x + x * this.cellWidth, this.y + height);
ctx.stroke();
}
for (let y = 0; y < this.rows + 1; y++) {
ctx.beginPath();
ctx.moveTo(this.x, this.y + y * this.cellHeight);
ctx.lineTo(this.x + width, this.y + y * this.cellHeight);
ctx.stroke();
}
const solid = Color.Red;
solid.a = 0.3;
this.data
.filter(function (cell) {
return cell.solid;
})
.forEach(function (cell) {
ctx.fillStyle = solid.toString();
ctx.fillRect(cell.x, cell.y, cell.width, cell.height);
});
if (this._collidingY > -1 && this._collidingX > -1) {
ctx.fillStyle = Color.Cyan.toString();
ctx.fillRect(
this.x + this._collidingX * this.cellWidth,
this.y + this._collidingY * this.cellHeight,
this.cellWidth,
this.cellHeight
);
}
ctx.restore();
}
}
/**
* [[include:Constructors.md]]
*/
export interface TileMapArgs extends Partial<TileMapImpl> {
x: number;
y: number;
cellWidth: number;
cellHeight: number;
rows: number;
cols: number;
}
/**
* The [[TileMap]] class provides a lightweight way to do large complex scenes with collision
* without the overhead of actors.
*
* [[include:TileMaps.md]]
*/
export class TileMap extends Configurable(TileMapImpl) {
constructor(config: TileMapArgs);
constructor(x: number, y: number, cellWidth: number, cellHeight: number, rows: number, cols: number);
constructor(xOrConfig: number | TileMapArgs, y?: number, cellWidth?: number, cellHeight?: number, rows?: number, cols?: number) {
super(xOrConfig, y, cellWidth, cellHeight, rows, cols);
}
}
/**
* Tile sprites are used to render a specific sprite from a [[TileMap]]'s spritesheet(s)
*/
export class TileSprite {
/**
* @param spriteSheetKey The key of the spritesheet to use
* @param spriteId The index of the sprite in the [[SpriteSheet]]
*/
constructor(public spriteSheetKey: string, public spriteId: number) {}
}
/**
* @hidden
*/
export class CellImpl {
private _bounds: BoundingBox;
public x: number;
public y: number;
public width: number;
public height: number;
public index: number;
public solid: boolean = false;
public sprites: TileSprite[] = [];
/**
* @param x Gets or sets x coordinate of the cell in world coordinates
* @param y Gets or sets y coordinate of the cell in world coordinates
* @param width Gets or sets the width of the cell
* @param height Gets or sets the height of the cell
* @param index The index of the cell in row major order
* @param solid Gets or sets whether this cell is solid
* @param sprites The list of tile sprites to use to draw in this cell (in order)
*/
constructor(
xOrConfig: number | CellArgs,
y: number,
width: number,
height: number,
index: number,
solid: boolean = false,
sprites: TileSprite[] = []
) {
if (xOrConfig && typeof xOrConfig === 'object') {
const config = xOrConfig;
xOrConfig = config.x;
y = config.y;
width = config.width;
height = config.height;
index = config.index;
solid = config.solid;
sprites = config.sprites;
}
this.x = <number>xOrConfig;
this.y = y;
this.width = width;
this.height = height;
this.index = index;
this.solid = solid;
this.sprites = sprites;
this._bounds = new BoundingBox(this.x, this.y, this.x + this.width, this.y + this.height);
}
public get bounds() {
return this._bounds;
}
public get center(): Vector {
return new Vector(this.x + this.width / 2, this.y + this.height / 2);
}
/**
* Add another [[TileSprite]] to this cell
*/
public pushSprite(tileSprite: TileSprite) {
this.sprites.push(tileSprite);
}
/**
* Remove an instance of [[TileSprite]] from this cell
*/
public removeSprite(tileSprite: TileSprite) {
let index = -1;
if ((index = this.sprites.indexOf(tileSprite)) > -1) {
this.sprites.splice(index, 1);
}
}
/**
* Clear all sprites from this cell
*/
public clearSprites() {
this.sprites.length = 0;
}
}
/**
* [[include:Constructors.md]]
*/
export interface CellArgs extends Partial<CellImpl> {
x: number;
y: number;
width: number;
height: number;
index: number;
solid?: boolean;
sprites?: TileSprite[];
}
/**
* TileMap Cell
*
* A light-weight object that occupies a space in a collision map. Generally
* created by a [[TileMap]].
*
* Cells can draw multiple sprites. Note that the order of drawing is the order
* of the sprites in the array so the last one will be drawn on top. You can
* use transparency to create layers this way.
*/
export class Cell extends Configurable(CellImpl) {
constructor(config: CellArgs);
constructor(x: number, y: number, width: number, height: number, index: number, solid?: boolean, sprites?: TileSprite[]);
constructor(
xOrConfig: number | CellArgs,
y?: number,
width?: number,
height?: number,
index?: number,
solid?: boolean,
sprites?: TileSprite[]
) {
super(xOrConfig, y, width, height, index, solid, sprites);
}
}