From c6a3056dcdf1f6f458bcd0ccc3b51d6b9796e6fb Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sat, 17 May 2025 18:43:17 +0200 Subject: [PATCH] fixed preloading and integrated vis --- src/app/assetPreloaderService.ts | 36 ++++- src/app/components/game/game.component.html | 2 +- src/app/components/game/game.component.ts | 149 +++----------------- src/app/components/game/sim/SimMain.ts | 38 +---- src/app/components/game/vis/VisEnemy.ts | 2 +- src/app/components/game/vis/VisLevel.ts | 4 +- src/app/components/game/vis/VisMain.ts | 28 ++-- 7 files changed, 74 insertions(+), 185 deletions(-) diff --git a/src/app/assetPreloaderService.ts b/src/app/assetPreloaderService.ts index 28a75f0..b824509 100644 --- a/src/app/assetPreloaderService.ts +++ b/src/app/assetPreloaderService.ts @@ -29,13 +29,36 @@ export class AssetPreloaderService { const url = URL.createObjectURL(blob); if (entry.type === 'image') { const img = new Image(); + const loadedPromise = new Promise((resolve, reject) => { + img.onload = () => { + URL.revokeObjectURL(url); + resolve(); + }; + img.onerror = (e) => { + console.error(`Failed to load image: ${entry.path}`, e); + URL.revokeObjectURL(url); + reject(e); + }; + }); img.src = url; - this.images.set(entry.path, img); + return loadedPromise.then(() => { + this.images.set(entry.path, img); + }); } else if (entry.type === 'audio') { const audio = new Audio(); + const loadedPromise = new Promise((resolve, reject) => { + audio.oncanplaythrough = () => resolve(); + audio.onerror = (e) => { + console.error(`Failed to load audio: ${entry.path}`, e); + resolve(); // Don't reject to avoid blocking all + }; + }); audio.src = url; - this.audio.set(entry.path, audio); + return loadedPromise.then(() => { + this.audio.set(entry.path, audio); + }); } + return Promise.resolve(); }) ); @@ -44,21 +67,24 @@ export class AssetPreloaderService { private async loadAsset(entry: AssetEntry): Promise { const response = await fetch(`/assets/${entry.path}`); + if (!response.ok) { + throw new Error(`Failed to fetch asset: ${entry.path}`); + } + const contentType = response.headers.get('Content-Type') || undefined; const reader = response.body?.getReader(); let loaded = 0; const chunks = []; while (true) { const { done, value } = await reader!.read(); - if (done) - break; + if (done) break; chunks.push(value); loaded += value.length; this.loadedBytes += value.length; this._progress.next(this.loadedBytes / this.totalBytes); } - return new Blob(chunks); + return new Blob(chunks, contentType ? { type: contentType } : undefined); } getImage(name: string): HTMLImageElement { diff --git a/src/app/components/game/game.component.html b/src/app/components/game/game.component.html index 6642ba5..c3fe8f5 100644 --- a/src/app/components/game/game.component.html +++ b/src/app/components/game/game.component.html @@ -3,7 +3,7 @@
- +
diff --git a/src/app/components/game/game.component.ts b/src/app/components/game/game.component.ts index 698c80c..0f5cf56 100644 --- a/src/app/components/game/game.component.ts +++ b/src/app/components/game/game.component.ts @@ -3,6 +3,8 @@ import { OptionsComponent } from '../options/options.component'; import { SimMain } from './sim/SimMain'; import { GdRoot } from './data/GdRoot'; import { SimCommand } from './sim/commands/SimCommand'; +import { VisMain } from './vis/VisMain'; +import { SplashComponent } from '../splash/splash.component'; @Component({ selector: 'app-game', @@ -10,33 +12,30 @@ import { SimCommand } from './sim/commands/SimCommand'; styleUrls: ['./game.component.css'], imports: [OptionsComponent], }) -export class GameComponent implements OnInit { - simMain!: SimMain; - private optionsOpen = false; - private isPaused = false; - private lastFrameTime = 0; - private tileSize = 10; // Size of each tile in pixels +export class GameComponent { + simMain: SimMain = new SimMain(); + visMain!: VisMain; @ViewChild('gameCanvas') canvasRef!: ElementRef; @ViewChild('canvasWrapper') wrapperRef!: ElementRef; - private logicalWidth = 800; - private logicalHeight = 600; scaleX: number = 1; scaleY: number = 1; pixelScale: number = 1; + optionsOpen: boolean = false; - async ngOnInit() { + async ngAfterViewInit() { const gdRoot = await this.loadGdRoot(); - this.simMain = new SimMain(gdRoot); - requestAnimationFrame(this.gameLoop.bind(this)); + this.simMain.setGdRoot(gdRoot); + this.visMain = new VisMain(this.simMain, SplashComponent.assetPreloader, this.wrapperRef.nativeElement, this.canvasRef.nativeElement); + window.addEventListener('resize', _ => this.visMain.onResized()); } start() { - this.simMain.start(performance.now()); + this.visMain.start(); } stop() { - this.simMain.stop(); + this.visMain.start(); } step() { @@ -48,14 +47,13 @@ export class GameComponent implements OnInit { } fastForward() { - this.simMain.fastForwardToEnd(); + this.simMain.executeToEnd(); } async loadGdRoot(): Promise { - const data = await fetch('/assets/data/gdRoot.json').then((r) => r.json()); - // if (!isGdRoot(data)) { - // throw new Error("Invalid GdRoot!"); - // } + const data = await fetch('/assets/data/gdRoot.json').then((r) => + r.json() + ); const gdRoot: GdRoot = data; return gdRoot; } @@ -64,126 +62,15 @@ export class GameComponent implements OnInit { this.simMain.setGdRoot(await this.loadGdRoot()); } - ngAfterViewInit(): void { - this.resizeCanvas(); - requestAnimationFrame(this.update); - window.addEventListener('resize', this.resizeCanvas.bind(this)); - } - - update = (now: number) => { - if (!this.simMain) { - return; - } - - this.simMain.update(now) - - const lastStepTime = this.simMain.isRunning - ? this.simMain.lastStepTime - : now; - const t = this.simMain.isRunning - ? Math.max( - Math.min((now - lastStepTime) / this.simMain.interval, 1), - 0 - ) - : 1; - - this.renderFrame(t); - - requestAnimationFrame(this.update); - }; - - renderFrame(t: number) { - const ctx = this.canvasRef.nativeElement.getContext('2d'); - if (!ctx) return; - - const canvas = this.canvasRef.nativeElement; - ctx.clearRect(0, 0, canvas.width, canvas.height); - } - - resizeCanvas(): void { - const wrapper = this.wrapperRef.nativeElement; - const canvas = this.canvasRef.nativeElement; - - const targetAspect = this.logicalWidth / this.logicalHeight; - const maxW = wrapper.clientWidth; - const maxH = wrapper.clientHeight; - - let width = maxW; - let height = width / targetAspect; - - if (height > maxH) { - height = maxH; - width = height * targetAspect; - } - - canvas.width = width; - canvas.height = height; - - this.scaleX = canvas.width / this.logicalWidth; // e.g. 800 - this.scaleY = canvas.height / this.logicalHeight; // e.g. 600 - - // Optionally use the smaller of the two for uniform scaling - this.pixelScale = Math.min(this.scaleX, this.scaleY); - } - - toScreen(x: number, y: number): [number, number] { - return [x * this.pixelScale, y * this.pixelScale]; - } - - fromScreen(px: number, py: number): [number, number] { - return [px / this.pixelScale, py / this.pixelScale]; - } - - onCanvasClick(event: MouseEvent) { - const canvas = this.canvasRef.nativeElement; - const rect = canvas.getBoundingClientRect(); - const scaleX = canvas.width / rect.width; - const scaleY = canvas.height / rect.height; - - const px = (event.clientX - rect.left) * scaleX; - const py = (event.clientY - rect.top) * scaleY; - - const [gx, gy] = this.fromScreen(px, py); - } - - private gameLoop(currentTime: number): void { - if (this.lastFrameTime === 0) { - this.lastFrameTime = currentTime; - } - const deltaTime = (currentTime - this.lastFrameTime) / 1000; - this.lastFrameTime = currentTime; - - // Only update the game state if not paused - if (!this.isPaused) { - this.updateGame(deltaTime); - requestAnimationFrame(this.gameLoop.bind(this)); - } - } - - private updateGame(deltaTime: number): void { - // Implement your game update logic here using deltaTime - } - // Called when opening options: pause the game public openOptions(): void { this.optionsOpen = true; - this.pauseGame(); + this.stop(); } // Called when closing options: resume the game public closeOptions(): void { this.optionsOpen = false; - this.resumeGame(); - } - - private pauseGame(): void { - this.isPaused = true; - } - - private resumeGame(): void { - this.isPaused = false; - // Reset the lastFrameTime to avoid a huge delta on resume - this.lastFrameTime = performance.now(); - requestAnimationFrame(this.gameLoop.bind(this)); + this.start(); } } diff --git a/src/app/components/game/sim/SimMain.ts b/src/app/components/game/sim/SimMain.ts index 55aaee2..05992ea 100644 --- a/src/app/components/game/sim/SimMain.ts +++ b/src/app/components/game/sim/SimMain.ts @@ -9,21 +9,14 @@ import { SimCommand } from './commands/SimCommand'; import { SimLevel } from './SimLevel'; export class SimMain { - executeUntilStep(targetStep: number) { - throw new Error("Method not implemented."); - } - lastTime = 0; currentStep = -1; - isRunning = false; - lastStepTime: number = 0; currentLevel: SimLevel | null = null; commandHistory: SimCommand[] = []; gdRoot: GdRoot = null!; interval: number = 0; private actions: ISimAction[] = []; - constructor(gdRoot: GdRoot) { - this.setGdRoot(gdRoot); + constructor() { this.actions.push(new SimActionMoveEnemies()); this.actions.push(new SimActionSpawnEnemies()); this.actions.push(new SimActionFireTowers()); @@ -33,15 +26,16 @@ export class SimMain { setGdRoot(gdRoot: GdRoot) { this.gdRoot = gdRoot; this.interval = 1.0 / this.gdRoot.simulation.stepsPerSecond; + this.currentLevel = new SimLevel(this.gdRoot, 0); } - fastForwardToEnd() { - this.fastForwardTo( + executeToEnd() { + this.executeUntilStep( this.commandHistory[this.commandHistory.length - 1].step ); } - private fastForwardTo(target: number) { + executeUntilStep(target: number) { this.currentStep = 0; while (this.currentStep < target) { this.step(); @@ -62,19 +56,6 @@ export class SimMain { for (const cmd of commands) { cmd.execute(this); } - - this.lastStepTime = performance.now(); - } - - start(now: number) { - if (this.isRunning) return; - - this.isRunning = true; - this.lastTime = now; - } - - stop() { - this.isRunning = false; } addCommand(command: SimCommand) { @@ -83,13 +64,4 @@ export class SimMain { ); this.commandHistory.push(command); } - - update = (now: number) => { - if (!this.isRunning) return; - - while (now - this.lastTime >= this.interval) { - this.step(); - this.lastTime += this.interval; - } - }; } diff --git a/src/app/components/game/vis/VisEnemy.ts b/src/app/components/game/vis/VisEnemy.ts index e9d50ba..c14439c 100644 --- a/src/app/components/game/vis/VisEnemy.ts +++ b/src/app/components/game/vis/VisEnemy.ts @@ -54,7 +54,7 @@ export class VisEnemy { ctx.scale(2, 2); break; } - ctx.drawImage(this.assets.getImage("enemy-" + (this.simEnemy.index | 0) + ".svg"), -this.image.width / 2, -this.image.height / 2, this.image.width, this.image.height); + ctx.drawImage(this.assets.getImage("images/enemy-" + (this.simEnemy.index | 0) + ".svg"), -this.image.width / 2, -this.image.height / 2, this.image.width, this.image.height); ctx.restore(); } } diff --git a/src/app/components/game/vis/VisLevel.ts b/src/app/components/game/vis/VisLevel.ts index c7c8c73..5c28b90 100644 --- a/src/app/components/game/vis/VisLevel.ts +++ b/src/app/components/game/vis/VisLevel.ts @@ -228,7 +228,7 @@ export class VisLevel { const pos = Vector2.lerp(positions[0], positions[1], t); const width = this.screenCellWidth * simProjectile.size; const height = this.screenCellHeight * simProjectile.size; - this.visMain.context.drawImage(this.assets.getImage("projectile.svg"), + this.visMain.context.drawImage(this.assets.getImage("images/projectile.svg"), this.screenXOffset + pos.x * this.hexSize - width / 2, this.screenYOffset + pos.y * this.hexSize - height / 2, width, @@ -293,6 +293,6 @@ export class VisLevel { private drawCellImage(context: CanvasRenderingContext2D, cell: SimCell, name: string) { const coords = this.getScreenCoords(cell.hex); - context.drawImage(this.assets.getImage(name), coords.x, coords.y, this.screenCellWidth, this.screenCellHeight); + context.drawImage(this.assets.getImage("images/" + name), coords.x, coords.y, this.screenCellWidth, this.screenCellHeight); } } diff --git a/src/app/components/game/vis/VisMain.ts b/src/app/components/game/vis/VisMain.ts index 2ccef06..1c9a1ee 100644 --- a/src/app/components/game/vis/VisMain.ts +++ b/src/app/components/game/vis/VisMain.ts @@ -14,19 +14,18 @@ export class VisMain { private ready: boolean = false; private gap: number = 0; assets: AssetPreloaderService; + wrapper: HTMLDivElement; - constructor(simMain: SimMain, assets: AssetPreloaderService, canvas: HTMLCanvasElement) { + constructor(simMain: SimMain, assets: AssetPreloaderService, wrapper: HTMLDivElement, canvas: HTMLCanvasElement) { this.assets = assets; this.simMain = simMain; this.canvas = canvas; + this.wrapper = wrapper; this.context = this.canvas.getContext("2d")!; this.context.globalCompositeOperation = "source-over"; - this.wallPattern = this.createPattern(assets.getImage("wall.png"), 48); + this.wallPattern = this.createPattern(assets.getImage("images/wall.png"), 48); this.visLevel = new VisLevel(this, this.simMain, this.simMain.gdRoot, assets); - const host = this; - requestAnimationFrame(function step(timestamp) { - host.step(timestamp); - }); + this.start(); } private createPattern(image: HTMLImageElement, size: number): CanvasPattern { @@ -45,9 +44,8 @@ export class VisMain { return; } - const host = this; requestAnimationFrame((timestamp: number) => { - host.step(timestamp); + this.step(timestamp); }); if (!this.startTimestamp) { @@ -70,9 +68,8 @@ export class VisMain { }; public onResized() { - const gameHost = document.getElementById("game-host") as HTMLDivElement; - const width = gameHost.clientWidth; - const height = gameHost.clientHeight; + const width = this.wrapper.clientWidth; + const height = this.wrapper.clientHeight; const ratio = window.devicePixelRatio; this.canvas.width = width * ratio; this.canvas.height = height * ratio; @@ -100,7 +97,14 @@ export class VisMain { this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); } - private stop() { + stop() { this.active = false; } + + start() { + this.active = true; + requestAnimationFrame((timestamp: number) => { + this.step(timestamp); + }); + } }