diff --git a/src/controllerInput.ts b/src/controllerInput.ts index 328bb50..dca821f 100644 --- a/src/controllerInput.ts +++ b/src/controllerInput.ts @@ -48,6 +48,7 @@ export class ControllerInput { private _onShootObservable: Observable = new Observable(); private _onCameraAdjustObservable: Observable = new Observable(); + private _onStatusScreenToggleObservable: Observable = new Observable(); constructor() { this._controllerObservable.add(this.handleControllerEvent.bind(this)); @@ -67,6 +68,13 @@ export class ControllerInput { return this._onCameraAdjustObservable; } + /** + * Get observable that fires when X button is pressed on left controller + */ + public get onStatusScreenToggleObservable(): Observable { + return this._onStatusScreenToggleObservable; + } + /** * Get current input state (stick positions) */ @@ -225,6 +233,12 @@ export class ControllerInput { direction: "up", }); } + if (controllerEvent.component.id === "x-button" && controllerEvent.hand === "left") { + // Only trigger on button press, not release + if (controllerEvent.pressed) { + this._onStatusScreenToggleObservable.notifyObservers(); + } + } console.log(controllerEvent); } } diff --git a/src/gameStats.ts b/src/gameStats.ts new file mode 100644 index 0000000..aabc68e --- /dev/null +++ b/src/gameStats.ts @@ -0,0 +1,109 @@ +/** + * Tracks game statistics for display on status screen + */ +export class GameStats { + private _gameStartTime: number = 0; + private _asteroidsDestroyed: number = 0; + private _hullDamageTaken: number = 0; + private _shotsFired: number = 0; + private _shotsHit: number = 0; + private _fuelConsumed: number = 0; + + /** + * Start the game timer + */ + public startTimer(): void { + this._gameStartTime = Date.now(); + } + + /** + * Get elapsed game time in seconds + */ + public getGameTime(): number { + if (this._gameStartTime === 0) { + return 0; + } + return Math.floor((Date.now() - this._gameStartTime) / 1000); + } + + /** + * Get formatted game time as MM:SS + */ + public getFormattedGameTime(): string { + const seconds = this.getGameTime(); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; + } + + /** + * Increment asteroids destroyed count + */ + public recordAsteroidDestroyed(): void { + this._asteroidsDestroyed++; + } + + /** + * Record hull damage taken + */ + public recordHullDamage(amount: number): void { + this._hullDamageTaken += amount; + } + + /** + * Record a shot fired + */ + public recordShotFired(): void { + this._shotsFired++; + } + + /** + * Record a shot that hit a target + */ + public recordShotHit(): void { + this._shotsHit++; + } + + /** + * Record fuel consumed + */ + public recordFuelConsumed(amount: number): void { + this._fuelConsumed += amount; + } + + /** + * Get accuracy percentage + */ + public getAccuracy(): number { + if (this._shotsFired === 0) { + return 0; + } + return Math.round((this._shotsHit / this._shotsFired) * 100); + } + + /** + * Get all statistics + */ + public getStats() { + return { + gameTime: this.getFormattedGameTime(), + asteroidsDestroyed: this._asteroidsDestroyed, + hullDamageTaken: Math.round(this._hullDamageTaken * 100), + shotsFired: this._shotsFired, + accuracy: this.getAccuracy(), + fuelConsumed: Math.round(this._fuelConsumed * 100) + }; + } + + /** + * Reset all statistics + */ + public reset(): void { + this._gameStartTime = Date.now(); + this._asteroidsDestroyed = 0; + this._hullDamageTaken = 0; + this._shotsFired = 0; + this._shotsHit = 0; + this._fuelConsumed = 0; + } +} diff --git a/src/ship.ts b/src/ship.ts index 9463e06..0f21e36 100644 --- a/src/ship.ts +++ b/src/ship.ts @@ -24,6 +24,8 @@ import { ControllerInput } from "./controllerInput"; import { ShipPhysics } from "./shipPhysics"; import { ShipAudio } from "./shipAudio"; import { WeaponSystem } from "./weaponSystem"; +import { StatusScreen } from "./statusScreen"; +import { GameStats } from "./gameStats"; export class Ship { private _ship: TransformNode; @@ -38,6 +40,8 @@ export class Ship { private _physics: ShipPhysics; private _audio: ShipAudio; private _weapons: WeaponSystem; + private _statusScreen: StatusScreen; + private _gameStats: GameStats; // Frame counter for physics updates private _frameCount: number = 0; @@ -74,6 +78,7 @@ export class Ship { public async initialize() { this._scoreboard = new Scoreboard(); + this._gameStats = new GameStats(); this._ship = new TransformNode("shipBase", DefaultScene.MainScene); const data = await loadAsset("ship.glb"); this._ship = data.container.transformNodes[0]; @@ -139,6 +144,13 @@ export class Ship { this.handleShoot(); }); + // Wire up status screen toggle event + this._controllerInput.onStatusScreenToggleObservable.add(() => { + if (this._statusScreen) { + this._statusScreen.toggle(); + } + }); + // Wire up camera adjustment events this._keyboardInput.onCameraChangeObservable.add((cameraKey) => { if (cameraKey === 1) { @@ -192,6 +204,10 @@ export class Ship { // Initialize scoreboard (it will retrieve and setup its own screen mesh) this._scoreboard.initialize(); + + // Initialize status screen + this._statusScreen = new StatusScreen(DefaultScene.MainScene, this._gameStats); + this._statusScreen.initialize(this._camera); } /** @@ -367,5 +383,9 @@ export class Ship { if (this._weapons) { this._weapons.dispose(); } + + if (this._statusScreen) { + this._statusScreen.dispose(); + } } } diff --git a/src/statusScreen.ts b/src/statusScreen.ts new file mode 100644 index 0000000..6761c98 --- /dev/null +++ b/src/statusScreen.ts @@ -0,0 +1,232 @@ +import { + AdvancedDynamicTexture, + Control, + Rectangle, + StackPanel, + TextBlock +} from "@babylonjs/gui"; +import { + Mesh, + MeshBuilder, + Scene, + StandardMaterial, + TransformNode, + Vector3 +} from "@babylonjs/core"; +import { GameStats } from "./gameStats"; + +/** + * Status screen that displays game statistics + * Floats in front of the user and can be toggled on/off + */ +export class StatusScreen { + private _scene: Scene; + private _gameStats: GameStats; + private _screenMesh: Mesh | null = null; + private _texture: AdvancedDynamicTexture | null = null; + private _isVisible: boolean = false; + private _camera: TransformNode | null = null; + + // Text blocks for statistics + private _gameTimeText: TextBlock; + private _asteroidsText: TextBlock; + private _hullDamageText: TextBlock; + private _shotsFiredText: TextBlock; + private _accuracyText: TextBlock; + private _fuelConsumedText: TextBlock; + + constructor(scene: Scene, gameStats: GameStats) { + this._scene = scene; + this._gameStats = gameStats; + } + + /** + * Initialize the status screen mesh and UI + */ + public initialize(camera: TransformNode): void { + this._camera = camera; + + // Create a plane mesh for the status screen + this._screenMesh = MeshBuilder.CreatePlane( + "statusScreen", + { width: 1.5, height: 1.0 }, + this._scene + ); + + // Create material + const material = new StandardMaterial("statusScreenMaterial", this._scene); + this._screenMesh.material = material; + + // Create AdvancedDynamicTexture + this._texture = AdvancedDynamicTexture.CreateForMesh( + this._screenMesh, + 1024, + 768 + ); + this._texture.background = "#1a1a2e"; + + // Create main container + const mainPanel = new StackPanel("mainPanel"); + mainPanel.width = "100%"; + mainPanel.height = "100%"; + mainPanel.isVertical = true; + mainPanel.paddingTop = "40px"; + mainPanel.paddingBottom = "40px"; + mainPanel.paddingLeft = "60px"; + mainPanel.paddingRight = "60px"; + + // Title + const title = this.createTitleText("GAME STATISTICS"); + mainPanel.addControl(title); + + // Add spacing + const spacer1 = this.createSpacer(40); + mainPanel.addControl(spacer1); + + // Create statistics display + this._gameTimeText = this.createStatText("Game Time: 00:00"); + mainPanel.addControl(this._gameTimeText); + + this._asteroidsText = this.createStatText("Asteroids Destroyed: 0"); + mainPanel.addControl(this._asteroidsText); + + this._hullDamageText = this.createStatText("Hull Damage Taken: 0%"); + mainPanel.addControl(this._hullDamageText); + + this._shotsFiredText = this.createStatText("Shots Fired: 0"); + mainPanel.addControl(this._shotsFiredText); + + this._accuracyText = this.createStatText("Accuracy: 0%"); + mainPanel.addControl(this._accuracyText); + + this._fuelConsumedText = this.createStatText("Fuel Consumed: 0%"); + mainPanel.addControl(this._fuelConsumedText); + + this._texture.addControl(mainPanel); + + // Initially hide the screen + this._screenMesh.setEnabled(false); + this._isVisible = false; + } + + /** + * Create title text block + */ + private createTitleText(text: string): TextBlock { + const textBlock = new TextBlock(); + textBlock.text = text; + textBlock.color = "#00ff88"; + textBlock.fontSize = "80px"; + textBlock.height = "100px"; + textBlock.fontWeight = "bold"; + textBlock.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; + return textBlock; + } + + /** + * Create stat text block + */ + private createStatText(text: string): TextBlock { + const textBlock = new TextBlock(); + textBlock.text = text; + textBlock.color = "#ffffff"; + textBlock.fontSize = "50px"; + textBlock.height = "70px"; + textBlock.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT; + textBlock.paddingTop = "10px"; + textBlock.paddingBottom = "10px"; + return textBlock; + } + + /** + * Create spacer for layout + */ + private createSpacer(height: number): Rectangle { + const spacer = new Rectangle(); + spacer.height = `${height}px`; + spacer.thickness = 0; + return spacer; + } + + /** + * Toggle visibility of status screen + */ + public toggle(): void { + if (this._isVisible) { + this.hide(); + } else { + this.show(); + } + } + + /** + * Show the status screen + */ + public show(): void { + if (!this._screenMesh || !this._camera) { + return; + } + + // Update statistics before showing + this.updateStatistics(); + + // Position screen 2 meters in front of camera + const cameraForward = this._camera.forward; + const offset = cameraForward.scale(2.0); + this._screenMesh.position = this._camera.position.add(offset); + + // Make screen face the camera + this._screenMesh.lookAt(this._camera.position); + + // Rotate 180 degrees to face the right way + this._screenMesh.rotate(Vector3.Up(), Math.PI); + + this._screenMesh.setEnabled(true); + this._isVisible = true; + } + + /** + * Hide the status screen + */ + public hide(): void { + if (!this._screenMesh) { + return; + } + + this._screenMesh.setEnabled(false); + this._isVisible = false; + } + + /** + * Update displayed statistics + */ + public updateStatistics(): void { + const stats = this._gameStats.getStats(); + + this._gameTimeText.text = `Game Time: ${stats.gameTime}`; + this._asteroidsText.text = `Asteroids Destroyed: ${stats.asteroidsDestroyed}`; + this._hullDamageText.text = `Hull Damage Taken: ${stats.hullDamageTaken}%`; + this._shotsFiredText.text = `Shots Fired: ${stats.shotsFired}`; + this._accuracyText.text = `Accuracy: ${stats.accuracy}%`; + this._fuelConsumedText.text = `Fuel Consumed: ${stats.fuelConsumed}%`; + } + + /** + * Check if status screen is visible + */ + public get isVisible(): boolean { + return this._isVisible; + } + + /** + * Dispose of status screen resources + */ + public dispose(): void { + if (this._texture) { + this._texture.dispose(); + } + if (this._screenMesh) { + this._screenMesh.dispose(); + } + } +}