All checks were successful
Build / build (push) Successful in 1m26s
Implemented a comprehensive ship status management system with event-driven gauge updates and integrated fuel consumption for both linear and angular thrust. **New File: src/shipStatus.ts** - ShipStatus class with Observable pattern for status change events - Manages fuel, hull, and ammo values with automatic clamping (0-1 range) - Configurable max values for each resource type - Public getters: fuel, hull, ammo, getValues() - Setter methods: setFuel(), setHull(), setAmmo() with automatic event firing - Convenience methods: addFuel(), consumeFuel(), damageHull(), repairHull(), addAmmo(), consumeAmmo() - Status check methods: isFuelEmpty(), isDestroyed(), isAmmoEmpty() - Utility methods: reset(), setMaxValues(), dispose() - ShipStatusChangeEvent interface with statusType, oldValue, newValue, delta fields **Modified: src/scoreboard.ts** - Integrated ShipStatus instance as private _shipStatus - Constructor subscribes to ShipStatus.onStatusChanged observable - Added public shipStatus getter to expose status manager - Created createGaugesDisplay() method with 3 bar gauges (FUEL, HULL, AMMO) - Created createGaugeBar() helper for individual gauge construction - Added getBarColor() with smooth RGB gradient: green (1.0) -> yellow (0.5) -> red (0.0) - Renamed public methods to private: updateFuelBar(), updateHullBar(), updateAmmoBar() - Observable subscription automatically updates gauge visuals when status changes - Added dispose() method for cleanup of ShipStatus and observables - Updated initialize() to retrieve and setup screen/gauges meshes from GLB - Set initial test values to full (1.0) for all gauges **Modified: src/shipPhysics.ts** - Added ShipStatus import and private _shipStatus property - Added setShipStatus() method to connect status manager - Modified applyForces() to check fuel availability before applying linear force - Linear thrust fuel consumption: linearMagnitude (0-1) * 0.005 per frame - Added fuel check and consumption for angular thrust (rotation) - Angular thrust fuel consumption: normalized angularMagnitude (0-1) * 0.005 per frame - Forces only applied when fuel > 0 **Modified: src/ship.ts** - Connected ShipPhysics to Scoreboard's ShipStatus via setShipStatus() - Called immediately after physics initialization (line 148) This creates a fully integrated system where: 1. Ship movement (linear and angular) consumes fuel proportional to thrust 2. Fuel depletion prevents further thrust application 3. Gauge displays automatically update via observable events with color coding 4. Other systems can monitor/modify ship status through the same interface 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
import {AdvancedDynamicTexture, Control, StackPanel, TextBlock, Rectangle, Container} from "@babylonjs/gui";
|
|
import {DefaultScene} from "./defaultScene";
|
|
import {
|
|
Mesh,
|
|
MeshBuilder,
|
|
Observable,
|
|
Vector3,
|
|
} from "@babylonjs/core";
|
|
import debugLog from './debug';
|
|
import { ShipStatus } from './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;
|
|
|
|
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;
|
|
}
|
|
|
|
public setRemainingCount(count: number) {
|
|
this._remaining = count;
|
|
}
|
|
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: 2:00';
|
|
|
|
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);
|
|
advancedTexture.addControl(panel);
|
|
let i = 0;
|
|
const afterRender = scene.onAfterRenderObservable.add(() => {
|
|
scoreText.text = `Score: ${this.calculateScore()}`;
|
|
remainingText.text = `Remaining: ${this._remaining}`;
|
|
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();
|
|
}
|
|
} |