All checks were successful
Build / build (push) Successful in 1m20s
## Major Reorganization
Reorganized all 57 TypeScript files from flat src/ directory into logical subdirectories for improved maintainability and discoverability.
## New Directory Structure
```
src/
├── core/ (4 files)
│ └── Foundation modules: defaultScene, gameConfig, debug, router
│
├── ship/ (10 files)
│ ├── Ship coordination and subsystems
│ └── input/ - VR controller and keyboard input
│
├── levels/ (10 files)
│ ├── config/ - Level schema, serialization, deserialization
│ ├── generation/ - Level generator and editor
│ └── ui/ - Level selector
│
├── environment/ (11 files)
│ ├── asteroids/ - Rock factory and explosions
│ ├── celestial/ - Suns, planets, textures
│ ├── stations/ - Star base loading
│ └── background/ - Stars, mirror, radar
│
├── ui/ (9 files)
│ ├── hud/ - Scoreboard and status screen
│ ├── screens/ - Login, settings, preloader
│ └── widgets/ - Discord integration
│
├── replay/ (7 files)
│ ├── Replay system components
│ └── recording/ - Physics recording and storage
│
├── game/ (3 files)
│ └── Game systems: stats, progression, demo
│
├── services/ (2 files)
│ └── External integrations: auth, social
│
└── utils/ (5 files)
└── Shared utilities and helpers
```
## Changes Made
### File Moves (57 files)
- Core modules: 4 files → core/
- Ship system: 10 files → ship/ + ship/input/
- Level system: 10 files → levels/ (+ 3 subdirs)
- Environment: 11 files → environment/ (+ 4 subdirs)
- UI components: 9 files → ui/ (+ 3 subdirs)
- Replay system: 7 files → replay/ + replay/recording/
- Game systems: 3 files → game/
- Services: 2 files → services/
- Utilities: 5 files → utils/
### Import Path Updates
- Updated ~200 import statements across all files
- Fixed relative paths based on new directory structure
- Fixed case-sensitive import issues (physicsRecorder, physicsStorage)
- Ensured consistent lowercase filenames for imports
## Benefits
1. **Easy Navigation** - Related code grouped together
2. **Clear Boundaries** - Logical separation of concerns
3. **Scalability** - Easy pattern for adding new features
4. **Discoverability** - Find ship code in /ship, levels in /levels, etc.
5. **Maintainability** - Isolated modules easier to update
6. **No Circular Dependencies** - Clean dependency graph maintained
## Testing
- All TypeScript compilation errors resolved
- Build succeeds with new structure
- Import paths verified and corrected
- Case-sensitivity issues fixed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
384 lines
12 KiB
TypeScript
384 lines
12 KiB
TypeScript
import {AdvancedDynamicTexture, Control, StackPanel, TextBlock, Rectangle, Container} from "@babylonjs/gui";
|
|
import {DefaultScene} from "../../core/defaultScene";
|
|
import {
|
|
Mesh,
|
|
MeshBuilder,
|
|
Observable,
|
|
Vector3,
|
|
} from "@babylonjs/core";
|
|
import debugLog from '../../core/debug';
|
|
import { ShipStatus } from '../../ship/shipStatus';
|
|
|
|
export type ScoreEvent = {
|
|
score: number,
|
|
message: string,
|
|
remaining: number,
|
|
timeRemaining? : number
|
|
}
|
|
export class Scoreboard {
|
|
private _score: number = 0;
|
|
private _remaining: number = 0;
|
|
private _startTime: number = Date.now();
|
|
|
|
private _active = false;
|
|
private _done = false;
|
|
public readonly onScoreObservable: Observable<ScoreEvent> = new Observable<ScoreEvent>();
|
|
|
|
// Gauge bar fill rectangles
|
|
private _fuelBar: Rectangle | null = null;
|
|
private _hullBar: Rectangle | null = null;
|
|
private _ammoBar: Rectangle | null = null;
|
|
|
|
// Ship status manager
|
|
private _shipStatus: ShipStatus;
|
|
|
|
// Reference to ship for velocity reading
|
|
private _ship: any = null;
|
|
|
|
constructor() {
|
|
this._shipStatus = new ShipStatus();
|
|
|
|
// Subscribe to status changes to automatically update gauges
|
|
this._shipStatus.onStatusChanged.add((event) => {
|
|
switch (event.statusType) {
|
|
case 'fuel':
|
|
this.updateFuelBar(event.newValue);
|
|
break;
|
|
case 'hull':
|
|
this.updateHullBar(event.newValue);
|
|
break;
|
|
case 'ammo':
|
|
this.updateAmmoBar(event.newValue);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
public get done() {
|
|
return this._done;
|
|
}
|
|
public set done(value: boolean) {
|
|
this._done = value;
|
|
}
|
|
|
|
/**
|
|
* Get the ship status manager
|
|
*/
|
|
public get shipStatus(): ShipStatus {
|
|
return this._shipStatus;
|
|
}
|
|
|
|
/**
|
|
* Get the number of asteroids remaining
|
|
*/
|
|
public get remaining(): number {
|
|
return this._remaining;
|
|
}
|
|
|
|
public setRemainingCount(count: number) {
|
|
this._remaining = count;
|
|
}
|
|
|
|
/**
|
|
* Set the ship reference for velocity reading
|
|
*/
|
|
public setShip(ship: any): void {
|
|
this._ship = ship;
|
|
}
|
|
|
|
public initialize(): void {
|
|
const scene = DefaultScene.MainScene;
|
|
|
|
const parent = scene.getNodeById('ship');
|
|
debugLog('Scoreboard parent:', parent);
|
|
debugLog('Initializing scoreboard');
|
|
let scoreboard: Mesh | null = null;
|
|
|
|
// Retrieve and setup screen mesh from the loaded GLB
|
|
const screen = scene.getMaterialById("Screen")?.getBindedMeshes()[0] as Mesh;
|
|
|
|
if (screen) {
|
|
// Setup screen mesh: adjust pivot point and rotation
|
|
const oldParent = screen.parent;
|
|
screen.setParent(null);
|
|
screen.setPivotPoint(screen.getBoundingInfo().boundingSphere.center);
|
|
screen.setParent(oldParent);
|
|
screen.rotation.y = Math.PI;
|
|
|
|
scoreboard = screen;
|
|
scoreboard.material.dispose();
|
|
}
|
|
|
|
// Retrieve and setup gauges mesh from the loaded GLB
|
|
const gauges = scene.getMaterialById("Gauges")?.getBindedMeshes()[0] as Mesh;
|
|
|
|
if (gauges) {
|
|
// Setup gauges mesh: adjust pivot point and rotation
|
|
const oldParent = gauges.parent;
|
|
gauges.setParent(null);
|
|
gauges.setPivotPoint(gauges.getBoundingInfo().boundingSphere.center);
|
|
gauges.setParent(oldParent);
|
|
//gauges.rotation.z = Math.PI;
|
|
|
|
// Create gauges display
|
|
this.createGaugesDisplay(gauges);
|
|
}
|
|
|
|
// Fallback: create a plane if screen mesh not found
|
|
if (!scoreboard) {
|
|
console.error('Screen mesh not found, creating fallback plane');
|
|
scoreboard = MeshBuilder.CreatePlane("scoreboard", {width: 1, height: 1}, scene);
|
|
scoreboard.parent = parent;
|
|
|
|
scoreboard.position.y = 1.05;
|
|
scoreboard.position.z = 2.1;
|
|
scoreboard.visibility = .5;
|
|
scoreboard.scaling = new Vector3(.4, .4, .4);
|
|
}
|
|
|
|
// scoreboard.renderingGroupId = 3;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const advancedTexture = AdvancedDynamicTexture.CreateForMesh(scoreboard, 512, 512);
|
|
advancedTexture.background = "black";
|
|
advancedTexture.hasAlpha = false;
|
|
const scoreText = this.createText();
|
|
|
|
const fpsText = this.createText();
|
|
fpsText.text = "FPS: 60";
|
|
|
|
const hullText = this.createText();
|
|
hullText.text = 'Hull: 100%';
|
|
|
|
const remainingText = this.createText();
|
|
remainingText.text = 'Remaining: 0';
|
|
|
|
const timeRemainingText = this.createText();
|
|
timeRemainingText.text = 'Time: 00:00';
|
|
|
|
const velocityText = this.createText();
|
|
velocityText.text = 'Velocity: 0 m/s';
|
|
|
|
|
|
const panel = new StackPanel();
|
|
panel.isVertical = true;
|
|
//panel.height = .5;
|
|
//panel.isVertical = true;
|
|
panel.addControl(scoreText);
|
|
panel.addControl(remainingText);
|
|
panel.addControl(fpsText);
|
|
panel.addControl(hullText);
|
|
panel.addControl(timeRemainingText);
|
|
panel.addControl(velocityText);
|
|
advancedTexture.addControl(panel);
|
|
let i = 0;
|
|
const afterRender = scene.onAfterRenderObservable.add(() => {
|
|
scoreText.text = `Score: ${this.calculateScore()}`;
|
|
remainingText.text = `Remaining: ${this._remaining}`;
|
|
|
|
// Update velocity from ship if available
|
|
if (this._ship && this._ship.velocity) {
|
|
const velocityMagnitude = this._ship.velocity.length();
|
|
velocityText.text = `Velocity: ${velocityMagnitude.toFixed(1)} m/s`;
|
|
} else {
|
|
velocityText.text = `Velocity: 0.0 m/s`;
|
|
}
|
|
|
|
const elapsed = Date.now() - this._startTime;
|
|
if (this._active && i++%30 == 0) {
|
|
timeRemainingText.text = `Time: ${Math.floor(elapsed/60000).toString().padStart(2,"0")}:${(Math.floor(elapsed/1000)%60).toString().padStart(2,"0")}`;
|
|
fpsText.text = `FPS: ${Math.floor(scene.getEngine().getFps())}`;
|
|
}
|
|
});
|
|
|
|
this.onScoreObservable.add((score: ScoreEvent) => {
|
|
this._score += score.score;
|
|
this._remaining += score.remaining;
|
|
});
|
|
|
|
this._active = true;
|
|
}
|
|
private createText(): TextBlock {
|
|
const text1 = new TextBlock();
|
|
text1.color = "white";
|
|
text1.fontSize = "60px";
|
|
text1.height = "80px";
|
|
text1.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
|
|
text1.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER;
|
|
return text1;
|
|
}
|
|
private calculateScore() {
|
|
return Math.floor(this._score);
|
|
}
|
|
|
|
/**
|
|
* Create the gauges display with 3 bar gauges (Fuel, Hull, Ammo)
|
|
*/
|
|
private createGaugesDisplay(gaugesMesh: Mesh): void {
|
|
// Store reference to old material to dispose after new one is created
|
|
const oldMaterial = gaugesMesh.material;
|
|
|
|
// Create AdvancedDynamicTexture for the gauges mesh
|
|
// This creates a new StandardMaterial and assigns it to the mesh
|
|
const gaugesTexture = AdvancedDynamicTexture.CreateForMesh(gaugesMesh, 512, 512);
|
|
gaugesTexture.coordinatesIndex = 2;
|
|
|
|
|
|
|
|
gaugesTexture.background = "#444444";
|
|
gaugesTexture.hasAlpha = false;
|
|
|
|
// Now dispose the old material after the new one is assigned
|
|
if (oldMaterial) {
|
|
oldMaterial.dispose(true, true);
|
|
}
|
|
|
|
debugLog('Gauges texture created, material:', gaugesMesh.material?.name);
|
|
|
|
// Create a vertical stack panel for the gauges
|
|
const panel = new StackPanel('GaugesPanel');
|
|
panel.rotation = Math.PI;
|
|
panel.isVertical = true;
|
|
panel.width = "100%";
|
|
panel.height = "100%";
|
|
|
|
// Create the three gauges
|
|
this._fuelBar = this.createGaugeBar("FUEL", "#00FF00", panel);
|
|
this._hullBar = this.createGaugeBar("HULL", "#00FF00", panel);
|
|
this._ammoBar = this.createGaugeBar("AMMO", "#00FF00", panel);
|
|
|
|
gaugesTexture.addControl(panel);
|
|
|
|
let i = 0;
|
|
// Force the texture to update
|
|
//gaugesTexture.markAsDirty();
|
|
|
|
// Set initial values to full (for testing visibility)
|
|
this._shipStatus.setFuel(1);
|
|
this._shipStatus.setHull(1);
|
|
this._shipStatus.setAmmo(1);
|
|
|
|
debugLog('Gauges display created with initial test values');
|
|
}
|
|
|
|
/**
|
|
* Create a single gauge bar with label
|
|
*/
|
|
private createGaugeBar(label: string, color: string, parent: Container): Rectangle {
|
|
// Container for this gauge (label + bar)
|
|
const gaugeContainer = new StackPanel();
|
|
gaugeContainer.isVertical = true;
|
|
gaugeContainer.height = "140px";
|
|
gaugeContainer.width = "100%";
|
|
gaugeContainer.paddingBottom = "15px";
|
|
|
|
// Label text
|
|
const labelText = new TextBlock();
|
|
labelText.text = label;
|
|
labelText.color = "#FFFFFF";
|
|
labelText.fontSize = "60px";
|
|
labelText.height = "70px";
|
|
labelText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
|
|
gaugeContainer.addControl(labelText);
|
|
|
|
// Bar background (border and empty space)
|
|
const barBackground = new Rectangle();
|
|
barBackground.height = "50px";
|
|
barBackground.width = "100%";
|
|
barBackground.thickness = 3;
|
|
barBackground.color = "#FFFFFF";
|
|
barBackground.background = "#333333";
|
|
barBackground.cornerRadius = 5;
|
|
|
|
// Bar fill (the actual gauge)
|
|
const barFill = new Rectangle();
|
|
barFill.height = "100%";
|
|
barFill.width = "100%"; // Will be updated dynamically
|
|
barFill.thickness = 0;
|
|
barFill.background = color;
|
|
barFill.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
|
|
barFill.cornerRadius = 3;
|
|
|
|
barBackground.addControl(barFill);
|
|
gaugeContainer.addControl(barBackground);
|
|
|
|
parent.addControl(gaugeContainer);
|
|
|
|
return barFill;
|
|
}
|
|
|
|
/**
|
|
* Get bar color based on value with smooth gradient
|
|
* Green (1.0) -> Yellow (0.5) -> Red (0.0)
|
|
*/
|
|
private getBarColor(value: number): string {
|
|
// Clamp value between 0 and 1
|
|
value = Math.max(0, Math.min(1, value));
|
|
|
|
let red: number, green: number;
|
|
|
|
if (value >= 0.5) {
|
|
// Interpolate from yellow (0.5) to green (1.0)
|
|
// At 0.5: RGB(255, 255, 0) yellow
|
|
// At 1.0: RGB(0, 255, 0) green
|
|
const t = (value - 0.5) * 2; // 0 to 1 range
|
|
red = Math.round(255 * (1 - t));
|
|
green = 255;
|
|
} else {
|
|
// Interpolate from red (0.0) to yellow (0.5)
|
|
// At 0.0: RGB(255, 0, 0) red
|
|
// At 0.5: RGB(255, 255, 0) yellow
|
|
const t = value * 2; // 0 to 1 range
|
|
red = 255;
|
|
green = Math.round(255 * t);
|
|
}
|
|
|
|
// Convert to hex
|
|
const redHex = red.toString(16).padStart(2, '0');
|
|
const greenHex = green.toString(16).padStart(2, '0');
|
|
return `#${redHex}${greenHex}00`;
|
|
}
|
|
/**
|
|
* Internal method to update fuel gauge bar
|
|
*/
|
|
private updateFuelBar(value: number): void {
|
|
if (this._fuelBar) {
|
|
this._fuelBar.width = `${value * 100}%`;
|
|
this._fuelBar.background = this.getBarColor(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to update hull gauge bar
|
|
*/
|
|
private updateHullBar(value: number): void {
|
|
if (this._hullBar) {
|
|
this._hullBar.width = `${value * 100}%`;
|
|
this._hullBar.background = this.getBarColor(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to update ammo gauge bar
|
|
*/
|
|
private updateAmmoBar(value: number): void {
|
|
if (this._ammoBar) {
|
|
this._ammoBar.width = `${value * 100}%`;
|
|
this._ammoBar.background = this.getBarColor(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispose of scoreboard resources
|
|
*/
|
|
public dispose(): void {
|
|
if (this._shipStatus) {
|
|
this._shipStatus.dispose();
|
|
}
|
|
this.onScoreObservable.clear();
|
|
}
|
|
} |