Add status screen with game statistics display
Some checks failed
Build / build (push) Failing after 20s
Some checks failed
Build / build (push) Failing after 20s
Implemented a toggleable status screen that displays game statistics, activated by pressing the X button on the left VR controller. **New File: src/gameStats.ts** - GameStats class to track game statistics - Tracks: game time, asteroids destroyed, hull damage taken, shots fired, shots hit, fuel consumed - Methods: startTimer(), recordAsteroidDestroyed(), recordHullDamage(), recordShotFired(), recordShotHit(), recordFuelConsumed() - Calculated stats: getGameTime(), getFormattedGameTime() (MM:SS), getAccuracy() (percentage) - getStats() returns formatted statistics object - reset() method to reset all statistics **New File: src/statusScreen.ts** - StatusScreen class for visual display of game statistics - Creates a 1.5x1.0 meter plane mesh with AdvancedDynamicTexture (1024x768) - Dark blue background (#1a1a2e) with green title (#00ff88) - Displays 6 statistics: - Game Time (MM:SS format) - Asteroids Destroyed (count) - Hull Damage Taken (percentage) - Shots Fired (count) - Accuracy (percentage) - Fuel Consumed (percentage) - toggle() method to show/hide screen - Positions 2 meters in front of camera when shown - Automatically faces camera with proper orientation - updateStatistics() refreshes displayed values from GameStats **Modified: src/controllerInput.ts** - Added _onStatusScreenToggleObservable for X button events - Added onStatusScreenToggleObservable getter - Added X button handler in handleControllerEvent() - Checks for "x-button" on left controller only - Fires observable on button press (not release) **Modified: src/ship.ts** - Added StatusScreen and GameStats imports - Added _statusScreen and _gameStats properties - Initialize GameStats in constructor - Initialize StatusScreen with camera reference after camera creation - Wired up controller observable to toggle status screen - Added statusScreen cleanup in dispose() method Features: - Toggle display with X button on left controller - Floats 2 meters in front of user's face - Always faces the camera - Clean, readable statistics layout - Currently displays placeholder data (statistics tracking integration pending) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
827dd2d359
commit
406cebcd96
@ -48,6 +48,7 @@ export class ControllerInput {
|
|||||||
private _onShootObservable: Observable<void> = new Observable<void>();
|
private _onShootObservable: Observable<void> = new Observable<void>();
|
||||||
private _onCameraAdjustObservable: Observable<CameraAdjustment> =
|
private _onCameraAdjustObservable: Observable<CameraAdjustment> =
|
||||||
new Observable<CameraAdjustment>();
|
new Observable<CameraAdjustment>();
|
||||||
|
private _onStatusScreenToggleObservable: Observable<void> = new Observable<void>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._controllerObservable.add(this.handleControllerEvent.bind(this));
|
this._controllerObservable.add(this.handleControllerEvent.bind(this));
|
||||||
@ -67,6 +68,13 @@ export class ControllerInput {
|
|||||||
return this._onCameraAdjustObservable;
|
return this._onCameraAdjustObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get observable that fires when X button is pressed on left controller
|
||||||
|
*/
|
||||||
|
public get onStatusScreenToggleObservable(): Observable<void> {
|
||||||
|
return this._onStatusScreenToggleObservable;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current input state (stick positions)
|
* Get current input state (stick positions)
|
||||||
*/
|
*/
|
||||||
@ -225,6 +233,12 @@ export class ControllerInput {
|
|||||||
direction: "up",
|
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);
|
console.log(controllerEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
src/gameStats.ts
Normal file
109
src/gameStats.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/ship.ts
20
src/ship.ts
@ -24,6 +24,8 @@ import { ControllerInput } from "./controllerInput";
|
|||||||
import { ShipPhysics } from "./shipPhysics";
|
import { ShipPhysics } from "./shipPhysics";
|
||||||
import { ShipAudio } from "./shipAudio";
|
import { ShipAudio } from "./shipAudio";
|
||||||
import { WeaponSystem } from "./weaponSystem";
|
import { WeaponSystem } from "./weaponSystem";
|
||||||
|
import { StatusScreen } from "./statusScreen";
|
||||||
|
import { GameStats } from "./gameStats";
|
||||||
|
|
||||||
export class Ship {
|
export class Ship {
|
||||||
private _ship: TransformNode;
|
private _ship: TransformNode;
|
||||||
@ -38,6 +40,8 @@ export class Ship {
|
|||||||
private _physics: ShipPhysics;
|
private _physics: ShipPhysics;
|
||||||
private _audio: ShipAudio;
|
private _audio: ShipAudio;
|
||||||
private _weapons: WeaponSystem;
|
private _weapons: WeaponSystem;
|
||||||
|
private _statusScreen: StatusScreen;
|
||||||
|
private _gameStats: GameStats;
|
||||||
|
|
||||||
// Frame counter for physics updates
|
// Frame counter for physics updates
|
||||||
private _frameCount: number = 0;
|
private _frameCount: number = 0;
|
||||||
@ -74,6 +78,7 @@ export class Ship {
|
|||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
this._scoreboard = new Scoreboard();
|
this._scoreboard = new Scoreboard();
|
||||||
|
this._gameStats = new GameStats();
|
||||||
this._ship = new TransformNode("shipBase", DefaultScene.MainScene);
|
this._ship = new TransformNode("shipBase", DefaultScene.MainScene);
|
||||||
const data = await loadAsset("ship.glb");
|
const data = await loadAsset("ship.glb");
|
||||||
this._ship = data.container.transformNodes[0];
|
this._ship = data.container.transformNodes[0];
|
||||||
@ -139,6 +144,13 @@ export class Ship {
|
|||||||
this.handleShoot();
|
this.handleShoot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wire up status screen toggle event
|
||||||
|
this._controllerInput.onStatusScreenToggleObservable.add(() => {
|
||||||
|
if (this._statusScreen) {
|
||||||
|
this._statusScreen.toggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Wire up camera adjustment events
|
// Wire up camera adjustment events
|
||||||
this._keyboardInput.onCameraChangeObservable.add((cameraKey) => {
|
this._keyboardInput.onCameraChangeObservable.add((cameraKey) => {
|
||||||
if (cameraKey === 1) {
|
if (cameraKey === 1) {
|
||||||
@ -192,6 +204,10 @@ export class Ship {
|
|||||||
|
|
||||||
// Initialize scoreboard (it will retrieve and setup its own screen mesh)
|
// Initialize scoreboard (it will retrieve and setup its own screen mesh)
|
||||||
this._scoreboard.initialize();
|
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) {
|
if (this._weapons) {
|
||||||
this._weapons.dispose();
|
this._weapons.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._statusScreen) {
|
||||||
|
this._statusScreen.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
232
src/statusScreen.ts
Normal file
232
src/statusScreen.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user