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 _onCameraAdjustObservable: Observable<CameraAdjustment> =
|
||||
new Observable<CameraAdjustment>();
|
||||
private _onStatusScreenToggleObservable: Observable<void> = new Observable<void>();
|
||||
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 { 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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