|
@@ -0,0 +1,261 @@
|
|
|
+import { EngineOptions, PlayerState, SpriteState } from './types';
|
|
|
+
|
|
|
+/**
|
|
|
+ * HTML element id for debug output (will only be used if it is defined)
|
|
|
+ */
|
|
|
+const debugOutputElementId = 'game-debug-out';
|
|
|
+
|
|
|
+/**
|
|
|
+ * Main display controller.
|
|
|
+ */
|
|
|
+export class MainDisplayController {
|
|
|
+ public running: boolean = false;
|
|
|
+
|
|
|
+ protected canvas: HTMLCanvasElement;
|
|
|
+ protected debugOutputElement: HTMLElement | null = null;
|
|
|
+ protected ctx: CanvasRenderingContext2D;
|
|
|
+ protected options: EngineOptions;
|
|
|
+
|
|
|
+ // Runtime state
|
|
|
+
|
|
|
+ protected player: PlayerState = {} as PlayerState;
|
|
|
+ protected sprites: SpriteState[] = [];
|
|
|
+
|
|
|
+ private animationFrame: number = 0;
|
|
|
+ //private lastRenderCycleTime: number = 0;
|
|
|
+
|
|
|
+ constructor(canvasElementId: string, options: EngineOptions) {
|
|
|
+ this.options = options;
|
|
|
+
|
|
|
+ const canvas = document.getElementById(
|
|
|
+ canvasElementId
|
|
|
+ ) as HTMLCanvasElement;
|
|
|
+
|
|
|
+ if (canvas === null) {
|
|
|
+ throw Error('Canvas element not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.canvas = canvas;
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ if (ctx === null) {
|
|
|
+ throw Error('Could not get canvas rendering context');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.ctx = ctx;
|
|
|
+
|
|
|
+ this.debugOutputElement = document.getElementById(
|
|
|
+ debugOutputElementId
|
|
|
+ ) as HTMLCanvasElement;
|
|
|
+
|
|
|
+ this.canvas.width = this.options.screenWidth;
|
|
|
+ this.canvas.height = this.options.screenHeight;
|
|
|
+ this.canvas.style.width = this.options.screenElementWidth + 'px';
|
|
|
+ this.canvas.style.height = this.options.screenElementHeight + 'px';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Register event handlers for the engine.
|
|
|
+ */
|
|
|
+ public registerEventHandlers(): void {
|
|
|
+ document.onkeydown = (e) => {
|
|
|
+ this.options.eventHandler.onkeydown(this.player, e);
|
|
|
+ };
|
|
|
+ document.onkeyup = (e) => {
|
|
|
+ this.options.eventHandler.onkeyup(this.player, e);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Deregister event handlers for the engine.
|
|
|
+ */
|
|
|
+ public deRegisterEventHandlers(): void {
|
|
|
+ document.onkeydown = null;
|
|
|
+ document.onkeyup = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Start the engine.
|
|
|
+ */
|
|
|
+ public start(playerState: PlayerState): void {
|
|
|
+ this.running = true;
|
|
|
+ this.player = playerState;
|
|
|
+ this.registerEventHandlers();
|
|
|
+ this.drawLoop();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Stop the engine.
|
|
|
+ */
|
|
|
+ public stop(): void {
|
|
|
+ this.running = false;
|
|
|
+ this.deRegisterEventHandlers();
|
|
|
+ this.options.stopHandler();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Add a sprite to the simulation.
|
|
|
+ */
|
|
|
+ public addSprite(spriteState: SpriteState): void {
|
|
|
+ this.sprites.push(spriteState);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Remove a sprite to the simulation.
|
|
|
+ */
|
|
|
+ public removeSprite(spriteState: SpriteState): void {
|
|
|
+ this.sprites.splice(this.sprites.indexOf(spriteState), 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Print debug information which are cleared with every draw loop.
|
|
|
+ */
|
|
|
+ public printDebug(s: string): void {
|
|
|
+ if (this.debugOutputElement !== null) {
|
|
|
+ this.debugOutputElement.innerHTML += s + '<br>';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Loop function which draws the game scene.
|
|
|
+ */
|
|
|
+ protected drawLoop(): void {
|
|
|
+ // Calculate animation frame
|
|
|
+
|
|
|
+ this.animationFrame++;
|
|
|
+ this.animationFrame = this.animationFrame % 1000;
|
|
|
+
|
|
|
+ // Clear screen canvas
|
|
|
+
|
|
|
+ if (this.options.backdrop === null) {
|
|
|
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
+ } else {
|
|
|
+ this.ctx.drawImage(
|
|
|
+ this.options.backdrop,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ this.canvas.width,
|
|
|
+ this.canvas.height
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Clear debug element if there is one
|
|
|
+
|
|
|
+ if (this.debugOutputElement !== null) {
|
|
|
+ this.debugOutputElement.innerHTML = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ let start = new Date().getTime();
|
|
|
+
|
|
|
+ this.drawSprites();
|
|
|
+ this.drawPlayer();
|
|
|
+
|
|
|
+ // Call external handler
|
|
|
+
|
|
|
+ this.options.drawHandler(this.ctx, this.player, this.sprites);
|
|
|
+
|
|
|
+ if (start !== 0) {
|
|
|
+ // Calculate FPS
|
|
|
+
|
|
|
+ let now = new Date().getTime();
|
|
|
+
|
|
|
+ const timeDelta = now - this.lastRenderCycleTime;
|
|
|
+ const fps = Math.floor(1000 / timeDelta);
|
|
|
+
|
|
|
+ this.lastRenderCycleTime = now;
|
|
|
+
|
|
|
+ this.printDebug('FPS: ' + fps);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.running) {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.drawLoop();
|
|
|
+ }, 20);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private lastRenderCycleTime: number = 0;
|
|
|
+
|
|
|
+ // Draw the player graphics.
|
|
|
+ //
|
|
|
+ drawPlayer(): void {
|
|
|
+ try {
|
|
|
+ // Call draw routine in player state
|
|
|
+
|
|
|
+ this.player.draw(this.ctx, this.player);
|
|
|
+ return;
|
|
|
+ } catch {}
|
|
|
+
|
|
|
+ // If no specific draw routine is specified then draw a placeholder
|
|
|
+
|
|
|
+ this.ctx.beginPath();
|
|
|
+ this.ctx.arc(
|
|
|
+ this.player.x,
|
|
|
+ this.player.y,
|
|
|
+ this.player.dim / 2,
|
|
|
+ 0,
|
|
|
+ 2 * Math.PI
|
|
|
+ );
|
|
|
+
|
|
|
+ this.ctx.moveTo(this.player.x, this.player.y);
|
|
|
+ this.ctx.lineTo(
|
|
|
+ this.player.x + Math.cos(this.player.rot) * 20,
|
|
|
+ this.player.y + Math.sin(this.player.rot) * 20
|
|
|
+ );
|
|
|
+ this.ctx.closePath();
|
|
|
+
|
|
|
+ this.ctx.stroke();
|
|
|
+
|
|
|
+ let oldStrokeStyle = this.ctx.strokeStyle;
|
|
|
+ let dimHalf = this.player.dim / 2;
|
|
|
+
|
|
|
+ this.ctx.strokeStyle = 'red';
|
|
|
+ this.ctx.rect(
|
|
|
+ this.player.x - dimHalf,
|
|
|
+ this.player.y - dimHalf,
|
|
|
+ this.player.dim,
|
|
|
+ this.player.dim
|
|
|
+ );
|
|
|
+ this.ctx.stroke();
|
|
|
+ this.ctx.strokeStyle = oldStrokeStyle;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Draw sprite graphics
|
|
|
+ //
|
|
|
+ drawSprites(): void {
|
|
|
+ for (let sprite of this.sprites) {
|
|
|
+ try {
|
|
|
+ // Call draw routine in sprite state
|
|
|
+
|
|
|
+ sprite.draw(this.ctx, sprite);
|
|
|
+ return;
|
|
|
+ } catch {}
|
|
|
+
|
|
|
+ this.ctx.beginPath();
|
|
|
+ this.ctx.arc(sprite.x, sprite.y, sprite.dim / 2, 0, 2 * Math.PI);
|
|
|
+
|
|
|
+ this.ctx.moveTo(sprite.x, sprite.y);
|
|
|
+ this.ctx.lineTo(
|
|
|
+ sprite.x + Math.cos(sprite.rot) * 20,
|
|
|
+ sprite.y + Math.sin(sprite.rot) * 20
|
|
|
+ );
|
|
|
+ this.ctx.closePath();
|
|
|
+
|
|
|
+ this.ctx.stroke();
|
|
|
+
|
|
|
+ let oldStrokeStyle = this.ctx.strokeStyle;
|
|
|
+ let dimHalf = sprite.dim / 2;
|
|
|
+
|
|
|
+ this.ctx.strokeStyle = 'red';
|
|
|
+ this.ctx.rect(
|
|
|
+ sprite.x - dimHalf,
|
|
|
+ sprite.y - dimHalf,
|
|
|
+ sprite.dim,
|
|
|
+ sprite.dim
|
|
|
+ );
|
|
|
+ this.ctx.stroke();
|
|
|
+ this.ctx.strokeStyle = oldStrokeStyle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|