Add status screen pause functionality with VR controller picking
Some checks failed
Build / build (push) Failing after 24s
Some checks failed
Build / build (push) Failing after 24s
Implemented comprehensive status screen system with pause/resume and game-end states: - Added enable/disable functionality to controller and keyboard input systems - X button and inspector key always work, even when controls disabled - Created Resume/Replay/Exit VR buttons in status screen - Resume button appears on manual pause, Replay appears on game end - Implemented automatic status screen display on game end conditions: * Death: hull < 0.01 outside landing zone * Stranded: fuel < 0.01 and velocity < 1 outside landing zone * Victory: all asteroids destroyed inside landing zone - Fixed landing zone detection to use mesh intersection instead of distance - Implemented dynamic VR pointer selection using attach/detach pattern - Pointer selection only enabled when status screen is visible - Ship controls automatically disabled when status screen shows 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d6b1744ce4
commit
31b498da7d
@ -49,6 +49,7 @@ export class ControllerInput {
|
|||||||
private _onCameraAdjustObservable: Observable<CameraAdjustment> =
|
private _onCameraAdjustObservable: Observable<CameraAdjustment> =
|
||||||
new Observable<CameraAdjustment>();
|
new Observable<CameraAdjustment>();
|
||||||
private _onStatusScreenToggleObservable: Observable<void> = new Observable<void>();
|
private _onStatusScreenToggleObservable: Observable<void> = new Observable<void>();
|
||||||
|
private _enabled: boolean = true;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._controllerObservable.add(this.handleControllerEvent.bind(this));
|
this._controllerObservable.add(this.handleControllerEvent.bind(this));
|
||||||
@ -79,12 +80,32 @@ export class ControllerInput {
|
|||||||
* Get current input state (stick positions)
|
* Get current input state (stick positions)
|
||||||
*/
|
*/
|
||||||
public getInputState() {
|
public getInputState() {
|
||||||
|
if (!this._enabled) {
|
||||||
|
return {
|
||||||
|
leftStick: Vector2.Zero(),
|
||||||
|
rightStick: Vector2.Zero(),
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
leftStick: this._leftStick.clone(),
|
leftStick: this._leftStick.clone(),
|
||||||
rightStick: this._rightStick.clone(),
|
rightStick: this._rightStick.clone(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable controller input
|
||||||
|
*/
|
||||||
|
public setEnabled(enabled: boolean): void {
|
||||||
|
this._enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
// Reset stick values when disabled
|
||||||
|
this._leftStick.x = 0;
|
||||||
|
this._leftStick.y = 0;
|
||||||
|
this._rightStick.x = 0;
|
||||||
|
this._rightStick.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a VR controller to the input system
|
* Add a VR controller to the input system
|
||||||
*/
|
*/
|
||||||
@ -199,6 +220,16 @@ export class ControllerInput {
|
|||||||
* Handle controller events (thumbsticks and buttons)
|
* Handle controller events (thumbsticks and buttons)
|
||||||
*/
|
*/
|
||||||
private handleControllerEvent(controllerEvent: ControllerEvent): void {
|
private handleControllerEvent(controllerEvent: ControllerEvent): void {
|
||||||
|
// Don't process ship control inputs when disabled (but allow status screen toggle)
|
||||||
|
if (!this._enabled && controllerEvent.type === "thumbstick") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._enabled && controllerEvent.type === "button" &&
|
||||||
|
!(controllerEvent.component.id === "x-button" && controllerEvent.hand === "left")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (controllerEvent.type === "thumbstick") {
|
if (controllerEvent.type === "thumbstick") {
|
||||||
if (controllerEvent.hand === "left") {
|
if (controllerEvent.hand === "left") {
|
||||||
this._leftStick.x = controllerEvent.axisData.x;
|
this._leftStick.x = controllerEvent.axisData.x;
|
||||||
@ -235,6 +266,7 @@ export class ControllerInput {
|
|||||||
}
|
}
|
||||||
if (controllerEvent.component.id === "x-button" && controllerEvent.hand === "left") {
|
if (controllerEvent.component.id === "x-button" && controllerEvent.hand === "left") {
|
||||||
// Only trigger on button press, not release
|
// Only trigger on button press, not release
|
||||||
|
// X button always works, even when disabled, to allow toggling status screen
|
||||||
if (controllerEvent.pressed) {
|
if (controllerEvent.pressed) {
|
||||||
this._onStatusScreenToggleObservable.notifyObservers();
|
this._onStatusScreenToggleObservable.notifyObservers();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export class KeyboardInput {
|
|||||||
private _onCameraChangeObservable: Observable<number> = new Observable<number>();
|
private _onCameraChangeObservable: Observable<number> = new Observable<number>();
|
||||||
private _onRecordingActionObservable: Observable<RecordingAction> = new Observable<RecordingAction>();
|
private _onRecordingActionObservable: Observable<RecordingAction> = new Observable<RecordingAction>();
|
||||||
private _scene: Scene;
|
private _scene: Scene;
|
||||||
|
private _enabled: boolean = true;
|
||||||
|
|
||||||
constructor(scene: Scene) {
|
constructor(scene: Scene) {
|
||||||
this._scene = scene;
|
this._scene = scene;
|
||||||
@ -51,12 +52,32 @@ export class KeyboardInput {
|
|||||||
* Get current input state (stick positions)
|
* Get current input state (stick positions)
|
||||||
*/
|
*/
|
||||||
public getInputState() {
|
public getInputState() {
|
||||||
|
if (!this._enabled) {
|
||||||
|
return {
|
||||||
|
leftStick: Vector2.Zero(),
|
||||||
|
rightStick: Vector2.Zero(),
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
leftStick: this._leftStick.clone(),
|
leftStick: this._leftStick.clone(),
|
||||||
rightStick: this._rightStick.clone(),
|
rightStick: this._rightStick.clone(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable keyboard input
|
||||||
|
*/
|
||||||
|
public setEnabled(enabled: boolean): void {
|
||||||
|
this._enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
// Reset stick values when disabled
|
||||||
|
this._leftStick.x = 0;
|
||||||
|
this._leftStick.y = 0;
|
||||||
|
this._rightStick.x = 0;
|
||||||
|
this._rightStick.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup keyboard and mouse event listeners
|
* Setup keyboard and mouse event listeners
|
||||||
*/
|
*/
|
||||||
@ -77,6 +98,28 @@ export class KeyboardInput {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.onkeydown = (ev) => {
|
document.onkeydown = (ev) => {
|
||||||
|
// Always allow inspector and camera toggle, even when disabled
|
||||||
|
if (ev.key === 'i') {
|
||||||
|
// Open Babylon Inspector
|
||||||
|
import("@babylonjs/inspector").then((inspector) => {
|
||||||
|
inspector.Inspector.Show(this._scene, {
|
||||||
|
overlay: true,
|
||||||
|
showExplorer: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.key === '1') {
|
||||||
|
this._onCameraChangeObservable.notifyObservers(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't process ship control inputs when disabled
|
||||||
|
if (!this._enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Recording controls (with modifiers)
|
// Recording controls (with modifiers)
|
||||||
/*if (ev.key === 'r' || ev.key === 'R') {
|
/*if (ev.key === 'r' || ev.key === 'R') {
|
||||||
if (ev.ctrlKey || ev.metaKey) {
|
if (ev.ctrlKey || ev.metaKey) {
|
||||||
@ -96,18 +139,6 @@ export class KeyboardInput {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case 'i':
|
|
||||||
// Open Babylon Inspector
|
|
||||||
import("@babylonjs/inspector").then((inspector) => {
|
|
||||||
inspector.Inspector.Show(this._scene, {
|
|
||||||
overlay: true,
|
|
||||||
showExplorer: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case '1':
|
|
||||||
this._onCameraChangeObservable.notifyObservers(1);
|
|
||||||
break;
|
|
||||||
case ' ':
|
case ' ':
|
||||||
this._onShootObservable.notifyObservers();
|
this._onShootObservable.notifyObservers();
|
||||||
break;
|
break;
|
||||||
|
|||||||
30
src/main.ts
30
src/main.ts
@ -99,11 +99,21 @@ export class Main {
|
|||||||
this._currentLevel.getReadyObservable().add(async () => {
|
this._currentLevel.getReadyObservable().add(async () => {
|
||||||
setLoadingMessage("Starting game...");
|
setLoadingMessage("Starting game...");
|
||||||
|
|
||||||
|
// Get ship and set up replay observable
|
||||||
|
const level1 = this._currentLevel as Level1;
|
||||||
|
const ship = (level1 as any)._ship;
|
||||||
|
|
||||||
|
// Listen for replay requests from the ship
|
||||||
|
if (ship) {
|
||||||
|
ship.onReplayRequestObservable.add(() => {
|
||||||
|
debugLog('Replay requested - reloading page');
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// If we entered XR before level creation, manually setup camera parenting
|
// If we entered XR before level creation, manually setup camera parenting
|
||||||
// (This is needed because onInitialXRPoseSetObservable won't fire if we're already in XR)
|
// (This is needed because onInitialXRPoseSetObservable won't fire if we're already in XR)
|
||||||
if (DefaultScene.XR && xrSession && DefaultScene.XR.baseExperience.state === 2) { // WebXRState.IN_XR = 2
|
if (DefaultScene.XR && xrSession && DefaultScene.XR.baseExperience.state === 2) { // WebXRState.IN_XR = 2
|
||||||
const level1 = this._currentLevel as Level1;
|
|
||||||
const ship = (level1 as any)._ship;
|
|
||||||
|
|
||||||
if (ship && ship.transformNode) {
|
if (ship && ship.transformNode) {
|
||||||
debugLog('Manually parenting XR camera to ship transformNode');
|
debugLog('Manually parenting XR camera to ship transformNode');
|
||||||
@ -315,7 +325,8 @@ export class Main {
|
|||||||
if (navigator.xr) {
|
if (navigator.xr) {
|
||||||
try {
|
try {
|
||||||
DefaultScene.XR = await WebXRDefaultExperience.CreateAsync(DefaultScene.MainScene, {
|
DefaultScene.XR = await WebXRDefaultExperience.CreateAsync(DefaultScene.MainScene, {
|
||||||
disablePointerSelection: true,
|
// Don't disable pointer selection - we need it for status screen buttons
|
||||||
|
// Will detach it during gameplay and attach when status screen is shown
|
||||||
disableTeleportation: true,
|
disableTeleportation: true,
|
||||||
disableNearInteraction: true,
|
disableNearInteraction: true,
|
||||||
disableHandTracking: true,
|
disableHandTracking: true,
|
||||||
@ -323,6 +334,19 @@ export class Main {
|
|||||||
});
|
});
|
||||||
debugLog(WebXRFeaturesManager.GetAvailableFeatures());
|
debugLog(WebXRFeaturesManager.GetAvailableFeatures());
|
||||||
debugLog("WebXR initialized successfully");
|
debugLog("WebXR initialized successfully");
|
||||||
|
|
||||||
|
// Store pointer selection feature reference and detach it initially
|
||||||
|
if (DefaultScene.XR) {
|
||||||
|
const pointerFeature = DefaultScene.XR.baseExperience.featuresManager.getEnabledFeature(
|
||||||
|
"xr-controller-pointer-selection"
|
||||||
|
);
|
||||||
|
if (pointerFeature) {
|
||||||
|
(DefaultScene.XR as any).pointerSelectionFeature = pointerFeature;
|
||||||
|
// Detach immediately to prevent interaction during gameplay
|
||||||
|
pointerFeature.detach();
|
||||||
|
debugLog("Pointer selection feature stored and detached");
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog("WebXR initialization failed, falling back to flat mode:", error);
|
debugLog("WebXR initialization failed, falling back to flat mode:", error);
|
||||||
DefaultScene.XR = null;
|
DefaultScene.XR = null;
|
||||||
|
|||||||
147
src/ship.ts
147
src/ship.ts
@ -3,6 +3,7 @@ import {
|
|||||||
Color3,
|
Color3,
|
||||||
FreeCamera,
|
FreeCamera,
|
||||||
Mesh,
|
Mesh,
|
||||||
|
Observable,
|
||||||
PhysicsAggregate,
|
PhysicsAggregate,
|
||||||
PhysicsMotionType,
|
PhysicsMotionType,
|
||||||
PhysicsShapeType,
|
PhysicsShapeType,
|
||||||
@ -52,6 +53,12 @@ export class Ship {
|
|||||||
private _isInLandingZone: boolean = false;
|
private _isInLandingZone: boolean = false;
|
||||||
private _isReplayMode: boolean;
|
private _isReplayMode: boolean;
|
||||||
|
|
||||||
|
// Observable for replay requests
|
||||||
|
public onReplayRequestObservable: Observable<void> = new Observable<void>();
|
||||||
|
|
||||||
|
// Auto-show status screen flag
|
||||||
|
private _statusScreenAutoShown: boolean = false;
|
||||||
|
|
||||||
constructor(audioEngine?: AudioEngineV2, isReplayMode: boolean = false) {
|
constructor(audioEngine?: AudioEngineV2, isReplayMode: boolean = false) {
|
||||||
this._audioEngine = audioEngine;
|
this._audioEngine = audioEngine;
|
||||||
this._isReplayMode = isReplayMode;
|
this._isReplayMode = isReplayMode;
|
||||||
@ -69,6 +76,10 @@ export class Ship {
|
|||||||
return this._keyboardInput;
|
return this._keyboardInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isInLandingZone(): boolean {
|
||||||
|
return this._isInLandingZone;
|
||||||
|
}
|
||||||
|
|
||||||
public set position(newPosition: Vector3) {
|
public set position(newPosition: Vector3) {
|
||||||
const body = this._ship.physicsBody;
|
const body = this._ship.physicsBody;
|
||||||
|
|
||||||
@ -159,7 +170,17 @@ export class Ship {
|
|||||||
// Wire up status screen toggle event
|
// Wire up status screen toggle event
|
||||||
this._controllerInput.onStatusScreenToggleObservable.add(() => {
|
this._controllerInput.onStatusScreenToggleObservable.add(() => {
|
||||||
if (this._statusScreen) {
|
if (this._statusScreen) {
|
||||||
this._statusScreen.toggle();
|
if (this._statusScreen.isVisible) {
|
||||||
|
// Hide status screen and re-enable controls
|
||||||
|
this._statusScreen.hide();
|
||||||
|
this._keyboardInput?.setEnabled(true);
|
||||||
|
this._controllerInput?.setEnabled(true);
|
||||||
|
} else {
|
||||||
|
// Show status screen (manual pause, not game end) and disable controls
|
||||||
|
this._statusScreen.show(false);
|
||||||
|
this._keyboardInput?.setEnabled(false);
|
||||||
|
this._controllerInput?.setEnabled(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -195,6 +216,9 @@ export class Ship {
|
|||||||
this._frameCount = 0;
|
this._frameCount = 0;
|
||||||
this.updatePhysics();
|
this.updatePhysics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check game end conditions every frame (but only acts once)
|
||||||
|
this.checkGameEndConditions();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup camera
|
// Setup camera
|
||||||
@ -240,11 +264,101 @@ export class Ship {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize status screen
|
// Initialize status screen with callbacks
|
||||||
this._statusScreen = new StatusScreen(DefaultScene.MainScene, this._gameStats);
|
this._statusScreen = new StatusScreen(
|
||||||
|
DefaultScene.MainScene,
|
||||||
|
this._gameStats,
|
||||||
|
() => this.handleReplayRequest(),
|
||||||
|
() => this.handleExitVR(),
|
||||||
|
() => this.handleResume()
|
||||||
|
);
|
||||||
this._statusScreen.initialize(this._camera);
|
this._statusScreen.initialize(this._camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle replay button click from status screen
|
||||||
|
*/
|
||||||
|
private handleReplayRequest(): void {
|
||||||
|
debugLog('Replay button clicked - notifying observers');
|
||||||
|
this.onReplayRequestObservable.notifyObservers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle exit VR button click from status screen
|
||||||
|
*/
|
||||||
|
private handleExitVR(): void {
|
||||||
|
debugLog('Exit VR button clicked - refreshing browser');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle resume button click from status screen
|
||||||
|
*/
|
||||||
|
private handleResume(): void {
|
||||||
|
debugLog('Resume button clicked - hiding status screen and re-enabling controls');
|
||||||
|
this._statusScreen.hide();
|
||||||
|
this._keyboardInput?.setEnabled(true);
|
||||||
|
this._controllerInput?.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check game-ending conditions and auto-show status screen
|
||||||
|
* Conditions:
|
||||||
|
* 1. Ship outside landing zone AND hull < 0.01 (death)
|
||||||
|
* 2. Ship outside landing zone AND fuel < 0.01 AND velocity < 1 (stranded)
|
||||||
|
* 3. All asteroids destroyed AND ship inside landing zone (victory)
|
||||||
|
*/
|
||||||
|
private checkGameEndConditions(): void {
|
||||||
|
// Skip if already auto-shown or status screen doesn't exist
|
||||||
|
if (this._statusScreenAutoShown || !this._statusScreen || !this._scoreboard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no physics body yet
|
||||||
|
if (!this._ship?.physicsBody) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current ship status
|
||||||
|
const hull = this._scoreboard.shipStatus.hull;
|
||||||
|
const fuel = this._scoreboard.shipStatus.fuel;
|
||||||
|
const asteroidsRemaining = this._scoreboard.remaining;
|
||||||
|
|
||||||
|
// Calculate total linear velocity
|
||||||
|
const linearVelocity = this._ship.physicsBody.getLinearVelocity();
|
||||||
|
const totalVelocity = linearVelocity.length();
|
||||||
|
|
||||||
|
// Check condition 1: Death by hull damage (outside landing zone)
|
||||||
|
if (!this._isInLandingZone && hull < 0.01) {
|
||||||
|
debugLog('Game end condition met: Hull critical outside landing zone');
|
||||||
|
this._statusScreen.show(true);
|
||||||
|
this._keyboardInput?.setEnabled(false);
|
||||||
|
this._controllerInput?.setEnabled(false);
|
||||||
|
this._statusScreenAutoShown = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check condition 2: Stranded (outside landing zone, no fuel, low velocity)
|
||||||
|
if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 1) {
|
||||||
|
debugLog('Game end condition met: Stranded (no fuel, low velocity)');
|
||||||
|
this._statusScreen.show(true);
|
||||||
|
this._keyboardInput?.setEnabled(false);
|
||||||
|
this._controllerInput?.setEnabled(false);
|
||||||
|
this._statusScreenAutoShown = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
|
||||||
|
if (asteroidsRemaining <= 0 && this._isInLandingZone) {
|
||||||
|
debugLog('Game end condition met: Victory (all asteroids destroyed)');
|
||||||
|
this._statusScreen.show(true);
|
||||||
|
this._keyboardInput?.setEnabled(false);
|
||||||
|
this._controllerInput?.setEnabled(false);
|
||||||
|
this._statusScreenAutoShown = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update physics based on combined input from all input sources
|
* Update physics based on combined input from all input sources
|
||||||
*/
|
*/
|
||||||
@ -302,16 +416,22 @@ export class Ship {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if ship is still in the landing zone by checking distance
|
// Check if ship mesh intersects with landing zone mesh
|
||||||
// Since it's a trigger, we need to track position
|
|
||||||
const shipPos = this._ship.physicsBody.transformNode.position;
|
|
||||||
const landingPos = this._landingAggregate.transformNode.position;
|
|
||||||
const distance = Vector3.Distance(shipPos, landingPos);
|
|
||||||
|
|
||||||
// Assume landing zone radius is approximately 20 units (adjust as needed)
|
|
||||||
const wasInZone = this._isInLandingZone;
|
const wasInZone = this._isInLandingZone;
|
||||||
this._isInLandingZone = distance < 20;
|
|
||||||
|
|
||||||
|
// Get the meshes from the transform nodes
|
||||||
|
const shipMesh = this._ship.getChildMeshes()[0];
|
||||||
|
const landingMesh = this._landingAggregate.transformNode as Mesh;
|
||||||
|
|
||||||
|
// Use mesh intersection for accurate zone detection
|
||||||
|
if (shipMesh && landingMesh) {
|
||||||
|
this._isInLandingZone = shipMesh.intersectsMesh(landingMesh, false);
|
||||||
|
} else {
|
||||||
|
// Fallback: if meshes not available, assume not in zone
|
||||||
|
this._isInLandingZone = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log zone transitions
|
||||||
if (this._isInLandingZone && !wasInZone) {
|
if (this._isInLandingZone && !wasInZone) {
|
||||||
debugLog("Ship entered landing zone - resupply active");
|
debugLog("Ship entered landing zone - resupply active");
|
||||||
} else if (!this._isInLandingZone && wasInZone) {
|
} else if (!this._isInLandingZone && wasInZone) {
|
||||||
@ -372,12 +492,11 @@ export class Ship {
|
|||||||
public setLandingZone(landingAggregate: PhysicsAggregate): void {
|
public setLandingZone(landingAggregate: PhysicsAggregate): void {
|
||||||
this._landingAggregate = landingAggregate;
|
this._landingAggregate = landingAggregate;
|
||||||
|
|
||||||
// Listen for trigger events to detect when ship enters/exits landing zone
|
// Listen for trigger events for debugging (actual detection uses mesh intersection)
|
||||||
landingAggregate.body.getCollisionObservable().add((collisionEvent) => {
|
landingAggregate.body.getCollisionObservable().add((collisionEvent) => {
|
||||||
// Check if the collision is with our ship
|
// Check if the collision is with our ship
|
||||||
if (collisionEvent.collider === this._ship.physicsBody) {
|
if (collisionEvent.collider === this._ship.physicsBody) {
|
||||||
this._isInLandingZone = true;
|
debugLog("Physics trigger fired for landing zone");
|
||||||
debugLog("Ship entered landing zone - resupply active");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AdvancedDynamicTexture,
|
AdvancedDynamicTexture,
|
||||||
|
Button,
|
||||||
Control,
|
Control,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
StackPanel,
|
StackPanel,
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
Vector3
|
Vector3
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import { GameStats } from "./gameStats";
|
import { GameStats } from "./gameStats";
|
||||||
|
import { DefaultScene } from "./defaultScene";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status screen that displays game statistics
|
* Status screen that displays game statistics
|
||||||
@ -35,9 +37,25 @@ export class StatusScreen {
|
|||||||
private _accuracyText: TextBlock;
|
private _accuracyText: TextBlock;
|
||||||
private _fuelConsumedText: TextBlock;
|
private _fuelConsumedText: TextBlock;
|
||||||
|
|
||||||
constructor(scene: Scene, gameStats: GameStats) {
|
// Buttons
|
||||||
|
private _replayButton: Button;
|
||||||
|
private _exitButton: Button;
|
||||||
|
private _resumeButton: Button;
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
private _onReplayCallback: (() => void) | null = null;
|
||||||
|
private _onExitCallback: (() => void) | null = null;
|
||||||
|
private _onResumeCallback: (() => void) | null = null;
|
||||||
|
|
||||||
|
// Track whether game has ended
|
||||||
|
private _isGameEnded: boolean = false;
|
||||||
|
|
||||||
|
constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void) {
|
||||||
this._scene = scene;
|
this._scene = scene;
|
||||||
this._gameStats = gameStats;
|
this._gameStats = gameStats;
|
||||||
|
this._onReplayCallback = onReplay || null;
|
||||||
|
this._onExitCallback = onExit || null;
|
||||||
|
this._onResumeCallback = onResume || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,6 +126,70 @@ export class StatusScreen {
|
|||||||
this._fuelConsumedText = this.createStatText("Fuel Consumed: 0%");
|
this._fuelConsumedText = this.createStatText("Fuel Consumed: 0%");
|
||||||
mainPanel.addControl(this._fuelConsumedText);
|
mainPanel.addControl(this._fuelConsumedText);
|
||||||
|
|
||||||
|
// Add spacing before buttons
|
||||||
|
const spacer2 = this.createSpacer(50);
|
||||||
|
mainPanel.addControl(spacer2);
|
||||||
|
|
||||||
|
// Create button bar
|
||||||
|
const buttonBar = new StackPanel("buttonBar");
|
||||||
|
buttonBar.isVertical = false;
|
||||||
|
buttonBar.height = "80px";
|
||||||
|
buttonBar.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||||
|
buttonBar.spacing = 20;
|
||||||
|
|
||||||
|
// Create Resume button (only shown when game hasn't ended)
|
||||||
|
this._resumeButton = Button.CreateSimpleButton("resumeButton", "RESUME GAME");
|
||||||
|
this._resumeButton.width = "300px";
|
||||||
|
this._resumeButton.height = "60px";
|
||||||
|
this._resumeButton.color = "white";
|
||||||
|
this._resumeButton.background = "#00ff88";
|
||||||
|
this._resumeButton.cornerRadius = 10;
|
||||||
|
this._resumeButton.thickness = 0;
|
||||||
|
this._resumeButton.fontSize = "30px";
|
||||||
|
this._resumeButton.fontWeight = "bold";
|
||||||
|
this._resumeButton.onPointerClickObservable.add(() => {
|
||||||
|
if (this._onResumeCallback) {
|
||||||
|
this._onResumeCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
buttonBar.addControl(this._resumeButton);
|
||||||
|
|
||||||
|
// Create Replay button (only shown when game has ended)
|
||||||
|
this._replayButton = Button.CreateSimpleButton("replayButton", "REPLAY LEVEL");
|
||||||
|
this._replayButton.width = "300px";
|
||||||
|
this._replayButton.height = "60px";
|
||||||
|
this._replayButton.color = "white";
|
||||||
|
this._replayButton.background = "#00ff88";
|
||||||
|
this._replayButton.cornerRadius = 10;
|
||||||
|
this._replayButton.thickness = 0;
|
||||||
|
this._replayButton.fontSize = "30px";
|
||||||
|
this._replayButton.fontWeight = "bold";
|
||||||
|
this._replayButton.onPointerClickObservable.add(() => {
|
||||||
|
if (this._onReplayCallback) {
|
||||||
|
this._onReplayCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
buttonBar.addControl(this._replayButton);
|
||||||
|
|
||||||
|
// Create Exit VR button
|
||||||
|
this._exitButton = Button.CreateSimpleButton("exitButton", "EXIT VR");
|
||||||
|
this._exitButton.width = "300px";
|
||||||
|
this._exitButton.height = "60px";
|
||||||
|
this._exitButton.color = "white";
|
||||||
|
this._exitButton.background = "#cc3333";
|
||||||
|
this._exitButton.cornerRadius = 10;
|
||||||
|
this._exitButton.thickness = 0;
|
||||||
|
this._exitButton.fontSize = "30px";
|
||||||
|
this._exitButton.fontWeight = "bold";
|
||||||
|
this._exitButton.onPointerClickObservable.add(() => {
|
||||||
|
if (this._onExitCallback) {
|
||||||
|
this._onExitCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
buttonBar.addControl(this._exitButton);
|
||||||
|
|
||||||
|
mainPanel.addControl(buttonBar);
|
||||||
|
|
||||||
this._texture.addControl(mainPanel);
|
this._texture.addControl(mainPanel);
|
||||||
|
|
||||||
// Initially hide the screen
|
// Initially hide the screen
|
||||||
@ -166,13 +248,60 @@ export class StatusScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the status screen
|
* Enable VR controller picking for button interaction
|
||||||
*/
|
*/
|
||||||
public show(): void {
|
private enablePointerSelection(): void {
|
||||||
|
// Get the stored pointer selection feature
|
||||||
|
const pointerFeature = (DefaultScene.XR as any)?.pointerSelectionFeature;
|
||||||
|
if (pointerFeature && DefaultScene.XR?.baseExperience?.state === 2) { // WebXRState.IN_XR = 2
|
||||||
|
try {
|
||||||
|
// Attach the feature to enable pointer interaction
|
||||||
|
pointerFeature.attach();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to attach pointer selection:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable VR controller picking
|
||||||
|
*/
|
||||||
|
private disablePointerSelection(): void {
|
||||||
|
// Get the stored pointer selection feature
|
||||||
|
const pointerFeature = (DefaultScene.XR as any)?.pointerSelectionFeature;
|
||||||
|
if (pointerFeature) {
|
||||||
|
try {
|
||||||
|
// Detach the feature to disable pointer interaction
|
||||||
|
pointerFeature.detach();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to detach pointer selection:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the status screen
|
||||||
|
* @param isGameEnded - true if game has ended (death/stranded/victory), false if manually paused
|
||||||
|
*/
|
||||||
|
public show(isGameEnded: boolean = false): void {
|
||||||
if (!this._screenMesh) {
|
if (!this._screenMesh) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store game ended state
|
||||||
|
this._isGameEnded = isGameEnded;
|
||||||
|
|
||||||
|
// Show/hide appropriate buttons based on whether game has ended
|
||||||
|
if (this._resumeButton) {
|
||||||
|
this._resumeButton.isVisible = !isGameEnded;
|
||||||
|
}
|
||||||
|
if (this._replayButton) {
|
||||||
|
this._replayButton.isVisible = isGameEnded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable pointer selection for button interaction
|
||||||
|
this.enablePointerSelection();
|
||||||
|
|
||||||
// Update statistics before showing
|
// Update statistics before showing
|
||||||
this.updateStatistics();
|
this.updateStatistics();
|
||||||
|
|
||||||
@ -189,6 +318,9 @@ export class StatusScreen {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable pointer selection when hiding
|
||||||
|
this.disablePointerSelection();
|
||||||
|
|
||||||
this._screenMesh.setEnabled(false);
|
this._screenMesh.setEnabled(false);
|
||||||
this._isVisible = false;
|
this._isVisible = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user