diff --git a/src/scoreboard.ts b/src/scoreboard.ts index b34f82f..5108e7f 100644 --- a/src/scoreboard.ts +++ b/src/scoreboard.ts @@ -1,12 +1,14 @@ -import {AdvancedDynamicTexture, Control, StackPanel, TextBlock} from "@babylonjs/gui"; +import {AdvancedDynamicTexture, Control, StackPanel, TextBlock, Rectangle, Container} from "@babylonjs/gui"; import {DefaultScene} from "./defaultScene"; import { - AbstractMesh, Mesh, + Mesh, MeshBuilder, - Observable, StandardMaterial, + Observable, Vector3, } from "@babylonjs/core"; import debugLog from './debug'; +import { ShipStatus } from './shipStatus'; + export type ScoreEvent = { score: number, message: string, @@ -21,8 +23,32 @@ export class Scoreboard { private _active = false; private _done = false; public readonly onScoreObservable: Observable = new Observable(); - constructor() { + // 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; @@ -30,26 +56,60 @@ export class Scoreboard { 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(baseMesh: Mesh) { + public initialize(): void { const scene = DefaultScene.MainScene; const parent = scene.getNodeById('ship'); debugLog('Scoreboard parent:', parent); debugLog('Initializing scoreboard'); - let scoreboard = null; + let scoreboard: Mesh | null = null; - if (baseMesh) { - scoreboard = baseMesh; + // 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(); - //scoreboard.material = new StandardMaterial("scoreboard", scene); + } - } else { + // 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.parent = parent; scoreboard.position.y = 1.05; scoreboard.position.z = 2.1; @@ -66,7 +126,7 @@ export class Scoreboard { const advancedTexture = AdvancedDynamicTexture.CreateForMesh(scoreboard, 512, 512); - advancedTexture.background = "green"; + advancedTexture.background = "black"; advancedTexture.hasAlpha = false; const scoreText = this.createText(); @@ -122,4 +182,171 @@ export class Scoreboard { 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(); + } } \ No newline at end of file diff --git a/src/ship.ts b/src/ship.ts index 1a66b00..b6c1dd7 100644 --- a/src/ship.ts +++ b/src/ship.ts @@ -145,6 +145,7 @@ export class Ship { // Initialize physics controller this._physics = new ShipPhysics(); + this._physics.setShipStatus(this._scoreboard.shipStatus); // Setup physics update loop (every 10 frames) DefaultScene.MainScene.onAfterRenderObservable.add(() => { @@ -174,21 +175,8 @@ export class Ship { centerGap: 0.5, }); - // Setup scoreboard on screen - console.log(data.meshes.get("Screen")); - const screen = DefaultScene.MainScene - .getMaterialById("Screen") - .getBindedMeshes()[0] as AbstractMesh; - console.log(screen); - const old = screen.parent; - screen.setParent(null); - screen.setPivotPoint(screen.getBoundingInfo().boundingSphere.center); - screen.setParent(old); - screen.rotation.y = Math.PI; - console.log(screen.rotation); - console.log(screen.scaling); - - this._scoreboard.initialize(screen as any); + // Initialize scoreboard (it will retrieve and setup its own screen mesh) + this._scoreboard.initialize(); } /** diff --git a/src/shipPhysics.ts b/src/shipPhysics.ts index 459d0ff..7d728ae 100644 --- a/src/shipPhysics.ts +++ b/src/shipPhysics.ts @@ -1,5 +1,6 @@ import { PhysicsBody, TransformNode, Vector2, Vector3 } from "@babylonjs/core"; import { GameConfig } from "./gameConfig"; +import { ShipStatus } from "./shipStatus"; export interface InputState { leftStick: Vector2; @@ -16,6 +17,14 @@ export interface ForceApplicationResult { * Reads physics parameters from GameConfig for runtime tuning */ export class ShipPhysics { + private _shipStatus: ShipStatus | null = null; + + /** + * Set the ship status instance for fuel consumption tracking + */ + public setShipStatus(shipStatus: ShipStatus): void { + this._shipStatus = shipStatus; + } /** * Apply forces to the ship based on input state * @param inputState - Current input state (stick positions) @@ -49,24 +58,31 @@ export class ShipPhysics { if (Math.abs(leftStick.y) > 0.1) { linearMagnitude = Math.abs(leftStick.y); - // Only apply force if we haven't reached max velocity - if (currentSpeed < config.maxLinearVelocity) { - // Get local direction (Z-axis for forward/backward thrust) - const localDirection = new Vector3(0, 0, -leftStick.y); - // Transform to world space - const worldDirection = Vector3.TransformNormal( - localDirection, - transformNode.getWorldMatrix() - ); - const force = worldDirection.scale(config.linearForceMultiplier); + // Check if we have fuel before applying force + if (this._shipStatus && this._shipStatus.fuel > 0) { + // Only apply force if we haven't reached max velocity + if (currentSpeed < config.maxLinearVelocity) { + // Get local direction (Z-axis for forward/backward thrust) + const localDirection = new Vector3(0, 0, -leftStick.y); + // Transform to world space + const worldDirection = Vector3.TransformNormal( + localDirection, + transformNode.getWorldMatrix() + ); + const force = worldDirection.scale(config.linearForceMultiplier); - // Calculate thrust point: center of mass + offset (0, 1, 0) in world space - const thrustPoint = Vector3.TransformCoordinates( - physicsBody.getMassProperties().centerOfMass.add(new Vector3(0, 1, 0)), - transformNode.getWorldMatrix() - ); + // Calculate thrust point: center of mass + offset (0, 1, 0) in world space + const thrustPoint = Vector3.TransformCoordinates( + physicsBody.getMassProperties().centerOfMass.add(new Vector3(0, 1, 0)), + transformNode.getWorldMatrix() + ); - physicsBody.applyForce(force, thrustPoint); + physicsBody.applyForce(force, thrustPoint); + + // Consume fuel: normalized magnitude (0-1) * 0.01 = max consumption of 0.01 per frame + const fuelConsumption = linearMagnitude * 0.005; + this._shipStatus.consumeFuel(fuelConsumption); + } } } @@ -78,24 +94,32 @@ export class ShipPhysics { // Apply angular forces if any stick has significant rotation input if (angularMagnitude > 0.1) { - const currentAngularSpeed = currentAngularVelocity.length(); + // Check if we have fuel before applying torque + if (this._shipStatus && this._shipStatus.fuel > 0) { + const currentAngularSpeed = currentAngularVelocity.length(); - // Only apply torque if we haven't reached max angular velocity - if (currentAngularSpeed < config.maxAngularVelocity) { - const yaw = -leftStick.x; - const pitch = rightStick.y; - const roll = rightStick.x; + // Only apply torque if we haven't reached max angular velocity + if (currentAngularSpeed < config.maxAngularVelocity) { + const yaw = -leftStick.x; + const pitch = rightStick.y; + const roll = rightStick.x; - // Create torque in local space, then transform to world space - const localTorque = new Vector3(pitch, yaw, roll).scale( - config.angularForceMultiplier - ); - const worldTorque = Vector3.TransformNormal( - localTorque, - transformNode.getWorldMatrix() - ); + // Create torque in local space, then transform to world space + const localTorque = new Vector3(pitch, yaw, roll).scale( + config.angularForceMultiplier + ); + const worldTorque = Vector3.TransformNormal( + localTorque, + transformNode.getWorldMatrix() + ); - physicsBody.applyAngularImpulse(worldTorque); + physicsBody.applyAngularImpulse(worldTorque); + + // Consume fuel: normalized magnitude (0-3 max) / 3 * 0.01 = max consumption of 0.01 per frame + const normalizedAngularMagnitude = Math.min(angularMagnitude / 3.0, 1.0); + const fuelConsumption = normalizedAngularMagnitude * 0.005; + this._shipStatus.consumeFuel(fuelConsumption); + } } } diff --git a/src/shipStatus.ts b/src/shipStatus.ts new file mode 100644 index 0000000..2c30bfe --- /dev/null +++ b/src/shipStatus.ts @@ -0,0 +1,210 @@ +import { Observable } from "@babylonjs/core"; + +/** + * Event data for ship status changes + */ +export interface ShipStatusChangeEvent { + statusType: "fuel" | "hull" | "ammo"; + oldValue: number; + newValue: number; + delta: number; +} + +/** + * Ship status values container + */ +export interface ShipStatusValues { + fuel: number; + hull: number; + ammo: number; +} + +/** + * Manages ship status values (fuel, hull integrity, ammo) + * Provides observable events for changes and automatic clamping to 0-1 range + */ +export class ShipStatus { + private _fuel: number = 1.0; + private _hull: number = 1.0; + private _ammo: number = 1.0; + + // Maximum values for each resource + private _maxFuel: number = 1.0; + private _maxHull: number = 1.0; + private _maxAmmo: number = 1.0; + + // Observable for status changes + public readonly onStatusChanged: Observable = + new Observable(); + + /** + * Get current fuel level (0-1) + */ + public get fuel(): number { + return this._fuel; + } + + /** + * Get current hull integrity (0-1) + */ + public get hull(): number { + return this._hull; + } + + /** + * Get current ammo level (0-1) + */ + public get ammo(): number { + return this._ammo; + } + + /** + * Get all status values + */ + public getValues(): ShipStatusValues { + return { + fuel: this._fuel, + hull: this._hull, + ammo: this._ammo, + }; + } + + /** + * Set fuel level directly (clamped to 0-1) + */ + public setFuel(value: number): void { + const oldValue = this._fuel; + this._fuel = Math.max(0, Math.min(this._maxFuel, value)); + + if (oldValue !== this._fuel) { + this.onStatusChanged.notifyObservers({ + statusType: "fuel", + oldValue, + newValue: this._fuel, + delta: this._fuel - oldValue, + }); + } + } + + /** + * Set hull integrity directly (clamped to 0-1) + */ + public setHull(value: number): void { + const oldValue = this._hull; + this._hull = Math.max(0, Math.min(this._maxHull, value)); + + if (oldValue !== this._hull) { + this.onStatusChanged.notifyObservers({ + statusType: "hull", + oldValue, + newValue: this._hull, + delta: this._hull - oldValue, + }); + } + } + + /** + * Set ammo level directly (clamped to 0-1) + */ + public setAmmo(value: number): void { + const oldValue = this._ammo; + this._ammo = Math.max(0, Math.min(this._maxAmmo, value)); + + if (oldValue !== this._ammo) { + this.onStatusChanged.notifyObservers({ + statusType: "ammo", + oldValue, + newValue: this._ammo, + delta: this._ammo - oldValue, + }); + } + } + + /** + * Increment fuel by delta amount + */ + public addFuel(delta: number): void { + this.setFuel(this._fuel + delta); + } + + /** + * Decrement fuel by delta amount + */ + public consumeFuel(delta: number): void { + this.setFuel(this._fuel - delta); + } + + /** + * Damage hull by delta amount + */ + public damageHull(delta: number): void { + this.setHull(this._hull - delta); + } + + /** + * Repair hull by delta amount + */ + public repairHull(delta: number): void { + this.setHull(this._hull + delta); + } + + /** + * Increment ammo by delta amount + */ + public addAmmo(delta: number): void { + this.setAmmo(this._ammo + delta); + } + + /** + * Decrement ammo by delta amount (fire weapon) + */ + public consumeAmmo(delta: number): void { + this.setAmmo(this._ammo - delta); + } + + /** + * Check if fuel is depleted + */ + public isFuelEmpty(): boolean { + return this._fuel <= 0; + } + + /** + * Check if hull is destroyed + */ + public isDestroyed(): boolean { + return this._hull <= 0; + } + + /** + * Check if ammo is depleted + */ + public isAmmoEmpty(): boolean { + return this._ammo <= 0; + } + + /** + * Reset all values to full + */ + public reset(): void { + this.setFuel(this._maxFuel); + this.setHull(this._maxHull); + this.setAmmo(this._maxAmmo); + } + + /** + * Set maximum values for resources + */ + public setMaxValues(fuel?: number, hull?: number, ammo?: number): void { + if (fuel !== undefined) this._maxFuel = fuel; + if (hull !== undefined) this._maxHull = hull; + if (ammo !== undefined) this._maxAmmo = ammo; + } + + /** + * Dispose observables + */ + public dispose(): void { + this.onStatusChanged.clear(); + } +}